diff --git a/lib/selectors/user-selectors.js b/lib/selectors/user-selectors.js --- a/lib/selectors/user-selectors.js +++ b/lib/selectors/user-selectors.js @@ -7,8 +7,6 @@ getAvatarForUser, getRandomDefaultEmojiAvatar, } from '../shared/avatar-utils.js'; -import SearchIndex from '../shared/search-index.js'; -import SentencePrefixSearchIndex from '../shared/sentence-prefix-search-index.js'; import { getSingleOtherUser } from '../shared/thread-utils.js'; import type { ClientEmojiAvatar } from '../types/avatar-types'; import type { MinimallyEncodedRawThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js'; @@ -170,30 +168,6 @@ state.dataLoaded ); -const addUsersToSearchIndex = ( - userInfos: UserInfos, - searchIndex: SearchIndex | SentencePrefixSearchIndex, -): void => { - for (const id in userInfos) { - const { username } = userInfos[id]; - if (!username) { - continue; - } - searchIndex.addEntry(id, username); - } -}; - -const userStoreMentionSearchIndex: ( - state: BaseAppState<>, -) => SentencePrefixSearchIndex = createSelector( - (state: BaseAppState<>) => state.userStore.userInfos, - (userInfos: UserInfos) => { - const searchIndex = new SentencePrefixSearchIndex(); - addUsersToSearchIndex(userInfos, searchIndex); - return searchIndex; - }, -); - const usersWithPersonalThreadSelector: ( state: BaseAppState<>, ) => $ReadOnlySet = createSelector( @@ -240,7 +214,6 @@ relativeMemberInfoSelectorForMembersOfThread, userInfoSelectorForPotentialMembers, isLoggedIn, - userStoreMentionSearchIndex, usersWithPersonalThreadSelector, savedEmojiAvatarSelectorForCurrentUser, }; diff --git a/lib/shared/mention-utils.js b/lib/shared/mention-utils.js --- a/lib/shared/mention-utils.js +++ b/lib/shared/mention-utils.js @@ -1,9 +1,13 @@ // @flow +import * as React from 'react'; + import { oldValidUsernameRegexString } from './account-utils.js'; import SentencePrefixSearchIndex from './sentence-prefix-search-index.js'; import { threadOtherMembers } from './thread-utils.js'; import { stringForUserExplicit } from './user-utils.js'; +import { useENSNames } from '../hooks/ens-cache.js'; +import { useUserSearchIndex } from '../selectors/nav-selectors.js'; import { threadTypes } from '../types/thread-types-enum.js'; import type { ChatMentionCandidates, @@ -100,21 +104,34 @@ return null; } -function getMentionTypeaheadUserSuggestions( - userSearchIndex: SentencePrefixSearchIndex, +const useENSNamesOptions = { allAtOnce: true }; +function useMentionTypeaheadUserSuggestions( threadMembers: $ReadOnlyArray, viewerID: ?string, - usernamePrefix: string, + typeaheadMatchedStrings: ?TypeaheadMatchedStrings, ): $ReadOnlyArray { - const userIDs = userSearchIndex.getSearchResults(usernamePrefix); - const usersInThread = threadOtherMembers(threadMembers, viewerID); - - return usersInThread - .filter(user => usernamePrefix.length === 0 || userIDs.includes(user.id)) - .sort((userA, userB) => - stringForUserExplicit(userA).localeCompare(stringForUserExplicit(userB)), - ) - .map(userInfo => ({ type: 'user', userInfo })); + const userSearchIndex = useUserSearchIndex(threadMembers); + const resolvedThredMembers = useENSNames(threadMembers, useENSNamesOptions); + const usernamePrefix: ?string = typeaheadMatchedStrings?.query; + + return React.useMemo(() => { + // If typeaheadMatchedStrings is undefined, we want to return no results + if (usernamePrefix === undefined || usernamePrefix === null) { + return []; + } + + const userIDs = userSearchIndex.getSearchResults(usernamePrefix); + const usersInThread = threadOtherMembers(resolvedThredMembers, viewerID); + + return usersInThread + .filter(user => usernamePrefix.length === 0 || userIDs.includes(user.id)) + .sort((userA, userB) => + stringForUserExplicit(userA).localeCompare( + stringForUserExplicit(userB), + ), + ) + .map(userInfo => ({ type: 'user', userInfo })); + }, [userSearchIndex, resolvedThredMembers, usernamePrefix, viewerID]); } function getMentionTypeaheadChatSuggestions( @@ -157,31 +174,33 @@ return { newText, newSelectionStart }; } -function getUserMentionsCandidates( +function useUserMentionsCandidates( threadInfo: ThreadInfo, parentThreadInfo: ?ThreadInfo, ): $ReadOnlyArray { - if (threadInfo.type !== threadTypes.SIDEBAR) { - return threadInfo.members; - } - if (parentThreadInfo) { - return parentThreadInfo.members; - } - // This scenario should not occur unless the user logs out while looking at a - // sidebar. In that scenario, the Redux store may be cleared before ReactNav - // finishes transitioning away from the previous screen - return []; + return React.useMemo(() => { + if (threadInfo.type !== threadTypes.SIDEBAR) { + return threadInfo.members; + } + if (parentThreadInfo) { + return parentThreadInfo.members; + } + // This scenario should not occur unless the user logs out while looking at + // a sidebar. In that scenario, the Redux store may be cleared before + // ReactNav finishes transitioning away from the previous screen + return []; + }, [threadInfo, parentThreadInfo]); } export { markdownUserMentionRegex, isUserMentioned, extractUserMentionsFromText, - getMentionTypeaheadUserSuggestions, + useMentionTypeaheadUserSuggestions, getMentionTypeaheadChatSuggestions, getNewTextAndSelection, getTypeaheadRegexMatches, - getUserMentionsCandidates, + useUserMentionsCandidates, chatMentionRegex, encodeChatMentionText, decodeChatMentionText, diff --git a/native/chat/chat-input-bar.react.js b/native/chat/chat-input-bar.react.js --- a/native/chat/chat-input-bar.react.js +++ b/native/chat/chat-input-bar.react.js @@ -37,15 +37,14 @@ } from 'lib/hooks/chat-mention-hooks.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import { threadInfoSelector } from 'lib/selectors/thread-selectors.js'; -import { userStoreMentionSearchIndex } from 'lib/selectors/user-selectors.js'; import { colorIsDark } from 'lib/shared/color-utils.js'; import { useEditMessage } from 'lib/shared/edit-messages-utils.js'; import { - getMentionTypeaheadUserSuggestions, + useMentionTypeaheadUserSuggestions, getMentionTypeaheadChatSuggestions, getTypeaheadRegexMatches, type Selection, - getUserMentionsCandidates, + useUserMentionsCandidates, type MentionTypeaheadSuggestionItem, type TypeaheadMatchedStrings, } from 'lib/shared/mention-utils.js'; @@ -297,7 +296,6 @@ +dispatchActionPromise: DispatchActionPromise, +joinThread: (request: ClientThreadJoinRequest) => Promise, +inputState: ?InputState, - +userSearchIndex: SentencePrefixSearchIndex, +userMentionsCandidates: $ReadOnlyArray, +chatMentionSearchIndex: SentencePrefixSearchIndex, +chatMentionCandidates: ChatMentionCandidates, @@ -1258,8 +1256,6 @@ const dispatchActionPromise = useDispatchActionPromise(); const callJoinThread = useJoinThread(); - const userSearchIndex = useSelector(userStoreMentionSearchIndex); - const { getChatMentionSearchIndex } = useChatMentionContext(); const chatMentionSearchIndex = getChatMentionSearchIndex(props.threadInfo); @@ -1268,7 +1264,7 @@ parentThreadID ? threadInfoSelector(state)[parentThreadID] : null, ); - const userMentionsCandidates = getUserMentionsCandidates( + const userMentionsCandidates = useUserMentionsCandidates( props.threadInfo, parentThreadInfo, ); @@ -1303,50 +1299,43 @@ [selectionState.text, selectionState.selection], ); - const typeaheadResults: { - typeaheadMatchedStrings: ?TypeaheadMatchedStrings, - suggestions: $ReadOnlyArray, - } = React.useMemo(() => { - if (!typeaheadRegexMatches) { + const typeaheadMatchedStrings: ?TypeaheadMatchedStrings = + React.useMemo(() => { + if (typeaheadRegexMatches === null) { + return null; + } return { - typeaheadMatchedStrings: null, - suggestions: [], + textBeforeAtSymbol: typeaheadRegexMatches[1] ?? '', + query: typeaheadRegexMatches[4] ?? '', }; - } + }, [typeaheadRegexMatches]); - const typeaheadMatchedStrings: TypeaheadMatchedStrings = { - textBeforeAtSymbol: typeaheadRegexMatches[1] ?? '', - query: typeaheadRegexMatches[4] ?? '', - }; + const suggestedUsers = useMentionTypeaheadUserSuggestions( + userMentionsCandidates, + viewerID, + typeaheadMatchedStrings, + ); - const suggestedUsers = getMentionTypeaheadUserSuggestions( - userSearchIndex, - userMentionsCandidates, - viewerID, - typeaheadMatchedStrings.query, - ); - const suggestedChats = getMentionTypeaheadChatSuggestions( - chatMentionSearchIndex, - chatMentionCandidates, - typeaheadMatchedStrings.query, - ); - const suggestions: $ReadOnlyArray = [ - ...suggestedUsers, - ...suggestedChats, - ]; + const suggestions: $ReadOnlyArray = + React.useMemo(() => { + if (!typeaheadRegexMatches || !typeaheadMatchedStrings) { + return []; + } - return { + const suggestedChats = getMentionTypeaheadChatSuggestions( + chatMentionSearchIndex, + chatMentionCandidates, + typeaheadMatchedStrings.query, + ); + + return [...suggestedUsers, ...suggestedChats]; + }, [ + chatMentionCandidates, + chatMentionSearchIndex, + typeaheadRegexMatches, typeaheadMatchedStrings, - suggestions, - }; - }, [ - chatMentionCandidates, - chatMentionSearchIndex, - typeaheadRegexMatches, - userMentionsCandidates, - userSearchIndex, - viewerID, - ]); + suggestedUsers, + ]); return ( ); } diff --git a/web/chat/chat-input-bar.react.js b/web/chat/chat-input-bar.react.js --- a/web/chat/chat-input-bar.react.js +++ b/web/chat/chat-input-bar.react.js @@ -16,14 +16,13 @@ } from 'lib/hooks/chat-mention-hooks.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import { threadInfoSelector } from 'lib/selectors/thread-selectors.js'; -import { userStoreMentionSearchIndex } from 'lib/selectors/user-selectors.js'; import { - getMentionTypeaheadUserSuggestions, getTypeaheadRegexMatches, - getUserMentionsCandidates, + useUserMentionsCandidates, getMentionTypeaheadChatSuggestions, type MentionTypeaheadSuggestionItem, type TypeaheadMatchedStrings, + useMentionTypeaheadUserSuggestions, } from 'lib/shared/mention-utils.js'; import { localIDPrefix, trimMessage } from 'lib/shared/message-utils.js'; import { @@ -586,7 +585,6 @@ const calendarQuery = useSelector(nonThreadCalendarQuery); const dispatchActionPromise = useDispatchActionPromise(); const callJoinThread = useJoinThread(); - const userSearchIndex = useSelector(userStoreMentionSearchIndex); const { getChatMentionSearchIndex } = useChatMentionContext(); const chatMentionSearchIndex = getChatMentionSearchIndex(props.threadInfo); @@ -595,7 +593,7 @@ parentThreadID ? threadInfoSelector(state)[parentThreadID] : null, ); - const userMentionsCandidates = getUserMentionsCandidates( + const userMentionsCandidates = useUserMentionsCandidates( props.threadInfo, parentThreadInfo, ); @@ -617,17 +615,16 @@ [props.inputState.textCursorPosition, props.inputState.draft], ); - const typeaheadMatchedStrings: ?TypeaheadMatchedStrings = React.useMemo( - () => - typeaheadRegexMatches !== null - ? { - textBeforeAtSymbol: - typeaheadRegexMatches.groups?.textPrefix ?? '', - query: typeaheadRegexMatches.groups?.mentionText ?? '', - } - : null, - [typeaheadRegexMatches], - ); + const typeaheadMatchedStrings: ?TypeaheadMatchedStrings = + React.useMemo(() => { + if (typeaheadRegexMatches === null) { + return null; + } + return { + textBeforeAtSymbol: typeaheadRegexMatches.groups?.textPrefix ?? '', + query: typeaheadRegexMatches.groups?.mentionText ?? '', + }; + }, [typeaheadRegexMatches]); React.useEffect(() => { if (props.inputState.typeaheadState.keepUpdatingThreadMembers) { @@ -644,16 +641,16 @@ chatMentionCandidates, ]); + const suggestedUsers = useMentionTypeaheadUserSuggestions( + props.inputState.typeaheadState.frozenUserMentionsCandidates, + viewerID, + typeaheadMatchedStrings, + ); + const suggestions = React.useMemo(() => { if (!typeaheadMatchedStrings) { return ([]: $ReadOnlyArray); } - const suggestedUsers = getMentionTypeaheadUserSuggestions( - userSearchIndex, - props.inputState.typeaheadState.frozenUserMentionsCandidates, - viewerID, - typeaheadMatchedStrings.query, - ); const suggestedChats = getMentionTypeaheadChatSuggestions( chatMentionSearchIndex, props.inputState.typeaheadState.frozenChatMentionsCandidates, @@ -664,11 +661,9 @@ ...suggestedChats, ]: $ReadOnlyArray); }, [ + suggestedUsers, typeaheadMatchedStrings, - userSearchIndex, - props.inputState.typeaheadState.frozenUserMentionsCandidates, props.inputState.typeaheadState.frozenChatMentionsCandidates, - viewerID, chatMentionSearchIndex, ]);