diff --git a/lib/hooks/search-threads.js b/lib/hooks/search-threads.js --- a/lib/hooks/search-threads.js +++ b/lib/hooks/search-threads.js @@ -8,6 +8,11 @@ useFilteredChatListData, } from '../selectors/chat-selectors.js'; import { useThreadSearchIndex } from '../selectors/nav-selectors.js'; +import { + type SidebarThreadItem, + getAllInitialSidebarItems, + getAllFinalSidebarItems, +} from '../shared/sidebar-item-utils.js'; import { threadIsChannel } from '../shared/thread-utils.js'; import type { SetState } from '../types/hook-types.js'; import type { @@ -29,7 +34,11 @@ +clearQuery: (event: SyntheticEvent) => void, }; -function useSearchThreads( +type ChildThreadInfos = { + +threadInfo: RawThreadInfo | ThreadInfo, + ... +}; +function useSearchThreads( threadInfo: ThreadInfo, childThreadInfos: $ReadOnlyArray, ): SearchThreadsResult { @@ -93,10 +102,72 @@ function useSearchSidebars( threadInfo: ThreadInfo, -): SearchThreadsResult { +): SearchThreadsResult { const sidebarsByParentID = useSidebarInfos(); const childThreadInfos = sidebarsByParentID[threadInfo.id] ?? emptyArray; - return useSearchThreads(threadInfo, childThreadInfos); + const initialSidebarItems = React.useMemo( + () => getAllInitialSidebarItems(childThreadInfos), + [childThreadInfos], + ); + const [sidebarItems, setSidebarItems] = + React.useState<$ReadOnlyArray>(initialSidebarItems); + + const prevChildThreadInfosRef = React.useRef(childThreadInfos); + React.useEffect(() => { + if (childThreadInfos === prevChildThreadInfosRef.current) { + return; + } + prevChildThreadInfosRef.current = childThreadInfos; + + setSidebarItems(initialSidebarItems); + + void (async () => { + const finalSidebarItems = await getAllFinalSidebarItems(childThreadInfos); + if (childThreadInfos !== prevChildThreadInfosRef.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 setSidebarItems(finalSidebarItems), but + // it has extra logic to preserve objects if they are unchanged. + setSidebarItems(prevSidebarItems => { + if (prevSidebarItems.length !== finalSidebarItems.length) { + console.log( + 'unexpected: prevSidebarItems.length !== finalSidebarItems.length', + ); + return finalSidebarItems; + } + let somethingChanged = false; + const result = []; + for (let i = 0; i < prevSidebarItems.length; i++) { + const prevSidebarItem = prevSidebarItems[i]; + const newSidebarItem = finalSidebarItems[i]; + if (prevSidebarItem.threadInfo.id !== newSidebarItem.threadInfo.id) { + console.log( + 'unexpected: prevSidebarItem.threadInfo.id !== ' + + 'newSidebarItem.threadInfo.id', + ); + return finalSidebarItems; + } + if ( + prevSidebarItem.lastUpdatedTime !== newSidebarItem.lastUpdatedTime + ) { + somethingChanged = true; + result[i] = newSidebarItem; + } else { + result[i] = prevSidebarItem; + } + } + if (somethingChanged) { + return result; + } else { + return prevSidebarItems; + } + }); + })(); + }, [childThreadInfos, initialSidebarItems]); + + return useSearchThreads(threadInfo, sidebarItems); } function useSearchSubchannels( diff --git a/lib/hooks/sidebar-hooks.js b/lib/hooks/sidebar-hooks.js --- a/lib/hooks/sidebar-hooks.js +++ b/lib/hooks/sidebar-hooks.js @@ -28,7 +28,7 @@ ) { continue; } - const { lastUpdatedAtLeastTime: lastUpdatedTime } = getLastUpdatedTimes( + const { lastUpdatedTime, lastUpdatedAtLeastTime } = getLastUpdatedTimes( childThreadInfo, messageStore, messageStore.messages, @@ -40,6 +40,7 @@ sidebarInfos.push({ threadInfo: childThreadInfo, lastUpdatedTime, + lastUpdatedAtLeastTime, mostRecentNonLocalMessage, }); } 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 @@ -25,6 +25,7 @@ import { messageSpecs } from '../shared/messages/message-specs.js'; import { getSidebarItems, + getAllInitialSidebarItems, type SidebarItem, } from '../shared/sidebar-item-utils.js'; import { threadInChatList, threadIsPending } from '../shared/thread-utils.js'; @@ -92,30 +93,28 @@ messageStore, ); - const { lastUpdatedAtLeastTime: lastUpdatedTime } = getLastUpdatedTimes( + const { lastUpdatedAtLeastTime } = getLastUpdatedTimes( threadInfo, messageStore, messageInfos, ); const sidebars = sidebarInfos[threadInfo.id] ?? []; - const allSidebarItems = sidebars.map(sidebarInfo => ({ - type: 'sidebar', - ...sidebarInfo, - })); - const lastUpdatedTimeIncludingSidebars = - allSidebarItems.length > 0 - ? Math.max(lastUpdatedTime, allSidebarItems[0].lastUpdatedTime) - : lastUpdatedTime; + const lastUpdatedAtLeastTimeIncludingSidebars = + sidebars.length > 0 + ? Math.max(lastUpdatedAtLeastTime, sidebars[0].lastUpdatedAtLeastTime) + : lastUpdatedAtLeastTime; - const sidebarItems = getSidebarItems(allSidebarItems); + const allInitialSidebarItems = getAllInitialSidebarItems(sidebars); + const sidebarItems = getSidebarItems(allInitialSidebarItems); return { type: 'chatThreadItem', threadInfo, mostRecentNonLocalMessage, - lastUpdatedTime, - lastUpdatedTimeIncludingSidebars, + lastUpdatedTime: lastUpdatedAtLeastTime, + lastUpdatedTimeIncludingSidebars: + lastUpdatedAtLeastTimeIncludingSidebars, sidebars: sidebarItems, }; }, diff --git a/lib/shared/sidebar-item-utils.js b/lib/shared/sidebar-item-utils.js --- a/lib/shared/sidebar-item-utils.js +++ b/lib/shared/sidebar-item-utils.js @@ -1,10 +1,14 @@ // @flow import type { ThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js'; -import { maxReadSidebars, maxUnreadSidebars } from '../types/thread-types.js'; +import { + maxReadSidebars, + maxUnreadSidebars, + type SidebarInfo, +} from '../types/thread-types.js'; import { threeDays } from '../utils/date-utils.js'; -type SidebarThreadItem = { +export type SidebarThreadItem = { +type: 'sidebar', +threadInfo: ThreadInfo, +mostRecentNonLocalMessage: ?string, @@ -65,4 +69,44 @@ return sidebarItems; } -export { getSidebarItems }; +function getAllInitialSidebarItems( + sidebarInfos: $ReadOnlyArray, +): SidebarThreadItem[] { + return sidebarInfos.map( + sidebarItem => { + const { + lastUpdatedTime, + lastUpdatedAtLeastTime, + ...rest + } = sidebarItem; + return { + ...rest, + type: 'sidebar', + lastUpdatedTime: lastUpdatedAtLeastTime, + }; + }, + ); +} + +async function getAllFinalSidebarItems( + sidebarInfos: $ReadOnlyArray, +): Promise<$ReadOnlyArray> { + const allSidebarItemPromises = sidebarInfos.map( + async sidebarItem => { + const { + lastUpdatedTime, + lastUpdatedAtLeastTime, + ...rest + } = sidebarItem; + const finalLastUpdatedTime = await lastUpdatedTime; + return { + ...rest, + type: 'sidebar', + lastUpdatedTime: finalLastUpdatedTime, + }; + }, + ); + return await Promise.all(allSidebarItemPromises); +} + +export { getSidebarItems, getAllInitialSidebarItems, getAllFinalSidebarItems }; diff --git a/lib/types/thread-types.js b/lib/types/thread-types.js --- a/lib/types/thread-types.js +++ b/lib/types/thread-types.js @@ -443,11 +443,11 @@ +lastUpdatedTime: Promise, }; -export type SidebarInfo = { +export type SidebarInfo = $ReadOnly<{ + ...LastUpdatedTimes, +threadInfo: ThreadInfo, - +lastUpdatedTime: number, +mostRecentNonLocalMessage: ?string, -}; +}>; export type ToggleMessagePinRequest = { +messageID: string, diff --git a/native/chat/chat-thread-list-item.react.js b/native/chat/chat-thread-list-item.react.js --- a/native/chat/chat-thread-list-item.react.js +++ b/native/chat/chat-thread-list-item.react.js @@ -50,10 +50,9 @@ () => data.sidebars.map((sidebarItem, index) => { if (sidebarItem.type === 'sidebar') { - const { type, ...sidebarInfo } = sidebarItem; return ( void, +onSwipeableWillOpen: (threadInfo: ThreadInfo) => void, +currentlyOpenedSwipeableId: string, @@ -26,14 +26,14 @@ const styles = useStyles(unboundStyles); const { - sidebarInfo, + sidebarItem, onSwipeableWillOpen, currentlyOpenedSwipeableId, onPressItem, extendArrow = false, } = props; - const { threadInfo } = sidebarInfo; + const { threadInfo } = sidebarItem; const onPress = React.useCallback( () => onPressItem(threadInfo), @@ -58,40 +58,40 @@ const unreadIndicator = React.useMemo( () => ( - + ), [ - sidebarInfo.threadInfo.currentUser.unread, + threadInfo.currentUser.unread, styles.unreadIndicatorContainer, ], ); - const sidebarItem = React.useMemo( - () => , - [sidebarInfo], + const sidebarItemElement = React.useMemo( + () => , + [sidebarItem], ); const swipeableThread = React.useMemo( () => ( - {sidebarItem} + {sidebarItemElement} ), [ currentlyOpenedSwipeableId, onSwipeableWillOpen, - sidebarInfo.mostRecentNonLocalMessage, - sidebarInfo.threadInfo, - sidebarItem, + sidebarItem.mostRecentNonLocalMessage, + threadInfo, + sidebarItemElement, styles.swipeableThreadContainer, ], ); diff --git a/native/chat/sidebar-item.react.js b/native/chat/sidebar-item.react.js --- a/native/chat/sidebar-item.react.js +++ b/native/chat/sidebar-item.react.js @@ -3,7 +3,7 @@ import * as React from 'react'; import { Text, View } from 'react-native'; -import type { SidebarInfo } from 'lib/types/thread-types.js'; +import type { SidebarThreadItem } from 'lib/shared/sidebar-item-utils.js'; import { shortAbsoluteDate } from 'lib/utils/date-utils.js'; import { useResolvedThreadInfo } from 'lib/utils/entity-helpers.js'; @@ -11,17 +11,17 @@ import { useStyles } from '../themes/colors.js'; type Props = { - +sidebarInfo: SidebarInfo, + +sidebarItem: SidebarThreadItem, }; function SidebarItem(props: Props): React.Node { - const { lastUpdatedTime } = props.sidebarInfo; + const { lastUpdatedTime } = props.sidebarItem; const lastActivity = React.useMemo( () => shortAbsoluteDate(lastUpdatedTime), [lastUpdatedTime], ); - const { threadInfo } = props.sidebarInfo; + const { threadInfo } = props.sidebarItem; const { uiName } = useResolvedThreadInfo(threadInfo); const styles = useStyles(unboundStyles); const unreadStyle = threadInfo.currentUser.unread ? styles.unread : null; diff --git a/native/chat/sidebar-list-modal.react.js b/native/chat/sidebar-list-modal.react.js --- a/native/chat/sidebar-list-modal.react.js +++ b/native/chat/sidebar-list-modal.react.js @@ -4,8 +4,8 @@ import { View } from 'react-native'; import { useSearchSidebars } from 'lib/hooks/search-threads.js'; +import type { SidebarThreadItem } from 'lib/shared/sidebar-item-utils.js'; import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; -import type { SidebarInfo } from 'lib/types/thread-types.js'; import { SidebarItem } from './sidebar-item.react.js'; import ThreadListModal from './thread-list-modal.react.js'; @@ -33,7 +33,7 @@ const createRenderItem = React.useCallback( (onPressItem: (threadInfo: ThreadInfo) => void) => // eslint-disable-next-line react/display-name - (row: { +item: SidebarInfo, +index: number, ... }) => { + (row: { +item: SidebarThreadItem, +index: number, ... }) => { let extendArrow: boolean = false; if (row.index < numOfSidebarsWithExtendedArrow) { extendArrow = true; @@ -64,7 +64,7 @@ } function Item(props: { - item: SidebarInfo, + item: SidebarThreadItem, onPressItem: (threadInfo: ThreadInfo) => void, extendArrow: boolean, }): React.Node { @@ -106,7 +106,7 @@ {arrow} - + diff --git a/native/chat/thread-list-modal.react.js b/native/chat/thread-list-modal.react.js --- a/native/chat/thread-list-modal.react.js +++ b/native/chat/thread-list-modal.react.js @@ -11,10 +11,8 @@ } from 'react-native'; import type { ThreadSearchState } from 'lib/hooks/search-threads.js'; -import type { ChatThreadItem } from 'lib/selectors/chat-selectors.js'; import type { SetState } from 'lib/types/hook-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 { ThreadInfo, RawThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; import { useNavigateToThread } from './message-list-types.js'; import Modal from '../components/modal.react.js'; @@ -24,11 +22,15 @@ import { useIndicatorStyle, useStyles } from '../themes/colors.js'; import { waitForModalInputFocus } from '../utils/timers.js'; -function keyExtractor(sidebarInfo: SidebarInfo | ChatThreadItem) { +type ChatItem = { + +threadInfo: RawThreadInfo | ThreadInfo, + ... +}; +function keyExtractor(sidebarInfo: ChatItem) { return sidebarInfo.threadInfo.id; } function getItemLayout( - data: ?$ReadOnlyArray, + data: ?$ReadOnlyArray, index: number, ) { return { length: 24, offset: 24 * index, index }; @@ -46,7 +48,7 @@ +searchPlaceholder?: string, +modalTitle: string, }; -function ThreadListModal( +function ThreadListModal( props: Props, ): React.Node { const { diff --git a/web/chat/chat-thread-list-item.react.js b/web/chat/chat-thread-list-item.react.js --- a/web/chat/chat-thread-list-item.react.js +++ b/web/chat/chat-thread-list-item.react.js @@ -90,12 +90,11 @@ const sidebars = item.sidebars.map((sidebarItem, index) => { if (sidebarItem.type === 'sidebar') { - const { type, ...sidebarInfo } = sidebarItem; return ( 0} - key={sidebarInfo.threadInfo.id} + key={sidebarItem.threadInfo.id} /> ); } else if (sidebarItem.type === 'seeMore') { diff --git a/web/chat/chat-thread-list-sidebar.react.js b/web/chat/chat-thread-list-sidebar.react.js --- a/web/chat/chat-thread-list-sidebar.react.js +++ b/web/chat/chat-thread-list-sidebar.react.js @@ -3,7 +3,7 @@ import classNames from 'classnames'; import * as React from 'react'; -import type { SidebarInfo } from 'lib/types/thread-types.js'; +import type { SidebarThreadItem } from 'lib/shared/sidebar-item-utils.js'; import ChatThreadListItemMenu from './chat-thread-list-item-menu.react.js'; import css from './chat-thread-list.css'; @@ -11,12 +11,12 @@ import { useThreadIsActive } from '../selectors/thread-selectors.js'; type Props = { - +sidebarInfo: SidebarInfo, + +sidebarItem: SidebarThreadItem, +isSubsequentItem: boolean, }; function ChatThreadListSidebar(props: Props): React.Node { - const { sidebarInfo, isSubsequentItem } = props; - const { threadInfo, mostRecentNonLocalMessage } = sidebarInfo; + const { sidebarItem, isSubsequentItem } = props; + const { threadInfo, mostRecentNonLocalMessage } = sidebarItem; const { currentUser: { unread }, id: threadID, @@ -35,7 +35,7 @@ })} >
{unreadDot}
- +