[Perf] Forward Modal: Fix missing contacts; Optimize text filtering in large lists

This commit is contained in:
Alexander Zinchuk 2021-12-24 01:26:06 +01:00
parent dac6c3a101
commit f6cd826e7f
15 changed files with 206 additions and 227 deletions

View File

@ -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<OwnProps & StateProps> = ({
};
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<OwnProps>(

View File

@ -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<OwnProps & StateProps & DispatchProps> = ({
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));

View File

@ -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<string, ApiUser>;
chatsById: Record<string, ApiChat>;
localContactIds?: string[];
searchQuery?: string;
@ -48,8 +45,6 @@ const NewChatStep1: FC<OwnProps & StateProps & DispatchProps> = ({
onSelectedMemberIdsChange,
onNextStep,
onReset,
currentUserId,
usersById,
chatsById,
localContactIds,
searchQuery,
@ -76,22 +71,9 @@ const NewChatStep1: FC<OwnProps & StateProps & DispatchProps> = ({
}, [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<OwnProps & StateProps & DispatchProps> = ({
...(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<OwnProps & StateProps & DispatchProps> = ({
export default memo(withGlobal<OwnProps>(
(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<OwnProps>(
const { userIds: localUserIds } = localResults || {};
return {
currentUserId,
usersById,
chatsById,
localContactIds,
searchQuery,

View File

@ -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<string, { byId: Record<number, ApiMessage> }>;
chatsById: Record<string, ApiChat>;
usersById: Record<string, ApiUser>;
fetchingStatus?: { chats?: boolean; messages?: boolean };
lastSyncTime?: number;
};
@ -52,14 +50,14 @@ type DispatchProps = Pick<GlobalActions, (
)>;
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<OwnProps & StateProps & DispatchProps> = ({
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<OwnProps & StateProps & DispatchProps> = ({
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<OwnProps & StateProps & DispatchProps> = ({
export default memo(withGlobal<OwnProps>(
(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<OwnProps>(
foundIds,
globalMessagesByChatId,
chatsById,
usersById,
fetchingStatus,
lastSyncTime,
};

View File

@ -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<OwnProps & StateProps & DispatchProps> = ({
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<string[]>((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<OwnProps & StateProps & DispatchProps> = ({
return (
<ChatOrUserPicker
isOpen={isOpen}
chatOrUserIds={filteredContactsId}
chatOrUserIds={filteredContactIds}
filterRef={filterRef}
filterPlaceholder={lang('BlockedUsers.BlockUser')}
filter={filter}

View File

@ -1,13 +1,17 @@
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, MAIN_THREAD_ID } from '../../api/types';
import { getCanPostInChat, getChatTitle, sortChatIds } from '../../modules/helpers';
import searchWords from '../../util/searchWords';
import {
filterChatsByName,
filterUsersByName,
getCanPostInChat,
sortChatIds,
} from '../../modules/helpers';
import { pick, unique } from '../../util/iteratees';
import useLang from '../../hooks/useLang';
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
@ -20,10 +24,10 @@ export type OwnProps = {
type StateProps = {
chatsById: Record<string, ApiChat>;
pinnedIds?: string[];
activeListIds?: string[];
archivedListIds?: string[];
orderedPinnedIds?: string[];
pinnedIds?: string[];
contactIds?: string[];
currentUserId?: string;
};
@ -31,9 +35,10 @@ type DispatchProps = Pick<GlobalActions, 'setForwardChatId' | 'exitForwardMode'
const ForwardPicker: FC<OwnProps & StateProps & DispatchProps> = ({
chatsById,
pinnedIds,
activeListIds,
archivedListIds,
pinnedIds,
contactIds,
currentUserId,
isOpen,
setForwardChatId,
@ -45,47 +50,45 @@ const ForwardPicker: FC<OwnProps & StateProps & DispatchProps> = ({
// eslint-disable-next-line no-null/no-null
const filterRef = useRef<HTMLInputElement>(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 (
<ChatOrUserPicker
currentUserId={currentUserId}
isOpen={isOpen}
chatOrUserIds={renderingChatIds}
chatOrUserIds={renderingChatAndContactIds}
filterRef={filterRef}
filterPlaceholder={lang('ForwardTo')}
filter={filter}
@ -110,9 +113,10 @@ export default memo(withGlobal<OwnProps>(
return {
chatsById,
pinnedIds: orderedPinnedIds.active,
activeListIds: listIds.active,
archivedListIds: listIds.archived,
pinnedIds: orderedPinnedIds.active,
contactIds: global.contactList?.userIds,
currentUserId,
};
},

View File

@ -85,7 +85,6 @@ const AttachmentModal: FC<OwnProps> = ({
groupChatMembers,
undefined,
currentUserId,
usersById,
);
const {
isEmojiTooltipOpen, closeEmojiTooltip, filteredEmojis, insertEmoji,

View File

@ -299,7 +299,6 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
groupChatMembers,
topInlineBotIds,
currentUserId,
usersById,
);
const {

View File

@ -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)
);
}

View File

@ -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<string, ApiUser>,
) {
const [isOpen, markIsOpen, unmarkIsOpen] = useFlag();
const [usersToMention, setUsersToMention] = useState<ApiUser[] | undefined>();
const topInlineBots = useMemo(() => {
return (topInlineBotIds || []).map((id) => usersById?.[id]).filter<ApiUser>(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) {

View File

@ -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<string, ApiUser>;
chatsById: Record<string, ApiChat>;
localContactIds?: string[];
searchQuery?: string;
@ -55,7 +53,6 @@ const AddChatMembers: FC<OwnProps & StateProps & DispatchProps> = ({
members,
onNextStep,
currentUserId,
usersById,
chatsById,
localContactIds,
isLoading,
@ -90,43 +87,33 @@ const AddChatMembers: FC<OwnProps & StateProps & DispatchProps> = ({
}, [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<OwnProps>(
(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<OwnProps>(
isChannel,
members: chat?.fullInfo?.members,
currentUserId,
usersById,
chatsById,
localContactIds,
searchQuery,

View File

@ -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<string, ApiUser>;
currentUserId?: string;
};
@ -28,7 +26,6 @@ type DispatchProps = Pick<GlobalActions, 'loadMoreMembers' | 'deleteChatMember'>
const RemoveGroupUserModal: FC<OwnProps & StateProps & DispatchProps> = ({
chat,
usersById,
currentUserId,
isOpen,
onClose,
@ -41,22 +38,19 @@ const RemoveGroupUserModal: FC<OwnProps & StateProps & DispatchProps> = ({
const filterRef = useRef<HTMLInputElement>(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<string[]>((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<OwnProps & StateProps & DispatchProps> = ({
export default memo(withGlobal<OwnProps>(
(global): StateProps => {
const {
users: {
byId: usersById,
},
currentUserId,
} = global;
const { currentUserId } = global;
return { usersById, currentUserId };
return { currentUserId };
},
(setGlobal, actions): DispatchProps => pick(actions, ['loadMoreMembers', 'deleteChatMember']),
)(RemoveGroupUserModal));

View File

@ -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<string, ApiChat>,
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));
});
}

View File

@ -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<string, ApiUser>, 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') {

View File

@ -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);
}