Skip to content

Commit

Permalink
[lib] Introduce chat mention context
Browse files Browse the repository at this point in the history
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
palys-swm committed Oct 19, 2023
1 parent 0fe8423 commit 5011d6a
Show file tree
Hide file tree
Showing 18 changed files with 314 additions and 243 deletions.
227 changes: 227 additions & 0 deletions lib/components/chat-mention-provider.react.js
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 };
47 changes: 47 additions & 0 deletions lib/hooks/chat-mention-hooks.js
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 };
Loading

0 comments on commit 5011d6a

Please sign in to comment.