From 7db33a67083c8443fe2a87cf6e58e5be5fa663fc Mon Sep 17 00:00:00 2001 From: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com> Date: Wed, 21 Aug 2024 18:48:29 -0700 Subject: [PATCH] Preload conversation open data Co-authored-by: Scott Nonnenberg --- .../AnnouncementsOnlyGroupBanner.tsx | 2 + ts/components/ConversationList.stories.tsx | 1 + ts/components/ConversationList.tsx | 4 + ts/components/ForwardMessagesModal.tsx | 1 + ts/components/LeftPane.stories.tsx | 1 + ts/components/LeftPane.tsx | 3 + ts/components/StoriesSettingsModal.tsx | 1 + .../BaseConversationListItem.tsx | 3 + .../conversationList/ConversationListItem.tsx | 7 + ts/models/conversations.ts | 137 +++++++++- ts/sql/Server.ts | 8 +- ts/state/ducks/conversations.ts | 240 +++++++++++++----- ts/state/selectors/conversations.ts | 5 + ts/state/smart/LeftPane.tsx | 8 + 14 files changed, 332 insertions(+), 89 deletions(-) diff --git a/ts/components/AnnouncementsOnlyGroupBanner.tsx b/ts/components/AnnouncementsOnlyGroupBanner.tsx index f40e7cccb4cc..b78835b7274d 100644 --- a/ts/components/AnnouncementsOnlyGroupBanner.tsx +++ b/ts/components/AnnouncementsOnlyGroupBanner.tsx @@ -2,6 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import React, { useState } from 'react'; +import { noop } from 'lodash'; import type { ConversationType, ShowConversationType, @@ -45,6 +46,7 @@ export function AnnouncementsOnlyGroupBanner({ onClick={() => { showConversation({ conversationId: admin.id }); }} + onMouseDown={noop} theme={theme} /> ))} diff --git a/ts/components/ConversationList.stories.tsx b/ts/components/ConversationList.stories.tsx index 6b0ee86f7624..76d40503b7f5 100644 --- a/ts/components/ConversationList.stories.tsx +++ b/ts/components/ConversationList.stories.tsx @@ -68,6 +68,7 @@ function Wrapper({ shouldRecomputeRowHeights={false} i18n={i18n} blockConversation={action('blockConversation')} + onPreloadConversation={action('onPreloadConversation')} onSelectConversation={action('onSelectConversation')} onOutgoingAudioCallInConversation={action( 'onOutgoingAudioCallInConversation' diff --git a/ts/components/ConversationList.tsx b/ts/components/ConversationList.tsx index 8f445df82249..924422ce8976 100644 --- a/ts/components/ConversationList.tsx +++ b/ts/components/ConversationList.tsx @@ -197,6 +197,7 @@ export type PropsType = { conversationId: string, disabledReason: undefined | ContactCheckboxDisabledReason ) => void; + onPreloadConversation: (conversationId: string, messageId?: string) => void; onSelectConversation: (conversationId: string, messageId?: string) => void; onOutgoingAudioCallInConversation: (conversationId: string) => void; onOutgoingVideoCallInConversation: (conversationId: string) => void; @@ -220,6 +221,7 @@ export function ConversationList({ blockConversation, onClickArchiveButton, onClickContactCheckbox, + onPreloadConversation, onSelectConversation, onOutgoingAudioCallInConversation, onOutgoingVideoCallInConversation, @@ -411,6 +413,7 @@ export function ConversationList({ })} key={key} badge={getPreferredBadge(badges)} + onMouseDown={onPreloadConversation} onClick={onSelectConversation} i18n={i18n} theme={theme} @@ -527,6 +530,7 @@ export function ConversationList({ onClickContactCheckbox, onOutgoingAudioCallInConversation, onOutgoingVideoCallInConversation, + onPreloadConversation, onSelectConversation, removeConversation, renderMessageSearchResult, diff --git a/ts/components/ForwardMessagesModal.tsx b/ts/components/ForwardMessagesModal.tsx index c64b425ad91f..6d0379d9c58d 100644 --- a/ts/components/ForwardMessagesModal.tsx +++ b/ts/components/ForwardMessagesModal.tsx @@ -400,6 +400,7 @@ export function ForwardMessagesModal({ showConversation={shouldNeverBeCalled} showUserNotFoundModal={shouldNeverBeCalled} setIsFetchingUUID={shouldNeverBeCalled} + onPreloadConversation={shouldNeverBeCalled} onSelectConversation={shouldNeverBeCalled} blockConversation={shouldNeverBeCalled} removeConversation={shouldNeverBeCalled} diff --git a/ts/components/LeftPane.stories.tsx b/ts/components/LeftPane.stories.tsx index e4fe703256bb..0b76a1067419 100644 --- a/ts/components/LeftPane.stories.tsx +++ b/ts/components/LeftPane.stories.tsx @@ -172,6 +172,7 @@ const useProps = (overrideProps: OverridePropsType = {}): PropsType => { makeFakeLookupConversationWithoutServiceId(), showUserNotFoundModal: action('showUserNotFoundModal'), setIsFetchingUUID, + preloadConversation: action('preloadConversation'), showConversation: action('showConversation'), blockConversation: action('blockConversation'), onOutgoingAudioCallInConversation: action( diff --git a/ts/components/LeftPane.tsx b/ts/components/LeftPane.tsx index 6ff881713b25..2af0470a33f2 100644 --- a/ts/components/LeftPane.tsx +++ b/ts/components/LeftPane.tsx @@ -139,6 +139,7 @@ export type PropsType = { showFindByUsername: () => void; showFindByPhoneNumber: () => void; showConversation: ShowConversationType; + preloadConversation: (conversationId: string) => void; showInbox: () => void; startComposing: () => void; startSearch: () => unknown; @@ -205,6 +206,7 @@ export function LeftPane({ openUsernameReservationModal, preferredWidthFromStorage, + preloadConversation, removeConversation, renderCaptchaDialog, renderCrashReportDialog, @@ -776,6 +778,7 @@ export function LeftPane({ } showConversation={showConversation} blockConversation={blockConversation} + onPreloadConversation={preloadConversation} onSelectConversation={onSelectConversation} onOutgoingAudioCallInConversation={ onOutgoingAudioCallInConversation diff --git a/ts/components/StoriesSettingsModal.tsx b/ts/components/StoriesSettingsModal.tsx index d98ca676a12e..e46e70f27771 100644 --- a/ts/components/StoriesSettingsModal.tsx +++ b/ts/components/StoriesSettingsModal.tsx @@ -1220,6 +1220,7 @@ export function EditDistributionListModal({ onClickContactCheckbox={(conversationId: string) => { toggleSelectedConversation(conversationId); }} + onPreloadConversation={shouldNeverBeCalled} onSelectConversation={shouldNeverBeCalled} blockConversation={shouldNeverBeCalled} removeConversation={shouldNeverBeCalled} diff --git a/ts/components/conversationList/BaseConversationListItem.tsx b/ts/components/conversationList/BaseConversationListItem.tsx index 52c9e387fa72..f3166f99f784 100644 --- a/ts/components/conversationList/BaseConversationListItem.tsx +++ b/ts/components/conversationList/BaseConversationListItem.tsx @@ -50,6 +50,7 @@ type PropsType = { messageText?: ReactNode; messageTextIsAlwaysFullSize?: boolean; onClick?: () => void; + onMouseDown?: () => void; shouldShowSpinner?: boolean; unreadCount?: number; unreadMentionsCount?: number; @@ -100,6 +101,7 @@ export const BaseConversationListItem: FunctionComponent = messageText, messageTextIsAlwaysFullSize, onClick, + onMouseDown, phoneNumber, profileName, sharedGroupNames, @@ -289,6 +291,7 @@ export const BaseConversationListItem: FunctionComponent = data-testid={testId} disabled={disabled} onClick={onClick} + onMouseDown={onMouseDown} type="button" > {contents} diff --git a/ts/components/conversationList/ConversationListItem.tsx b/ts/components/conversationList/ConversationListItem.tsx index 56b2d14e4fa3..175d4168ec90 100644 --- a/ts/components/conversationList/ConversationListItem.tsx +++ b/ts/components/conversationList/ConversationListItem.tsx @@ -74,6 +74,7 @@ type PropsHousekeeping = { buttonAriaLabel?: string; i18n: LocalizerType; onClick: (id: string) => void; + onMouseDown: (id: string) => void; theme: ThemeType; }; @@ -98,6 +99,7 @@ export const ConversationListItem: FunctionComponent = React.memo( markedUnread, muteExpiresAt, onClick, + onMouseDown, phoneNumber, profileName, removalStage, @@ -202,6 +204,10 @@ export const ConversationListItem: FunctionComponent = React.memo( } const onClickItem = useCallback(() => onClick(id), [onClick, id]); + const onMouseDownItem = useCallback( + () => onMouseDown(id), + [onMouseDown, id] + ); return ( = React.memo( messageText={messageText} messageTextIsAlwaysFullSize onClick={onClickItem} + onMouseDown={onMouseDownItem} phoneNumber={phoneNumber} profileName={profileName} sharedGroupNames={sharedGroupNames} diff --git a/ts/models/conversations.ts b/ts/models/conversations.ts index 25f837a70402..f65ca700831e 100644 --- a/ts/models/conversations.ts +++ b/ts/models/conversations.ts @@ -137,6 +137,7 @@ import { isIncoming, isStory, } from '../state/selectors/message'; +import { getPreloadedConversationId } from '../state/selectors/conversations'; import { conversationJobQueue, conversationQueueJobEnum, @@ -180,6 +181,7 @@ import { getConversationToDelete, getMessageToDelete, } from '../util/deleteForMe'; +import { explodePromise } from '../util/explodePromise'; import { getCallHistorySelector } from '../state/selectors/callHistory'; /* eslint-disable more/no-then */ @@ -1503,24 +1505,32 @@ export class ConversationModel extends window.Backbone } } - private setInProgressFetch(): () => unknown { + private async setInProgressFetch(): Promise<() => void> { const logId = `setInProgressFetch(${this.idForLogging()})`; + while (this.inProgressFetch != null) { + log.warn(`${logId}: blocked, waiting`); + // eslint-disable-next-line no-await-in-loop + await this.inProgressFetch; + } const start = Date.now(); - let resolvePromise: (value?: unknown) => void; - this.inProgressFetch = new Promise(resolve => { - resolvePromise = resolve; - }); + const { resolve, promise } = explodePromise(); + this.inProgressFetch = promise; + let isFinished = false; let timeout: NodeJS.Timeout; const finish = () => { + strictAssert(!isFinished, 'inProgressFetch.finish called twice'); + isFinished = true; + const duration = Date.now() - start; if (duration > 500) { log.warn(`${logId}: in progress fetch took ${duration}ms`); } - resolvePromise(); + resolve(); clearTimeout(timeout); + strictAssert(this.inProgressFetch === promise, `${logId}: conflict`); this.inProgressFetch = undefined; }; timeout = setTimeout(() => { @@ -1531,13 +1541,85 @@ export class ConversationModel extends window.Backbone return finish; } + async preloadNewestMessages(): Promise { + const logId = `preloadNewestMessages/${this.idForLogging()}`; + + const { addPreloadData } = window.reduxActions.conversations; + + // Bail-out of complex paths + if (!this.getAccepted()) { + log.info(`${logId}: not accepted, skipping`); + return; + } + + const finish = await this.setInProgressFetch(); + log.info(`${logId}: starting`); + try { + let metrics = await getMessageMetricsForConversation({ + conversationId: this.id, + includeStoryReplies: !isGroup(this.attributes), + }); + + let messages: ReadonlyArray; + if (metrics.oldestUnseen) { + const unseen = await getMessageById(metrics.oldestUnseen.id); + if (!unseen) { + throw new Error( + `preloadNewestMessages: failed to load oldestUnseen ${metrics.oldestUnseen.id}` + ); + } + + const receivedAt = unseen.received_at; + const sentAt = unseen.sent_at; + const { + older, + newer, + metrics: freshMetrics, + } = await getConversationRangeCenteredOnMessage({ + conversationId: this.id, + includeStoryReplies: !isGroup(this.attributes), + limit: MESSAGE_LOAD_CHUNK_SIZE, + messageId: unseen.id, + receivedAt, + sentAt, + storyId: undefined, + }); + messages = [...older, unseen, ...newer]; + + metrics = freshMetrics; + } else { + messages = await getOlderMessagesByConversation({ + conversationId: this.id, + includeStoryReplies: !isGroup(this.attributes), + limit: MESSAGE_LOAD_CHUNK_SIZE, + storyId: undefined, + }); + } + + const cleaned = await this.cleanAttributes(messages); + + log.info( + `${logId}: preloaded ${cleaned.length} messages, ` + + `latest timestamp=${cleaned.at(-1)?.sent_at}` + ); + + addPreloadData({ + conversationId: this.id, + messages: cleaned, + metrics, + }); + } finally { + finish(); + } + } + async loadNewestMessages( newestMessageId: string | undefined, setFocus: boolean | undefined ): Promise { const logId = `loadNewestMessages/${this.idForLogging()}`; - const { messagesReset, setMessageLoadingState } = + const { messagesReset, setMessageLoadingState, consumePreloadData } = window.reduxActions.conversations; const conversationId = this.id; @@ -1545,11 +1627,29 @@ export class ConversationModel extends window.Backbone conversationId, TimelineMessageLoadingState.DoingInitialLoad ); - const finish = this.setInProgressFetch(); + let finish: undefined | (() => void) = await this.setInProgressFetch(); + const preloadedId = getPreloadedConversationId( + window.reduxStore.getState() + ); try { let scrollToLatestUnread = true; + if ( + // Arguments provided by onConversationOpened + newestMessageId == null && + !setFocus && + // Cache conditions for preloadNewestMessages above (in case they are + // invalidated after loading cache) + this.getAccepted() && + // Existing preload + preloadedId === conversationId + ) { + log.info(`${logId}: preload cache still valid, skipping`); + consumePreloadData(preloadedId); + return; + } + if (newestMessageId) { const newestInMemoryMessage = await getMessageById(newestMessageId); if (newestInMemoryMessage) { @@ -1581,7 +1681,11 @@ export class ConversationModel extends window.Backbone metrics.oldest ) { log.info(`${logId}: scrolling to oldest ${metrics.oldest.sent_at}`); - void this.loadAndScroll(metrics.oldest.id, { disableScroll: true }); + void this.loadAndScroll(metrics.oldest.id, { + disableScroll: true, + onFinish: finish, + }); + finish = undefined; return; } @@ -1591,7 +1695,9 @@ export class ConversationModel extends window.Backbone ); void this.loadAndScroll(metrics.oldestUnseen.id, { disableScroll: !setFocus, + onFinish: finish, }); + finish = undefined; return; } @@ -1628,7 +1734,7 @@ export class ConversationModel extends window.Backbone setMessageLoadingState(conversationId, undefined); throw error; } finally { - finish(); + finish?.(); } } async loadOlderMessages(oldestMessageId: string): Promise { @@ -1642,7 +1748,7 @@ export class ConversationModel extends window.Backbone conversationId, TimelineMessageLoadingState.LoadingOlderMessages ); - const finish = this.setInProgressFetch(); + const finish = await this.setInProgressFetch(); try { const message = await getMessageById(oldestMessageId); @@ -1699,7 +1805,7 @@ export class ConversationModel extends window.Backbone conversationId, TimelineMessageLoadingState.LoadingNewerMessages ); - const finish = this.setInProgressFetch(); + const finish = await this.setInProgressFetch(); try { const message = await getMessageById(newestMessageId); @@ -1744,7 +1850,7 @@ export class ConversationModel extends window.Backbone async loadAndScroll( messageId: string, - options?: { disableScroll?: boolean } + options: { disableScroll?: boolean; onFinish?: () => void } = {} ): Promise { const { messagesReset, setMessageLoadingState } = window.reduxActions.conversations; @@ -1754,7 +1860,10 @@ export class ConversationModel extends window.Backbone conversationId, TimelineMessageLoadingState.DoingInitialLoad ); - const finish = this.setInProgressFetch(); + let { onFinish: finish } = options; + if (!finish) { + finish = await this.setInProgressFetch(); + } try { const message = await getMessageById(messageId); diff --git a/ts/sql/Server.ts b/ts/sql/Server.ts index 6739edc0ee51..3374f8a0de23 100644 --- a/ts/sql/Server.ts +++ b/ts/sql/Server.ts @@ -3443,11 +3443,9 @@ function getMessageMetricsForConversation( ); return { - oldest: oldest ? pick(oldest, ['received_at', 'sent_at', 'id']) : undefined, - newest: newest ? pick(newest, ['received_at', 'sent_at', 'id']) : undefined, - oldestUnseen: oldestUnseen - ? pick(oldestUnseen, ['received_at', 'sent_at', 'id']) - : undefined, + oldest, + newest, + oldestUnseen, totalUnseen, }; } diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 1f610cd598c9..968dcf0cef81 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -435,6 +435,11 @@ export type ConversationMessageType = ReadonlyDeep<{ scrollToMessageId?: string; scrollToMessageCounter: number; }>; +export type ConversationPreloadDataType = ReadonlyDeep<{ + conversationId: string; + messages: ReadonlyArray; + metrics: MessageMetricsType; +}>; export type MessagesByConversationType = ReadonlyDeep<{ [key: string]: ConversationMessageType | undefined; @@ -550,6 +555,8 @@ export type ConversationsStateType = ReadonlyDeep<{ // Note: it's very important that both of these locations are always kept up to date messagesLookup: MessageLookupType; messagesByConversation: MessagesByConversationType; + + preloadData?: ConversationPreloadDataType; }>; // Helpers @@ -979,9 +986,20 @@ type ReplaceAvatarsActionType = ReadonlyDeep<{ avatars: ReadonlyArray; }; }>; +export type AddPreloadDataActionType = ReadonlyDeep<{ + type: 'ADD_PRELOAD_DATA'; + payload: ConversationPreloadDataType; +}>; +export type ConsumePreloadDataActionType = ReadonlyDeep<{ + type: 'CONSUME_PRELOAD_DATA'; + payload: { + conversationId: string; + }; +}>; // eslint-disable-next-line local-rules/type-alias-readonlydeep export type ConversationActionType = + | AddPreloadDataActionType | CancelVerificationDataByConversationActionType | ClearCancelledVerificationActionType | ClearGroupCreationErrorActionType @@ -997,6 +1015,7 @@ export type ConversationActionType = | ComposeDeleteAvatarActionType | ComposeReplaceAvatarsActionType | ComposeSaveAvatarActionType + | ConsumePreloadDataActionType | ConversationAddedActionType | ConversationChangedActionType | ConversationRemovedActionType @@ -1057,6 +1076,7 @@ export const actions = { acceptConversation, acknowledgeGroupMemberNameCollisions, addMembersToGroup, + addPreloadData, approvePendingMembershipFromGroupV2, reportSpam, blockAndReportSpam, @@ -1076,6 +1096,7 @@ export const actions = { composeDeleteAvatarFromDisk, composeReplaceAvatar, composeSaveAvatarToDisk, + consumePreloadData, conversationAdded, conversationChanged, conversationRemoved, @@ -3028,6 +3049,33 @@ function messagesReset({ }, }; } +function addPreloadData( + preloadData: ConversationPreloadDataType +): AddPreloadDataActionType { + const { messages, conversationId } = preloadData; + for (const message of messages) { + strictAssert( + message.conversationId === conversationId, + `addPreloadData(${conversationId}): invalid message conversationId ` + + `${message.conversationId}` + ); + } + + return { + type: 'ADD_PRELOAD_DATA', + payload: preloadData, + }; +} +function consumePreloadData( + conversationId: string +): ConsumePreloadDataActionType { + return { + type: 'CONSUME_PRELOAD_DATA', + payload: { + conversationId, + }, + }; +} function setMessageLoadingState( conversationId: string, messageLoadingState: undefined | TimelineMessageLoadingState @@ -4795,6 +4843,94 @@ function updateNicknameAndNote( }; } +function updateMessageLookup( + state: ConversationsStateType, + { + conversationId, + messages, + metrics, + scrollToMessageId, + unboundedFetch, + }: { + conversationId: string; + messages: ReadonlyArray; + metrics: MessageMetricsType; + scrollToMessageId?: string | undefined; + unboundedFetch: boolean; + } +): ConversationsStateType { + const { messagesByConversation, messagesLookup } = state; + const existingConversation = messagesByConversation[conversationId]; + + const lookup = fromPairs(messages.map(message => [message.id, message])); + const sorted = orderBy( + values(lookup), + ['received_at', 'sent_at'], + ['ASC', 'ASC'] + ); + + let { newest, oldest } = metrics; + + // If our metrics are a little out of date, we'll fix them up + if (sorted.length > 0) { + const first = sorted[0]; + if (first && (!oldest || first.received_at <= oldest.received_at)) { + oldest = pick(first, ['id', 'received_at', 'sent_at']); + } + + const last = sorted[sorted.length - 1]; + if ( + last && + (!newest || unboundedFetch || last.received_at >= newest.received_at) + ) { + newest = pick(last, ['id', 'received_at', 'sent_at']); + } + } + + const messageIds = sorted.map(message => message.id); + + return { + ...state, + preloadData: undefined, + ...(state.selectedConversationId === conversationId + ? { + targetedMessage: scrollToMessageId, + targetedMessageCounter: state.targetedMessageCounter + 1, + targetedMessageSource: TargetedMessageSource.Reset, + } + : {}), + messagesLookup: { + ...messagesLookup, + ...lookup, + }, + messagesByConversation: { + ...messagesByConversation, + [conversationId]: { + messageChangeCounter: 0, + scrollToMessageId, + scrollToMessageCounter: existingConversation + ? existingConversation.scrollToMessageCounter + 1 + : 0, + messageIds, + metrics: { + ...metrics, + newest, + oldest, + }, + }, + }, + }; +} + +function dropPreloadData( + state: ConversationsStateType +): ConversationsStateType { + if (state.preloadData == null) { + return state; + } + return { ...state, preloadData: undefined }; +} + export function reducer( state: Readonly = getEmptyState(), action: Readonly< @@ -5395,7 +5531,7 @@ export function reducer( if (!existingConversation) { return maybeUpdateSelectedMessageForDetails( { messageId: id, targetedMessageForDetails: data }, - state + dropPreloadData(state) ); } @@ -5404,14 +5540,14 @@ export function reducer( if (!existingMessage) { return maybeUpdateSelectedMessageForDetails( { messageId: id, targetedMessageForDetails: data }, - state + dropPreloadData(state) ); } const conversationAttrs = state.conversationLookup[conversationId]; const isGroupStoryReply = isGroup(conversationAttrs) && data.storyId; if (isGroupStoryReply) { - return state; + return dropPreloadData(state); } const hasNewEdit = @@ -5434,6 +5570,7 @@ export function reducer( }, state ), + preloadData: undefined, messagesByConversation: { ...state.messagesByConversation, [conversationId]: { @@ -5514,75 +5651,32 @@ export function reducer( } if (action.type === 'MESSAGES_RESET') { - const { - conversationId, - messages, - metrics, - scrollToMessageId, - unboundedFetch, - } = action.payload; - const { messagesByConversation, messagesLookup } = state; - - const existingConversation = messagesByConversation[conversationId]; - - const lookup = fromPairs(messages.map(message => [message.id, message])); - const sorted = orderBy( - values(lookup), - ['received_at', 'sent_at'], - ['ASC', 'ASC'] - ); - - let { newest, oldest } = metrics; - - // If our metrics are a little out of date, we'll fix them up - if (sorted.length > 0) { - const first = sorted[0]; - if (first && (!oldest || first.received_at <= oldest.received_at)) { - oldest = pick(first, ['id', 'received_at', 'sent_at']); - } - - const last = sorted[sorted.length - 1]; - if ( - last && - (!newest || unboundedFetch || last.received_at >= newest.received_at) - ) { - newest = pick(last, ['id', 'received_at', 'sent_at']); - } - } - - const messageIds = sorted.map(message => message.id); - + return updateMessageLookup(state, action.payload); + } + if (action.type === 'ADD_PRELOAD_DATA') { return { ...state, - ...(state.selectedConversationId === conversationId - ? { - targetedMessage: scrollToMessageId, - targetedMessageCounter: state.targetedMessageCounter + 1, - targetedMessageSource: TargetedMessageSource.Reset, - } - : {}), - messagesLookup: { - ...messagesLookup, - ...lookup, - }, - messagesByConversation: { - ...messagesByConversation, - [conversationId]: { - messageChangeCounter: 0, - scrollToMessageId, - scrollToMessageCounter: existingConversation - ? existingConversation.scrollToMessageCounter + 1 - : 0, - messageIds, - metrics: { - ...metrics, - newest, - oldest, - }, - }, - }, + preloadData: action.payload, }; } + if (action.type === 'CONSUME_PRELOAD_DATA') { + const { preloadData, selectedConversationId } = state; + const { conversationId } = action.payload; + if (!preloadData) { + return state; + } + if ( + preloadData.conversationId !== conversationId || + selectedConversationId !== conversationId + ) { + return dropPreloadData(state); + } + + return updateMessageLookup(state, { + ...preloadData, + unboundedFetch: true, + }); + } if (action.type === 'SET_MESSAGE_LOADING_STATE') { const { payload } = action; const { conversationId, messageLoadingState } = payload; @@ -5676,7 +5770,7 @@ export function reducer( if (!existingConversation) { return maybeUpdateSelectedMessageForDetails( { messageId: id, targetedMessageForDetails: undefined }, - state + dropPreloadData(state) ); } @@ -5725,6 +5819,7 @@ export function reducer( { messageId: id, targetedMessageForDetails: undefined }, state ), + preloadData: undefined, messagesLookup: omit(messagesLookup, id), messagesByConversation: { [conversationId]: { @@ -5863,7 +5958,7 @@ export function reducer( ); } - return state; + return dropPreloadData(state); } } @@ -5908,6 +6003,7 @@ export function reducer( return { ...state, + preloadData: undefined, messagesLookup: { ...messagesLookup, ...lookup, @@ -5978,6 +6074,10 @@ export function reducer( const nextState = { ...state, + preloadData: + state.preloadData?.conversationId === conversationId + ? state.preloadData + : undefined, hasContactSpoofingReview: false, selectedConversationId: conversationId, targetedMessage: messageId, diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 1fb2242d7991..03367eeaea78 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -1335,3 +1335,8 @@ export const getLastEditableMessageId = createSelector( return undefined; } ); + +export const getPreloadedConversationId = createSelector( + getConversations, + ({ preloadData }): string | undefined => preloadData?.conversationId +); diff --git a/ts/state/smart/LeftPane.tsx b/ts/state/smart/LeftPane.tsx index 2d84ad539be9..17afc136e6e2 100644 --- a/ts/state/smart/LeftPane.tsx +++ b/ts/state/smart/LeftPane.tsx @@ -20,6 +20,7 @@ import { getCountryDataForLocale } from '../../util/getCountryData'; import { lookupConversationWithoutServiceId } from '../../util/lookupConversationWithoutServiceId'; import { missingCaseError } from '../../util/missingCaseError'; import { isDone as isRegistrationDone } from '../../util/registration'; +import { drop } from '../../util/drop'; import { useCallingActions } from '../ducks/calling'; import { useConversationsActions } from '../ducks/conversations'; import { ComposerStep, OneTimeModalState } from '../ducks/conversationsEnums'; @@ -256,6 +257,12 @@ const getModeSpecificProps = ( } }; +function preloadConversation(conversationId: string): void { + drop( + window.ConversationController.get(conversationId)?.preloadNewestMessages() + ); +} + export const SmartLeftPane = memo(function SmartLeftPane({ hasFailedStorySends, hasPendingUpdate, @@ -385,6 +392,7 @@ export const SmartLeftPane = memo(function SmartLeftPane({ openUsernameReservationModal={openUsernameReservationModal} otherTabsUnreadStats={otherTabsUnreadStats} preferredWidthFromStorage={preferredWidthFromStorage} + preloadConversation={preloadConversation} removeConversation={removeConversation} renderCaptchaDialog={renderCaptchaDialog} renderCrashReportDialog={renderCrashReportDialog}