diff --git a/keyserver/src/creators/thread-creator.js b/keyserver/src/creators/thread-creator.js --- a/keyserver/src/creators/thread-creator.js +++ b/keyserver/src/creators/thread-creator.js @@ -79,14 +79,12 @@ throw new ServerError('not_logged_in'); } - const forceAddMembers = options?.forceAddMembers ?? false; const updatesForCurrentSession = options?.updatesForCurrentSession ?? 'return'; const silentlyFailMembers = options?.silentlyFailMembers ?? false; const threadType = request.type; - const shouldCreateRelationships = - forceAddMembers || threadType === threadTypes.PERSONAL; + const shouldCreateRelationships = options?.forceAddMembers ?? false; let parentThreadID = request.parentThreadID ? request.parentThreadID : null; const initialMemberIDsFromRequest = request.initialMemberIDs && request.initialMemberIDs.length > 0 diff --git a/keyserver/src/responders/thread-responders.js b/keyserver/src/responders/thread-responders.js --- a/keyserver/src/responders/thread-responders.js +++ b/keyserver/src/responders/thread-responders.js @@ -155,6 +155,7 @@ return await createThread(viewer, request, { silentlyFailMembers: request.type === threadTypes.SIDEBAR, + forceAddMembers: true, }); } 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: { @@ -215,4 +217,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 @@ -2,7 +2,6 @@ import classNames from 'classnames'; import invariant from 'invariant'; -import _isEqual from 'lodash/fp/isEqual'; import * as React from 'react'; import { useDrop } from 'react-dnd'; import { NativeTypes } from 'react-dnd-html5-backend'; @@ -17,7 +16,6 @@ threadIsPending, } from 'lib/shared/thread-utils'; import { threadTypes } from 'lib/types/thread-types'; -import type { AccountUserInfo } from 'lib/types/user-types'; import { InputStateContext } from '../input/input-state'; import { updateNavInfoActionType } from '../redux/action-types'; @@ -37,12 +35,32 @@ 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 dispatch = useDispatch(); + React.useEffect(() => { + if (isChatCreation) { + dispatch({ + type: updateNavInfoActionType, + payload: { + selectedUserList: userInfoInputArray.map(user => user.id), + }, + }); + } + }, [dispatch, isChatCreation, userInfoInputArray]); + const viewerID = useSelector(state => state.currentUserInfo?.id); invariant(viewerID, 'should be set'); @@ -105,36 +123,6 @@ ]); 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, - ]); - React.useEffect(() => { if (isChatCreation && activeChatThreadID !== threadInfo?.id) { let payload = { @@ -226,6 +214,7 @@ const chatUserSelection = ( , + +setUserInfoInputArray: ( + ($ReadOnlyArray) => $ReadOnlyArray, + ) => void, +otherUserInfos: { [id: string]: AccountUserInfo }, +threadID: string, +inputState: InputState, @@ -29,12 +35,61 @@ | '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 userSearchIndex = useSelector(userSearchIndexForPotentialMembers); + const userInfos = useSelector(state => state.userStore.userInfos); + const viewerID = useSelector(state => state.currentUserInfo?.id); + const [serverSearchUserInfos, setServerSearchUserInfos] = React.useState<{ + [id: string]: AccountUserInfo, + }>({}); + const callSearchUsers = useServerCall(searchUsers); + React.useEffect(() => { + (async () => { + if (usernameInputText.length === 0) { + setServerSearchUserInfos({}); + } else { + const { userInfos: serverUserInfos } = await callSearchUsers( + usernameInputText, + ); + const result = {}; + for (const user of serverUserInfos) { + if (!(user.id in userInfos)) { + result[user.id] = user; + } + } + const potentialMembers = filterPotentialMembers(result, viewerID); + setServerSearchUserInfos(potentialMembers); + } + })(); + }, [userInfos, callSearchUsers, usernameInputText, viewerID]); + + const { + mergedUserInfos, + userSearchIndex, + }: { + mergedUserInfos: { [id: string]: AccountUserInfo }, + userSearchIndex: SearchIndex, + } = React.useMemo(() => { + const bothUserInfos = { ...serverSearchUserInfos, ...otherUserInfos }; + + const searchIndex = new SearchIndex(); + for (const id in bothUserInfos) { + searchIndex.addEntry(id, bothUserInfos[id].username); + } + + return { + mergedUserInfos: bothUserInfos, + userSearchIndex: searchIndex, + }; + }, [serverSearchUserInfos, otherUserInfos]); const userInfoInputIDs = React.useMemo( () => userInfoInputArray.map(userInfo => userInfo.id), @@ -45,41 +100,31 @@ () => getPotentialMemberItems( usernameInputText, - otherUserInfos, + mergedUserInfos, userSearchIndex, userInfoInputIDs, ), - [usernameInputText, otherUserInfos, userSearchIndex, userInfoInputIDs], + [usernameInputText, mergedUserInfos, userSearchIndex, userInfoInputIDs], ); 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(() => { @@ -96,7 +141,12 @@