diff --git a/src/components/auth/CountryCodeInput.tsx b/src/components/auth/CountryCodeInput.tsx index 4a735b417..8374d5665 100644 --- a/src/components/auth/CountryCodeInput.tsx +++ b/src/components/auth/CountryCodeInput.tsx @@ -6,7 +6,7 @@ import { withGlobal } from '../../lib/teact/teactn'; import { ApiCountryCode } from '../../api/types'; import { ANIMATION_END_DELAY } from '../../config'; -import searchWords from '../../util/searchWords'; +import { prepareSearchWordsForNeedle } from '../../util/searchWords'; import buildClassName from '../../util/buildClassName'; import renderText from '../common/helpers/renderText'; import useLang from '../../hooks/useLang'; @@ -159,11 +159,15 @@ const CountryCodeInput: FC = ({ }; function getFilteredList(countryList: ApiCountryCode[], filter = ''): ApiCountryCode[] { - const filtered = filter.length - ? countryList.filter((country) => ( - searchWords(country.defaultName, filter) || (country.name && searchWords(country.name, filter)) - )) : countryList; - return filtered; + if (!filter.length) { + return countryList; + } + + const searchWords = prepareSearchWordsForNeedle(filter); + + return countryList.filter((country) => ( + searchWords(country.defaultName) || (country.name && searchWords(country.name)) + )); } export default memo(withGlobal( diff --git a/src/components/left/main/ContactList.tsx b/src/components/left/main/ContactList.tsx index 44b9d24b0..448df0085 100644 --- a/src/components/left/main/ContactList.tsx +++ b/src/components/left/main/ContactList.tsx @@ -8,9 +8,8 @@ import { ApiUser, ApiUserStatus } from '../../../api/types'; import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment'; import { throttle } from '../../../util/schedulers'; -import searchWords from '../../../util/searchWords'; import { pick } from '../../../util/iteratees'; -import { getUserFullName, sortUserIds } from '../../../modules/helpers'; +import { filterUsersByName, sortUserIds } from '../../../modules/helpers'; import useInfiniteScroll from '../../../hooks/useInfiniteScroll'; import useHistoryBack from '../../../hooks/useHistoryBack'; @@ -66,16 +65,9 @@ const ContactList: FC = ({ return undefined; } - const resultIds = filter ? contactIds.filter((id) => { - const user = usersById[id]; - if (!user) { - return false; - } - const fullName = getUserFullName(user); - return fullName && searchWords(fullName, filter); - }) : contactIds; + const filteredIds = filterUsersByName(contactIds, usersById, filter); - return sortUserIds(resultIds, usersById, userStatusesById, undefined, serverTimeOffset); + return sortUserIds(filteredIds, usersById, userStatusesById, undefined, serverTimeOffset); }, [contactIds, filter, usersById, userStatusesById, serverTimeOffset]); const [viewportIds, getMore] = useInfiniteScroll(undefined, listIds, Boolean(filter)); diff --git a/src/components/left/newChat/NewChatStep1.tsx b/src/components/left/newChat/NewChatStep1.tsx index 51275addb..094ea53b2 100644 --- a/src/components/left/newChat/NewChatStep1.tsx +++ b/src/components/left/newChat/NewChatStep1.tsx @@ -1,15 +1,14 @@ import React, { FC, useCallback, useEffect, useMemo, memo, } from '../../../lib/teact/teact'; -import { withGlobal } from '../../../lib/teact/teactn'; +import { getGlobal, withGlobal } from '../../../lib/teact/teactn'; import { GlobalActions } from '../../../global/types'; -import { ApiChat, ApiUser } from '../../../api/types'; +import { ApiChat } from '../../../api/types'; import { pick, unique } from '../../../util/iteratees'; import { throttle } from '../../../util/schedulers'; -import searchWords from '../../../util/searchWords'; -import { getUserFullName, isUserBot, sortChatIds } from '../../../modules/helpers'; +import { filterUsersByName, isUserBot, sortChatIds } from '../../../modules/helpers'; import useLang from '../../../hooks/useLang'; import useHistoryBack from '../../../hooks/useHistoryBack'; @@ -27,8 +26,6 @@ export type OwnProps = { }; type StateProps = { - currentUserId?: string; - usersById: Record; chatsById: Record; localContactIds?: string[]; searchQuery?: string; @@ -48,8 +45,6 @@ const NewChatStep1: FC = ({ onSelectedMemberIdsChange, onNextStep, onReset, - currentUserId, - usersById, chatsById, localContactIds, searchQuery, @@ -76,22 +71,9 @@ const NewChatStep1: FC = ({ }, [setGlobalSearchQuery]); const displayedIds = useMemo(() => { - const contactIds = localContactIds - ? sortChatIds(localContactIds.filter((id) => id !== currentUserId), chatsById) - : []; - - if (!searchQuery) { - return contactIds; - } - - const foundContactIds = contactIds.filter((id) => { - const user = usersById[id]; - if (!user) { - return false; - } - const fullName = getUserFullName(user); - return fullName && searchWords(fullName, searchQuery); - }); + // No need for expensive global updates on users, so we avoid them + const usersById = getGlobal().users.byId; + const foundContactIds = localContactIds ? filterUsersByName(localContactIds, usersById, searchQuery) : []; return sortChatIds( unique([ @@ -100,17 +82,17 @@ const NewChatStep1: FC = ({ ...(globalUserIds || []), ]).filter((contactId) => { const user = usersById[contactId]; + if (!user) { + return true; + } - return !user || !isUserBot(user) || user.canBeInvitedToGroup; + return user.canBeInvitedToGroup && !user.isSelf && !isUserBot(user); }), chatsById, false, selectedMemberIds, ); - }, [ - localContactIds, chatsById, searchQuery, localUserIds, globalUserIds, selectedMemberIds, - currentUserId, usersById, - ]); + }, [localContactIds, chatsById, searchQuery, localUserIds, globalUserIds, selectedMemberIds]); const handleNextStep = useCallback(() => { if (selectedMemberIds.length || isChannel) { @@ -160,9 +142,7 @@ const NewChatStep1: FC = ({ export default memo(withGlobal( (global): StateProps => { const { userIds: localContactIds } = global.contactList || {}; - const { byId: usersById } = global.users; const { byId: chatsById } = global.chats; - const { currentUserId } = global; const { query: searchQuery, @@ -174,8 +154,6 @@ export default memo(withGlobal( const { userIds: localUserIds } = localResults || {}; return { - currentUserId, - usersById, chatsById, localContactIds, searchQuery, diff --git a/src/components/left/search/ChatResults.tsx b/src/components/left/search/ChatResults.tsx index d2820e7c2..b56270aba 100644 --- a/src/components/left/search/ChatResults.tsx +++ b/src/components/left/search/ChatResults.tsx @@ -1,16 +1,15 @@ import React, { FC, memo, useCallback, useMemo, useState, } from '../../../lib/teact/teact'; -import { withGlobal } from '../../../lib/teact/teactn'; +import { getGlobal, withGlobal } from '../../../lib/teact/teactn'; -import { ApiUser, ApiChat, ApiMessage } from '../../../api/types'; +import { ApiChat, ApiMessage } from '../../../api/types'; import { GlobalActions } from '../../../global/types'; import { LoadMoreDirection } from '../../../types'; import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment'; -import searchWords from '../../../util/searchWords'; import { unique, pick } from '../../../util/iteratees'; -import { getUserFullName, getMessageSummaryText, sortChatIds } from '../../../modules/helpers'; +import { getMessageSummaryText, sortChatIds, filterUsersByName } from '../../../modules/helpers'; import { MEMO_EMPTY_ARRAY } from '../../../util/memo'; import { throttle } from '../../../util/schedulers'; import useLang from '../../../hooks/useLang'; @@ -42,7 +41,6 @@ type StateProps = { foundIds?: string[]; globalMessagesByChatId?: Record }>; chatsById: Record; - usersById: Record; fetchingStatus?: { chats?: boolean; messages?: boolean }; lastSyncTime?: number; }; @@ -52,14 +50,14 @@ type DispatchProps = Pick; const MIN_QUERY_LENGTH_FOR_GLOBAL_SEARCH = 4; -const LESS_LIST_ITEMS_AMOUNT = 3; +const LESS_LIST_ITEMS_AMOUNT = 5; const runThrottled = throttle((cb) => cb(), 500, true); const ChatResults: FC = ({ searchQuery, searchDate, dateSearchQuery, currentUserId, localContactIds, localChatIds, localUserIds, globalChatIds, globalUserIds, - foundIds, globalMessagesByChatId, chatsById, usersById, fetchingStatus, lastSyncTime, + foundIds, globalMessagesByChatId, chatsById, fetchingStatus, lastSyncTime, onReset, onSearchDateSelect, openChat, addRecentlyFoundChatId, searchMessagesGlobal, setGlobalSearchChatId, }) => { const lang = useLang(); @@ -102,37 +100,33 @@ const ChatResults: FC = ({ return MEMO_EMPTY_ARRAY; } - const foundContactIds = localContactIds - ? localContactIds.filter((id) => { - const user = usersById[id]; - if (!user) { - return false; - } - - const fullName = getUserFullName(user); - return (fullName && searchWords(fullName, searchQuery)) || searchWords(user.username, searchQuery); - }) - : []; + const contactIdsWithMe = [ + ...(currentUserId ? [currentUserId] : []), + ...(localContactIds || []), + ]; + // No need for expensive global updates on users, so we avoid them + const usersById = getGlobal().users.byId; + const foundContactIds = filterUsersByName(contactIdsWithMe, usersById, searchQuery); return [ - ...(currentUserId && searchWords(lang('SavedMessages'), searchQuery) ? [currentUserId] : []), ...sortChatIds(unique([ - ...foundContactIds, + ...(foundContactIds || []), ...(localChatIds || []), ...(localUserIds || []), - ]), chatsById), + ]), chatsById, undefined, currentUserId ? [currentUserId] : undefined), ]; - }, [ - searchQuery, localContactIds, currentUserId, lang, localChatIds, localUserIds, chatsById, usersById, - ]); + }, [searchQuery, localContactIds, currentUserId, localChatIds, localUserIds, chatsById]); const globalResults = useMemo(() => { if (!searchQuery || searchQuery.length < MIN_QUERY_LENGTH_FOR_GLOBAL_SEARCH || !globalChatIds || !globalUserIds) { return MEMO_EMPTY_ARRAY; } - return sortChatIds(unique([...globalChatIds, ...globalUserIds]), - chatsById, true); + return sortChatIds( + unique([...globalChatIds, ...globalUserIds]), + chatsById, + true, + ); }, [chatsById, globalChatIds, globalUserIds, searchQuery]); const foundMessages = useMemo(() => { @@ -278,14 +272,12 @@ const ChatResults: FC = ({ export default memo(withGlobal( (global): StateProps => { const { byId: chatsById } = global.chats; - const { byId: usersById } = global.users; const { userIds: localContactIds } = global.contactList || {}; if (!localContactIds) { return { chatsById, - usersById, }; } @@ -310,7 +302,6 @@ export default memo(withGlobal( foundIds, globalMessagesByChatId, chatsById, - usersById, fetchingStatus, lastSyncTime, }; diff --git a/src/components/left/settings/BlockUserModal.tsx b/src/components/left/settings/BlockUserModal.tsx index 5373f983c..77312535c 100644 --- a/src/components/left/settings/BlockUserModal.tsx +++ b/src/components/left/settings/BlockUserModal.tsx @@ -6,8 +6,7 @@ import { withGlobal } from '../../../lib/teact/teactn'; import { GlobalActions } from '../../../global/types'; import { ApiUser } from '../../../api/types'; -import { getUserFullName } from '../../../modules/helpers'; -import searchWords from '../../../util/searchWords'; +import { filterUsersByName, getUserFullName } from '../../../modules/helpers'; import { pick, unique } from '../../../util/iteratees'; import useLang from '../../../hooks/useLang'; @@ -49,23 +48,15 @@ const BlockUserModal: FC = ({ setUserSearchQuery({ query: filter }); }, [filter, setUserSearchQuery]); - const filteredContactsId = useMemo(() => { - const availableContactsId = (contactIds || []).concat(localContactIds || []).filter((contactId) => { - return !blockedIds.includes(contactId) && contactId !== currentUserId; - }); + const filteredContactIds = useMemo(() => { + const availableContactIds = unique([ + ...(contactIds || []), + ...(localContactIds || []), + ].filter((contactId) => { + return contactId !== currentUserId && !blockedIds.includes(contactId); + })); - return unique(availableContactsId).reduce((acc, contactId) => { - if ( - !filter - || !usersById[contactId] - || searchWords(getUserFullName(usersById[contactId]) || '', filter) - || usersById[contactId]?.username.toLowerCase().includes(filter) - ) { - acc.push(contactId); - } - - return acc; - }, []) + return filterUsersByName(availableContactIds, usersById, filter) .sort((firstId, secondId) => { const firstName = getUserFullName(usersById[firstId]) || ''; const secondName = getUserFullName(usersById[secondId]) || ''; @@ -86,7 +77,7 @@ const BlockUserModal: FC = ({ return ( ; - pinnedIds?: string[]; activeListIds?: string[]; archivedListIds?: string[]; - orderedPinnedIds?: string[]; + pinnedIds?: string[]; + contactIds?: string[]; currentUserId?: string; }; @@ -31,9 +35,10 @@ type DispatchProps = Pick = ({ chatsById, - pinnedIds, activeListIds, archivedListIds, + pinnedIds, + contactIds, currentUserId, isOpen, setForwardChatId, @@ -45,47 +50,45 @@ const ForwardPicker: FC = ({ // eslint-disable-next-line no-null/no-null const filterRef = useRef(null); - const chatIds = useMemo(() => { + const chatAndContactIds = useMemo(() => { if (!isOpen) { return undefined; } - const listIds = [...(activeListIds || []), ...(archivedListIds || [])]; - let priorityIds = pinnedIds || []; if (currentUserId) { priorityIds = unique([currentUserId, ...priorityIds]); } - return sortChatIds(listIds.filter((id) => { + const chatIds = [ + ...(activeListIds || []), + ...(archivedListIds || []), + ].filter((id) => { const chat = chatsById[id]; - if (!chat) { - return true; - } - if (!getCanPostInChat(chat, MAIN_THREAD_ID)) { - return false; - } + return chat && getCanPostInChat(chat, MAIN_THREAD_ID); + }); - if (!filter) { - return true; - } + // No need for expensive global updates on users, so we avoid them + const usersById = getGlobal().users.byId; - return searchWords(getChatTitle(lang, chatsById[id], undefined, id === currentUserId), filter); - }), chatsById, undefined, priorityIds); - }, [activeListIds, archivedListIds, chatsById, currentUserId, filter, isOpen, lang, pinnedIds]); + return sortChatIds(unique([ + ...filterChatsByName(lang, chatIds, chatsById, filter, currentUserId), + ...(contactIds ? filterUsersByName(contactIds, usersById, filter) : []), + ]), chatsById, undefined, priorityIds); + }, [activeListIds, archivedListIds, chatsById, contactIds, currentUserId, filter, isOpen, lang, pinnedIds]); const handleSelectUser = useCallback((userId: string) => { setForwardChatId({ id: userId }); }, [setForwardChatId]); - const renderingChatIds = useCurrentOrPrev(chatIds)!; + const renderingChatAndContactIds = useCurrentOrPrev(chatAndContactIds)!; return ( ( return { chatsById, - pinnedIds: orderedPinnedIds.active, activeListIds: listIds.active, archivedListIds: listIds.archived, + pinnedIds: orderedPinnedIds.active, + contactIds: global.contactList?.userIds, currentUserId, }; }, diff --git a/src/components/middle/composer/AttachmentModal.tsx b/src/components/middle/composer/AttachmentModal.tsx index 644ccc470..3029bd907 100644 --- a/src/components/middle/composer/AttachmentModal.tsx +++ b/src/components/middle/composer/AttachmentModal.tsx @@ -85,7 +85,6 @@ const AttachmentModal: FC = ({ groupChatMembers, undefined, currentUserId, - usersById, ); const { isEmojiTooltipOpen, closeEmojiTooltip, filteredEmojis, insertEmoji, diff --git a/src/components/middle/composer/Composer.tsx b/src/components/middle/composer/Composer.tsx index 1b5fe6821..fa99fe6cd 100644 --- a/src/components/middle/composer/Composer.tsx +++ b/src/components/middle/composer/Composer.tsx @@ -299,7 +299,6 @@ const Composer: FC = ({ groupChatMembers, topInlineBotIds, currentUserId, - usersById, ); const { diff --git a/src/components/middle/composer/helpers/searchUserName.ts b/src/components/middle/composer/helpers/searchUserName.ts deleted file mode 100644 index c6e68d804..000000000 --- a/src/components/middle/composer/helpers/searchUserName.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ApiUser } from '../../../../api/types'; -import { getUserFullName } from '../../../../modules/helpers'; -import searchWords from '../../../../util/searchWords'; - -// TODO: Support cyrillic translit search -export default function searchUserName(filter: string, user: ApiUser) { - const usernameLowered = user.username.toLowerCase(); - const fullName = getUserFullName(user); - const fullNameLowered = fullName && fullName.toLowerCase(); - const filterLowered = filter.toLowerCase(); - - return usernameLowered.startsWith(filterLowered) || ( - fullNameLowered && searchWords(fullNameLowered, filterLowered) - ); -} diff --git a/src/components/middle/composer/hooks/useMentionTooltip.ts b/src/components/middle/composer/hooks/useMentionTooltip.ts index 218f17940..85393fe49 100644 --- a/src/components/middle/composer/hooks/useMentionTooltip.ts +++ b/src/components/middle/composer/hooks/useMentionTooltip.ts @@ -1,15 +1,15 @@ import { - useCallback, useEffect, useState, useMemo, + useCallback, useEffect, useState, } from '../../../../lib/teact/teact'; +import { getGlobal } from '../../../../lib/teact/teactn'; import { ApiMessageEntityTypes, ApiChatMember, ApiUser } from '../../../../api/types'; import { EDITABLE_INPUT_ID } from '../../../../config'; -import { getUserFirstOrLastName } from '../../../../modules/helpers'; -import searchUserName from '../helpers/searchUserName'; +import { filterUsersByName, getUserFirstOrLastName } from '../../../../modules/helpers'; import { prepareForRegExp } from '../helpers/prepareForRegExp'; import focusEditableElement from '../../../../util/focusEditableElement'; import useFlag from '../../../../hooks/useFlag'; -import { unique } from '../../../../util/iteratees'; +import { pickTruthy, unique } from '../../../../util/iteratees'; import { throttle } from '../../../../util/schedulers'; const runThrottled = throttle((cb) => cb(), 500, true); @@ -30,39 +30,37 @@ export default function useMentionTooltip( groupChatMembers?: ApiChatMember[], topInlineBotIds?: string[], currentUserId?: string, - usersById?: Record, ) { const [isOpen, markIsOpen, unmarkIsOpen] = useFlag(); const [usersToMention, setUsersToMention] = useState(); - const topInlineBots = useMemo(() => { - return (topInlineBotIds || []).map((id) => usersById?.[id]).filter(Boolean as any); - }, [topInlineBotIds, usersById]); + const updateFilteredUsers = useCallback((filter, withInlineBots: boolean) => { + // No need for expensive global updates on users, so we avoid them + const usersById = getGlobal().users.byId; - const getFilteredUsers = useCallback((filter, withInlineBots: boolean) => { if (!(groupChatMembers || topInlineBotIds) || !usersById) { setUsersToMention(undefined); return; } + runThrottled(() => { - const inlineBots = (withInlineBots ? topInlineBots : []).filter((inlineBot) => { - return !filter || searchUserName(filter, inlineBot); - }); + const memberIds = groupChatMembers?.reduce((acc: string[], member) => { + if (member.userId !== currentUserId) { + acc.push(member.userId); + } - const chatMembers = (groupChatMembers || []) - .map(({ userId }) => usersById[userId]) - .filter((user) => { - if (!user || user.id === currentUserId) { - return false; - } + return acc; + }, []); - return !filter || searchUserName(filter, user); - }); + const filteredIds = filterUsersByName(unique([ + ...((withInlineBots && topInlineBotIds) || []), + ...(memberIds || []), + ]), usersById, filter); - setUsersToMention(unique(inlineBots.concat(chatMembers))); + setUsersToMention(Object.values(pickTruthy(usersById, filteredIds))); }); - }, [currentUserId, groupChatMembers, topInlineBotIds, topInlineBots, usersById]); + }, [currentUserId, groupChatMembers, topInlineBotIds]); useEffect(() => { if (!canSuggestMembers || !html.length) { @@ -74,11 +72,11 @@ export default function useMentionTooltip( if (usernameFilter) { const filter = usernameFilter ? usernameFilter.substr(1) : ''; - getFilteredUsers(filter, canSuggestInlineBots(html)); + updateFilteredUsers(filter, canSuggestInlineBots(html)); } else { unmarkIsOpen(); } - }, [canSuggestMembers, html, getFilteredUsers, markIsOpen, unmarkIsOpen]); + }, [canSuggestMembers, html, updateFilteredUsers, markIsOpen, unmarkIsOpen]); useEffect(() => { if (usersToMention?.length) { diff --git a/src/components/right/AddChatMembers.tsx b/src/components/right/AddChatMembers.tsx index e381341c5..aec1969a5 100644 --- a/src/components/right/AddChatMembers.tsx +++ b/src/components/right/AddChatMembers.tsx @@ -1,19 +1,18 @@ import React, { FC, useCallback, useMemo, memo, useState, useEffect, } from '../../lib/teact/teact'; -import { withGlobal } from '../../lib/teact/teactn'; +import { getGlobal, withGlobal } from '../../lib/teact/teactn'; import { GlobalActions } from '../../global/types'; import { - ApiChat, ApiChatMember, ApiUpdateConnectionStateType, ApiUser, + ApiChat, ApiChatMember, ApiUpdateConnectionStateType, } from '../../api/types'; import { NewChatMembersProgress } from '../../types'; import { pick, unique } from '../../util/iteratees'; import { selectChat } from '../../modules/selectors'; -import searchWords from '../../util/searchWords'; import { - getUserFullName, isChatChannel, isUserBot, sortChatIds, + filterUsersByName, isChatChannel, isUserBot, sortChatIds, } from '../../modules/helpers'; import useLang from '../../hooks/useLang'; import usePrevious from '../../hooks/usePrevious'; @@ -37,7 +36,6 @@ type StateProps = { isChannel?: boolean; members?: ApiChatMember[]; currentUserId?: string; - usersById: Record; chatsById: Record; localContactIds?: string[]; searchQuery?: string; @@ -55,7 +53,6 @@ const AddChatMembers: FC = ({ members, onNextStep, currentUserId, - usersById, chatsById, localContactIds, isLoading, @@ -90,43 +87,33 @@ const AddChatMembers: FC = ({ }, [setUserSearchQuery]); const displayedIds = useMemo(() => { - const contactIds = localContactIds - ? sortChatIds(localContactIds.filter((id) => id !== currentUserId), chatsById) - : []; - - if (!searchQuery) { - return contactIds.filter((id) => !memberIds.includes(id)); - } - - const foundContactIds = contactIds.filter((id) => { - const user = usersById[id]; - if (!user) { - return false; - } - const fullName = getUserFullName(user); - return fullName && searchWords(fullName, searchQuery); - }); + // No need for expensive global updates on users, so we avoid them + const usersById = getGlobal().users.byId; + const filteredContactIds = localContactIds ? filterUsersByName(localContactIds, usersById, searchQuery) : []; return sortChatIds( unique([ - ...foundContactIds, + ...filteredContactIds, ...(localUserIds || []), ...(globalUserIds || []), - ]).filter((contactId) => { - const user = usersById[contactId]; + ]).filter((userId) => { + const user = usersById[userId]; // The user can be added to the chat if the following conditions are met: // the user has not yet been added to the current chat + // AND it is not the current user, // AND (it is not found (user from global search) OR it is not a bot OR it is a bot, // but the current chat is not a channel AND the appropriate permission is set). - return !memberIds.includes(contactId) - && (!user || !isUserBot(user) || (!isChannel && user.canBeInvitedToGroup)); + return ( + !memberIds.includes(userId) + && userId !== currentUserId + && (!user || !isUserBot(user) || (!isChannel && user.canBeInvitedToGroup)) + ); }), chatsById, ); }, [ - localContactIds, chatsById, searchQuery, localUserIds, globalUserIds, - currentUserId, usersById, memberIds, isChannel, + localContactIds, chatsById, searchQuery, localUserIds, globalUserIds, currentUserId, memberIds, isChannel, ]); const handleNextStep = useCallback(() => { @@ -172,7 +159,6 @@ export default memo(withGlobal( (global, { chatId }): StateProps => { const chat = selectChat(global, chatId); const { userIds: localContactIds } = global.contactList || {}; - const { byId: usersById } = global.users; const { byId: chatsById } = global.chats; const { currentUserId, newChatMembersProgress, connectionState } = global; const isChannel = chat && isChatChannel(chat); @@ -188,7 +174,6 @@ export default memo(withGlobal( isChannel, members: chat?.fullInfo?.members, currentUserId, - usersById, chatsById, localContactIds, searchQuery, diff --git a/src/components/right/management/RemoveGroupUserModal.tsx b/src/components/right/management/RemoveGroupUserModal.tsx index 706b9344f..b9732d049 100644 --- a/src/components/right/management/RemoveGroupUserModal.tsx +++ b/src/components/right/management/RemoveGroupUserModal.tsx @@ -1,13 +1,12 @@ import React, { FC, useMemo, useState, memo, useRef, useCallback, } from '../../../lib/teact/teact'; -import { withGlobal } from '../../../lib/teact/teactn'; +import { getGlobal, withGlobal } from '../../../lib/teact/teactn'; import { GlobalActions } from '../../../global/types'; -import { ApiChat, ApiUser } from '../../../api/types'; +import { ApiChat } from '../../../api/types'; -import { getUserFullName } from '../../../modules/helpers'; -import searchWords from '../../../util/searchWords'; +import { filterUsersByName } from '../../../modules/helpers'; import { pick } from '../../../util/iteratees'; import useLang from '../../../hooks/useLang'; @@ -20,7 +19,6 @@ export type OwnProps = { }; type StateProps = { - usersById: Record; currentUserId?: string; }; @@ -28,7 +26,6 @@ type DispatchProps = Pick const RemoveGroupUserModal: FC = ({ chat, - usersById, currentUserId, isOpen, onClose, @@ -41,22 +38,19 @@ const RemoveGroupUserModal: FC = ({ const filterRef = useRef(null); const usersId = useMemo(() => { - const availableMembers = (chat.fullInfo?.members || []).filter((member) => { - return !member.isAdmin && !member.isOwner && member.userId !== currentUserId; - }); + const availableMemberIds = (chat.fullInfo?.members || []) + .reduce((acc: string[], member) => { + if (!member.isAdmin && !member.isOwner && member.userId !== currentUserId) { + acc.push(member.userId); + } + return acc; + }, []); - return availableMembers.reduce((acc, member) => { - if ( - !filter - || !usersById[member.userId] - || searchWords(getUserFullName(usersById[member.userId]) || '', filter) - ) { - acc.push(member.userId); - } + // No need for expensive global updates on users, so we avoid them + const usersById = getGlobal().users.byId; - return acc; - }, []); - }, [chat.fullInfo?.members, currentUserId, filter, usersById]); + return filterUsersByName(availableMemberIds, usersById, filter); + }, [chat.fullInfo?.members, currentUserId, filter]); const handleRemoveUser = useCallback((userId: string) => { deleteChatMember({ chatId: chat.id, userId }); @@ -80,14 +74,9 @@ const RemoveGroupUserModal: FC = ({ export default memo(withGlobal( (global): StateProps => { - const { - users: { - byId: usersById, - }, - currentUserId, - } = global; + const { currentUserId } = global; - return { usersById, currentUserId }; + return { currentUserId }; }, (setGlobal, actions): DispatchProps => pick(actions, ['loadMoreMembers', 'deleteChatMember']), )(RemoveGroupUserModal)); diff --git a/src/modules/helpers/chats.ts b/src/modules/helpers/chats.ts index 0e7cda381..639478a1a 100644 --- a/src/modules/helpers/chats.ts +++ b/src/modules/helpers/chats.ts @@ -15,6 +15,7 @@ import { ARCHIVED_FOLDER_ID, REPLIES_USER_ID } from '../../config'; import { orderBy } from '../../util/iteratees'; import { getUserFirstOrLastName } from './users'; import { formatDateToString, formatTime } from '../../util/dateFormat'; +import { prepareSearchWordsForNeedle } from '../../util/searchWords'; const FOREVER_BANNED_DATE = Date.now() / 1000 + 31622400; // 366 days @@ -560,3 +561,26 @@ export function sortChatIds( return priority; }, 'desc'); } + +export function filterChatsByName( + lang: LangFn, + chatIds: string[], + chatsById: Record, + query?: string, + currentUserId?: string, +) { + if (!query) { + return chatIds; + } + + const searchWords = prepareSearchWordsForNeedle(query); + + return chatIds.filter((id) => { + const chat = chatsById[id]; + if (!chat) { + return false; + } + + return searchWords(getChatTitle(lang, chat, undefined, id === currentUserId)); + }); +} diff --git a/src/modules/helpers/users.ts b/src/modules/helpers/users.ts index 6f09bd141..6c26cf698 100644 --- a/src/modules/helpers/users.ts +++ b/src/modules/helpers/users.ts @@ -5,6 +5,7 @@ import { formatFullDate, formatTime } from '../../util/dateFormat'; import { orderBy } from '../../util/iteratees'; import { LangFn } from '../../hooks/useLang'; import { getServerTime } from '../../util/serverTime'; +import { prepareSearchWordsForNeedle } from '../../util/searchWords'; const USER_COLOR_KEYS = [1, 8, 5, 2, 7, 4, 6]; @@ -231,6 +232,24 @@ export function sortUserIds( }, 'desc'); } +export function filterUsersByName(userIds: string[], usersById: Record, query?: string) { + if (!query) { + return userIds; + } + + const searchWords = prepareSearchWordsForNeedle(query); + + return userIds.filter((id) => { + const user = usersById[id]; + if (!user) { + return false; + } + + const name = getUserFullName(user); + return (name && searchWords(name)) || searchWords(user.username); + }); +} + export function getUserIdDividend(userId: string) { // Workaround for old-fashioned IDs stored locally if (typeof userId === 'number') { diff --git a/src/util/searchWords.ts b/src/util/searchWords.ts index d423430a3..4e6befef1 100644 --- a/src/util/searchWords.ts +++ b/src/util/searchWords.ts @@ -7,15 +7,36 @@ try { RE_NOT_LETTER = new RegExp('[^\\wа-яё]+', 'i'); } -export default function searchWords(haystack: string, needle: string) { +export default function searchWords(haystack: string, needle: string | string[]) { if (!haystack || !needle) { return false; } - const haystackWords = haystack.toLowerCase().split(RE_NOT_LETTER); + const needleWords = typeof needle === 'string' ? needle.toLowerCase().split(RE_NOT_LETTER) : needle; + const haystackLower = haystack.toLowerCase(); + + // @optimization + if (needleWords.length === 1 && !haystackLower.includes(needleWords[0])) { + return false; + } + + let haystackWords: string[]; + + return needleWords.every((needleWord) => { + if (!haystackLower.includes(needleWord)) { + return false; + } + + if (!haystackWords) { + haystackWords = haystackLower.split(RE_NOT_LETTER); + } + + return haystackWords.some((haystackWord) => haystackWord.startsWith(needleWord)); + }); +} + +export function prepareSearchWordsForNeedle(needle: string) { const needleWords = needle.toLowerCase().split(RE_NOT_LETTER); - return needleWords.every((needleWord) => ( - haystackWords.some((haystackWord) => haystackWord.startsWith(needleWord)) - )); + return (haystack: string) => searchWords(haystack, needleWords); }