38 private const LIMIT = 30;
39 private const ENTITY_ID =
'im-recent-v2';
40 private const ENTITY_TYPE_USER =
'im-user';
41 private const ENTITY_TYPE_CHAT =
'im-chat';
42 private const WITH_CHAT_BY_USERS_OPTION =
'withChatByUsers';
43 private const WITH_CHAT_BY_USERS_DEFAULT =
false;
45 private string $preparedSearchString;
46 private string $originalSearchString;
47 private array $userIds;
48 private array $chatIds;
49 private bool $sortEnable =
true;
53 $this->options[self::WITH_CHAT_BY_USERS_OPTION] = self::WITH_CHAT_BY_USERS_DEFAULT;
54 if (isset(
$options[self::WITH_CHAT_BY_USERS_OPTION]) && is_bool(
$options[self::WITH_CHAT_BY_USERS_OPTION]))
56 $this->options[self::WITH_CHAT_BY_USERS_OPTION] =
$options[self::WITH_CHAT_BY_USERS_OPTION];
58 parent::__construct();
65 return $USER->IsAuthorized();
70 $this->originalSearchString = $searchQuery->
getQuery();
71 $this->preparedSearchString = $this->prepareSearchString($searchQuery->
getQuery());
72 if (!Content::canUseFulltextSearch($this->preparedSearchString))
77 $items = $this->getSortedLimitedBlankItems();
78 $this->fillItems($items);
79 $dialog->addItems($items);
84 if (!Loader::includeModule(
'intranet'))
89 $requiredCountToFill = self::LIMIT - $dialog->getRecentItems()->count();
91 if ($requiredCountToFill <= 0)
96 $result = \CIntranetUtils::getDepartmentColleagues(
null,
true,
false,
'Y', [
'ID']);
99 while (($row = $result->Fetch()))
101 $colleaguesIds[] = (int)$row[
'ID'];
104 rsort($colleaguesIds);
105 $colleaguesIds = array_slice($colleaguesIds, 0, $requiredCountToFill);
107 foreach ($colleaguesIds as $userId)
109 $dialog->getRecentItems()->add(
new RecentItem([
'id' => $userId,
'entityId' => self::ENTITY_ID]));
115 $this->sortEnable =
false;
116 $ids = array_slice($ids, 0, self::LIMIT);
117 $this->setUserAndChatIds($ids);
118 $items = $this->getItemsWithDates();
119 $this->fillItems($items);
139 private function setUserAndChatIds(array $ids): void
141 foreach ($ids as $id)
143 if ($this->isChatId($id))
145 $chatId = substr($id, 4);
146 $this->chatIds[$chatId] = $chatId;
150 $this->userIds[$id] = $id;
155 private function getBlankItems(array $ids, array $datesUpdate = [], array $datesCreate = []): array
159 foreach ($ids as $id)
161 $result[] = $this->getBlankItem($id, $datesUpdate[$id] ??
null, $datesCreate[$id] ??
null);
167 private function getBlankItem(
string $dialogId, ?DateTime $dateMessage =
null, ?DateTime $dateCreate =
null): Item
170 $entityType = self::ENTITY_TYPE_USER;
171 if ($this->isChatId($dialogId))
173 $id = substr($dialogId, 4);
174 $entityType = self::ENTITY_TYPE_CHAT;
176 $customData = [
'id' => $id];
178 $customData[
'dateMessage'] = $dateMessage;
179 $customData[
'dateCreateTs'] = $dateCreate instanceof DateTime ? $dateCreate->getTimestamp() : 0;
180 if (isset($dateMessage))
182 if ($this->sortEnable)
184 $sort = $dateMessage->getTimestamp();
190 'entityId' => self::ENTITY_ID,
191 'entityType' => $entityType,
193 'customData' => $customData,
201 private function fillItems(array $items): void
205 foreach ($items as $item)
207 $id = $item->getCustomData()->get(
'id');
208 if ($item->getEntityType() === self::ENTITY_TYPE_USER)
217 $users =
new UserCollection($userIds);
218 $users->fillOnlineData();
220 foreach ($items as $item)
222 $customData = $item->getCustomData()->getValues();
223 if ($item->getEntityType() === self::ENTITY_TYPE_USER)
225 $user = $users->getById($customData[
'id']);
226 $customData = array_merge($customData, $user->toRestFormat());
227 $item->setTitle($user->getName())->setAvatar($user->getAvatar())->setCustomData($customData);
229 if ($item->getEntityType() === self::ENTITY_TYPE_CHAT)
231 $chat = $chats[$customData[
'id']] ??
null;
236 $customData = array_merge($customData, $chat->toRestFormat([
'CHAT_SHORT_FORMAT' =>
true]));
237 $item->setTitle($chat->getTitle())->setAvatar($chat->getAvatar())->setCustomData($customData);
242 private function getItemsWithDates(): array
244 $userItemsWithDate = $this->getUserItemsWithDate();
245 $chatItemsWithDate = $this->getChatItemsWithDate();
247 return $this->mergeByKey($userItemsWithDate, $chatItemsWithDate);
250 private function getSortedLimitedBlankItems(): array
252 $items = $this->getItemsWithDates();
253 usort($items,
function(Item $a, Item $b) {
254 if ($b->getSort() === $a->getSort())
256 if (!$this->isChatId($b->getId()) && !$this->isChatId($a->getId()))
258 $bUser = User::getInstance($b->getId());
259 $aUser = User::getInstance($a->getId());
260 if ($aUser->isExtranet() === $bUser->isExtranet())
262 return $bUser->getId() <=> $aUser->getId();
265 return $aUser->isExtranet() <=> $bUser->isExtranet();
267 return (
int)$b->getCustomData()->get(
'dateCreateTs') <=> (
int)$a->getCustomData()->get(
'dateCreateTs');
269 return $b->getSort() <=> $a->getSort();
272 return array_slice($items, 0, self::LIMIT);
275 private function getChatItemsWithDate(): array
277 if (isset($this->preparedSearchString))
279 return $this->mergeByKey(
280 $this->getChatItemsWithDateByUsers(),
281 $this->getChatItemsWithDateByTitle()
285 if (isset($this->chatIds) && !empty($this->chatIds))
287 return $this->getChatItemsWithDateByIds();
293 private function getChatItemsWithDateByIds(): array
295 if (!isset($this->chatIds) || empty($this->chatIds))
300 $result = $this->getCommonChatQuery()->whereIn(
'ID', $this->chatIds)->fetchAll();
302 return $this->getChatItemsByRawResult($result);
305 private function getChatItemsWithDateByTitle(): array
307 if (!isset($this->preparedSearchString))
313 ->getCommonChatQuery()
314 ->whereMatch(
'INDEX.SEARCH_TITLE', $this->preparedSearchString)
315 ->setOrder([
'LAST_MESSAGE_ID' =>
'DESC',
'DATE_CREATE' =>
'DESC'])
319 return $this->getChatItemsByRawResult($result, [
'byUser' =>
false]);
322 private function getChatItemsWithDateByUsers(): array
324 if (!isset($this->preparedSearchString) || !$this->withChatByUsers())
330 ->getCommonChatQuery(Join::TYPE_INNER)
331 ->setOrder([
'LAST_MESSAGE_ID' =>
'DESC',
'DATE_CREATE' =>
'DESC'])
332 ->registerRuntimeField(
336 Entity::getInstanceByQuery($this->getChatsByUserNameQuery()),
337 Join::on(
'this.ID',
'ref.CHAT_ID')
338 ))->configureJoinType(Join::TYPE_INNER)
343 return $this->getChatItemsByRawResult($result, [
'byUser' =>
true]);
346 private function getChatsByUserNameQuery(): Query
348 return RelationTable::query()
349 ->setSelect([
'CHAT_ID'])
350 ->registerRuntimeField(
354 \
Bitrix\Main\UserTable::class,
355 Join::on(
'this.USER_ID',
'ref.ID'),
356 ))->configureJoinType(Join::TYPE_INNER)
358 ->registerRuntimeField(
362 UserIndexTable::class,
363 Join::on(
'this.USER_ID',
'ref.USER_ID'),
364 ))->configureJoinType(Join::TYPE_INNER)
367 ->where(
'USER.IS_REAL_USER',
'Y')
368 ->whereMatch(
'USER_INDEX.SEARCH_USER_CONTENT', $this->preparedSearchString)
369 ->setGroup([
'CHAT_ID'])
373 private function getChatItemsByRawResult(array $raw, array $additionalCustomData = []): array
377 foreach ($raw as $row)
379 $dialogId =
'chat' . $row[
'ID'];
380 $item = $this->getBlankItem($dialogId, $row[
'MESSAGE_DATE_CREATE'], $row[
'DATE_CREATE']);
381 if (!empty($additionalCustomData))
383 $customData = $item->getCustomData()->getValues();
384 $item->setCustomData(array_merge($customData, $additionalCustomData));
386 $result[$dialogId] = $item;
392 private function getCommonChatQuery(
string $joinType = Join::TYPE_LEFT): Query
394 $query = ChatTable::query()
395 ->setSelect([
'ID',
'MESSAGE_DATE_CREATE' =>
'MESSAGE.DATE_CREATE',
'DATE_CREATE'])
396 ->registerRuntimeField(
new Reference(
398 RelationTable::class,
399 Join::on(
'this.ID',
'ref.CHAT_ID')
400 ->where(
'ref.USER_ID', $this->
getContext()->getUserId())
402 [
'join_type' => $joinType]
405 ->registerRuntimeField(
409 Join::on(
'this.LAST_MESSAGE_ID',
'ref.ID'),
410 [
'join_type' => Join::TYPE_LEFT]
413 ->setLimit(self::LIMIT)
416 if ($joinType === Join::TYPE_LEFT)
418 $query->where($this->getRelationFilter());
424 private function getRelationFilter(): ConditionTree
426 if (User::getCurrent()->isExtranet())
428 return Query::filter()->whereNotNull(
'RELATION.USER_ID');
431 return Query::filter()
433 ->whereNotNull(
'RELATION.USER_ID')
438 private function getUserItemsWithDate(): array
441 $query = UserTable::query()
442 ->setSelect([
'ID',
'DATE_MESSAGE' =>
'RECENT.DATE_MESSAGE',
'IS_INTRANET_USER',
'DATE_CREATE' =>
'DATE_REGISTER'])
443 ->where(
'ACTIVE',
true)
444 ->registerRuntimeField(
449 Join::on(
'this.ID',
'ref.ITEM_ID')
450 ->where(
'ref.USER_ID', $this->
getContext()->getUserId())
452 [
'join_type' => Join::TYPE_LEFT]
455 ->setLimit(self::LIMIT)
458 if (isset($this->preparedSearchString))
461 ->whereMatch(
'INDEX.SEARCH_USER_CONTENT', $this->preparedSearchString)
462 ->setOrder([
'RECENT.DATE_MESSAGE' =>
'DESC',
'IS_INTRANET_USER' =>
'DESC',
'DATE_CREATE' =>
'DESC'])
465 elseif (isset($this->userIds) && !empty($this->userIds))
467 $query->whereIn(
'ID', $this->userIds);
474 $query->where($this->getIntranetFilter());
476 $raw = $query->fetchAll();
478 foreach ($raw as $row)
480 if ($this->isHiddenBot((
int)$row[
'ID']))
485 $result[(int)$row[
'ID']] = $this->getBlankItem((
int)$row[
'ID'], $row[
'DATE_MESSAGE'], $row[
'DATE_CREATE']);
488 $result = $this->getAdditionalUsers($result);
493 private function getAdditionalUsers(array $foundUserItems): array
495 if ($this->needAddFavoriteChat($foundUserItems))
497 $foundUserItems[$this->
getContext()->getUserId()] = $this->getFavoriteChatUserItem();
500 return $foundUserItems;
503 private function getFavoriteChatUserItem(): Item
506 $row = ChatTable::query()
507 ->setSelect([
'DATE_MESSAGE' =>
'MESSAGE.DATE_CREATE',
'DATE_CREATE'])
508 ->registerRuntimeField(
512 Join::on(
'this.LAST_MESSAGE_ID',
'ref.ID'),
513 [
'join_type' => Join::TYPE_LEFT]
517 ->where(
'ENTITY_ID', $userId)
520 $dateMessage = $row[
'DATE_MESSAGE'] ??
null;
521 $dateCreate = $row[
'DATE_CREATE'] ??
null;
523 return $this->getBlankItem($this->
getContext()->getUserId(), $dateMessage, $dateCreate);
526 private function needAddFavoriteChat(array $foundUserItems): bool
529 !isset($foundUserItems[$this->
getContext()->getUserId()])
530 && isset($this->originalSearchString)
535 private static function isPhraseFoundBySearchQuery(
string $phrase,
string $searchQuery): bool
537 $searchWords = explode(
' ', $searchQuery);
538 $phraseWords = explode(
' ', $phrase);
540 foreach ($searchWords as $searchWord)
542 $searchWordLowerCase = mb_strtolower($searchWord);
544 foreach ($phraseWords as $phraseWord)
546 $phraseWordLowerCase = mb_strtolower($phraseWord);
547 if (str_starts_with($phraseWordLowerCase, $searchWordLowerCase))
562 private function isHiddenBot(
int $userId): bool
566 if ($user instanceof UserBot && $user->isBot())
568 $botData = $user->getBotData()->toRestFormat();
569 if ($botData[
'isHidden'])
578 private function getIntranetFilter(): ConditionTree
580 $filter = Query::filter();
581 if (!Loader::includeModule(
'intranet'))
583 return $filter->where($this->getRealUserOrBotCondition());
586 $subQuery = $this->getExtranetUsersQuery();
587 if (!User::getCurrent()->isExtranet())
589 $filter->logic(
'or');
590 $filter->where(
'IS_INTRANET_USER',
true);
591 if ($subQuery !==
null)
593 $filter->whereIn(
'ID', $subQuery);
598 $filter->where($this->getRealUserOrBotCondition());
599 if ($subQuery !==
null)
601 $filter->whereIn(
'ID', $subQuery);
605 $filter->where(
new ExpressionField(
'EMPTY_LIST',
'1'),
'!=', 1);
611 private function getRealUserOrBotCondition(): ConditionTree
613 return Query::filter()
615 ->whereNotIn(
'EXTERNAL_AUTH_ID', UserTable::filterExternalUserTypes([
'bot']))
616 ->whereNull(
'EXTERNAL_AUTH_ID')
620 private function getExtranetUsersQuery(): ?Query
622 if (!Loader::includeModule(
'socialnetwork'))
627 $extranetSiteId = Option::get(
'extranet',
'extranet_site');
632 || \CSocNetUser::isCurrentUserModuleAdmin()
640 \Bitrix\Socialnetwork\UserToGroupTable::ROLE_USER,
641 \Bitrix\Socialnetwork\UserToGroupTable::ROLE_OWNER,
642 \Bitrix\Socialnetwork\UserToGroupTable::ROLE_MODERATOR,
643 \Bitrix\Socialnetwork\UserToGroupTable::ROLE_REQUEST,
646 $query = \Bitrix\Socialnetwork\UserToGroupTable::query();
647 $query->addSelect(
new ExpressionField(
'DISTINCT_USER_ID',
'DISTINCT %s',
'USER.ID'));
648 $query->whereIn(
'ROLE', $extranetRoles);
649 $query->registerRuntimeField(
652 \
Bitrix\Socialnetwork\WorkgroupSiteTable::class,
653 Join::on(
'ref.GROUP_ID',
'this.GROUP_ID')->where(
'ref.SITE_ID', $extranetSiteId),
654 [
'join_type' =>
'INNER']
658 $query->registerRuntimeField(
661 \
Bitrix\Socialnetwork\UserToGroupTable::class,
662 Join::on(
'ref.GROUP_ID',
'this.GROUP_ID')
663 ->where(
'ref.USER_ID', $this->
getContext()->getUserId())
664 ->whereIn(
'ref.ROLE', $extranetRoles),
665 [
'join_type' =>
'INNER']
672 private function mergeByKey(array ...$arrays): array
675 foreach ($arrays as $array)
677 foreach ($array as $key => $value)
679 $result[$key] = $value;
686 private function isChatId(
string $id): bool
688 return substr($id, 0, 4) ===
'chat';
691 private function withChatByUsers(): bool
693 return $this->options[self::WITH_CHAT_BY_USERS_OPTION] ?? self::WITH_CHAT_BY_USERS_DEFAULT;
696 private function prepareSearchString(
string $searchString): string
698 $searchString = trim($searchString);
700 return Helper::matchAgainstWildcard(Content::prepareStringToken($searchString));