Improve timeline rendering performance

This commit is contained in:
trevor-signal 2024-02-27 11:01:25 -05:00 committed by GitHub
parent c319a089d2
commit 167b2f4f1c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 329 additions and 106 deletions

View File

@ -1,6 +1,7 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import memoizee from 'memoizee';
import { makeEnumParser } from '../util/enum';
/**
@ -153,22 +154,111 @@ const STATE_TRANSITIONS: Record<SendActionType, SendStatus> = {
export type SendStateByConversationId = Record<string, SendState>;
/** Test all of sendStateByConversationId for predicate */
export const someSendStatus = (
sendStateByConversationId: undefined | Readonly<SendStateByConversationId>,
sendStateByConversationId: SendStateByConversationId,
predicate: (value: SendStatus) => boolean
): boolean =>
Object.values(sendStateByConversationId || {}).some(sendState =>
predicate(sendState.status)
);
): boolean => {
return [
...summarizeMessageSendStatuses(sendStateByConversationId).statuses,
].some(predicate);
};
/** Test sendStateByConversationId, excluding ourConversationId, for predicate */
export const someRecipientSendStatus = (
sendStateByConversationId: SendStateByConversationId,
ourConversationId: string | undefined,
predicate: (value: SendStatus) => boolean
): boolean => {
return getStatusesIgnoringOurConversationId(
sendStateByConversationId,
ourConversationId
).some(predicate);
};
export const isMessageJustForMe = (
sendStateByConversationId: undefined | Readonly<SendStateByConversationId>,
sendStateByConversationId: SendStateByConversationId,
ourConversationId: string | undefined
): boolean => {
const conversationIds = Object.keys(sendStateByConversationId || {});
return Boolean(
ourConversationId &&
conversationIds.length === 1 &&
conversationIds[0] === ourConversationId
const { length } = summarizeMessageSendStatuses(sendStateByConversationId);
return (
ourConversationId !== undefined &&
length === 1 &&
Object.hasOwn(sendStateByConversationId, ourConversationId)
);
};
export const getHighestSuccessfulRecipientStatus = (
sendStateByConversationId: SendStateByConversationId,
ourConversationId: string | undefined
): SendStatus => {
return getStatusesIgnoringOurConversationId(
sendStateByConversationId,
ourConversationId
).reduce(
(result: SendStatus, status) => maxStatus(result, status),
SendStatus.Pending
);
};
const getStatusesIgnoringOurConversationId = (
sendStateByConversationId: SendStateByConversationId,
ourConversationId: string | undefined
): Array<SendStatus> => {
const { statuses, statusesWithOnlyOneConversationId } =
summarizeMessageSendStatuses(sendStateByConversationId);
const statusesIgnoringOurConversationId = [];
for (const status of statuses) {
if (
ourConversationId &&
statusesWithOnlyOneConversationId.get(status) === ourConversationId
) {
// ignore this status; it only applies to us
} else {
statusesIgnoringOurConversationId.push(status);
}
}
return statusesIgnoringOurConversationId;
};
// Looping through each value in sendStateByConversationId can be quite slow, especially
// if sendStateByConversationId is large (e.g. in a large group) and if it is actually a
// proxy (e.g. being called via useProxySelector) -- that's why we memoize it here.
const summarizeMessageSendStatuses = memoizee(
(
sendStateByConversationId: SendStateByConversationId
): {
statuses: Set<SendStatus>;
statusesWithOnlyOneConversationId: Map<SendStatus, string>;
length: number;
} => {
const statuses: Set<SendStatus> = new Set();
// We keep track of statuses with only one conversationId associated with it
// so that we can ignore a status if it is only for ourConversationId, as needed
const statusesWithOnlyOneConversationId: Map<SendStatus, string> =
new Map();
const entries = Object.entries(sendStateByConversationId);
for (const [conversationId, { status }] of entries) {
if (!statuses.has(status)) {
statuses.add(status);
statusesWithOnlyOneConversationId.set(status, conversationId);
} else {
statusesWithOnlyOneConversationId.delete(status);
}
}
return {
statuses,
statusesWithOnlyOneConversationId,
length: entries.length,
};
},
{ max: 100 }
);

View File

@ -2,13 +2,11 @@
// SPDX-License-Identifier: AGPL-3.0-only
import {
isEmpty,
isNumber,
isObject,
mapValues,
maxBy,
noop,
omit,
partition,
pick,
union,
@ -58,7 +56,7 @@ import {
SendStatus,
isSent,
sendStateReducer,
someSendStatus,
someRecipientSendStatus,
} from '../messages/MessageSendState';
import { migrateLegacyReadStatus } from '../messages/migrateLegacyReadStatus';
import { migrateLegacySendAttributes } from '../messages/migrateLegacySendAttributes';
@ -824,11 +822,14 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
public hasSuccessfulDelivery(): boolean {
const sendStateByConversationId = this.get('sendStateByConversationId');
const withoutMe = omit(
sendStateByConversationId,
window.ConversationController.getOurConversationIdOrThrow()
const ourConversationId =
window.ConversationController.getOurConversationIdOrThrow();
return someRecipientSendStatus(
sendStateByConversationId ?? {},
ourConversationId,
isSent
);
return isEmpty(withoutMe) || someSendStatus(withoutMe, isSent);
}
/**

View File

@ -2,7 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
import type { ThunkAction } from 'redux-thunk';
import { mapValues } from 'lodash';
import { isEqual, mapValues } from 'lodash';
import type { ReadonlyDeep } from 'type-fest';
import type { StateType as RootStateType } from '../reducer';
import type { BadgeType, BadgeImageType } from '../../badges/types';
@ -147,6 +147,9 @@ export function reducer(
}
});
if (isEqual(state.byId, newById)) {
return state;
}
return {
...state,
byId: newById,

View File

@ -904,7 +904,7 @@ export const getConversationByServiceIdSelector = createSelector(
getOwn(conversationsByServiceId, serviceId)
);
const getCachedConversationMemberColorsSelector = createSelector(
export const getCachedConversationMemberColorsSelector = createSelector(
getConversationSelector,
getUserConversationId,
(
@ -958,23 +958,30 @@ export const getContactNameColorSelector = createSelector(
conversationId: string,
contactId: string | undefined
): ContactNameColorType => {
if (!contactId) {
log.warn('No color generated for missing contactId');
return ContactNameColors[0];
}
const contactNameColors =
conversationMemberColorsSelector(conversationId);
const color = contactNameColors.get(contactId);
if (!color) {
log.warn(`No color generated for contact ${contactId}`);
return ContactNameColors[0];
}
return color;
return getContactNameColor(contactNameColors, contactId);
};
}
);
export const getContactNameColor = (
contactNameColors: Map<string, string>,
contactId: string | undefined
): string => {
if (!contactId) {
log.warn('No color generated for missing contactId');
return ContactNameColors[0];
}
const color = contactNameColors.get(contactId);
if (!color) {
log.warn(`No color generated for contact ${contactId}`);
return ContactNameColors[0];
}
return color;
};
export function _conversationMessagesSelector(
conversation: ConversationMessageType
): TimelinePropsType {

View File

@ -1,7 +1,7 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { groupBy, isEmpty, isNumber, isObject, map, omit } from 'lodash';
import { groupBy, isEmpty, isNumber, isObject, map } from 'lodash';
import { createSelector } from 'reselect';
import filesize from 'filesize';
import getDirection from 'direction';
@ -59,7 +59,7 @@ import { getMentionsRegex } from '../../types/Message';
import { SignalService as Proto } from '../../protobuf';
import type { AttachmentType } from '../../types/Attachment';
import { isVoiceMessage, canBeDownloaded } from '../../types/Attachment';
import type { DefaultConversationColorType } from '../../types/Colors';
import { type DefaultConversationColorType } from '../../types/Colors';
import { ReadStatus } from '../../messages/MessageReadStatus';
import type { CallingNotificationType } from '../../util/callingNotification';
@ -74,12 +74,13 @@ import { canEditMessage } from '../../util/canEditMessage';
import { getAccountSelector } from './accounts';
import { getDefaultConversationColor } from './items';
import {
getContactNameColorSelector,
getConversationSelector,
getSelectedMessageIds,
getTargetedMessage,
isMissingRequiredProfileSharing,
getMessages,
getCachedConversationMemberColorsSelector,
getContactNameColor,
} from './conversations';
import {
getIntl,
@ -97,19 +98,17 @@ import type {
import type { AccountSelectorType } from './accounts';
import type { CallSelectorType, CallStateType } from './calling';
import type {
GetConversationByIdType,
ContactNameColorSelectorType,
} from './conversations';
import type { GetConversationByIdType } from './conversations';
import {
SendStatus,
isDelivered,
isFailed,
isMessageJustForMe,
isRead,
isSent,
isViewed,
maxStatus,
isMessageJustForMe,
someRecipientSendStatus,
getHighestSuccessfulRecipientStatus,
someSendStatus,
} from '../../messages/MessageSendState';
import * as log from '../../logging/log';
@ -179,7 +178,7 @@ export type GetPropsForBubbleOptions = Readonly<{
callHistorySelector: CallHistorySelectorType;
activeCall?: CallStateType;
accountSelector: AccountSelectorType;
contactNameColorSelector: ContactNameColorSelectorType;
contactNameColors: Map<string, string>;
defaultConversationColor: DefaultConversationColorType;
}>;
@ -581,7 +580,7 @@ export type GetPropsForMessageOptions = Pick<
| 'selectedMessageIds'
| 'regionCode'
| 'accountSelector'
| 'contactNameColorSelector'
| 'contactNameColors'
| 'defaultConversationColor'
>;
@ -679,7 +678,7 @@ export const getPropsForMessage = (
targetedMessageId,
targetedMessageCounter,
selectedMessageIds,
contactNameColorSelector,
contactNameColors,
defaultConversationColor,
} = options;
@ -708,7 +707,7 @@ export const getPropsForMessage = (
ourNumber,
ourAci,
});
const contactNameColor = contactNameColorSelector(conversationId, authorId);
const contactNameColor = getContactNameColor(contactNameColors, authorId);
const { conversationColor, customColor } = getConversationColorAttributes(
conversation,
@ -786,7 +785,7 @@ export const getMessagePropsSelector = createSelector(
getUserNumber,
getRegionCode,
getAccountSelector,
getContactNameColorSelector,
getCachedConversationMemberColorsSelector,
getTargetedMessage,
getSelectedMessageIds,
getDefaultConversationColor,
@ -798,15 +797,18 @@ export const getMessagePropsSelector = createSelector(
ourNumber,
regionCode,
accountSelector,
contactNameColorSelector,
cachedConversationMemberColorsSelector,
targetedMessage,
selectedMessageIds,
defaultConversationColor
) =>
(message: MessageWithUIFieldsType) => {
const contactNameColors = cachedConversationMemberColorsSelector(
message.conversationId
);
return getPropsForMessage(message, {
accountSelector,
contactNameColorSelector,
contactNameColors,
conversationSelector,
ourConversationId,
ourNumber,
@ -1646,14 +1648,9 @@ export function getMessagePropStatus(
return sent ? 'viewed' : 'sending';
}
const sendStates = Object.values(
const highestSuccessfulStatus = getHighestSuccessfulRecipientStatus(
sendStateByConversationId,
ourConversationId
? omit(sendStateByConversationId, ourConversationId)
: sendStateByConversationId
);
const highestSuccessfulStatus = sendStates.reduce(
(result: SendStatus, { status }) => maxStatus(result, status),
SendStatus.Pending
);
if (
@ -1758,8 +1755,8 @@ function canReplyOrReact(
MessageWithUIFieldsType,
| 'canReplyToStory'
| 'deletedForEveryone'
| 'sendStateByConversationId'
| 'payment'
| 'sendStateByConversationId'
| 'type'
>,
ourConversationId: string | undefined,
@ -1800,11 +1797,10 @@ function canReplyOrReact(
if (isOutgoing(message)) {
return (
isMessageJustForMe(sendStateByConversationId, ourConversationId) ||
someSendStatus(
ourConversationId
? omit(sendStateByConversationId, ourConversationId)
: sendStateByConversationId,
isMessageJustForMe(sendStateByConversationId ?? {}, ourConversationId) ||
someRecipientSendStatus(
sendStateByConversationId ?? {},
ourConversationId,
isSent
)
);
@ -1884,7 +1880,7 @@ export function canDeleteForEveryone(
// Is it too old to delete? (we relax that requirement in Note to Self)
(isMoreRecentThan(message.sent_at, DAY) || isMe) &&
// Is it sent to anyone?
someSendStatus(message.sendStateByConversationId, isSent)
someSendStatus(message.sendStateByConversationId ?? {}, isSent)
);
}
@ -1971,7 +1967,7 @@ const OUTGOING_KEY_ERROR = 'OutgoingIdentityKeyError';
export const getMessageDetails = createSelector(
getAccountSelector,
getContactNameColorSelector,
getCachedConversationMemberColorsSelector,
getConversationSelector,
getIntl,
getRegionCode,
@ -1984,7 +1980,7 @@ export const getMessageDetails = createSelector(
getDefaultConversationColor,
(
accountSelector,
contactNameColorSelector,
cachedConversationMemberColorsSelector,
conversationSelector,
i18n,
regionCode,
@ -2122,7 +2118,9 @@ export const getMessageDetails = createSelector(
errors,
message: getPropsForMessage(message, {
accountSelector,
contactNameColorSelector,
contactNameColors: cachedConversationMemberColorsSelector(
message.conversationId
),
conversationSelector,
ourAci,
ourPni,

View File

@ -1,15 +1,16 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { useSelector } from 'react-redux';
import type { TimelineItemType } from '../../components/conversation/TimelineItem';
import type { StateType } from '../reducer';
import {
getContactNameColorSelector,
getConversationSelector,
getTargetedMessage,
getMessages,
getSelectedMessageIds,
getMessages,
getCachedConversationMemberColorsSelector,
} from './conversations';
import { getAccountSelector } from './accounts';
import {
@ -23,18 +24,20 @@ import { getDefaultConversationColor } from './items';
import { getActiveCall, getCallSelector } from './calling';
import { getPropsForBubble } from './message';
import { getCallHistorySelector } from './callHistory';
import { useProxySelector } from '../../hooks/useProxySelector';
export const getTimelineItem = (
const getTimelineItem = (
state: StateType,
id?: string
messageId: string | undefined,
contactNameColors: Map<string, string>
): TimelineItemType | undefined => {
if (id === undefined) {
if (messageId === undefined) {
return undefined;
}
const messageLookup = getMessages(state);
const message = messageLookup[id];
const message = messageLookup[messageId];
if (!message) {
return undefined;
}
@ -50,7 +53,6 @@ export const getTimelineItem = (
const callHistorySelector = getCallHistorySelector(state);
const activeCall = getActiveCall(state);
const accountSelector = getAccountSelector(state);
const contactNameColorSelector = getContactNameColorSelector(state);
const selectedMessageIds = getSelectedMessageIds(state);
const defaultConversationColor = getDefaultConversationColor(state);
@ -63,7 +65,7 @@ export const getTimelineItem = (
regionCode,
targetedMessageId: targetedMessage?.id,
targetedMessageCounter: targetedMessage?.counter,
contactNameColorSelector,
contactNameColors,
callSelector,
callHistorySelector,
activeCall,
@ -72,3 +74,18 @@ export const getTimelineItem = (
defaultConversationColor,
});
};
export const useTimelineItem = (
messageId: string | undefined,
conversationId: string
): TimelineItemType | undefined => {
// Generating contact name colors can take a while in large groups. We don't want to do
// this inside of useProxySelector, since the proxied state invalidates the memoization
// from createSelector. So we do the expensive part outside of useProxySelector, taking
// advantage of reselect's global cache.
const contactNameColors = useSelector(
getCachedConversationMemberColorsSelector
)(conversationId);
return useProxySelector(getTimelineItem, messageId, contactNameColors);
};

View File

@ -2,7 +2,6 @@
// SPDX-License-Identifier: AGPL-3.0-only
import { isEmpty, pick } from 'lodash';
import type { RefObject } from 'react';
import React from 'react';
import { connect } from 'react-redux';
@ -25,7 +24,7 @@ import {
} from '../selectors/conversations';
import { selectAudioPlayerActive } from '../selectors/audioPlayer';
import { SmartTimelineItem } from './TimelineItem';
import { SmartTimelineItem, type SmartTimelineItemProps } from './TimelineItem';
import { SmartCollidingAvatars } from './CollidingAvatars';
import type { PropsType as SmartCollidingAvatarsPropsType } from './CollidingAvatars';
import { SmartContactSpoofingReviewDialog } from './ContactSpoofingReviewDialog';
@ -40,8 +39,6 @@ import {
getCollisionsFromMemberships,
} from '../../util/groupMemberNameCollisions';
import { ContactSpoofingType } from '../../util/contactSpoofing';
import type { UnreadIndicatorPlacement } from '../../util/timelineUtil';
import type { WidthBreakpoint } from '../../components/_util';
import { getPreferredBadgeSelector } from '../selectors/badges';
import { SmartMiniPlayer } from './MiniPlayer';
@ -58,16 +55,7 @@ function renderItem({
nextMessageId,
previousMessageId,
unreadIndicatorPlacement,
}: {
containerElementRef: RefObject<HTMLElement>;
containerWidthBreakpoint: WidthBreakpoint;
conversationId: string;
isOldestTimelineItem: boolean;
messageId: string;
nextMessageId: undefined | string;
previousMessageId: undefined | string;
unreadIndicatorPlacement: undefined | UnreadIndicatorPlacement;
}): JSX.Element {
}: SmartTimelineItemProps): JSX.Element {
return (
<SmartTimelineItem
containerElementRef={containerElementRef}

View File

@ -7,7 +7,6 @@ import { useSelector } from 'react-redux';
import { TimelineItem } from '../../components/conversation/TimelineItem';
import type { WidthBreakpoint } from '../../components/_util';
import { useProxySelector } from '../../hooks/useProxySelector';
import { useConversationsActions } from '../ducks/conversations';
import { useComposerActions } from '../ducks/composer';
import { useGlobalModalActions } from '../ducks/globalModals';
@ -23,7 +22,7 @@ import {
getPlatform,
} from '../selectors/user';
import { getTargetedMessage } from '../selectors/conversations';
import { getTimelineItem } from '../selectors/timeline';
import { useTimelineItem } from '../selectors/timeline';
import {
areMessagesInSameGroup,
shouldCurrentMessageHideMetadata,
@ -37,7 +36,7 @@ import { renderAudioAttachment } from './renderAudioAttachment';
import { renderEmojiPicker } from './renderEmojiPicker';
import { renderReactionPicker } from './renderReactionPicker';
type ExternalProps = {
export type SmartTimelineItemProps = {
containerElementRef: RefObject<HTMLElement>;
containerWidthBreakpoint: WidthBreakpoint;
conversationId: string;
@ -55,8 +54,7 @@ function renderContact(contactId: string): JSX.Element {
function renderUniversalTimerNotification(): JSX.Element {
return <SmartUniversalTimerNotification />;
}
export function SmartTimelineItem(props: ExternalProps): JSX.Element {
export function SmartTimelineItem(props: SmartTimelineItemProps): JSX.Element {
const {
containerElementRef,
containerWidthBreakpoint,
@ -73,10 +71,10 @@ export function SmartTimelineItem(props: ExternalProps): JSX.Element {
const interactionMode = useSelector(getInteractionMode);
const theme = useSelector(getTheme);
const platform = useSelector(getPlatform);
const item = useProxySelector(getTimelineItem, messageId);
const previousItem = useProxySelector(getTimelineItem, previousMessageId);
const nextItem = useProxySelector(getTimelineItem, nextMessageId);
const item = useTimelineItem(messageId, conversationId);
const previousItem = useTimelineItem(previousMessageId, conversationId);
const nextItem = useTimelineItem(nextMessageId, conversationId);
const targetedMessage = useSelector(getTargetedMessage);
const isTargeted = Boolean(
targetedMessage && messageId === targetedMessage.id

View File

@ -7,9 +7,8 @@ import { useSelector } from 'react-redux';
import { TypingBubble } from '../../components/conversation/TypingBubble';
import { useGlobalModalActions } from '../ducks/globalModals';
import { useProxySelector } from '../../hooks/useProxySelector';
import { getIntl, getTheme } from '../selectors/user';
import { getTimelineItem } from '../selectors/timeline';
import { useTimelineItem } from '../selectors/timeline';
import {
getConversationSelector,
getConversationMessagesSelector,
@ -37,7 +36,7 @@ export function SmartTypingBubble({
conversationId
);
const lastMessageId = last(conversationMessages.items);
const lastItem = useProxySelector(getTimelineItem, lastMessageId);
const lastItem = useTimelineItem(lastMessageId, conversationId);
let lastItemAuthorId: string | undefined;
let lastItemTimestamp: number | undefined;
if (lastItem?.data) {

View File

@ -13,6 +13,7 @@ import type {
import {
SendActionType,
SendStatus,
getHighestSuccessfulRecipientStatus,
isDelivered,
isFailed,
isMessageJustForMe,
@ -21,6 +22,7 @@ import {
isViewed,
maxStatus,
sendStateReducer,
someRecipientSendStatus,
someSendStatus,
} from '../../messages/MessageSendState';
@ -123,29 +125,37 @@ describe('message send state utilities', () => {
});
});
describe('someSendStatus', () => {
describe('someRecipientSendStatus', () => {
const ourConversationId = uuid();
it('returns false if there are no send states', () => {
const alwaysTrue = () => true;
assert.isFalse(someSendStatus(undefined, alwaysTrue));
assert.isFalse(someSendStatus({}, alwaysTrue));
assert.isFalse(
someRecipientSendStatus({}, ourConversationId, alwaysTrue)
);
assert.isFalse(someRecipientSendStatus({}, undefined, alwaysTrue));
});
it('returns false if no send states match', () => {
it('returns false if no send states match, excluding our own', () => {
const sendStateByConversationId: SendStateByConversationId = {
abc: {
status: SendStatus.Sent,
updatedAt: Date.now(),
},
def: {
status: SendStatus.Delivered,
updatedAt: Date.now(),
},
[ourConversationId]: {
status: SendStatus.Read,
updatedAt: Date.now(),
},
};
assert.isFalse(
someSendStatus(
someRecipientSendStatus(
sendStateByConversationId,
(status: SendStatus) => status === SendStatus.Delivered
ourConversationId,
(status: SendStatus) => status === SendStatus.Read
)
);
});
@ -160,6 +170,67 @@ describe('message send state utilities', () => {
status: SendStatus.Read,
updatedAt: Date.now(),
},
[ourConversationId]: {
status: SendStatus.Read,
updatedAt: Date.now(),
},
};
assert.isTrue(
someRecipientSendStatus(
sendStateByConversationId,
ourConversationId,
(status: SendStatus) => status === SendStatus.Read
)
);
});
});
describe('someSendStatus', () => {
const ourConversationId = uuid();
it('returns false if there are no send states', () => {
const alwaysTrue = () => true;
assert.isFalse(someSendStatus({}, alwaysTrue));
});
it('returns false if no send states match', () => {
const sendStateByConversationId: SendStateByConversationId = {
abc: {
status: SendStatus.Sent,
updatedAt: Date.now(),
},
def: {
status: SendStatus.Read,
updatedAt: Date.now(),
},
[ourConversationId]: {
status: SendStatus.Delivered,
updatedAt: Date.now(),
},
};
assert.isFalse(
someSendStatus(
sendStateByConversationId,
(status: SendStatus) => status === SendStatus.Viewed
)
);
});
it("returns true if at least one send state matches, even if it's ours", () => {
const sendStateByConversationId: SendStateByConversationId = {
abc: {
status: SendStatus.Sent,
updatedAt: Date.now(),
},
[ourConversationId]: {
status: SendStatus.Read,
updatedAt: Date.now(),
},
def: {
status: SendStatus.Delivered,
updatedAt: Date.now(),
},
};
assert.isTrue(
@ -171,11 +242,44 @@ describe('message send state utilities', () => {
});
});
describe('getHighestSuccessfulRecipientStatus', () => {
const ourConversationId = uuid();
it('returns pending if the conversation has an empty send state', () => {
assert.equal(
getHighestSuccessfulRecipientStatus({}, ourConversationId),
SendStatus.Pending
);
});
it('returns highest status, excluding our conversation', () => {
const sendStateByConversationId: SendStateByConversationId = {
abc: {
status: SendStatus.Sent,
updatedAt: Date.now(),
},
[ourConversationId]: {
status: SendStatus.Read,
updatedAt: Date.now(),
},
def: {
status: SendStatus.Delivered,
updatedAt: Date.now(),
},
};
assert.equal(
getHighestSuccessfulRecipientStatus(
sendStateByConversationId,
ourConversationId
),
SendStatus.Delivered
);
});
});
describe('isMessageJustForMe', () => {
const ourConversationId = uuid();
it('returns false if the conversation has an empty send state', () => {
assert.isFalse(isMessageJustForMe(undefined, ourConversationId));
assert.isFalse(isMessageJustForMe({}, ourConversationId));
});
@ -195,6 +299,22 @@ describe('message send state utilities', () => {
ourConversationId
)
);
assert.isFalse(
isMessageJustForMe(
{
[uuid()]: {
status: SendStatus.Pending,
updatedAt: 123,
},
[ourConversationId]: {
status: SendStatus.Sent,
updatedAt: 123,
},
},
ourConversationId
)
);
// This is an invalid state, but we still want to test the behavior.
assert.isFalse(
isMessageJustForMe(

View File

@ -183,8 +183,10 @@ Bootstrap.benchmark(async (bootstrap: Bootstrap): Promise<void> => {
debug('waiting for timing from the app');
const { timestamp, delta } = await app.waitForMessageSend();
// Sleep to allow any receipts from previous rounds to be processed
await sleep(1000);
if (GROUP_DELIVERY_RECEIPTS > 1) {
// Sleep to allow any receipts from previous rounds to be processed
await sleep(1000);
}
debug('sending delivery receipts');
receiptsFromPreviousMessage = await Promise.all(