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 @@ -3,32 +3,18 @@ import _orderBy from 'lodash/fp/orderBy.js'; import * as React from 'react'; +import { useGetLastUpdatedTimes } from './thread-time.js'; import { childThreadInfos } from '../selectors/thread-selectors.js'; import { getMostRecentNonLocalMessageID } from '../shared/message-utils.js'; import { threadInChatList } from '../shared/thread-utils.js'; -import type { MessageStore, RawMessageInfo } from '../types/message-types.js'; -import type { ThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js'; import { threadTypeIsSidebar } from '../types/thread-types-enum.js'; import type { SidebarInfo } from '../types/thread-types.js'; import { useSelector } from '../utils/redux-utils.js'; -function getMostRecentRawMessageInfo( - threadInfo: ThreadInfo, - messageStore: MessageStore, -): ?RawMessageInfo { - const thread = messageStore.threads[threadInfo.id]; - if (!thread) { - return null; - } - for (const messageID of thread.messageIDs) { - return messageStore.messages[messageID]; - } - return null; -} - function useSidebarInfos(): { +[id: string]: $ReadOnlyArray } { const childThreadInfoByParentID = useSelector(childThreadInfos); const messageStore = useSelector(state => state.messageStore); + const getLastUpdatedTimes = useGetLastUpdatedTimes(); return React.useMemo(() => { const result: { [id: string]: $ReadOnlyArray } = {}; @@ -42,12 +28,11 @@ ) { continue; } - const mostRecentRawMessageInfo = getMostRecentRawMessageInfo( + const { lastUpdatedAtLeastTime: lastUpdatedTime } = getLastUpdatedTimes( childThreadInfo, messageStore, + messageStore.messages, ); - const lastUpdatedTime = - mostRecentRawMessageInfo?.time ?? childThreadInfo.creationTime; const mostRecentNonLocalMessage = getMostRecentNonLocalMessageID( childThreadInfo.id, messageStore, @@ -61,7 +46,7 @@ result[parentID] = _orderBy('lastUpdatedTime')('desc')(sidebarInfos); } return result; - }, [childThreadInfoByParentID, messageStore]); + }, [childThreadInfoByParentID, messageStore, getLastUpdatedTimes]); } export { useSidebarInfos }; diff --git a/lib/hooks/thread-time.js b/lib/hooks/thread-time.js --- a/lib/hooks/thread-time.js +++ b/lib/hooks/thread-time.js @@ -1,25 +1,107 @@ // @flow -import type { MessageInfo, MessageStore } from '../types/message-types.js'; +import * as React from 'react'; + +import { useGetLatestMessageEdit } from './latest-message-edit.js'; +import { messageSpecs } from '../shared/messages/message-specs.js'; +import type { + MessageInfo, + RawMessageInfo, + MessageStore, +} from '../types/message-types.js'; import type { ThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js'; +import type { LastUpdatedTimes } from '../types/thread-types.js'; +import { useSelector } from '../utils/redux-utils.js'; -function getLastUpdatedTime( +function useGetLastUpdatedTimes(): ( threadInfo: ThreadInfo, messageStore: MessageStore, - messages: { +[id: string]: ?MessageInfo }, -): number { - const thread = messageStore.threads[threadInfo.id]; - if (!thread) { - return threadInfo.creationTime; - } - for (const messageID of thread.messageIDs) { - const messageInfo = messages[messageID]; - if (!messageInfo) { - continue; - } - return messageInfo.time; - } - return threadInfo.creationTime; + messages: { +[id: string]: ?MessageInfo | RawMessageInfo }, +) => LastUpdatedTimes { + const viewerID = useSelector(state => state.currentUserInfo?.id); + const fetchMessage = useGetLatestMessageEdit(); + return React.useCallback( + (threadInfo, messageStore, messages) => { + // This callback returns two variables: + // - lastUpdatedTime: this is a Promise that resolves with the final value + // - lastUpdatedAtLeastTime: this is a number that represents what we + // should use while we're waiting for lastUpdatedTime to resolve. It's + // set based on the most recent message whose spec returns a non-Promise + // when getLastUpdatedTime is called + let lastUpdatedAtLeastTime = threadInfo.creationTime; + + const thread = messageStore.threads[threadInfo.id]; + if (!thread || !viewerID) { + return { + lastUpdatedAtLeastTime, + lastUpdatedTime: Promise.resolve(lastUpdatedAtLeastTime), + }; + } + + const getLastUpdatedTimeParams = { + threadInfo, + viewerID, + fetchMessage, + }; + + let lastUpdatedTime: ?Promise; + for (const messageID of thread.messageIDs) { + const messageInfo = messages[messageID]; + if (!messageInfo) { + continue; + } + + // We call getLastUpdatedTime on the message spec. It can return either + // ?number or Promise. If the message spec doesn't implement + // getLastUpdatedTime, then we default to messageInfo.time. + const { getLastUpdatedTime } = messageSpecs[messageInfo.type]; + const lastUpdatedTimePromisable = getLastUpdatedTime + ? getLastUpdatedTime(messageInfo, getLastUpdatedTimeParams) + : messageInfo.time; + + // We rely on the fact that thread.messageIDs is ordered chronologically + // (newest first) to chain together lastUpdatedTime. An older message's + // lastUpdatedTime is only considered if all of the newer messages + // return falsey. + lastUpdatedTime = (async () => { + if (lastUpdatedTime) { + const earlierChecks = await lastUpdatedTime; + if (earlierChecks) { + return earlierChecks; + } + } + return await lastUpdatedTimePromisable; + })(); + + if (typeof lastUpdatedTimePromisable === 'number') { + // We break from the loop the first time this condition is met. + // There's no need to consider any older messages, since both + // lastUpdated and lastUpdatedAtLeastTime will be this value (or + // higher, in the case of lastUpdated). That is also why this loop + // only sets lastUpdatedAtLeastTime once: once we get to this + // "baseline" case, there's no need to consider any more messages. + lastUpdatedAtLeastTime = lastUpdatedTimePromisable; + break; + } + } + + const lastUpdatedWithFallback = (async () => { + if (lastUpdatedTime) { + const earlierChecks = await lastUpdatedTime; + if (earlierChecks) { + return earlierChecks; + } + } + return lastUpdatedAtLeastTime; + })(); + + return { + lastUpdatedAtLeastTime, + lastUpdatedTime: lastUpdatedWithFallback, + }; + }, + [viewerID, fetchMessage], + ); } -export { getLastUpdatedTime }; +export { useGetLastUpdatedTimes }; 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 @@ -14,7 +14,7 @@ threadInfoSelector, } from './thread-selectors.js'; import { useSidebarInfos } from '../hooks/sidebar-hooks.js'; -import { getLastUpdatedTime } from '../hooks/thread-time.js'; +import { useGetLastUpdatedTimes } from '../hooks/thread-time.js'; import { createMessageInfo, getMostRecentNonLocalMessageID, @@ -97,13 +97,15 @@ const messageInfos = useSelector(messageInfoSelector); const sidebarInfos = useSidebarInfos(); const messageStore = useSelector(state => state.messageStore); + const getLastUpdatedTimes = useGetLastUpdatedTimes(); return React.useCallback( threadInfo => { const mostRecentNonLocalMessage = getMostRecentNonLocalMessageID( threadInfo.id, messageStore, ); - const lastUpdatedTime = getLastUpdatedTime( + + const { lastUpdatedAtLeastTime: lastUpdatedTime } = getLastUpdatedTimes( threadInfo, messageStore, messageInfos, @@ -170,7 +172,7 @@ sidebars: sidebarItems, }; }, - [messageInfos, messageStore, sidebarInfos], + [messageInfos, messageStore, sidebarInfos, getLastUpdatedTimes], ); } diff --git a/lib/shared/messages/message-spec.js b/lib/shared/messages/message-spec.js --- a/lib/shared/messages/message-spec.js +++ b/lib/shared/messages/message-spec.js @@ -141,4 +141,8 @@ messageInfo: Info, params: ShowInMessagePreviewParams, ) => Promise, + +getLastUpdatedTime?: ( + messageInfoOrRawMessageInfo: Info | RawInfo, + params: ShowInMessagePreviewParams, + ) => ?number | Promise, }; diff --git a/lib/shared/messages/multimedia-message-spec.js b/lib/shared/messages/multimedia-message-spec.js --- a/lib/shared/messages/multimedia-message-spec.js +++ b/lib/shared/messages/multimedia-message-spec.js @@ -369,6 +369,14 @@ showInMessagePreview: (messageInfo: MediaMessageInfo | ImagesMessageInfo) => Promise.resolve(messageInfo.media.length > 0), + + getLastUpdatedTime: ( + messageInfo: + | MediaMessageInfo + | ImagesMessageInfo + | RawMediaMessageInfo + | RawImagesMessageInfo, + ) => (messageInfo.media.length > 0 ? messageInfo.time : null), }); // Four photos were uploaded before dimensions were calculated server-side, 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 @@ -436,6 +436,13 @@ +offset: number, }; +export type LastUpdatedTimes = { + // The last updated time is at least this number, but possibly higher + // We won't know for sure until the below Promise resolves + +lastUpdatedAtLeastTime: number, + +lastUpdatedTime: Promise, +}; + export type SidebarInfo = { +threadInfo: ThreadInfo, +lastUpdatedTime: number,