diff --git a/lib/selectors/chat-selectors.js b/lib/selectors/chat-selectors.js --- a/lib/selectors/chat-selectors.js +++ b/lib/selectors/chat-selectors.js @@ -1,9 +1,6 @@ // @flow import invariant from 'invariant'; -import _filter from 'lodash/fp/filter.js'; -import _flow from 'lodash/fp/flow.js'; -import _map from 'lodash/fp/map.js'; import _orderBy from 'lodash/fp/orderBy.js'; import * as React from 'react'; import { createSelector } from 'reselect'; @@ -26,6 +23,7 @@ import { getSidebarItems, getAllInitialSidebarItems, + getAllFinalSidebarItems, type SidebarItem, } from '../shared/sidebar-item-utils.js'; import { threadInChatList, threadIsPending } from '../shared/thread-utils.js'; @@ -44,6 +42,7 @@ } from '../types/minimally-encoded-thread-permissions-types.js'; import type { BaseAppState } from '../types/redux-types.js'; import { threadTypeIsSidebar } from '../types/thread-types-enum.js'; +import type { SidebarInfo, LastUpdatedTimes } from '../types/thread-types.js'; import type { AccountUserInfo, RelativeUserInfo, @@ -53,15 +52,18 @@ import memoize2 from '../utils/memoize.js'; import { useSelector } from '../utils/redux-utils.js'; -export type ChatThreadItem = { +type ChatThreadItemBase = { +type: 'chatThreadItem', +threadInfo: ThreadInfo, +mostRecentNonLocalMessage: ?string, - +lastUpdatedTime: number, - +lastUpdatedTimeIncludingSidebars: number, +sidebars: $ReadOnlyArray, +pendingPersonalThreadUserInfo?: UserInfo, }; +export type ChatThreadItem = $ReadOnly<{ + ...ChatThreadItemBase, + +lastUpdatedTime: number, + +lastUpdatedTimeIncludingSidebars: number, +}>; const messageInfoSelector: (state: BaseAppState<>) => { +[id: string]: ?MessageInfo, @@ -81,7 +83,14 @@ ); } -function useCreateChatThreadItem(): ThreadInfo => ChatThreadItem { +type CreatedChatThreadItem = $ReadOnly<{ + ...ChatThreadItemBase, + ...LastUpdatedTimes, + +lastUpdatedTimeIncludingSidebars: Promise, + +lastUpdatedAtLeastTimeIncludingSidebars: number, + +allSidebarInfos: $ReadOnlyArray, +}>; +function useCreateChatThreadItem(): ThreadInfo => CreatedChatThreadItem { const messageInfos = useSelector(messageInfoSelector); const sidebarInfos = useSidebarInfos(); const messageStore = useSelector(state => state.messageStore); @@ -93,7 +102,7 @@ messageStore, ); - const { lastUpdatedAtLeastTime } = getLastUpdatedTimes( + const { lastUpdatedTime, lastUpdatedAtLeastTime } = getLastUpdatedTimes( threadInfo, messageStore, messageInfos, @@ -102,16 +111,18 @@ const sidebars = sidebarInfos[threadInfo.id] ?? []; const lastUpdatedAtLeastTimeIncludingSidebars = sidebars.length > 0 - ? Math.max( - lastUpdatedAtLeastTime, - sidebars[0].lastUpdatedAtLeastTime, - ) + ? Math.max(lastUpdatedAtLeastTime, sidebars[0].lastUpdatedAtLeastTime) : lastUpdatedAtLeastTime; - const allSidebarItems = sidebars.map(sidebarInfo => ({ - type: 'sidebar', - ...sidebarInfo, - })); + const lastUpdatedTimeIncludingSidebars = (async () => { + if (sidebars.length === 0) { + return await lastUpdatedTime; + } + const [lastUpdatedTimeResult, sidebarLastUpdatedTimeResult] = + await Promise.all([lastUpdatedTime, sidebars[0].lastUpdatedTime]); + return Math.max(lastUpdatedTimeResult, sidebarLastUpdatedTimeResult); + })(); + const allInitialSidebarItems = getAllInitialSidebarItems(sidebars); const sidebarItems = getSidebarItems(allInitialSidebarItems); @@ -119,9 +130,12 @@ type: 'chatThreadItem', threadInfo, mostRecentNonLocalMessage, - lastUpdatedTime: lastUpdatedAtLeastTime, - lastUpdatedTimeIncludingSidebars: lastUpdatedAtLeastTimeIncludingSidebars, + lastUpdatedTime, + lastUpdatedAtLeastTime, + lastUpdatedTimeIncludingSidebars, + lastUpdatedAtLeastTimeIncludingSidebars, sidebars: sidebarItems, + allSidebarInfos: sidebars, }; }, [messageInfos, messageStore, sidebarInfos], @@ -136,16 +150,158 @@ filterFunction: (threadInfo: ?(ThreadInfo | RawThreadInfo)) => boolean, ): $ReadOnlyArray { const threadInfos = useSelector(threadInfoSelector); + const filteredThreadInfos = React.useMemo( + () => Object.values(threadInfos).filter(filterFunction), + [threadInfos, filterFunction], + ); + return useChatThreadItems(filteredThreadInfos); +} + +const sortFunc = _orderBy('lastUpdatedTimeIncludingSidebars')('desc'); +function useChatThreadItems( + threadInfos: $ReadOnlyArray, +): $ReadOnlyArray { const getChatThreadItem = useCreateChatThreadItem(); - return React.useMemo( + const createdChatThreadItems = React.useMemo( + () => threadInfos.map(getChatThreadItem), + [threadInfos, getChatThreadItem], + ); + + const initialChatThreadItems = React.useMemo( () => - _flow( - _filter(filterFunction), - _map(getChatThreadItem), - _orderBy('lastUpdatedTimeIncludingSidebars')('desc'), - )(threadInfos), - [getChatThreadItem, filterFunction, threadInfos], + createdChatThreadItems.map(createdChatThreadItem => { + const { + allSidebarInfos, + lastUpdatedTime, + lastUpdatedAtLeastTime, + lastUpdatedTimeIncludingSidebars, + lastUpdatedAtLeastTimeIncludingSidebars, + ...rest + } = createdChatThreadItem; + return { + ...rest, + lastUpdatedTime: lastUpdatedAtLeastTime, + lastUpdatedTimeIncludingSidebars: + lastUpdatedAtLeastTimeIncludingSidebars, + }; + }), + [createdChatThreadItems], ); + + const [chatThreadItems, setChatThreadItems] = React.useState< + $ReadOnlyArray, + >(initialChatThreadItems); + + const prevCreatedChatThreadItemsRef = React.useRef(createdChatThreadItems); + React.useEffect(() => { + if (createdChatThreadItems === prevCreatedChatThreadItemsRef.current) { + return; + } + prevCreatedChatThreadItemsRef.current = createdChatThreadItems; + + setChatThreadItems(initialChatThreadItems); + + void (async () => { + const finalChatThreadItems = await Promise.all( + createdChatThreadItems.map(async createdChatThreadItem => { + const { + allSidebarInfos, + lastUpdatedTime: lastUpdatedTimePromise, + lastUpdatedAtLeastTime, + lastUpdatedTimeIncludingSidebars: + lastUpdatedTimeIncludingSidebarsPromise, + lastUpdatedAtLeastTimeIncludingSidebars, + ...rest + } = createdChatThreadItem; + const [ + lastUpdatedTime, + lastUpdatedTimeIncludingSidebars, + allSidebarItems, + ] = await Promise.all([ + lastUpdatedTimePromise, + lastUpdatedTimeIncludingSidebarsPromise, + getAllFinalSidebarItems(allSidebarInfos), + ]); + const sidebars = getSidebarItems(allSidebarItems); + return { + ...rest, + lastUpdatedTime, + lastUpdatedTimeIncludingSidebars, + sidebars, + }; + }), + ); + if (createdChatThreadItems !== prevCreatedChatThreadItemsRef.current) { + // If these aren't equal, it indicates that the effect has fired again. + // We should discard this result as it is now outdated. + return; + } + // The callback below is basically + // setChatThreadItems(finalChatThreadItems), but it has extra logic to + // preserve objects if they are unchanged. + setChatThreadItems(prevChatThreadItems => { + if (prevChatThreadItems.length !== finalChatThreadItems.length) { + console.log( + 'unexpected: prevChatThreadItems.length !== ' + + 'finalChatThreadItems.length', + ); + return finalChatThreadItems; + } + let somethingChanged = false; + const result: Array = []; + for (let i = 0; i < prevChatThreadItems.length; i++) { + const prevChatThreadItem = prevChatThreadItems[i]; + const newChatThreadItem = finalChatThreadItems[i]; + if ( + prevChatThreadItem.threadInfo.id !== newChatThreadItem.threadInfo.id + ) { + console.log( + 'unexpected: prevChatThreadItem.threadInfo.id !== ' + + 'newChatThreadItem.threadInfo.id', + ); + return finalChatThreadItems; + } + if ( + prevChatThreadItem.lastUpdatedTime !== + newChatThreadItem.lastUpdatedTime || + prevChatThreadItem.lastUpdatedTimeIncludingSidebars !== + newChatThreadItem.lastUpdatedTimeIncludingSidebars || + prevChatThreadItem.sidebars.length !== + newChatThreadItem.sidebars.length + ) { + somethingChanged = true; + result[i] = newChatThreadItem; + continue; + } + const sidebarsMatching = prevChatThreadItem.sidebars.map( + (prevSidebar, j) => { + const newSidebar = newChatThreadItem.sidebars[j]; + if ( + newSidebar.type !== 'sidebar' || + prevSidebar.type !== 'sidebar' + ) { + return newSidebar.type === prevSidebar.type; + } + return newSidebar.threadInfo.id === prevSidebar.threadInfo.id; + }, + ); + if (sidebarsMatching.includes(false)) { + somethingChanged = true; + result[i] = newChatThreadItem; + continue; + } + result[i] = prevChatThreadItem; + } + if (somethingChanged) { + return result; + } else { + return prevChatThreadItems; + } + }); + })(); + }, [createdChatThreadItems, initialChatThreadItems]); + + return React.useMemo(() => sortFunc(chatThreadItems), [chatThreadItems]); } export type RobotextChatMessageInfoItem = { @@ -610,10 +766,10 @@ export { messageInfoSelector, - useCreateChatThreadItem, createChatMessageItems, messageListData, useFlattenedChatListData, useFilteredChatListData, + useChatThreadItems, useMessageListData, }; diff --git a/web/selectors/chat-selectors.js b/web/selectors/chat-selectors.js --- a/web/selectors/chat-selectors.js +++ b/web/selectors/chat-selectors.js @@ -4,7 +4,7 @@ import { type ChatThreadItem, - useCreateChatThreadItem, + useChatThreadItems, } from 'lib/selectors/chat-selectors.js'; import { threadInfoSelector } from 'lib/selectors/thread-selectors.js'; import { threadIsPending } from 'lib/shared/thread-utils.js'; @@ -13,13 +13,12 @@ import { useSelector } from '../redux/redux-utils.js'; function useChatThreadItem(threadInfo: ?ThreadInfo): ?ChatThreadItem { - const createChatThreadItem = useCreateChatThreadItem(); - return React.useMemo(() => { - if (!threadInfo) { - return null; - } - return createChatThreadItem(threadInfo); - }, [createChatThreadItem, threadInfo]); + const threadInfos = React.useMemo( + () => [threadInfo].filter(Boolean), + [threadInfo], + ); + const [item] = useChatThreadItems(threadInfos); + return item; } function useActiveChatThreadItem(): ?ChatThreadItem {