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 @@ -119,36 +119,38 @@ baseRelativeMemberInfoSelectorForMembersOfThread, ); +function filterPotentialMembers( + userInfos: UserInfos, + currentUserID: ?string, +): { [id: string]: AccountUserInfo } { + const availableUsers: { [id: string]: AccountUserInfo } = {}; + + for (const id in userInfos) { + const { username, relationshipStatus } = userInfos[id]; + if (id === currentUserID || !username) { + continue; + } + if ( + relationshipStatus !== userRelationshipStatus.BLOCKED_VIEWER && + relationshipStatus !== userRelationshipStatus.BOTH_BLOCKED + ) { + availableUsers[id] = { id, username, relationshipStatus }; + } + } + return availableUsers; +} + const userInfoSelectorForPotentialMembers: (state: BaseAppState<*>) => { [id: string]: AccountUserInfo, } = createSelector( (state: BaseAppState<*>) => state.userStore.userInfos, (state: BaseAppState<*>) => state.currentUserInfo && state.currentUserInfo.id, - ( - userInfos: UserInfos, - currentUserID: ?string, - ): { [id: string]: AccountUserInfo } => { - const availableUsers: { [id: string]: AccountUserInfo } = {}; - - for (const id in userInfos) { - const { username, relationshipStatus } = userInfos[id]; - if (id === currentUserID || !username) { - continue; - } - if ( - relationshipStatus !== userRelationshipStatus.BLOCKED_VIEWER && - relationshipStatus !== userRelationshipStatus.BOTH_BLOCKED - ) { - availableUsers[id] = { id, username, relationshipStatus }; - } - } - return availableUsers; - }, + filterPotentialMembers, ); function searchIndexFromUserInfos(userInfos: { [id: string]: AccountUserInfo, -}) { +}): SearchIndex { const searchIndex = new SearchIndex(); for (const id in userInfos) { searchIndex.addEntry(id, userInfos[id].username); @@ -235,5 +237,7 @@ isLoggedIn, userStoreSearchIndex, usersWithPersonalThreadSelector, + filterPotentialMembers, + searchIndexFromUserInfos, savedEmojiAvatarSelectorForCurrentUser, }; diff --git a/lib/shared/search-utils.js b/lib/shared/search-utils.js --- a/lib/shared/search-utils.js +++ b/lib/shared/search-utils.js @@ -29,12 +29,16 @@ function getPotentialMemberItems( text: string, userInfos: { +[id: string]: AccountUserInfo }, - searchIndex: SearchIndex, + inputSearchIndex: SearchIndex | $ReadOnlyArray, excludeUserIDs: $ReadOnlyArray, inputParentThreadInfo: ?ThreadInfo, inputCommunityThreadInfo: ?ThreadInfo, threadType: ?ThreadType, ): UserListItem[] { + const searchIndexes = Array.isArray(inputSearchIndex) + ? inputSearchIndex + : [inputSearchIndex]; + const communityThreadInfo = inputCommunityThreadInfo && inputCommunityThreadInfo.id !== genesis.id ? inputCommunityThreadInfo @@ -61,6 +65,9 @@ if (excludeUserIDs.includes(id)) { return; } + if (results.some(item => item.id === id)) { + return; + } if ( communityThreadInfo && !threadMemberHasPermission( @@ -82,9 +89,11 @@ appendUserInfo(userInfos[id]); } } else { - const ids = searchIndex.getSearchResults(text); - for (const id of ids) { - appendUserInfo(userInfos[id]); + for (const searchIndex of searchIndexes) { + const ids = searchIndex.getSearchResults(text); + for (const id of ids) { + appendUserInfo(userInfos[id]); + } } } diff --git a/web/chat/chat-message-list-container.react.js b/web/chat/chat-message-list-container.react.js --- a/web/chat/chat-message-list-container.react.js +++ b/web/chat/chat-message-list-container.react.js @@ -33,57 +33,58 @@ selectedUserIDs, otherUserInfos, userInfoInputArray, + setUserInfoInputArray, } = useInfosForPendingThread(); + React.useEffect(() => { + if (!isChatCreation) { + setUserInfoInputArray([]); + } + // TODO: Check here + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isChatCreation]); + const threadInfo = useThreadInfoForPossiblyPendingThread(activeChatThreadID); invariant(threadInfo, 'ThreadInfo should be set'); const dispatch = useDispatch(); - - // The effect removes members from list in navInfo - // if some of the user IDs don't exist in redux store React.useEffect(() => { if (!isChatCreation) { return; } - const existingSelectedUsersSet = new Set( - userInfoInputArray.map(userInfo => userInfo.id), - ); - if ( - selectedUserIDs?.length !== existingSelectedUsersSet.size || - !_isEqual(new Set(selectedUserIDs), existingSelectedUsersSet) - ) { - dispatch({ - type: updateNavInfoActionType, - payload: { - selectedUserList: Array.from(existingSelectedUsersSet), - }, - }); + const newSelectedUserIDs = userInfoInputArray.map(user => user.id); + if (_isEqual(new Set(selectedUserIDs), new Set(newSelectedUserIDs))) { + return; } - }, [ - dispatch, - isChatCreation, - otherUserInfos, - selectedUserIDs, - userInfoInputArray, - ]); + const payload = { + selectedUserList: newSelectedUserIDs, + }; + dispatch({ + type: updateNavInfoActionType, + payload, + }); + }, [dispatch, isChatCreation, selectedUserIDs, userInfoInputArray]); React.useEffect(() => { - if (isChatCreation && activeChatThreadID !== threadInfo?.id) { - let payload = { - activeChatThreadID: threadInfo?.id, + if (!isChatCreation) { + return; + } + if (activeChatThreadID === threadInfo?.id) { + return; + } + let payload = { + activeChatThreadID: threadInfo?.id, + }; + if (threadIsPending(threadInfo?.id)) { + payload = { + ...payload, + pendingThread: threadInfo, }; - if (threadIsPending(threadInfo?.id)) { - payload = { - ...payload, - pendingThread: threadInfo, - }; - } - dispatch({ - type: updateNavInfoActionType, - payload, - }); } + dispatch({ + type: updateNavInfoActionType, + payload, + }); }, [activeChatThreadID, dispatch, isChatCreation, threadInfo]); const inputState = React.useContext(InputStateContext); @@ -159,6 +160,7 @@ const chatUserSelection = ( , + +setUserInfoInputArray: SetState<$ReadOnlyArray>, +otherUserInfos: { [id: string]: AccountUserInfo }, +threadID: string, +inputState: InputState, @@ -32,12 +44,56 @@ | 'keep-active-thread'; function ChatThreadComposer(props: Props): React.Node { - const { userInfoInputArray, otherUserInfos, threadID, inputState } = props; + const { + userInfoInputArray, + setUserInfoInputArray, + otherUserInfos, + threadID, + inputState, + } = props; const [usernameInputText, setUsernameInputText] = React.useState(''); - const dispatch = useDispatch(); + const userInfos = useSelector(state => state.userStore.userInfos); + const viewerID = useSelector(state => state.currentUserInfo?.id); + + const [serverSearchUserInfos, setServerSearchUserInfos] = React.useState< + $ReadOnlyArray, + >([]); + const callSearchUsers = useServerCall(searchUsers); + React.useEffect(() => { + (async () => { + if (usernameInputText.length === 0) { + setServerSearchUserInfos([]); + } else { + const { userInfos: serverUserInfos } = await callSearchUsers( + usernameInputText, + ); + setServerSearchUserInfos(serverUserInfos); + } + })(); + }, [callSearchUsers, usernameInputText]); + + const filteredServerUserInfos = React.useMemo(() => { + const result = {}; + for (const user of serverSearchUserInfos) { + if (!(user.id in userInfos)) { + result[user.id] = user; + } + } + return filterPotentialMembers(result, viewerID); + }, [serverSearchUserInfos, userInfos, viewerID]); + + const mergedUserInfos = React.useMemo( + () => ({ ...filteredServerUserInfos, ...otherUserInfos }), + [filteredServerUserInfos, otherUserInfos], + ); + const userSearchIndex = useSelector(userSearchIndexForPotentialMembers); + const filteredServerUsersSearchIndex = React.useMemo( + () => searchIndexFromUserInfos(filteredServerUserInfos), + [filteredServerUserInfos], + ); const userInfoInputIDs = React.useMemo( () => userInfoInputArray.map(userInfo => userInfo.id), @@ -48,28 +104,32 @@ () => getPotentialMemberItems( usernameInputText, - otherUserInfos, - userSearchIndex, + mergedUserInfos, + [userSearchIndex, filteredServerUsersSearchIndex], userInfoInputIDs, ), - [usernameInputText, otherUserInfos, userSearchIndex, userInfoInputIDs], + [ + usernameInputText, + mergedUserInfos, + userSearchIndex, + filteredServerUsersSearchIndex, + userInfoInputIDs, + ], ); const userListItemsWithENSNames = useENSNames(userListItems); const onSelectUserFromSearch = React.useCallback( - (id: string) => { - const selectedUserIDs = userInfoInputArray.map(user => user.id); - dispatch({ - type: updateNavInfoActionType, - payload: { - selectedUserList: [...selectedUserIDs, id], - }, - }); + (id: string, username: string) => { + setUserInfoInputArray(previousUserInfoInputArray => [ + ...previousUserInfoInputArray, + { id, username }, + ]); setUsernameInputText(''); }, - [dispatch, userInfoInputArray], + [setUserInfoInputArray], ); + const dispatch = useDispatch(); const onRemoveUserFromSelected = React.useCallback( (id: string) => { const selectedUserIDs = userInfoInputArray.map(user => user.id); @@ -85,7 +145,6 @@ }, [dispatch, userInfoInputArray], ); - const usernameStyle = React.useMemo( () => ({ marginLeft: shouldRenderAvatars ? 8 : 0, @@ -106,7 +165,12 @@