diff --git a/lib/components/chat-mention-provider.react.js b/lib/components/chat-mention-provider.react.js index 1e6987bed..82a1771da 100644 --- a/lib/components/chat-mention-provider.react.js +++ b/lib/components/chat-mention-provider.react.js @@ -1,279 +1,291 @@ // @flow import * as React from 'react'; import genesis from '../facts/genesis.js'; import { threadInfoSelector } from '../selectors/thread-selectors.js'; import SentencePrefixSearchIndex from '../shared/sentence-prefix-search-index.js'; +import { threadIsPending } from '../shared/thread-utils.js'; import type { ResolvedThreadInfo, ThreadInfo, } from '../types/minimally-encoded-thread-permissions-types.js'; import { threadTypes } from '../types/thread-types-enum.js'; import type { ChatMentionCandidate, ChatMentionCandidatesObj, } from '../types/thread-types.js'; import { useResolvedThreadInfosObj } from '../utils/entity-helpers.js'; import { getNameForThreadEntity } from '../utils/entity-text.js'; import { useSelector } from '../utils/redux-utils.js'; type Props = { +children: React.Node, }; export type ChatMentionContextType = { +getChatMentionSearchIndex: ( threadInfo: ThreadInfo, ) => SentencePrefixSearchIndex, - +communityThreadIDForGenesisThreads: { +[id: string]: string }, + +getCommunityThreadIDForGenesisThreads: (threadInfo: ThreadInfo) => string, +chatMentionCandidatesObj: ChatMentionCandidatesObj, }; const emptySearchIndex = new SentencePrefixSearchIndex(); const ChatMentionContext: React.Context = React.createContext({ getChatMentionSearchIndex: () => emptySearchIndex, - communityThreadIDForGenesisThreads: {}, + getCommunityThreadIDForGenesisThreads: () => '', chatMentionCandidatesObj: {}, }); function ChatMentionContextProvider(props: Props): React.Node { const { children } = props; - const { communityThreadIDForGenesisThreads, chatMentionCandidatesObj } = + const { chatMentionCandidatesObj, getCommunityThreadIDForGenesisThreads } = useChatMentionCandidatesObjAndUtils(); const searchIndices = useChatMentionSearchIndex(chatMentionCandidatesObj); const getChatMentionSearchIndex = React.useCallback( (threadInfo: ThreadInfo) => { if (threadInfo.community === genesis.id) { - return searchIndices[communityThreadIDForGenesisThreads[threadInfo.id]]; + const communityThreadID = + getCommunityThreadIDForGenesisThreads(threadInfo); + return searchIndices[communityThreadID]; } return searchIndices[threadInfo.community ?? threadInfo.id]; }, - [communityThreadIDForGenesisThreads, searchIndices], + [getCommunityThreadIDForGenesisThreads, searchIndices], ); const value = React.useMemo( () => ({ getChatMentionSearchIndex, - communityThreadIDForGenesisThreads, + getCommunityThreadIDForGenesisThreads, chatMentionCandidatesObj, }), [ getChatMentionSearchIndex, - communityThreadIDForGenesisThreads, + getCommunityThreadIDForGenesisThreads, chatMentionCandidatesObj, ], ); return ( {children} ); } function getChatMentionCandidates( threadInfos: { +[id: string]: ThreadInfo }, resolvedThreadInfos: { +[id: string]: ResolvedThreadInfo, }, ): { chatMentionCandidatesObj: ChatMentionCandidatesObj, communityThreadIDForGenesisThreads: { +[id: string]: string }, } { const result: { [string]: { [string]: ChatMentionCandidate, }, } = {}; const visitedGenesisThreads = new Set(); const communityThreadIDForGenesisThreads: { [string]: string } = {}; for (const currentThreadID in resolvedThreadInfos) { const currentResolvedThreadInfo = resolvedThreadInfos[currentThreadID]; const { community: currentThreadCommunity } = currentResolvedThreadInfo; if (!currentThreadCommunity) { if (!result[currentThreadID]) { result[currentThreadID] = { [currentThreadID]: { threadInfo: currentResolvedThreadInfo, rawChatName: threadInfos[currentThreadID].uiName, }, }; } continue; } if (!result[currentThreadCommunity]) { result[currentThreadCommunity] = {}; result[currentThreadCommunity][currentThreadCommunity] = { threadInfo: resolvedThreadInfos[currentThreadCommunity], rawChatName: threadInfos[currentThreadCommunity].uiName, }; } // Handle GENESIS community case: mentioning inside GENESIS should only // show chats and threads inside the top level that is below GENESIS. if ( resolvedThreadInfos[currentThreadCommunity].type === threadTypes.GENESIS ) { if (visitedGenesisThreads.has(currentThreadID)) { continue; } const threadTraversePath = [currentResolvedThreadInfo]; visitedGenesisThreads.add(currentThreadID); let currentlySelectedThreadID = currentResolvedThreadInfo.parentThreadID; while (currentlySelectedThreadID) { const currentlySelectedThreadInfo = resolvedThreadInfos[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 = resolvedThreadInfos[ lastThreadInTraversePath.parentThreadID ] ? lastThreadInTraversePath.parentThreadID : lastThreadInTraversePath.id; } else { lastThreadInTraversePathParentID = lastThreadInTraversePath.id; } if ( resolvedThreadInfos[lastThreadInTraversePathParentID].type === threadTypes.GENESIS ) { if (!result[lastThreadInTraversePath.id]) { result[lastThreadInTraversePath.id] = {}; } for (const threadInfo of threadTraversePath) { result[lastThreadInTraversePath.id][threadInfo.id] = { threadInfo, rawChatName: threadInfos[threadInfo.id].uiName, }; communityThreadIDForGenesisThreads[threadInfo.id] = lastThreadInTraversePath.id; } if ( lastThreadInTraversePath.type !== threadTypes.PERSONAL && lastThreadInTraversePath.type !== threadTypes.PRIVATE ) { result[genesis.id][lastThreadInTraversePath.id] = { threadInfo: lastThreadInTraversePath, rawChatName: threadInfos[lastThreadInTraversePath.id].uiName, }; } } else { if ( !communityThreadIDForGenesisThreads[lastThreadInTraversePathParentID] ) { result[lastThreadInTraversePathParentID] = {}; communityThreadIDForGenesisThreads[lastThreadInTraversePathParentID] = lastThreadInTraversePathParentID; } const lastThreadInTraversePathParentCommunityThreadID = communityThreadIDForGenesisThreads[lastThreadInTraversePathParentID]; for (const threadInfo of threadTraversePath) { result[lastThreadInTraversePathParentCommunityThreadID][ threadInfo.id ] = { threadInfo, rawChatName: threadInfos[threadInfo.id].uiName, }; communityThreadIDForGenesisThreads[threadInfo.id] = lastThreadInTraversePathParentCommunityThreadID; } } continue; } result[currentThreadCommunity][currentThreadID] = { threadInfo: currentResolvedThreadInfo, rawChatName: threadInfos[currentThreadID].uiName, }; } return { chatMentionCandidatesObj: result, communityThreadIDForGenesisThreads, }; } // Without allAtOnce, useChatMentionCandidatesObjAndUtils is very expensive. // useResolvedThreadInfosObj would trigger its recalculation for each ENS name // as it streams in, but we would prefer to trigger its recaculation just once // for every update of the underlying Redux data. const useResolvedThreadInfosObjOptions = { allAtOnce: true }; function useChatMentionCandidatesObjAndUtils(): { chatMentionCandidatesObj: ChatMentionCandidatesObj, - resolvedThreadInfos: { - +[id: string]: ResolvedThreadInfo, - }, - communityThreadIDForGenesisThreads: { +[id: string]: string }, + getCommunityThreadIDForGenesisThreads: (threadInfo: ThreadInfo) => string, } { const threadInfos = useSelector(threadInfoSelector); const resolvedThreadInfos = useResolvedThreadInfosObj( threadInfos, useResolvedThreadInfosObjOptions, ); const { chatMentionCandidatesObj, communityThreadIDForGenesisThreads } = React.useMemo( () => getChatMentionCandidates(threadInfos, resolvedThreadInfos), [threadInfos, resolvedThreadInfos], ); + + const getCommunityThreadIDForGenesisThreads = React.useCallback( + (threadInfo: ThreadInfo): string => { + const threadID = + threadIsPending(threadInfo.id) && threadInfo.containingThreadID + ? threadInfo.containingThreadID + : threadInfo.id; + + return communityThreadIDForGenesisThreads[threadID]; + }, + [communityThreadIDForGenesisThreads], + ); + return { chatMentionCandidatesObj, - resolvedThreadInfos, - communityThreadIDForGenesisThreads, + getCommunityThreadIDForGenesisThreads, }; } function useChatMentionSearchIndex( chatMentionCandidatesObj: ChatMentionCandidatesObj, ): { +[id: string]: SentencePrefixSearchIndex, } { return React.useMemo(() => { const result: { [string]: SentencePrefixSearchIndex } = {}; for (const communityThreadID in chatMentionCandidatesObj) { const searchIndex = new SentencePrefixSearchIndex(); const searchIndexEntries = []; for (const threadID in chatMentionCandidatesObj[communityThreadID]) { searchIndexEntries.push({ id: threadID, uiName: chatMentionCandidatesObj[communityThreadID][threadID].threadInfo .uiName, rawChatName: chatMentionCandidatesObj[communityThreadID][threadID].rawChatName, }); } // Sort the keys so that the order of the search result is consistent searchIndexEntries.sort(({ uiName: uiNameA }, { uiName: uiNameB }) => uiNameA.localeCompare(uiNameB), ); for (const { id, uiName, rawChatName } of searchIndexEntries) { const names = [uiName]; if (rawChatName) { typeof rawChatName === 'string' ? names.push(rawChatName) : names.push(getNameForThreadEntity(rawChatName)); } searchIndex.addEntry(id, names.join(' ')); } result[communityThreadID] = searchIndex; } return result; }, [chatMentionCandidatesObj]); } export { ChatMentionContextProvider, ChatMentionContext }; diff --git a/lib/hooks/chat-mention-hooks.js b/lib/hooks/chat-mention-hooks.js index 64e1db2a6..be423228b 100644 --- a/lib/hooks/chat-mention-hooks.js +++ b/lib/hooks/chat-mention-hooks.js @@ -1,45 +1,50 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { ChatMentionContext, type ChatMentionContextType, } from '../components/chat-mention-provider.react.js'; import genesis from '../facts/genesis.js'; import type { ThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js'; import type { ChatMentionCandidates } from '../types/thread-types.js'; function useChatMentionContext(): ChatMentionContextType { const context = React.useContext(ChatMentionContext); invariant(context, 'ChatMentionContext not found'); return context; } function useThreadChatMentionCandidates( threadInfo: ThreadInfo, ): ChatMentionCandidates { - const { communityThreadIDForGenesisThreads, chatMentionCandidatesObj } = + const { getCommunityThreadIDForGenesisThreads, chatMentionCandidatesObj } = useChatMentionContext(); + + const communityThreadIDForGenesisThreads = + getCommunityThreadIDForGenesisThreads(threadInfo); + return React.useMemo(() => { const communityID = threadInfo.community === genesis.id - ? communityThreadIDForGenesisThreads[threadInfo.id] + ? communityThreadIDForGenesisThreads : threadInfo.community ?? threadInfo.id; + const allChatsWithinCommunity = chatMentionCandidatesObj[communityID]; if (!allChatsWithinCommunity) { return {}; } const { [threadInfo.id]: _, ...result } = allChatsWithinCommunity; return result; }, [ chatMentionCandidatesObj, communityThreadIDForGenesisThreads, threadInfo.community, threadInfo.id, ]); } export { useThreadChatMentionCandidates, useChatMentionContext };