Preload conversation open data
Co-authored-by: Scott Nonnenberg <scott@signal.org>
This commit is contained in:
parent
6ea47d9c6b
commit
7db33a6708
14 changed files with 332 additions and 89 deletions
|
@ -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}
|
||||
/>
|
||||
))}
|
||||
|
|
|
@ -68,6 +68,7 @@ function Wrapper({
|
|||
shouldRecomputeRowHeights={false}
|
||||
i18n={i18n}
|
||||
blockConversation={action('blockConversation')}
|
||||
onPreloadConversation={action('onPreloadConversation')}
|
||||
onSelectConversation={action('onSelectConversation')}
|
||||
onOutgoingAudioCallInConversation={action(
|
||||
'onOutgoingAudioCallInConversation'
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -400,6 +400,7 @@ export function ForwardMessagesModal({
|
|||
showConversation={shouldNeverBeCalled}
|
||||
showUserNotFoundModal={shouldNeverBeCalled}
|
||||
setIsFetchingUUID={shouldNeverBeCalled}
|
||||
onPreloadConversation={shouldNeverBeCalled}
|
||||
onSelectConversation={shouldNeverBeCalled}
|
||||
blockConversation={shouldNeverBeCalled}
|
||||
removeConversation={shouldNeverBeCalled}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1220,6 +1220,7 @@ export function EditDistributionListModal({
|
|||
onClickContactCheckbox={(conversationId: string) => {
|
||||
toggleSelectedConversation(conversationId);
|
||||
}}
|
||||
onPreloadConversation={shouldNeverBeCalled}
|
||||
onSelectConversation={shouldNeverBeCalled}
|
||||
blockConversation={shouldNeverBeCalled}
|
||||
removeConversation={shouldNeverBeCalled}
|
||||
|
|
|
@ -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<PropsType> =
|
|||
messageText,
|
||||
messageTextIsAlwaysFullSize,
|
||||
onClick,
|
||||
onMouseDown,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
sharedGroupNames,
|
||||
|
@ -289,6 +291,7 @@ export const BaseConversationListItem: FunctionComponent<PropsType> =
|
|||
data-testid={testId}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
onMouseDown={onMouseDown}
|
||||
type="button"
|
||||
>
|
||||
{contents}
|
||||
|
|
|
@ -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<Props> = React.memo(
|
|||
markedUnread,
|
||||
muteExpiresAt,
|
||||
onClick,
|
||||
onMouseDown,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
removalStage,
|
||||
|
@ -202,6 +204,10 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
|
|||
}
|
||||
|
||||
const onClickItem = useCallback(() => onClick(id), [onClick, id]);
|
||||
const onMouseDownItem = useCallback(
|
||||
() => onMouseDown(id),
|
||||
[onMouseDown, id]
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseConversationListItem
|
||||
|
@ -223,6 +229,7 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
|
|||
messageText={messageText}
|
||||
messageTextIsAlwaysFullSize
|
||||
onClick={onClickItem}
|
||||
onMouseDown={onMouseDownItem}
|
||||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
|
|
|
@ -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<void>();
|
||||
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<void> {
|
||||
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<MessageAttributesType>;
|
||||
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<void> {
|
||||
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<void> {
|
||||
|
@ -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<void> {
|
||||
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);
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -435,6 +435,11 @@ export type ConversationMessageType = ReadonlyDeep<{
|
|||
scrollToMessageId?: string;
|
||||
scrollToMessageCounter: number;
|
||||
}>;
|
||||
export type ConversationPreloadDataType = ReadonlyDeep<{
|
||||
conversationId: string;
|
||||
messages: ReadonlyArray<ReadonlyMessageAttributesType>;
|
||||
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<AvatarDataType>;
|
||||
};
|
||||
}>;
|
||||
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<ReadonlyMessageAttributesType>;
|
||||
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<ConversationsStateType> = 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,
|
||||
|
|
|
@ -1335,3 +1335,8 @@ export const getLastEditableMessageId = createSelector(
|
|||
return undefined;
|
||||
}
|
||||
);
|
||||
|
||||
export const getPreloadedConversationId = createSelector(
|
||||
getConversations,
|
||||
({ preloadData }): string | undefined => preloadData?.conversationId
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
|
|
Loading…
Reference in a new issue