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 @@ -113,36 +113,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); @@ -214,4 +216,6 @@ isLoggedIn, userStoreSearchIndex, usersWithPersonalThreadSelector, + filterPotentialMembers, + searchIndexFromUserInfos, }; 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 @@ -21,12 +21,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 @@ -74,9 +78,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 @@ -18,7 +18,6 @@ threadIsPending, } from 'lib/shared/thread-utils.js'; import { threadTypes } from 'lib/types/thread-types.js'; -import type { AccountUserInfo } from 'lib/types/user-types.js'; import ChatInputBar from './chat-input-bar.react.js'; import css from './chat-message-list-container.css'; @@ -38,13 +37,20 @@ const isChatCreation = useSelector(state => state.navInfo.chatMode) === 'create'; - const selectedUserIDs = useSelector(state => state.navInfo.selectedUserList); + const selectedUserIDs = useSelector( + state => state.navInfo.selectedUserList ?? [], + ); const otherUserInfos = useSelector(userInfoSelectorForPotentialMembers); - const userInfoInputArray: $ReadOnlyArray = React.useMemo( - () => selectedUserIDs?.map(id => otherUserInfos[id]).filter(Boolean) ?? [], - [otherUserInfos, selectedUserIDs], + const [userInfoInputArray, setUserInfoInputArray] = React.useState(() => + selectedUserIDs.map(id => otherUserInfos[id]).filter(Boolean), ); + React.useEffect(() => { + if (!isChatCreation) { + setUserInfoInputArray([]); + } + }, [isChatCreation]); + const loggedInUserInfo = useLoggedInUserInfo(); invariant(loggedInUserInfo, 'loggedInUserInfo should be set'); @@ -110,52 +116,46 @@ 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), - }, - }); - } - }, [ - dispatch, - isChatCreation, - otherUserInfos, - selectedUserIDs, - userInfoInputArray, - ]); + if (isChatCreation) { + let payload = null; - React.useEffect(() => { - if (isChatCreation && activeChatThreadID !== threadInfo?.id) { - let payload = { - activeChatThreadID: threadInfo?.id, - }; - if (threadIsPending(threadInfo?.id)) { + const newSelectedUserIDs = userInfoInputArray.map(user => user.id); + if (!_isEqual(new Set(selectedUserIDs), new Set(newSelectedUserIDs))) { + payload = { + ...payload, + selectedUserList: newSelectedUserIDs, + }; + } + + if (activeChatThreadID !== threadInfo?.id) { payload = { ...payload, - pendingThread: threadInfo, + activeChatThreadID: threadInfo?.id, }; + if (threadIsPending(threadInfo?.id)) { + payload = { + ...payload, + pendingThread: threadInfo, + }; + } + } + if (!payload) { + return; } dispatch({ type: updateNavInfoActionType, payload, }); } - }, [activeChatThreadID, dispatch, isChatCreation, threadInfo]); + }, [ + activeChatThreadID, + dispatch, + isChatCreation, + selectedUserIDs, + threadInfo, + userInfoInputArray, + ]); const inputState = React.useContext(InputStateContext); invariant(inputState, 'InputState should be set'); @@ -230,6 +230,7 @@ const chatUserSelection = ( , + +setUserInfoInputArray: SetState<$ReadOnlyArray>, +otherUserInfos: { [id: string]: AccountUserInfo }, +threadID: string, +inputState: InputState, @@ -30,12 +42,55 @@ | '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(() => { + return searchIndexFromUserInfos(filteredServerUserInfos); + }, [filteredServerUserInfos]); const userInfoInputIDs = React.useMemo( () => userInfoInputArray.map(userInfo => userInfo.id), @@ -46,42 +101,38 @@ () => 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 onRemoveUserFromSelected = React.useCallback( (id: string) => { - const selectedUserIDs = userInfoInputArray.map(user => user.id); - if (!selectedUserIDs.includes(id)) { - return; - } - dispatch({ - type: updateNavInfoActionType, - payload: { - selectedUserList: selectedUserIDs.filter(userID => userID !== id), - }, - }); + setUserInfoInputArray(previousUserInfoInputArray => + previousUserInfoInputArray.filter(user => user.id !== id), + ); }, - [dispatch, userInfoInputArray], + [setUserInfoInputArray], ); const userSearchResultList = React.useMemo(() => { @@ -98,7 +149,12 @@