diff --git a/web/avatars/web-edit-thread-avatar-provider.react.js b/web/avatars/web-edit-thread-avatar-provider.react.js index 8ef56839c..060559781 100644 --- a/web/avatars/web-edit-thread-avatar-provider.react.js +++ b/web/avatars/web-edit-thread-avatar-provider.react.js @@ -1,25 +1,24 @@ // @flow import * as React from 'react'; import { BaseEditThreadAvatarProvider } from 'lib/components/base-edit-thread-avatar-provider.react.js'; -import { useSelector } from '../redux/redux-utils.js'; -import { activeChatThreadItem as activeChatThreadItemSelector } from '../selectors/chat-selectors.js'; +import { useActiveChatThreadItem } from '../selectors/chat-selectors.js'; type Props = { +children: React.Node, }; function WebEditThreadAvatarProvider(props: Props): React.Node { const { children } = props; - const activeChatThreadItem = useSelector(activeChatThreadItemSelector); + const activeChatThreadItem = useActiveChatThreadItem(); const activeThreadID = activeChatThreadItem?.threadInfo?.id ?? ''; return ( {children} ); } export default WebEditThreadAvatarProvider; diff --git a/web/chat/thread-list-provider.js b/web/chat/thread-list-provider.js index 90c1b30fe..cfabc8f2f 100644 --- a/web/chat/thread-list-provider.js +++ b/web/chat/thread-list-provider.js @@ -1,261 +1,261 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { useLoggedInUserInfo } from 'lib/hooks/account-hooks.js'; import { useThreadListSearch } from 'lib/hooks/thread-search-hooks.js'; import { type ChatThreadItem, useFlattenedChatListData, } from 'lib/selectors/chat-selectors.js'; import { threadInfoSelector } from 'lib/selectors/thread-selectors.js'; import { threadInBackgroundChatList, threadInHomeChatList, getThreadListSearchResults, threadIsPending, useIsThreadInChatList, } from 'lib/shared/thread-utils.js'; import { threadTypeIsSidebar } from 'lib/types/thread-types-enum.js'; import { useSelector } from '../redux/redux-utils.js'; import { useChatThreadItem, - activeChatThreadItem as activeChatThreadItemSelector, + useActiveChatThreadItem, } from '../selectors/chat-selectors.js'; type ChatTabType = 'Home' | 'Muted'; type ThreadListContextType = { +activeTab: ChatTabType, +setActiveTab: (newActiveTab: ChatTabType) => void, +threadList: $ReadOnlyArray, +searchText: string, +setSearchText: (searchText: string) => void, }; const ThreadListContext: React.Context = React.createContext(); type ThreadListProviderProps = { +children: React.Node, }; function ThreadListProvider(props: ThreadListProviderProps): React.Node { const [activeTab, setActiveTab] = React.useState('Home'); - const activeChatThreadItem = useSelector(activeChatThreadItemSelector); + const activeChatThreadItem = useActiveChatThreadItem(); const activeThreadInfo = activeChatThreadItem?.threadInfo; const activeThreadID = activeThreadInfo?.id; const activeSidebarParentThreadInfo = useSelector(state => { if (!activeThreadInfo || !threadTypeIsSidebar(activeThreadInfo.type)) { return null; } const { parentThreadID } = activeThreadInfo; invariant(parentThreadID, 'sidebar must have parent thread'); return threadInfoSelector(state)[parentThreadID]; }); const activeTopLevelThreadInfo = activeThreadInfo && threadTypeIsSidebar(activeThreadInfo?.type) ? activeSidebarParentThreadInfo : activeThreadInfo; const activeTopLevelThreadIsFromHomeTab = activeTopLevelThreadInfo?.currentUser.subscription.home; const activeTopLevelThreadIsFromDifferentTab = (activeTab === 'Home' && activeTopLevelThreadIsFromHomeTab) || (activeTab === 'Muted' && !activeTopLevelThreadIsFromHomeTab); const activeTopLevelThreadIsInChatList = useIsThreadInChatList( activeTopLevelThreadInfo, ); const shouldChangeTab = activeTopLevelThreadIsInChatList && activeTopLevelThreadIsFromDifferentTab; const prevActiveThreadIDRef = React.useRef(); React.useEffect(() => { const prevActiveThreadID = prevActiveThreadIDRef.current; prevActiveThreadIDRef.current = activeThreadID; if (activeThreadID !== prevActiveThreadID && shouldChangeTab) { setActiveTab(activeTopLevelThreadIsFromHomeTab ? 'Home' : 'Muted'); } }, [activeThreadID, shouldChangeTab, activeTopLevelThreadIsFromHomeTab]); const activeThreadOriginalTab = React.useMemo(() => { if (activeTopLevelThreadIsInChatList) { return null; } return activeTab; // eslint-disable-next-line react-hooks/exhaustive-deps }, [activeTopLevelThreadIsInChatList, activeThreadID]); const makeSureActivePendingThreadIsIncluded = React.useCallback( ( threadListData: $ReadOnlyArray, ): $ReadOnlyArray => { if ( activeChatThreadItem && threadIsPending(activeThreadID) && (!activeThreadInfo || !threadTypeIsSidebar(activeThreadInfo.type)) && !threadListData .map(thread => thread.threadInfo.id) .includes(activeThreadID) ) { return [activeChatThreadItem, ...threadListData]; } return threadListData; }, [activeChatThreadItem, activeThreadID, activeThreadInfo], ); const makeSureActiveSidebarIsIncluded = React.useCallback( (threadListData: $ReadOnlyArray) => { if ( !activeChatThreadItem || !threadTypeIsSidebar(activeChatThreadItem.threadInfo.type) ) { return threadListData; } const sidebarParentIndex = threadListData.findIndex( thread => thread.threadInfo.id === activeChatThreadItem.threadInfo.parentThreadID, ); if (sidebarParentIndex === -1) { return threadListData; } const parentItem = threadListData[sidebarParentIndex]; for (const sidebarItem of parentItem.sidebars) { if (sidebarItem.type !== 'sidebar') { continue; } else if ( sidebarItem.threadInfo.id === activeChatThreadItem.threadInfo.id ) { return threadListData; } } let indexToInsert = parentItem.sidebars.findIndex( sidebar => sidebar.lastUpdatedTime === undefined || sidebar.lastUpdatedTime < activeChatThreadItem.lastUpdatedTime, ); if (indexToInsert === -1) { indexToInsert = parentItem.sidebars.length; } const activeSidebar = { type: 'sidebar', lastUpdatedTime: activeChatThreadItem.lastUpdatedTime, mostRecentNonLocalMessage: activeChatThreadItem.mostRecentNonLocalMessage, threadInfo: activeChatThreadItem.threadInfo, }; const newSidebarItems = [...parentItem.sidebars]; newSidebarItems.splice(indexToInsert, 0, activeSidebar); const newThreadListData = [...threadListData]; newThreadListData[sidebarParentIndex] = { ...parentItem, sidebars: newSidebarItems, }; return newThreadListData; }, [activeChatThreadItem], ); const chatListData = useFlattenedChatListData(); const [searchText, setSearchText] = React.useState(''); const loggedInUserInfo = useLoggedInUserInfo(); const viewerID = loggedInUserInfo?.id; const { threadSearchResults, usersSearchResults } = useThreadListSearch( searchText, viewerID, ); const threadFilter = activeTab === 'Muted' ? threadInBackgroundChatList : threadInHomeChatList; const chatListDataWithoutFilter = React.useMemo( () => getThreadListSearchResults( chatListData, searchText, threadFilter, threadSearchResults, usersSearchResults, loggedInUserInfo, ), [ chatListData, searchText, threadFilter, threadSearchResults, usersSearchResults, loggedInUserInfo, ], ); const activeTopLevelChatThreadItem = useChatThreadItem( activeTopLevelThreadInfo, ); const threadList = React.useMemo(() => { let threadListWithTopLevelItem = chatListDataWithoutFilter; if ( activeTopLevelChatThreadItem && !activeTopLevelThreadIsInChatList && activeThreadOriginalTab === activeTab ) { threadListWithTopLevelItem = [ activeTopLevelChatThreadItem, ...threadListWithTopLevelItem, ]; } const threadListWithCurrentPendingThread = makeSureActivePendingThreadIsIncluded(threadListWithTopLevelItem); return makeSureActiveSidebarIsIncluded(threadListWithCurrentPendingThread); }, [ activeTab, activeThreadOriginalTab, activeTopLevelChatThreadItem, activeTopLevelThreadIsInChatList, chatListDataWithoutFilter, makeSureActivePendingThreadIsIncluded, makeSureActiveSidebarIsIncluded, ]); const isChatCreationMode = useSelector( state => state.navInfo.chatMode === 'create', ); const orderedThreadList = React.useMemo(() => { if (!isChatCreationMode) { return threadList; } return [ ...threadList.filter(thread => thread.threadInfo.id === activeThreadID), ...threadList.filter(thread => thread.threadInfo.id !== activeThreadID), ]; }, [activeThreadID, isChatCreationMode, threadList]); const threadListContext = React.useMemo( () => ({ activeTab, threadList: orderedThreadList, setActiveTab, searchText, setSearchText, }), [activeTab, orderedThreadList, searchText], ); return ( {props.children} ); } export { ThreadListProvider, ThreadListContext }; diff --git a/web/selectors/chat-selectors.js b/web/selectors/chat-selectors.js index 22b7418e3..5a8879167 100644 --- a/web/selectors/chat-selectors.js +++ b/web/selectors/chat-selectors.js @@ -1,77 +1,52 @@ // @flow import * as React from 'react'; -import { createSelector } from 'reselect'; import { type ChatThreadItem, createChatThreadItem, messageInfoSelector, } from 'lib/selectors/chat-selectors.js'; import { sidebarInfoSelector } from 'lib/selectors/sidebar-selectors.js'; import { threadInfoSelector } from 'lib/selectors/thread-selectors.js'; import { threadIsPending } from 'lib/shared/thread-utils.js'; -import type { MessageInfo, MessageStore } from 'lib/types/message-types.js'; import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; -import type { SidebarInfo } from 'lib/types/thread-types.js'; -import type { AppState } from '../redux/redux-setup.js'; import { useSelector } from '../redux/redux-utils.js'; -const activeChatThreadItem: (state: AppState) => ?ChatThreadItem = - createSelector( - threadInfoSelector, - (state: AppState) => state.messageStore, - messageInfoSelector, - (state: AppState) => state.navInfo.activeChatThreadID, - (state: AppState) => state.navInfo.pendingThread, - sidebarInfoSelector, - ( - threadInfos: { - +[id: string]: ThreadInfo, - }, - messageStore: MessageStore, - messageInfos: { +[id: string]: ?MessageInfo }, - activeChatThreadID: ?string, - pendingThreadInfo: ?ThreadInfo, - sidebarInfos: { +[id: string]: $ReadOnlyArray }, - ): ?ChatThreadItem => { - if (!activeChatThreadID) { - return null; - } - const isPending = threadIsPending(activeChatThreadID); - const threadInfo = isPending - ? pendingThreadInfo - : threadInfos[activeChatThreadID]; - - if (!threadInfo) { - return null; - } - return createChatThreadItem( - threadInfo, - messageStore, - messageInfos, - sidebarInfos[threadInfo.id], - ); - }, - ); - function useChatThreadItem(threadInfo: ?ThreadInfo): ?ChatThreadItem { const messageInfos = useSelector(messageInfoSelector); const sidebarInfos = useSelector(sidebarInfoSelector); const messageStore = useSelector(state => state.messageStore); return React.useMemo(() => { if (!threadInfo) { return null; } return createChatThreadItem( threadInfo, messageStore, messageInfos, sidebarInfos[threadInfo.id], ); }, [messageInfos, messageStore, sidebarInfos, threadInfo]); } -export { useChatThreadItem, activeChatThreadItem }; + +function useActiveChatThreadItem(): ?ChatThreadItem { + const activeChatThreadID = useSelector( + state => state.navInfo.activeChatThreadID, + ); + const pendingThreadInfo = useSelector(state => state.navInfo.pendingThread); + const threadInfos = useSelector(threadInfoSelector); + const threadInfo = React.useMemo(() => { + if (!activeChatThreadID) { + return null; + } + const isPending = threadIsPending(activeChatThreadID); + return isPending ? pendingThreadInfo : threadInfos[activeChatThreadID]; + }, [activeChatThreadID, pendingThreadInfo, threadInfos]); + return useChatThreadItem(threadInfo); +} + +export { useChatThreadItem, useActiveChatThreadItem };