48 private const ENTITY_TYPE_USER =
'im-user';
49 private const ENTITY_TYPE_CHAT =
'im-chat';
50 private const WITH_CHAT_BY_USERS_OPTION =
'withChatByUsers';
51 private const ONLY_WITH_MANAGE_MESSAGES_RIGHT_OPTION =
'onlyWithManageMessagesRight';
55 private const EXCLUDE_FROM_RECENT_OPTION =
'excludeFromRecent';
57 private const EXCLUDE_OPTION =
'exclude';
58 private const SEARCH_FLAGS_OPTION =
'searchFlags';
62 private const ALLOWED_SEARCH_FLAGS = [self::FLAG_USERS, self::FLAG_CHATS, self::FLAG_BOTS];
68 Chat::IM_TYPE_CHANNEL,
69 Chat::IM_TYPE_OPEN_CHANNEL,
72 private const WITH_CHAT_BY_USERS_DEFAULT =
false;
73 private const ONLY_WITH_MANAGE_MESSAGE_RIGHT_DEFAULT =
false;
74 private const ONLY_WITH_OWNER_RIGHT_DEFAULT =
false;
75 private const ONLY_WITH_NULL_ENTITY_TYPE_DEFAULT =
false;
76 private const SEARCH_FLAGS_DEFAULT = [
77 self::FLAG_USERS =>
true,
78 self::FLAG_CHATS =>
true,
80 private string $preparedSearchString;
81 private string $originalSearchString;
82 private array $userIds;
83 private array $chatIds;
84 private bool $sortEnable =
true;
88 $this->options[self::WITH_CHAT_BY_USERS_OPTION] = self::WITH_CHAT_BY_USERS_DEFAULT;
89 $this->options[self::ONLY_WITH_MANAGE_MESSAGES_RIGHT_OPTION] = self::ONLY_WITH_MANAGE_MESSAGE_RIGHT_DEFAULT;
90 $this->options[self::ONLY_WITH_OWNER_RIGHT_OPTION] = self::ONLY_WITH_OWNER_RIGHT_DEFAULT;
91 $this->options[self::ONLY_WITH_NULL_ENTITY_TYPE_OPTION] = self::ONLY_WITH_NULL_ENTITY_TYPE_DEFAULT;
92 if (isset(
$options[self::WITH_CHAT_BY_USERS_OPTION]) && is_bool(
$options[self::WITH_CHAT_BY_USERS_OPTION]))
94 $this->options[self::WITH_CHAT_BY_USERS_OPTION] =
$options[self::WITH_CHAT_BY_USERS_OPTION];
96 if (isset(
$options[self::ONLY_WITH_MANAGE_MESSAGES_RIGHT_OPTION]) && is_bool(
$options[self::ONLY_WITH_MANAGE_MESSAGES_RIGHT_OPTION]))
98 $this->options[self::ONLY_WITH_MANAGE_MESSAGES_RIGHT_OPTION] =
$options[self::ONLY_WITH_MANAGE_MESSAGES_RIGHT_OPTION];
100 if (isset(
$options[self::EXCLUDE_FROM_RECENT_OPTION]) && is_array(
$options[self::EXCLUDE_FROM_RECENT_OPTION]))
102 $this->options[self::EXCLUDE_FROM_RECENT_OPTION] =
$options[self::EXCLUDE_FROM_RECENT_OPTION];
104 if (isset(
$options[self::SEARCH_CHAT_TYPES_OPTION]) && is_array(
$options[self::SEARCH_CHAT_TYPES_OPTION]))
106 $this->options[self::SEARCH_CHAT_TYPES_OPTION] =
$options[self::SEARCH_CHAT_TYPES_OPTION];
110 $this->options[self::SEARCH_CHAT_TYPES_OPTION] = static::ALLOWED_SEARCH_CHAT_TYPES;
112 if (isset(
$options[self::EXCLUDE_IDS_OPTION]) && is_array(
$options[self::EXCLUDE_IDS_OPTION]))
114 $this->options[self::EXCLUDE_IDS_OPTION] =
$options[self::EXCLUDE_IDS_OPTION];
116 $this->prepareSearchFlags(
$options);
117 parent::__construct();
124 return $USER->IsAuthorized();
129 $this->originalSearchString = $searchQuery->
getQuery();
130 $this->preparedSearchString = $this->prepareSearchString($searchQuery->
getQuery());
132 if (!Content::canUseFulltextSearch($this->preparedSearchString))
136 $items = $this->getSortedLimitedBlankItems();
143 if (!Loader::includeModule(
'intranet'))
148 $requiredCountToFill = self::LIMIT - $dialog->
getRecentItems()->count();
150 if ($requiredCountToFill <= 0)
157 rsort($defaultItems);
158 $defaultItems = array_slice($defaultItems, 0, $requiredCountToFill);
160 foreach ($defaultItems as $itemId)
168 if (!$this->getContext()->getUser()->isExtranet())
170 return Department::getInstance()->getColleagues();
173 return Group::getUsersInSameGroups($this->getContext()->getUserId());
178 $this->sortEnable =
false;
179 $this->setUserAndChatIds($ids);
180 $items = $this->getItemsWithDates();
204 private function setUserAndChatIds(
array $ids): void
206 $needExcludeChats = isset($this->options[self::EXCLUDE_FROM_RECENT_OPTION][self::FLAG_CHATS]);
207 $needExcludeUsers = isset($this->options[self::EXCLUDE_FROM_RECENT_OPTION][self::FLAG_USERS]);
208 foreach ($ids as $id)
210 if ($this->isChatId($id) && !$needExcludeChats)
212 $chatId = substr($id, 4);
213 $this->chatIds[$chatId] = $chatId;
215 elseif (!$needExcludeUsers)
217 $this->userIds[$id] = $id;
222 private function getBlankItem(
string $dialogId, ?
DateTime $dateMessage =
null, ?
DateTime $secondDate =
null): Item
225 $entityType = self::ENTITY_TYPE_USER;
226 if ($this->isChatId($dialogId))
228 $id = substr($dialogId, 4);
229 $entityType = self::ENTITY_TYPE_CHAT;
231 $customData = [
'id' => $id];
233 $customData[
'dateMessage'] = $dateMessage;
234 $customData[
'secondSort'] = $secondDate instanceof DateTime ? $secondDate->getTimestamp() : 0;
235 if (isset($dateMessage))
237 if ($this->sortEnable)
239 $sort = $dateMessage->getTimestamp();
245 'entityId' => static::ENTITY_ID,
246 'entityType' => $entityType,
248 'customData' => $customData,
262 $id = $item->getCustomData()->get(
'id');
263 if ($item->getEntityType() === self::ENTITY_TYPE_USER)
273 $users =
new UserCollection($userIds);
274 $users->fillOnlineData();
275 $privateChatIds = \Bitrix\Im\Dialog::getChatIds($userIds, $this->
getContext()->getUserId());
276 $copilotRoles = $this->getCopilotRoles($this->filterCopilotChats($chats));
277 Chat::fillSelfRelations($chats);
281 $customData = $item->getCustomData()->getValues();
282 if ($item->getEntityType() === self::ENTITY_TYPE_USER)
284 $user = $users->getById($customData[
'id']);
285 $customData[
'user'] =
$user->toRestFormat();
287 $chatId = (int)$privateChatIds[$customData[
'id']];
288 $customData[
'chat'][
'textFieldEnabled'] = (
new TextFieldEnabled($chatId))->
get();
289 $customData[
'chat'][
'backgroundId'] = (
new Background($chatId))->
get();
290 $customData[
'copilot'] =
null;
293 ->setTitle(
$user->getName())
294 ->setAvatar(
$user->getAvatar())
295 ->setCustomData($customData)
298 if ($item->getEntityType() === self::ENTITY_TYPE_CHAT)
300 $chat = $chats[$customData[
'id']] ??
null;
306 $customData[
'chat'] = $chat->toRestFormat([
'CHAT_SHORT_FORMAT' =>
true]);
307 $customData[
'copilot'] = $copilotRoles[$chat->getId()] ??
null;
309 ->setTitle($chat->getTitle())
310 ->setAvatar($chat->getAvatar())
311 ->setCustomData($customData)
321 private function getCopilotRoles(
array $copilotChats):
array
323 $roleManager =
new RoleManager();
326 foreach ($copilotChats as $chat)
329 $roleCodes[
$chatId] = $roleManager->getMainRole($chatId);
332 $roles = $roleManager->getRoles($roleCodes);
335 foreach ($roleCodes as $chatId =>
$code)
347 private function filterCopilotChats(
array $chats):
array
349 return array_filter($chats,
static fn($chat) => $chat instanceof Chat\CopilotChat);
352 private function getItemsWithDates():
array
354 $userItemsWithDate = $this->getUserItemsWithDate();
355 $chatItemsWithDate = $this->getChatItemsWithDate();
357 return $this->mergeByKey($userItemsWithDate, $chatItemsWithDate);
360 private function getSortedLimitedBlankItems():
array
362 $items = $this->getItemsWithDates();
363 usort(
$items,
function(Item
$a, Item $b) {
364 if ($b->getSort() ===
$a->getSort())
366 if (!$this->isChatId($b->getId()) && !$this->isChatId(
$a->getId()))
370 if ($aUser->isExtranet() === $bUser->isExtranet())
372 return $bUser->getId() <=> $aUser->getId();
375 return $aUser->isExtranet() <=> $bUser->isExtranet();
377 return (
int)$b->getCustomData()->get(
'secondSort') <=> (int)
$a->getCustomData()->get(
'secondSort');
379 return $b->getSort() <=>
$a->getSort();
382 return array_slice(
$items, 0, self::LIMIT);
385 private function getChatItemsWithDate():
array
387 if (!$this->needSearch(self::FLAG_CHATS))
392 if (isset($this->preparedSearchString))
394 return $this->mergeByKey(
395 $this->getChatItemsWithDateByUsers(),
396 $this->getChatItemsWithDateByTitle()
400 if (isset($this->chatIds) && !empty($this->chatIds))
402 return $this->getChatItemsWithDateByIds();
408 private function getChatItemsWithDateByIds():
array
410 if (!isset($this->chatIds) || empty($this->chatIds))
420 private function getChatItemsWithDateByTitle():
array
422 if (!isset($this->preparedSearchString))
428 ->getCommonChatQueryWithOrder()
429 ->whereMatch(
'INDEX.SEARCH_TITLE', $this->preparedSearchString)
436 private function getChatItemsWithDateByUsers():
array
438 if (!isset($this->preparedSearchString) || !$this->withChatByUsers())
444 ->getCommonChatQueryWithOrder(Join::TYPE_INNER)
445 ->registerRuntimeField(
449 Entity::getInstanceByQuery($this->getChatsByUserNameQuery()),
450 Join::on(
'this.ID',
'ref.CHAT_ID')
451 ))->configureJoinType(Join::TYPE_INNER)
459 private function getChatsByUserNameQuery(): Query
461 return RelationTable::query()
462 ->setSelect([
'CHAT_ID'])
463 ->registerRuntimeField(
467 \Bitrix\Main\UserTable::class,
468 Join::on(
'this.USER_ID',
'ref.ID'),
469 ))->configureJoinType(Join::TYPE_INNER)
471 ->registerRuntimeField(
475 UserIndexTable::class,
476 Join::on(
'this.USER_ID',
'ref.USER_ID'),
477 ))->configureJoinType(Join::TYPE_INNER)
479 ->whereIn(
'MESSAGE_TYPE', [Chat::IM_TYPE_CHAT, Chat::IM_TYPE_OPEN])
480 ->where(
'USER.IS_REAL_USER',
'Y')
481 ->whereMatch(
'USER_INDEX.SEARCH_USER_CONTENT', $this->preparedSearchString)
482 ->setGroup([
'CHAT_ID'])
490 foreach ($raw as $row)
492 $dialogId =
'chat' . $row[
'ID'];
493 $messageDate = $row[
'MESSAGE_DATE_CREATE'] ??
null;
494 $secondDate = $row[
'MESSAGE_DATE_CREATE'] ??
null;
495 if (($row[
'IS_MEMBER'] ??
'Y') ===
'N')
499 $item = $this->getBlankItem($dialogId, $messageDate, $secondDate);
500 if (!empty($additionalCustomData))
502 $customData = $item->getCustomData()->getValues();
503 $item->setCustomData(array_merge($customData, $additionalCustomData));
514 ->setOrder([
'IS_MEMBER' =>
'DESC',
'LAST_MESSAGE_ID' =>
'DESC',
'DATE_CREATE' =>
'DESC'])
520 $query = ChatTable::query()
521 ->setSelect([
'ID',
'IS_MEMBER',
'MESSAGE_DATE_CREATE' =>
'MESSAGE.DATE_CREATE',
'DATE_CREATE'])
524 RelationTable::class,
525 Join::on(
'this.ID',
'ref.CHAT_ID')
526 ->where(
'ref.USER_ID', $this->getContext()->getUserId()),
527 [
'join_type' => $joinType]
530 ->registerRuntimeField(
534 Join::on(
'this.LAST_MESSAGE_ID',
'ref.ID'),
535 [
'join_type' => Join::TYPE_LEFT]
538 ->registerRuntimeField(
542 "CASE WHEN %s IS NULL THEN 'N' ELSE 'Y' END",
544 ))->configureValueType(BooleanField::class)
549 if ($joinType === Join::TYPE_LEFT)
551 $query->where($this->getRelationFilter());
554 if ($this->options[self::ONLY_WITH_MANAGE_MESSAGES_RIGHT_OPTION])
559 if ($this->options[self::ONLY_WITH_MANAGE_USERS_ADD_RIGHT_OPTION])
564 if ($this->options[self::ONLY_WITH_OWNER_RIGHT_OPTION])
566 $query->where(
'AUTHOR_ID', $this->getContext()->getUserId());
569 if ($this->options[self::ONLY_WITH_NULL_ENTITY_TYPE_OPTION])
571 $query->where(Query::filter()
573 ->whereNull(
'ENTITY_TYPE')
574 ->where(
'ENTITY_TYPE',
''))
578 if (isset($this->options[self::EXCLUDE_IDS_OPTION]) && is_array($this->options[self::EXCLUDE_IDS_OPTION]))
580 $query->whereNotIn(
'ID', $this->options[self::EXCLUDE_IDS_OPTION]);
591 Chat::IM_TYPE_CHANNEL,
592 Chat::IM_TYPE_OPEN_CHANNEL,
593 Chat::IM_TYPE_COLLAB,
594 Chat::IM_TYPE_COPILOT,
595 Chat::IM_TYPE_AI_ASSISTANT,
603 if (User::getCurrent()->isExtranet())
605 return Query::filter()->whereNotNull(
'RELATION.USER_ID');
608 return Query::filter()
610 ->whereNotNull(
'RELATION.USER_ID')
611 ->whereIn(
'TYPE', [Chat::IM_TYPE_OPEN, Chat::IM_TYPE_OPEN_CHANNEL])
615 private function getUserItemsWithDate():
array
618 if (!$this->needSearch(self::FLAG_USERS))
622 $query = UserTable::query()
623 ->setSelect([
'ID',
'DATE_MESSAGE' =>
'RECENT.DATE_MESSAGE',
'IS_INTRANET_USER',
'DATE_CREATE' =>
'DATE_REGISTER'])
624 ->where(
'ACTIVE',
true)
625 ->registerRuntimeField(
630 Join::on(
'this.ID',
'ref.ITEM_ID')
631 ->where(
'ref.USER_ID', $this->getContext()->getUserId())
632 ->where(
'ref.ITEM_TYPE', Chat::IM_TYPE_PRIVATE),
633 [
'join_type' => Join::TYPE_LEFT]
638 if (isset($this->preparedSearchString))
641 ->whereMatch(
'INDEX.SEARCH_USER_CONTENT', $this->preparedSearchString)
642 ->setOrder([
'RECENT.DATE_MESSAGE' =>
'DESC',
'IS_INTRANET_USER' =>
'DESC',
'DATE_CREATE' =>
'DESC'])
643 ->setLimit(self::LIMIT)
646 elseif (isset($this->userIds) && !empty($this->userIds))
648 $query->whereIn(
'ID', $this->userIds)->setLimit(self::TECHNICAL_LIMIT);
655 $query->where($this->getIntranetFilter());
657 $raw =
$query->fetchAll();
659 foreach ($raw as $row)
661 if ($this->isHiddenBot((
int)$row[
'ID']))
666 $result[(int)$row[
'ID']] = $this->getBlankItem((
int)$row[
'ID'], $row[
'DATE_MESSAGE'], $row[
'DATE_CREATE']);
674 private function getAdditionalUsers(
array $foundUserItems):
array
676 if ($this->needAddFavoriteChat($foundUserItems))
678 $foundUserItems[$this->
getContext()->getUserId()] = $this->getFavoriteChatUserItem();
681 return $foundUserItems;
684 private function getFavoriteChatUserItem(): Item
687 $row = ChatTable::query()
688 ->setSelect([
'DATE_MESSAGE' =>
'MESSAGE.DATE_CREATE',
'DATE_CREATE'])
689 ->registerRuntimeField(
693 Join::on(
'this.LAST_MESSAGE_ID',
'ref.ID'),
694 [
'join_type' => Join::TYPE_LEFT]
697 ->where(
'ENTITY_TYPE', Chat::ENTITY_TYPE_FAVORITE)
701 $dateMessage = $row[
'DATE_MESSAGE'] ??
null;
702 $dateCreate = $row[
'DATE_CREATE'] ??
null;
704 return $this->getBlankItem($this->
getContext()->getUserId(), $dateMessage, $dateCreate);
707 private function needAddFavoriteChat(
array $foundUserItems): bool
710 !isset($foundUserItems[$this->
getContext()->getUserId()])
711 && isset($this->originalSearchString)
716 private static function isPhraseFoundBySearchQuery(
string $phrase,
string $searchQuery): bool
718 $searchWords = explode(
' ', $searchQuery);
719 $phraseWords = explode(
' ', $phrase);
721 foreach ($searchWords as $searchWord)
723 $searchWordLowerCase = mb_strtolower($searchWord);
725 foreach ($phraseWords as $phraseWord)
727 $phraseWordLowerCase = mb_strtolower($phraseWord);
728 if (str_starts_with($phraseWordLowerCase, $searchWordLowerCase))
743 private function isHiddenBot(
int $userId): bool
747 if (
$user instanceof UserBot &&
$user->isBot())
749 if (!$this->needSearch(self::FLAG_BOTS))
754 $botData =
$user->getBotData()->toRestFormat();
755 if ($botData[
'isHidden'])
764 private function getIntranetFilter(): ConditionTree
767 if (!Loader::includeModule(
'intranet'))
769 return $filter->where($this->getRealUserOrBotCondition());
772 $subQuery = Group::getExtranetAccessibleUsersQuery($this->
getContext()->getUserId());
773 if (!User::getCurrent()->isExtranet())
776 $filter->where(
'IS_INTRANET_USER',
true);
777 if ($subQuery !==
null)
779 $filter->whereIn(
'ID', $subQuery);
784 $filter->where($this->getRealUserOrBotCondition());
785 if ($subQuery !==
null)
787 $filter->whereIn(
'ID', $subQuery);
791 $filter->where(
new ExpressionField(
'EMPTY_LIST',
'1'),
'!=', 1);
797 private function getRealUserOrBotCondition(): ConditionTree
799 return Query::filter()
801 ->whereNotIn(
'EXTERNAL_AUTH_ID', UserTable::filterExternalUserTypes([
'bot']))
802 ->whereNull(
'EXTERNAL_AUTH_ID')
808 $this->options[self::SEARCH_FLAGS_OPTION] = self::SEARCH_FLAGS_DEFAULT;
810 if (isset(
$options[self::INCLUDE_ONLY_OPTION]) && is_array(
$options[self::INCLUDE_ONLY_OPTION]))
812 foreach (self::ALLOWED_SEARCH_FLAGS as $searchFlag)
814 $this->options[self::SEARCH_FLAGS_OPTION][$searchFlag] =
false;
817 foreach (
$options[self::INCLUDE_ONLY_OPTION] as $searchFlag)
819 if ($this->isValidSearchFlag($searchFlag))
821 $this->options[self::SEARCH_FLAGS_OPTION][$searchFlag] =
true;
827 foreach (
$options[self::EXCLUDE_OPTION] as $searchFlag)
829 if ($this->isValidSearchFlag($searchFlag))
831 $this->options[self::SEARCH_FLAGS_OPTION][$searchFlag] =
false;
837 private function isValidSearchFlag(
string $searchFlag): bool
839 return in_array($searchFlag, self::ALLOWED_SEARCH_FLAGS,
true);
842 private function needSearch(
string $flag): bool
844 return $this->options[self::SEARCH_FLAGS_OPTION][$flag] ??
true;
847 private function mergeByKey(
array ...$arrays):
array
850 foreach ($arrays as $array)
852 foreach ($array as
$key => $value)
861 private function isChatId(
string $id): bool
863 return substr($id, 0, 4) ===
'chat';
866 private function withChatByUsers(): bool
868 return $this->options[self::WITH_CHAT_BY_USERS_OPTION] ?? self::WITH_CHAT_BY_USERS_DEFAULT;
871 private function prepareSearchString(
string $searchString): string
873 $searchString = trim($searchString);
875 return Helper::matchAgainstWildcard(Content::prepareStringToken($searchString));