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,31 +113,33 @@ 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: { @@ -214,4 +216,5 @@ isLoggedIn, userStoreSearchIndex, usersWithPersonalThreadSelector, + filterPotentialMembers, }; 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,44 @@ 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 = {}; - 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, + }; + } } + 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 +228,7 @@ const chatUserSelection = ( , + +setUserInfoInputArray: SetState<$ReadOnlyArray>, +otherUserInfos: { [id: string]: AccountUserInfo }, +threadID: string, +inputState: InputState, @@ -30,12 +42,59 @@ | '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 mergedUserSearchIndex = React.useMemo(() => { + const searchIndex = _cloneDeep(userSearchIndex); + for (const id in filteredServerUserInfos) { + searchIndex.addEntry(id, filteredServerUserInfos[id].username); + } + return searchIndex; + }, [filteredServerUserInfos, userSearchIndex]); const userInfoInputIDs = React.useMemo( () => userInfoInputArray.map(userInfo => userInfo.id), @@ -46,42 +105,37 @@ () => getPotentialMemberItems( usernameInputText, - otherUserInfos, - userSearchIndex, + mergedUserInfos, + mergedUserSearchIndex, userInfoInputIDs, ), - [usernameInputText, otherUserInfos, userSearchIndex, userInfoInputIDs], + [ + usernameInputText, + mergedUserInfos, + mergedUserSearchIndex, + 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 +152,12 @@