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 @@ -1,6 +1,7 @@ // @flow import invariant from 'invariant'; +import _clone from 'lodash/clone.js'; import _find from 'lodash/fp/find.js'; import _mapValues from 'lodash/fp/mapValues.js'; import _omitBy from 'lodash/fp/omitBy.js'; @@ -77,6 +78,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 +96,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 +1626,121 @@ } within this ${communityOrThreadNoun(threadInfo)}`; } +function getChatMentionCandidates(threadInfos: { + +[id: string]: ResolvedThreadInfo, +}): ChatMentionCandidatesObj { + const result = {}; + const visitedGenesisThreads = new Set(); + 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.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 + ) { + const threadObjectFromTraversePath = {}; + for (const threadInfo of threadTraversePath) { + threadObjectFromTraversePath[threadInfo.id] = threadInfo; + result[threadInfo.id] = threadObjectFromTraversePath; + } + } else { + if (!result[lastThreadInTraversePathParentID]) { + result[lastThreadInTraversePathParentID] = {}; + } + for (const threadInfo of threadTraversePath) { + result[lastThreadInTraversePathParentID][threadInfo.id] = threadInfo; + result[threadInfo.id] = result[lastThreadInTraversePathParentID]; + } + } + continue; + } + result[currentThreadCommunity][currentThreadID] = currentThreadInfo; + result[currentThreadID] = result[currentThreadCommunity]; + } + + for (const currentThreadID in threadInfos) { + result[currentThreadID] = _clone(result[currentThreadID]); + delete result[currentThreadID][currentThreadID]; + } + + return result; +} + +function useChatMentionCandidatesObj(): ChatMentionCandidatesObj { + const threadInfos = useSelector(threadInfoSelector); + const resolvedThreadInfos = useResolvedThreadInfosObj(threadInfos); + return React.useMemo( + () => getChatMentionCandidates(resolvedThreadInfos), + [resolvedThreadInfos], + ); +} + +function threadChatMentionCandidates( + threadInfo: ThreadInfo, + chatMentionCandidates: ChatMentionCandidatesObj, +): ChatMentionCandidates { + return ( + chatMentionCandidates[threadInfo.id] ?? + (threadInfo.containingThreadID && + chatMentionCandidates[threadInfo.containingThreadID]) + ); +} + +function useThreadChatMentionCandidates( + threadInfo: ThreadInfo, +): ChatMentionCandidates { + const chatMentionCandidates = useChatMentionCandidatesObj(); + return React.useMemo( + () => threadChatMentionCandidates(threadInfo, chatMentionCandidates), + [chatMentionCandidates, threadInfo], + ); +} + export { threadHasPermission, viewerIsMember, @@ -1689,4 +1809,7 @@ useRoleMemberCountsForCommunity, useRoleUserSurfacedPermissions, getThreadsToDeleteText, + useChatMentionCandidatesObj, + threadChatMentionCandidates, + 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, +};