diff --git a/lib/shared/thread-utils.js b/lib/shared/thread-utils.js --- a/lib/shared/thread-utils.js +++ b/lib/shared/thread-utils.js @@ -77,6 +77,9 @@ type ClientNewThreadRequest, type NewThreadResult, type ChangeThreadSettingsPayload, + type ResolvedThreadInfo, + type ChatMentionCandidatesObj, + type ChatMentionCandidates, } from '../types/thread-types.js'; import { updateTypes } from '../types/update-types-enum.js'; import { type ClientUpdateInfo } from '../types/update-types.js'; @@ -92,6 +95,7 @@ } from '../utils/action-utils.js'; import type { DispatchActionPromise } from '../utils/action-utils.js'; import type { GetENSNames } from '../utils/ens-helpers.js'; +import { useResolvedThreadInfosObj } from '../utils/entity-helpers.js'; import { ET, entityTextToRawString, @@ -1621,6 +1625,154 @@ } within this ${communityOrThreadNoun(threadInfo)}`; } +function getChatMentionCandidates(threadInfos: { + +[id: string]: ResolvedThreadInfo, +}): { + chatMentionCandidatesObj: ChatMentionCandidatesObj, + communityThreadIDForGenesisThreads: { +[id: string]: string }, +} { + const result = {}; + const visitedGenesisThreads = new Set(); + const communityThreadIDForGenesisThreads = {}; + for (const currentThreadID in threadInfos) { + const currentThreadInfo = threadInfos[currentThreadID]; + const { community: currentThreadCommunity } = currentThreadInfo; + if (!currentThreadCommunity) { + if (!result[currentThreadID]) { + result[currentThreadID] = { [currentThreadID]: currentThreadInfo }; + } + continue; + } + if (!result[currentThreadCommunity]) { + result[currentThreadCommunity] = { + [currentThreadCommunity]: threadInfos[currentThreadCommunity], + }; + } + // Handle GENESIS community case: mentioning inside GENESIS should only + // show chats and threads inside the top level that is below GENESIS. + if (threadInfos[currentThreadCommunity].type === threadTypes.GENESIS) { + if (visitedGenesisThreads.has(currentThreadID)) { + continue; + } + const threadTraversePath = [currentThreadInfo]; + visitedGenesisThreads.add(currentThreadID); + let currentlySelectedThreadID = currentThreadInfo.parentThreadID; + while (currentlySelectedThreadID) { + const currentlySelectedThreadInfo = + threadInfos[currentlySelectedThreadID]; + if ( + visitedGenesisThreads.has(currentlySelectedThreadID) || + !currentlySelectedThreadInfo || + currentlySelectedThreadInfo.type === threadTypes.GENESIS + ) { + break; + } + threadTraversePath.push(currentlySelectedThreadInfo); + visitedGenesisThreads.add(currentlySelectedThreadID); + currentlySelectedThreadID = currentlySelectedThreadInfo.parentThreadID; + } + const lastThreadInTraversePath = + threadTraversePath[threadTraversePath.length - 1]; + let lastThreadInTraversePathParentID; + if (lastThreadInTraversePath.parentThreadID) { + lastThreadInTraversePathParentID = threadInfos[ + lastThreadInTraversePath.parentThreadID + ] + ? lastThreadInTraversePath.parentThreadID + : lastThreadInTraversePath.id; + } else { + lastThreadInTraversePathParentID = lastThreadInTraversePath.id; + } + if ( + threadInfos[lastThreadInTraversePathParentID].type === + threadTypes.GENESIS + ) { + if (!result[lastThreadInTraversePath.id]) { + result[lastThreadInTraversePath.id] = {}; + } + for (const threadInfo of threadTraversePath) { + result[lastThreadInTraversePath.id][threadInfo.id] = threadInfo; + communityThreadIDForGenesisThreads[threadInfo.id] = + lastThreadInTraversePath.id; + } + } else { + if ( + !communityThreadIDForGenesisThreads[lastThreadInTraversePathParentID] + ) { + result[lastThreadInTraversePathParentID] = {}; + communityThreadIDForGenesisThreads[lastThreadInTraversePathParentID] = + lastThreadInTraversePathParentID; + } + const lastThreadInTraversePathParentCommunityThreadID = + communityThreadIDForGenesisThreads[lastThreadInTraversePathParentID]; + for (const threadInfo of threadTraversePath) { + result[lastThreadInTraversePathParentCommunityThreadID][ + threadInfo.id + ] = threadInfo; + communityThreadIDForGenesisThreads[threadInfo.id] = + lastThreadInTraversePathParentCommunityThreadID; + } + } + continue; + } + result[currentThreadCommunity][currentThreadID] = currentThreadInfo; + } + return { + chatMentionCandidatesObj: result, + communityThreadIDForGenesisThreads, + }; +} + +function useChatMentionCandidatesObjAndUtils(): { + chatMentionCandidatesObj: ChatMentionCandidatesObj, + resolvedThreadInfos: ChatMentionCandidates, + communityThreadIDForGenesisThreads: { +[id: string]: string }, +} { + const threadInfos = useSelector(threadInfoSelector); + const resolvedThreadInfos = useResolvedThreadInfosObj(threadInfos); + const { chatMentionCandidatesObj, communityThreadIDForGenesisThreads } = + React.useMemo( + () => getChatMentionCandidates(resolvedThreadInfos), + [resolvedThreadInfos], + ); + return { + chatMentionCandidatesObj, + resolvedThreadInfos, + communityThreadIDForGenesisThreads, + }; +} + +function useChatMentionCandidatesObj(): ChatMentionCandidatesObj { + const { chatMentionCandidatesObj } = useChatMentionCandidatesObjAndUtils(); + return chatMentionCandidatesObj; +} + +function useThreadChatMentionCandidates( + threadInfo: ThreadInfo, +): ChatMentionCandidates { + const { chatMentionCandidatesObj, communityThreadIDForGenesisThreads } = + useChatMentionCandidatesObjAndUtils(); + return React.useMemo(() => { + let communityID, + result = {}; + if (threadInfo.community === genesis.id) { + communityID = communityThreadIDForGenesisThreads[threadInfo.id]; + } else { + communityID = threadInfo.community ?? threadInfo.id; + } + if (chatMentionCandidatesObj[communityID]) { + result = { ...chatMentionCandidatesObj[communityID] }; + } + delete result[threadInfo.id]; + return result; + }, [ + chatMentionCandidatesObj, + communityThreadIDForGenesisThreads, + threadInfo.community, + threadInfo.id, + ]); +} + export { threadHasPermission, viewerIsMember, @@ -1689,4 +1841,6 @@ useRoleMemberCountsForCommunity, useRoleUserSurfacedPermissions, getThreadsToDeleteText, + useChatMentionCandidatesObj, + useThreadChatMentionCandidates, }; diff --git a/lib/types/thread-types.js b/lib/types/thread-types.js --- a/lib/types/thread-types.js +++ b/lib/types/thread-types.js @@ -456,3 +456,8 @@ export const maxUnreadSidebars = 5; export type ThreadStoreThreadInfos = { +[id: string]: RawThreadInfo }; + +export type ChatMentionCandidates = { +[id: string]: ResolvedThreadInfo }; +export type ChatMentionCandidatesObj = { + +[id: string]: ChatMentionCandidates, +};