-
Notifications
You must be signed in to change notification settings - Fork 29
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[lib] Introduce chat mention context
Summary: In the previous implementation, we were correctly memoizing computations, but it was done using hooks, so in each component where these were used, we were doing the same computations again. In this approach, the memoization is moved to a new context, so that computations aren't repeated. Depends on D9465 https://linear.app/comm/issue/ENG-5224/ashoats-js-thread-freezes-on-app-start-on-build-268 Test Plan: Check if `getChatMentionCandidates` function invocations happen every time with new inputs (by adding code that saves the input to a global variable, compares new arguments with the previous ones, and console logs the result). Check if chat mentioning works correctly on the web and native. Reviewers: kamil, inka, atul, ashoat Reviewed By: ashoat Subscribers: wyilio Differential Revision: https://phab.comm.dev/D9466
- Loading branch information
Showing
18 changed files
with
314 additions
and
243 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,227 @@ | ||
// @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 { threadTypes } from '../types/thread-types-enum.js'; | ||
import type { | ||
ChatMentionCandidates, | ||
ChatMentionCandidatesObj, | ||
ResolvedThreadInfo, | ||
ThreadInfo, | ||
} from '../types/thread-types.js'; | ||
import { useResolvedThreadInfosObj } from '../utils/entity-helpers.js'; | ||
import { useSelector } from '../utils/redux-utils.js'; | ||
|
||
type Props = { | ||
+children: React.Node, | ||
}; | ||
export type ChatMentionContextType = { | ||
+getChatMentionSearchIndex: ( | ||
threadInfo: ThreadInfo, | ||
) => SentencePrefixSearchIndex, | ||
+communityThreadIDForGenesisThreads: { +[id: string]: string }, | ||
+chatMentionCandidatesObj: ChatMentionCandidatesObj, | ||
}; | ||
|
||
const emptySearchIndex = new SentencePrefixSearchIndex(); | ||
const ChatMentionContext: React.Context<?ChatMentionContextType> = | ||
React.createContext<?ChatMentionContextType>({ | ||
getChatMentionSearchIndex: () => emptySearchIndex, | ||
communityThreadIDForGenesisThreads: {}, | ||
chatMentionCandidatesObj: {}, | ||
}); | ||
|
||
function ChatMentionContextProvider(props: Props): React.Node { | ||
const { children } = props; | ||
|
||
const { communityThreadIDForGenesisThreads, chatMentionCandidatesObj } = | ||
useChatMentionCandidatesObjAndUtils(); | ||
const searchIndices = useChatMentionSearchIndex(chatMentionCandidatesObj); | ||
|
||
const getChatMentionSearchIndex = React.useCallback( | ||
(threadInfo: ThreadInfo) => { | ||
if (threadInfo.community === genesis.id) { | ||
return searchIndices[communityThreadIDForGenesisThreads[threadInfo.id]]; | ||
} | ||
return searchIndices[threadInfo.community ?? threadInfo.id]; | ||
}, | ||
[communityThreadIDForGenesisThreads, searchIndices], | ||
); | ||
|
||
const value = React.useMemo( | ||
() => ({ | ||
getChatMentionSearchIndex, | ||
communityThreadIDForGenesisThreads, | ||
chatMentionCandidatesObj, | ||
}), | ||
[ | ||
getChatMentionSearchIndex, | ||
communityThreadIDForGenesisThreads, | ||
chatMentionCandidatesObj, | ||
], | ||
); | ||
|
||
return ( | ||
<ChatMentionContext.Provider value={value}> | ||
{children} | ||
</ChatMentionContext.Provider> | ||
); | ||
} | ||
|
||
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; | ||
} | ||
if ( | ||
lastThreadInTraversePath.type !== threadTypes.PERSONAL && | ||
lastThreadInTraversePath.type !== threadTypes.PRIVATE | ||
) { | ||
result[genesis.id][lastThreadInTraversePath.id] = | ||
lastThreadInTraversePath; | ||
} | ||
} 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 useChatMentionSearchIndex( | ||
chatMentionCandidatesObj: ChatMentionCandidatesObj, | ||
): { | ||
+[id: string]: SentencePrefixSearchIndex, | ||
} { | ||
return React.useMemo(() => { | ||
const result = {}; | ||
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].uiName, | ||
}); | ||
} | ||
// 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 } of searchIndexEntries) { | ||
searchIndex.addEntry(id, uiName); | ||
} | ||
result[communityThreadID] = searchIndex; | ||
} | ||
return result; | ||
}, [chatMentionCandidatesObj]); | ||
} | ||
|
||
export { ChatMentionContextProvider, ChatMentionContext }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
// @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 { | ||
ChatMentionCandidates, | ||
ThreadInfo, | ||
} 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 } = | ||
useChatMentionContext(); | ||
return React.useMemo(() => { | ||
const communityID = | ||
threadInfo.community === genesis.id | ||
? communityThreadIDForGenesisThreads[threadInfo.id] | ||
: 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 }; |
Oops, something went wrong.