[Perf] Forward Modal: Fix missing contacts; Optimize text filtering in large lists
This commit is contained in:
parent
dac6c3a101
commit
f6cd826e7f
@ -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>(
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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,
|
||||
};
|
||||
},
|
||||
|
||||
@ -85,7 +85,6 @@ const AttachmentModal: FC<OwnProps> = ({
|
||||
groupChatMembers,
|
||||
undefined,
|
||||
currentUserId,
|
||||
usersById,
|
||||
);
|
||||
const {
|
||||
isEmojiTooltipOpen, closeEmojiTooltip, filteredEmojis, insertEmoji,
|
||||
|
||||
@ -299,7 +299,6 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
groupChatMembers,
|
||||
topInlineBotIds,
|
||||
currentUserId,
|
||||
usersById,
|
||||
);
|
||||
|
||||
const {
|
||||
|
||||
@ -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)
|
||||
);
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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));
|
||||
});
|
||||
}
|
||||
|
||||
@ -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') {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user