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 @@ -23,6 +23,7 @@ ChatMessageInfoItem, MessageListData, } from '../selectors/chat-selectors.js'; +import { relationshipBlockedInEitherDirection } from '../shared/relationship-utils.js'; import type { MessageInfo, RawMessageInfo } from '../types/message-types.js'; import { userRelationshipStatus } from '../types/relationship-types.js'; import { threadPermissions } from '../types/thread-permission-types.js'; @@ -43,7 +44,49 @@ const notFriendNotice = 'not friend'; -function getPotentialMemberItems({ +function appendUserInfo({ + results, + excludeUserIDs, + userInfo, + parentThreadInfo, + communityThreadInfo, + containingThreadInfo, +}: { + +results: { + [id: string]: { + ...AccountUserInfo | GlobalAccountUserInfo, + isMemberOfParentThread: boolean, + isMemberOfContainingThread: boolean, + }, + }, + +excludeUserIDs: $ReadOnlyArray, + +userInfo: AccountUserInfo | GlobalAccountUserInfo, + +parentThreadInfo: ?ThreadInfo, + +communityThreadInfo: ?ThreadInfo, + +containingThreadInfo: ?ThreadInfo, +}) { + const { id } = userInfo; + if (excludeUserIDs.includes(id) || id in results) { + return; + } + if ( + communityThreadInfo && + !threadMemberHasPermission( + communityThreadInfo, + id, + threadPermissions.KNOW_OF, + ) + ) { + return; + } + results[id] = { + ...userInfo, + isMemberOfParentThread: userIsMember(parentThreadInfo, id), + isMemberOfContainingThread: userIsMember(containingThreadInfo, id), + }; +} + +function usePotentialMemberItems({ text, userInfos, searchIndex, @@ -62,185 +105,202 @@ +inputCommunityThreadInfo?: ?ThreadInfo, +threadType?: ?ThreadType, }): UserListItem[] { - const communityThreadInfo = - inputCommunityThreadInfo && inputCommunityThreadInfo.id !== genesis.id - ? inputCommunityThreadInfo - : null; - const parentThreadInfo = - inputParentThreadInfo && inputParentThreadInfo.id !== genesis.id - ? inputParentThreadInfo - : null; + const communityThreadInfo = React.useMemo( + () => + inputCommunityThreadInfo && inputCommunityThreadInfo.id !== genesis.id + ? inputCommunityThreadInfo + : null, + [inputCommunityThreadInfo], + ); + const parentThreadInfo = React.useMemo( + () => + inputParentThreadInfo && inputParentThreadInfo.id !== genesis.id + ? inputParentThreadInfo + : null, + [inputParentThreadInfo], + ); const containgThreadID = threadType ? getContainingThreadID(parentThreadInfo, threadType) : null; - let containingThreadInfo = null; - if (containgThreadID === parentThreadInfo?.id) { - containingThreadInfo = parentThreadInfo; - } else if (containgThreadID === communityThreadInfo?.id) { - containingThreadInfo = communityThreadInfo; - } - - const results: { - [id: string]: { - ...AccountUserInfo | GlobalAccountUserInfo, - isMemberOfParentThread: boolean, - isMemberOfContainingThread: boolean, - }, - } = {}; - const appendUserInfo = ( - userInfo: AccountUserInfo | GlobalAccountUserInfo, - ) => { - const { id } = userInfo; - if (excludeUserIDs.includes(id) || id in results) { - return; - } - if ( - communityThreadInfo && - !threadMemberHasPermission( - communityThreadInfo, - id, - threadPermissions.KNOW_OF, - ) - ) { - return; + const containingThreadInfo = React.useMemo(() => { + if (containgThreadID === parentThreadInfo?.id) { + return parentThreadInfo; + } else if (containgThreadID === communityThreadInfo?.id) { + return communityThreadInfo; } - results[id] = { - ...userInfo, - isMemberOfParentThread: userIsMember(parentThreadInfo, id), - isMemberOfContainingThread: userIsMember(containingThreadInfo, id), - }; - }; - if (text === '') { - for (const id in userInfos) { - appendUserInfo(userInfos[id]); + return null; + }, [containgThreadID, communityThreadInfo, parentThreadInfo]); + + const filteredUserResults = React.useMemo(() => { + const results: { + [id: string]: { + ...AccountUserInfo | GlobalAccountUserInfo, + isMemberOfParentThread: boolean, + isMemberOfContainingThread: boolean, + }, + } = {}; + if (text === '') { + for (const id in userInfos) { + appendUserInfo({ + results, + excludeUserIDs, + userInfo: userInfos[id], + parentThreadInfo, + communityThreadInfo, + containingThreadInfo, + }); + } + } else { + const ids = searchIndex.getSearchResults(text); + for (const id of ids) { + appendUserInfo({ + results, + excludeUserIDs, + userInfo: userInfos[id], + parentThreadInfo, + communityThreadInfo, + containingThreadInfo, + }); + } } - } else { - const ids = searchIndex.getSearchResults(text); - for (const id of ids) { - appendUserInfo(userInfos[id]); + + if (includeServerSearchUsers) { + for (const userInfo of includeServerSearchUsers) { + appendUserInfo({ + results, + excludeUserIDs, + userInfo, + parentThreadInfo, + communityThreadInfo, + containingThreadInfo, + }); + } } - } - if (includeServerSearchUsers) { - for (const userInfo of includeServerSearchUsers) { - appendUserInfo(userInfo); + let userResults = values(results); + if (text === '') { + userResults = userResults.filter(userInfo => { + if (!containingThreadInfo) { + return userInfo.relationshipStatus === userRelationshipStatus.FRIEND; + } + if (!userInfo.isMemberOfContainingThread) { + return false; + } + const { relationshipStatus } = userInfo; + if (!relationshipStatus) { + return true; + } + return !relationshipBlockedInEitherDirection(relationshipStatus); + }); } - } - const blockedRelationshipsStatuses = new Set([ - userRelationshipStatus.BLOCKED_BY_VIEWER, - userRelationshipStatus.BLOCKED_VIEWER, - userRelationshipStatus.BOTH_BLOCKED, + return userResults; + }, [ + text, + userInfos, + searchIndex, + excludeUserIDs, + includeServerSearchUsers, + parentThreadInfo, + containingThreadInfo, + communityThreadInfo, ]); - let userResults = values(results); - if (text === '') { - userResults = userResults.filter(userInfo => { - if (!containingThreadInfo) { - return userInfo.relationshipStatus === userRelationshipStatus.FRIEND; - } - if (!userInfo.isMemberOfContainingThread) { - return false; - } - const { relationshipStatus } = userInfo; - if (!relationshipStatus) { - return true; - } - return !blockedRelationshipsStatuses.has(relationshipStatus); - }); - } - - const nonFriends = []; - const blockedUsers = []; - const friends = []; - const containingThreadMembers = []; - const parentThreadMembers = []; - - for (const userResult of userResults) { - const { relationshipStatus } = userResult; - if ( - relationshipStatus && - blockedRelationshipsStatuses.has(relationshipStatus) - ) { - blockedUsers.push(userResult); - } else if (userResult.isMemberOfParentThread) { - parentThreadMembers.push(userResult); - } else if (userResult.isMemberOfContainingThread) { - containingThreadMembers.push(userResult); - } else if (relationshipStatus === userRelationshipStatus.FRIEND) { - friends.push(userResult); - } else { - nonFriends.push(userResult); - } - } + const sortedMembers = React.useMemo(() => { + const nonFriends = []; + const blockedUsers = []; + const friends = []; + const containingThreadMembers = []; + const parentThreadMembers = []; - const sortedResults = parentThreadMembers - .concat(containingThreadMembers) - .concat(friends) - .concat(nonFriends) - .concat(blockedUsers); - - return sortedResults.map( - ({ - isMemberOfContainingThread, - isMemberOfParentThread, - relationshipStatus, - ...result - }) => { - let notice, alert; - const username = result.username; + for (const userResult of filteredUserResults) { + const { relationshipStatus } = userResult; if ( relationshipStatus && - blockedRelationshipsStatuses.has(relationshipStatus) + relationshipBlockedInEitherDirection(relationshipStatus) ) { - notice = 'user is blocked'; - alert = { - title: 'User is blocked', - text: - `Before you add ${username} to this chat, ` + - 'you’ll need to unblock them. You can do this from the Block List ' + - 'in the Profile tab.', - }; - } else if (!isMemberOfContainingThread && containingThreadInfo) { - if (threadType !== threadTypes.SIDEBAR) { - notice = 'not in community'; + blockedUsers.push(userResult); + } else if (userResult.isMemberOfParentThread) { + parentThreadMembers.push(userResult); + } else if (userResult.isMemberOfContainingThread) { + containingThreadMembers.push(userResult); + } else if (relationshipStatus === userRelationshipStatus.FRIEND) { + friends.push(userResult); + } else { + nonFriends.push(userResult); + } + } + + const sortedResults = parentThreadMembers + .concat(containingThreadMembers) + .concat(friends) + .concat(nonFriends) + .concat(blockedUsers); + + return sortedResults.map( + ({ + isMemberOfContainingThread, + isMemberOfParentThread, + relationshipStatus, + ...result + }) => { + let notice, alert; + const username = result.username; + if ( + relationshipStatus && + relationshipBlockedInEitherDirection(relationshipStatus) + ) { + notice = 'user is blocked'; alert = { - title: 'Not in community', - text: 'You can only add members of the community to this chat', + title: 'User is blocked', + text: + `Before you add ${username} to this chat, ` + + 'you’ll need to unblock them. You can do this from the Block List ' + + 'in the Profile tab.', }; - } else { - notice = 'not in parent chat'; + } else if (!isMemberOfContainingThread && containingThreadInfo) { + if (threadType !== threadTypes.SIDEBAR) { + notice = 'not in community'; + alert = { + title: 'Not in community', + text: 'You can only add members of the community to this chat', + }; + } else { + notice = 'not in parent chat'; + alert = { + title: 'Not in parent chat', + text: 'You can only add members of the parent chat to a thread', + }; + } + } else if ( + !containingThreadInfo && + relationshipStatus !== userRelationshipStatus.FRIEND + ) { + notice = notFriendNotice; alert = { - title: 'Not in parent chat', - text: 'You can only add members of the parent chat to a thread', + title: 'Not a friend', + text: + `Before you add ${username} to this chat, ` + + 'you’ll need to send them a friend request. ' + + 'You can do this from the Friend List in the Profile tab.', }; + } else if (parentThreadInfo && !isMemberOfParentThread) { + notice = 'not in parent chat'; } - } else if ( - !containingThreadInfo && - relationshipStatus !== userRelationshipStatus.FRIEND - ) { - notice = notFriendNotice; - alert = { - title: 'Not a friend', - text: - `Before you add ${username} to this chat, ` + - 'you’ll need to send them a friend request. ' + - 'You can do this from the Friend List in the Profile tab.', - }; - } else if (parentThreadInfo && !isMemberOfParentThread) { - notice = 'not in parent chat'; - } - if (notice) { - result = { ...result, notice }; - } - if (alert) { - result = { ...result, alert }; - } - return result; - }, - ); + if (notice) { + result = { ...result, notice }; + } + if (alert) { + result = { ...result, alert }; + } + return result; + }, + ); + }, [containingThreadInfo, filteredUserResults, parentThreadInfo, threadType]); + + return sortedMembers; } function useSearchMessages(): ( @@ -382,7 +442,7 @@ } export { - getPotentialMemberItems, + usePotentialMemberItems, notFriendNotice, useSearchMessages, useSearchUsers, diff --git a/native/chat/compose-subchannel.react.js b/native/chat/compose-subchannel.react.js --- a/native/chat/compose-subchannel.react.js +++ b/native/chat/compose-subchannel.react.js @@ -17,7 +17,7 @@ userInfoSelectorForPotentialMembers, userSearchIndexForPotentialMembers, } from 'lib/selectors/user-selectors.js'; -import { getPotentialMemberItems } from 'lib/shared/search-utils.js'; +import { usePotentialMemberItems } from 'lib/shared/search-utils.js'; import { threadInFilterList, userIsMember } from 'lib/shared/thread-utils.js'; import { type ThreadType, threadTypes } from 'lib/types/thread-types-enum.js'; import type { ThreadInfo } from 'lib/types/thread-types.js'; @@ -200,27 +200,15 @@ const communityThreadInfo = useSelector(state => community ? threadInfoSelector(state)[community] : null, ); - const userSearchResults = React.useMemo( - () => - getPotentialMemberItems({ - text: usernameInputText, - userInfos: otherUserInfos, - searchIndex: userSearchIndex, - excludeUserIDs: userInfoInputIDs, - inputParentThreadInfo: parentThreadInfo, - inputCommunityThreadInfo: communityThreadInfo, - threadType, - }), - [ - usernameInputText, - otherUserInfos, - userSearchIndex, - userInfoInputIDs, - parentThreadInfo, - communityThreadInfo, - threadType, - ], - ); + const userSearchResults = usePotentialMemberItems({ + text: usernameInputText, + userInfos: otherUserInfos, + searchIndex: userSearchIndex, + excludeUserIDs: userInfoInputIDs, + inputParentThreadInfo: parentThreadInfo, + inputCommunityThreadInfo: communityThreadInfo, + threadType, + }); const existingThreads: $ReadOnlyArray = React.useMemo(() => { if (userInfoInputIDs.length === 0) { diff --git a/native/chat/message-list-container.react.js b/native/chat/message-list-container.react.js --- a/native/chat/message-list-container.react.js +++ b/native/chat/message-list-container.react.js @@ -13,7 +13,7 @@ userSearchIndexForPotentialMembers, } from 'lib/selectors/user-selectors.js'; import { - getPotentialMemberItems, + usePotentialMemberItems, useSearchUsers, } from 'lib/shared/search-utils.js'; import { @@ -253,21 +253,13 @@ const serverSearchResults = useSearchUsers(usernameInputText); - const userSearchResults = React.useMemo(() => { - return getPotentialMemberItems({ - text: usernameInputText, - userInfos: otherUserInfos, - searchIndex: userSearchIndex, - excludeUserIDs: userInfoInputArray.map(userInfo => userInfo.id), - includeServerSearchUsers: serverSearchResults, - }); - }, [ - usernameInputText, - otherUserInfos, - userSearchIndex, - userInfoInputArray, - serverSearchResults, - ]); + const userSearchResults = usePotentialMemberItems({ + text: usernameInputText, + userInfos: otherUserInfos, + searchIndex: userSearchIndex, + excludeUserIDs: userInfoInputArray.map(userInfo => userInfo.id), + includeServerSearchUsers: serverSearchResults, + }); const [baseThreadInfo, setBaseThreadInfo] = React.useState( props.route.params.threadInfo, diff --git a/native/chat/settings/add-users-modal.react.js b/native/chat/settings/add-users-modal.react.js --- a/native/chat/settings/add-users-modal.react.js +++ b/native/chat/settings/add-users-modal.react.js @@ -14,7 +14,7 @@ userInfoSelectorForPotentialMembers, userSearchIndexForPotentialMembers, } from 'lib/selectors/user-selectors.js'; -import { getPotentialMemberItems } from 'lib/shared/search-utils.js'; +import { usePotentialMemberItems } from 'lib/shared/search-utils.js'; import { threadActualMembers } from 'lib/shared/thread-utils.js'; import type { ThreadInfo } from 'lib/types/thread-types.js'; import { type AccountUserInfo } from 'lib/types/user-types.js'; @@ -173,27 +173,15 @@ const communityThreadInfo = useSelector(state => community ? threadInfoSelector(state)[community] : null, ); - const userSearchResults = React.useMemo( - () => - getPotentialMemberItems({ - text: usernameInputText, - userInfos: otherUserInfos, - searchIndex: userSearchIndex, - excludeUserIDs, - inputParentThreadInfo: parentThreadInfo, - inputCommunityThreadInfo: communityThreadInfo, - threadType: threadInfo.type, - }), - [ - usernameInputText, - otherUserInfos, - userSearchIndex, - excludeUserIDs, - parentThreadInfo, - communityThreadInfo, - threadInfo.type, - ], - ); + const userSearchResults = usePotentialMemberItems({ + text: usernameInputText, + userInfos: otherUserInfos, + searchIndex: userSearchIndex, + excludeUserIDs, + inputParentThreadInfo: parentThreadInfo, + inputCommunityThreadInfo: communityThreadInfo, + threadType: threadInfo.type, + }); const onChangeTagInput = React.useCallback( (newUserInfoInputArray: $ReadOnlyArray) => { diff --git a/native/community-creation/community-creation-members.react.js b/native/community-creation/community-creation-members.react.js --- a/native/community-creation/community-creation-members.react.js +++ b/native/community-creation/community-creation-members.react.js @@ -13,7 +13,7 @@ userInfoSelectorForPotentialMembers, userSearchIndexForPotentialMembers, } from 'lib/selectors/user-selectors.js'; -import { getPotentialMemberItems } from 'lib/shared/search-utils.js'; +import { usePotentialMemberItems } from 'lib/shared/search-utils.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import { threadTypes } from 'lib/types/thread-types-enum.js'; import type { AccountUserInfo } from 'lib/types/user-types.js'; @@ -144,25 +144,15 @@ setOptions, ]); - const userSearchResults = React.useMemo( - () => - getPotentialMemberItems({ - text: usernameInputText, - userInfos: otherUserInfos, - searchIndex: userSearchIndex, - excludeUserIDs: selectedUserIDs, - threadType: announcement - ? threadTypes.COMMUNITY_ANNOUNCEMENT_ROOT - : threadTypes.COMMUNITY_ROOT, - }), - [ - announcement, - otherUserInfos, - selectedUserIDs, - userSearchIndex, - usernameInputText, - ], - ); + const userSearchResults = usePotentialMemberItems({ + text: usernameInputText, + userInfos: otherUserInfos, + searchIndex: userSearchIndex, + excludeUserIDs: selectedUserIDs, + threadType: announcement + ? threadTypes.COMMUNITY_ANNOUNCEMENT_ROOT + : threadTypes.COMMUNITY_ROOT, + }); const onSelectUser = React.useCallback( ({ id }: AccountUserInfo) => { diff --git a/web/chat/chat-thread-composer.react.js b/web/chat/chat-thread-composer.react.js --- a/web/chat/chat-thread-composer.react.js +++ b/web/chat/chat-thread-composer.react.js @@ -11,7 +11,7 @@ import { useENSNames } from 'lib/hooks/ens-cache.js'; import { userSearchIndexForPotentialMembers } from 'lib/selectors/user-selectors.js'; import { - getPotentialMemberItems, + usePotentialMemberItems, useSearchUsers, notFriendNotice, } from 'lib/shared/search-utils.js'; @@ -60,23 +60,13 @@ const serverSearchResults = useSearchUsers(usernameInputText); - const userListItems = React.useMemo( - () => - getPotentialMemberItems({ - text: usernameInputText, - userInfos: otherUserInfos, - searchIndex: userSearchIndex, - excludeUserIDs: userInfoInputIDs, - includeServerSearchUsers: serverSearchResults, - }), - [ - usernameInputText, - otherUserInfos, - userSearchIndex, - userInfoInputIDs, - serverSearchResults, - ], - ); + const userListItems = usePotentialMemberItems({ + text: usernameInputText, + userInfos: otherUserInfos, + searchIndex: userSearchIndex, + excludeUserIDs: userInfoInputIDs, + includeServerSearchUsers: serverSearchResults, + }); const userListItemsWithENSNames = useENSNames(userListItems); diff --git a/web/modals/threads/members/add-members-modal.react.js b/web/modals/threads/members/add-members-modal.react.js --- a/web/modals/threads/members/add-members-modal.react.js +++ b/web/modals/threads/members/add-members-modal.react.js @@ -12,7 +12,7 @@ userSearchIndexForPotentialMembers, userInfoSelectorForPotentialMembers, } from 'lib/selectors/user-selectors.js'; -import { getPotentialMemberItems } from 'lib/shared/search-utils.js'; +import { usePotentialMemberItems } from 'lib/shared/search-utils.js'; import { threadActualMembers } from 'lib/shared/thread-utils.js'; import { useDispatchActionPromise } from 'lib/utils/action-utils.js'; @@ -54,27 +54,15 @@ [pendingUsersToAdd, threadInfo.members], ); - const userSearchResults = React.useMemo( - () => - getPotentialMemberItems({ - text: searchText, - userInfos: otherUserInfos, - searchIndex: userSearchIndex, - excludeUserIDs, - inputParentThreadInfo: parentThreadInfo, - inputCommunityThreadInfo: communityThreadInfo, - threadType: threadInfo.type, - }), - [ - communityThreadInfo, - excludeUserIDs, - otherUserInfos, - parentThreadInfo, - searchText, - threadInfo.type, - userSearchIndex, - ], - ); + const userSearchResults = usePotentialMemberItems({ + text: searchText, + userInfos: otherUserInfos, + searchIndex: userSearchIndex, + excludeUserIDs, + inputParentThreadInfo: parentThreadInfo, + inputCommunityThreadInfo: communityThreadInfo, + threadType: threadInfo.type, + }); const userSearchResultsWithENSNames = useENSNames(userSearchResults); const onSwitchUser = React.useCallback(