Improve timeline rendering performance
This commit is contained in:
parent
c319a089d2
commit
167b2f4f1c
11 changed files with 329 additions and 106 deletions
|
@ -1,6 +1,7 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import memoizee from 'memoizee';
|
||||||
import { makeEnumParser } from '../util/enum';
|
import { makeEnumParser } from '../util/enum';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -153,22 +154,111 @@ const STATE_TRANSITIONS: Record<SendActionType, SendStatus> = {
|
||||||
|
|
||||||
export type SendStateByConversationId = Record<string, SendState>;
|
export type SendStateByConversationId = Record<string, SendState>;
|
||||||
|
|
||||||
|
/** Test all of sendStateByConversationId for predicate */
|
||||||
export const someSendStatus = (
|
export const someSendStatus = (
|
||||||
sendStateByConversationId: undefined | Readonly<SendStateByConversationId>,
|
sendStateByConversationId: SendStateByConversationId,
|
||||||
predicate: (value: SendStatus) => boolean
|
predicate: (value: SendStatus) => boolean
|
||||||
): boolean =>
|
): boolean => {
|
||||||
Object.values(sendStateByConversationId || {}).some(sendState =>
|
return [
|
||||||
predicate(sendState.status)
|
...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 = (
|
export const isMessageJustForMe = (
|
||||||
sendStateByConversationId: undefined | Readonly<SendStateByConversationId>,
|
sendStateByConversationId: SendStateByConversationId,
|
||||||
ourConversationId: string | undefined
|
ourConversationId: string | undefined
|
||||||
): boolean => {
|
): boolean => {
|
||||||
const conversationIds = Object.keys(sendStateByConversationId || {});
|
const { length } = summarizeMessageSendStatuses(sendStateByConversationId);
|
||||||
return Boolean(
|
|
||||||
ourConversationId &&
|
return (
|
||||||
conversationIds.length === 1 &&
|
ourConversationId !== undefined &&
|
||||||
conversationIds[0] === ourConversationId
|
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 }
|
||||||
|
);
|
||||||
|
|
|
@ -2,13 +2,11 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import {
|
import {
|
||||||
isEmpty,
|
|
||||||
isNumber,
|
isNumber,
|
||||||
isObject,
|
isObject,
|
||||||
mapValues,
|
mapValues,
|
||||||
maxBy,
|
maxBy,
|
||||||
noop,
|
noop,
|
||||||
omit,
|
|
||||||
partition,
|
partition,
|
||||||
pick,
|
pick,
|
||||||
union,
|
union,
|
||||||
|
@ -58,7 +56,7 @@ import {
|
||||||
SendStatus,
|
SendStatus,
|
||||||
isSent,
|
isSent,
|
||||||
sendStateReducer,
|
sendStateReducer,
|
||||||
someSendStatus,
|
someRecipientSendStatus,
|
||||||
} from '../messages/MessageSendState';
|
} from '../messages/MessageSendState';
|
||||||
import { migrateLegacyReadStatus } from '../messages/migrateLegacyReadStatus';
|
import { migrateLegacyReadStatus } from '../messages/migrateLegacyReadStatus';
|
||||||
import { migrateLegacySendAttributes } from '../messages/migrateLegacySendAttributes';
|
import { migrateLegacySendAttributes } from '../messages/migrateLegacySendAttributes';
|
||||||
|
@ -824,11 +822,14 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
|
|
||||||
public hasSuccessfulDelivery(): boolean {
|
public hasSuccessfulDelivery(): boolean {
|
||||||
const sendStateByConversationId = this.get('sendStateByConversationId');
|
const sendStateByConversationId = this.get('sendStateByConversationId');
|
||||||
const withoutMe = omit(
|
const ourConversationId =
|
||||||
sendStateByConversationId,
|
window.ConversationController.getOurConversationIdOrThrow();
|
||||||
window.ConversationController.getOurConversationIdOrThrow()
|
|
||||||
|
return someRecipientSendStatus(
|
||||||
|
sendStateByConversationId ?? {},
|
||||||
|
ourConversationId,
|
||||||
|
isSent
|
||||||
);
|
);
|
||||||
return isEmpty(withoutMe) || someSendStatus(withoutMe, isSent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import type { ThunkAction } from 'redux-thunk';
|
import type { ThunkAction } from 'redux-thunk';
|
||||||
import { mapValues } from 'lodash';
|
import { isEqual, mapValues } from 'lodash';
|
||||||
import type { ReadonlyDeep } from 'type-fest';
|
import type { ReadonlyDeep } from 'type-fest';
|
||||||
import type { StateType as RootStateType } from '../reducer';
|
import type { StateType as RootStateType } from '../reducer';
|
||||||
import type { BadgeType, BadgeImageType } from '../../badges/types';
|
import type { BadgeType, BadgeImageType } from '../../badges/types';
|
||||||
|
@ -147,6 +147,9 @@ export function reducer(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (isEqual(state.byId, newById)) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
byId: newById,
|
byId: newById,
|
||||||
|
|
|
@ -904,7 +904,7 @@ export const getConversationByServiceIdSelector = createSelector(
|
||||||
getOwn(conversationsByServiceId, serviceId)
|
getOwn(conversationsByServiceId, serviceId)
|
||||||
);
|
);
|
||||||
|
|
||||||
const getCachedConversationMemberColorsSelector = createSelector(
|
export const getCachedConversationMemberColorsSelector = createSelector(
|
||||||
getConversationSelector,
|
getConversationSelector,
|
||||||
getUserConversationId,
|
getUserConversationId,
|
||||||
(
|
(
|
||||||
|
@ -958,23 +958,30 @@ export const getContactNameColorSelector = createSelector(
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
contactId: string | undefined
|
contactId: string | undefined
|
||||||
): ContactNameColorType => {
|
): ContactNameColorType => {
|
||||||
if (!contactId) {
|
|
||||||
log.warn('No color generated for missing contactId');
|
|
||||||
return ContactNameColors[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
const contactNameColors =
|
const contactNameColors =
|
||||||
conversationMemberColorsSelector(conversationId);
|
conversationMemberColorsSelector(conversationId);
|
||||||
const color = contactNameColors.get(contactId);
|
return getContactNameColor(contactNameColors, contactId);
|
||||||
if (!color) {
|
|
||||||
log.warn(`No color generated for contact ${contactId}`);
|
|
||||||
return ContactNameColors[0];
|
|
||||||
}
|
|
||||||
return color;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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(
|
export function _conversationMessagesSelector(
|
||||||
conversation: ConversationMessageType
|
conversation: ConversationMessageType
|
||||||
): TimelinePropsType {
|
): TimelinePropsType {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// 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 { createSelector } from 'reselect';
|
||||||
import filesize from 'filesize';
|
import filesize from 'filesize';
|
||||||
import getDirection from 'direction';
|
import getDirection from 'direction';
|
||||||
|
@ -59,7 +59,7 @@ import { getMentionsRegex } from '../../types/Message';
|
||||||
import { SignalService as Proto } from '../../protobuf';
|
import { SignalService as Proto } from '../../protobuf';
|
||||||
import type { AttachmentType } from '../../types/Attachment';
|
import type { AttachmentType } from '../../types/Attachment';
|
||||||
import { isVoiceMessage, canBeDownloaded } 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 { ReadStatus } from '../../messages/MessageReadStatus';
|
||||||
|
|
||||||
import type { CallingNotificationType } from '../../util/callingNotification';
|
import type { CallingNotificationType } from '../../util/callingNotification';
|
||||||
|
@ -74,12 +74,13 @@ import { canEditMessage } from '../../util/canEditMessage';
|
||||||
import { getAccountSelector } from './accounts';
|
import { getAccountSelector } from './accounts';
|
||||||
import { getDefaultConversationColor } from './items';
|
import { getDefaultConversationColor } from './items';
|
||||||
import {
|
import {
|
||||||
getContactNameColorSelector,
|
|
||||||
getConversationSelector,
|
getConversationSelector,
|
||||||
getSelectedMessageIds,
|
getSelectedMessageIds,
|
||||||
getTargetedMessage,
|
getTargetedMessage,
|
||||||
isMissingRequiredProfileSharing,
|
isMissingRequiredProfileSharing,
|
||||||
getMessages,
|
getMessages,
|
||||||
|
getCachedConversationMemberColorsSelector,
|
||||||
|
getContactNameColor,
|
||||||
} from './conversations';
|
} from './conversations';
|
||||||
import {
|
import {
|
||||||
getIntl,
|
getIntl,
|
||||||
|
@ -97,19 +98,17 @@ import type {
|
||||||
|
|
||||||
import type { AccountSelectorType } from './accounts';
|
import type { AccountSelectorType } from './accounts';
|
||||||
import type { CallSelectorType, CallStateType } from './calling';
|
import type { CallSelectorType, CallStateType } from './calling';
|
||||||
import type {
|
import type { GetConversationByIdType } from './conversations';
|
||||||
GetConversationByIdType,
|
|
||||||
ContactNameColorSelectorType,
|
|
||||||
} from './conversations';
|
|
||||||
import {
|
import {
|
||||||
SendStatus,
|
SendStatus,
|
||||||
isDelivered,
|
isDelivered,
|
||||||
isFailed,
|
isFailed,
|
||||||
isMessageJustForMe,
|
|
||||||
isRead,
|
isRead,
|
||||||
isSent,
|
isSent,
|
||||||
isViewed,
|
isViewed,
|
||||||
maxStatus,
|
isMessageJustForMe,
|
||||||
|
someRecipientSendStatus,
|
||||||
|
getHighestSuccessfulRecipientStatus,
|
||||||
someSendStatus,
|
someSendStatus,
|
||||||
} from '../../messages/MessageSendState';
|
} from '../../messages/MessageSendState';
|
||||||
import * as log from '../../logging/log';
|
import * as log from '../../logging/log';
|
||||||
|
@ -179,7 +178,7 @@ export type GetPropsForBubbleOptions = Readonly<{
|
||||||
callHistorySelector: CallHistorySelectorType;
|
callHistorySelector: CallHistorySelectorType;
|
||||||
activeCall?: CallStateType;
|
activeCall?: CallStateType;
|
||||||
accountSelector: AccountSelectorType;
|
accountSelector: AccountSelectorType;
|
||||||
contactNameColorSelector: ContactNameColorSelectorType;
|
contactNameColors: Map<string, string>;
|
||||||
defaultConversationColor: DefaultConversationColorType;
|
defaultConversationColor: DefaultConversationColorType;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
@ -581,7 +580,7 @@ export type GetPropsForMessageOptions = Pick<
|
||||||
| 'selectedMessageIds'
|
| 'selectedMessageIds'
|
||||||
| 'regionCode'
|
| 'regionCode'
|
||||||
| 'accountSelector'
|
| 'accountSelector'
|
||||||
| 'contactNameColorSelector'
|
| 'contactNameColors'
|
||||||
| 'defaultConversationColor'
|
| 'defaultConversationColor'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
@ -679,7 +678,7 @@ export const getPropsForMessage = (
|
||||||
targetedMessageId,
|
targetedMessageId,
|
||||||
targetedMessageCounter,
|
targetedMessageCounter,
|
||||||
selectedMessageIds,
|
selectedMessageIds,
|
||||||
contactNameColorSelector,
|
contactNameColors,
|
||||||
defaultConversationColor,
|
defaultConversationColor,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
|
@ -708,7 +707,7 @@ export const getPropsForMessage = (
|
||||||
ourNumber,
|
ourNumber,
|
||||||
ourAci,
|
ourAci,
|
||||||
});
|
});
|
||||||
const contactNameColor = contactNameColorSelector(conversationId, authorId);
|
const contactNameColor = getContactNameColor(contactNameColors, authorId);
|
||||||
|
|
||||||
const { conversationColor, customColor } = getConversationColorAttributes(
|
const { conversationColor, customColor } = getConversationColorAttributes(
|
||||||
conversation,
|
conversation,
|
||||||
|
@ -786,7 +785,7 @@ export const getMessagePropsSelector = createSelector(
|
||||||
getUserNumber,
|
getUserNumber,
|
||||||
getRegionCode,
|
getRegionCode,
|
||||||
getAccountSelector,
|
getAccountSelector,
|
||||||
getContactNameColorSelector,
|
getCachedConversationMemberColorsSelector,
|
||||||
getTargetedMessage,
|
getTargetedMessage,
|
||||||
getSelectedMessageIds,
|
getSelectedMessageIds,
|
||||||
getDefaultConversationColor,
|
getDefaultConversationColor,
|
||||||
|
@ -798,15 +797,18 @@ export const getMessagePropsSelector = createSelector(
|
||||||
ourNumber,
|
ourNumber,
|
||||||
regionCode,
|
regionCode,
|
||||||
accountSelector,
|
accountSelector,
|
||||||
contactNameColorSelector,
|
cachedConversationMemberColorsSelector,
|
||||||
targetedMessage,
|
targetedMessage,
|
||||||
selectedMessageIds,
|
selectedMessageIds,
|
||||||
defaultConversationColor
|
defaultConversationColor
|
||||||
) =>
|
) =>
|
||||||
(message: MessageWithUIFieldsType) => {
|
(message: MessageWithUIFieldsType) => {
|
||||||
|
const contactNameColors = cachedConversationMemberColorsSelector(
|
||||||
|
message.conversationId
|
||||||
|
);
|
||||||
return getPropsForMessage(message, {
|
return getPropsForMessage(message, {
|
||||||
accountSelector,
|
accountSelector,
|
||||||
contactNameColorSelector,
|
contactNameColors,
|
||||||
conversationSelector,
|
conversationSelector,
|
||||||
ourConversationId,
|
ourConversationId,
|
||||||
ourNumber,
|
ourNumber,
|
||||||
|
@ -1646,14 +1648,9 @@ export function getMessagePropStatus(
|
||||||
return sent ? 'viewed' : 'sending';
|
return sent ? 'viewed' : 'sending';
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendStates = Object.values(
|
const highestSuccessfulStatus = getHighestSuccessfulRecipientStatus(
|
||||||
|
sendStateByConversationId,
|
||||||
ourConversationId
|
ourConversationId
|
||||||
? omit(sendStateByConversationId, ourConversationId)
|
|
||||||
: sendStateByConversationId
|
|
||||||
);
|
|
||||||
const highestSuccessfulStatus = sendStates.reduce(
|
|
||||||
(result: SendStatus, { status }) => maxStatus(result, status),
|
|
||||||
SendStatus.Pending
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -1758,8 +1755,8 @@ function canReplyOrReact(
|
||||||
MessageWithUIFieldsType,
|
MessageWithUIFieldsType,
|
||||||
| 'canReplyToStory'
|
| 'canReplyToStory'
|
||||||
| 'deletedForEveryone'
|
| 'deletedForEveryone'
|
||||||
| 'sendStateByConversationId'
|
|
||||||
| 'payment'
|
| 'payment'
|
||||||
|
| 'sendStateByConversationId'
|
||||||
| 'type'
|
| 'type'
|
||||||
>,
|
>,
|
||||||
ourConversationId: string | undefined,
|
ourConversationId: string | undefined,
|
||||||
|
@ -1800,11 +1797,10 @@ function canReplyOrReact(
|
||||||
|
|
||||||
if (isOutgoing(message)) {
|
if (isOutgoing(message)) {
|
||||||
return (
|
return (
|
||||||
isMessageJustForMe(sendStateByConversationId, ourConversationId) ||
|
isMessageJustForMe(sendStateByConversationId ?? {}, ourConversationId) ||
|
||||||
someSendStatus(
|
someRecipientSendStatus(
|
||||||
ourConversationId
|
sendStateByConversationId ?? {},
|
||||||
? omit(sendStateByConversationId, ourConversationId)
|
ourConversationId,
|
||||||
: sendStateByConversationId,
|
|
||||||
isSent
|
isSent
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -1884,7 +1880,7 @@ export function canDeleteForEveryone(
|
||||||
// Is it too old to delete? (we relax that requirement in Note to Self)
|
// Is it too old to delete? (we relax that requirement in Note to Self)
|
||||||
(isMoreRecentThan(message.sent_at, DAY) || isMe) &&
|
(isMoreRecentThan(message.sent_at, DAY) || isMe) &&
|
||||||
// Is it sent to anyone?
|
// 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(
|
export const getMessageDetails = createSelector(
|
||||||
getAccountSelector,
|
getAccountSelector,
|
||||||
getContactNameColorSelector,
|
getCachedConversationMemberColorsSelector,
|
||||||
getConversationSelector,
|
getConversationSelector,
|
||||||
getIntl,
|
getIntl,
|
||||||
getRegionCode,
|
getRegionCode,
|
||||||
|
@ -1984,7 +1980,7 @@ export const getMessageDetails = createSelector(
|
||||||
getDefaultConversationColor,
|
getDefaultConversationColor,
|
||||||
(
|
(
|
||||||
accountSelector,
|
accountSelector,
|
||||||
contactNameColorSelector,
|
cachedConversationMemberColorsSelector,
|
||||||
conversationSelector,
|
conversationSelector,
|
||||||
i18n,
|
i18n,
|
||||||
regionCode,
|
regionCode,
|
||||||
|
@ -2122,7 +2118,9 @@ export const getMessageDetails = createSelector(
|
||||||
errors,
|
errors,
|
||||||
message: getPropsForMessage(message, {
|
message: getPropsForMessage(message, {
|
||||||
accountSelector,
|
accountSelector,
|
||||||
contactNameColorSelector,
|
contactNameColors: cachedConversationMemberColorsSelector(
|
||||||
|
message.conversationId
|
||||||
|
),
|
||||||
conversationSelector,
|
conversationSelector,
|
||||||
ourAci,
|
ourAci,
|
||||||
ourPni,
|
ourPni,
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
import type { TimelineItemType } from '../../components/conversation/TimelineItem';
|
import type { TimelineItemType } from '../../components/conversation/TimelineItem';
|
||||||
|
|
||||||
import type { StateType } from '../reducer';
|
import type { StateType } from '../reducer';
|
||||||
import {
|
import {
|
||||||
getContactNameColorSelector,
|
|
||||||
getConversationSelector,
|
getConversationSelector,
|
||||||
getTargetedMessage,
|
getTargetedMessage,
|
||||||
getMessages,
|
|
||||||
getSelectedMessageIds,
|
getSelectedMessageIds,
|
||||||
|
getMessages,
|
||||||
|
getCachedConversationMemberColorsSelector,
|
||||||
} from './conversations';
|
} from './conversations';
|
||||||
import { getAccountSelector } from './accounts';
|
import { getAccountSelector } from './accounts';
|
||||||
import {
|
import {
|
||||||
|
@ -23,18 +24,20 @@ import { getDefaultConversationColor } from './items';
|
||||||
import { getActiveCall, getCallSelector } from './calling';
|
import { getActiveCall, getCallSelector } from './calling';
|
||||||
import { getPropsForBubble } from './message';
|
import { getPropsForBubble } from './message';
|
||||||
import { getCallHistorySelector } from './callHistory';
|
import { getCallHistorySelector } from './callHistory';
|
||||||
|
import { useProxySelector } from '../../hooks/useProxySelector';
|
||||||
|
|
||||||
export const getTimelineItem = (
|
const getTimelineItem = (
|
||||||
state: StateType,
|
state: StateType,
|
||||||
id?: string
|
messageId: string | undefined,
|
||||||
|
contactNameColors: Map<string, string>
|
||||||
): TimelineItemType | undefined => {
|
): TimelineItemType | undefined => {
|
||||||
if (id === undefined) {
|
if (messageId === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageLookup = getMessages(state);
|
const messageLookup = getMessages(state);
|
||||||
|
|
||||||
const message = messageLookup[id];
|
const message = messageLookup[messageId];
|
||||||
if (!message) {
|
if (!message) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@ -50,7 +53,6 @@ export const getTimelineItem = (
|
||||||
const callHistorySelector = getCallHistorySelector(state);
|
const callHistorySelector = getCallHistorySelector(state);
|
||||||
const activeCall = getActiveCall(state);
|
const activeCall = getActiveCall(state);
|
||||||
const accountSelector = getAccountSelector(state);
|
const accountSelector = getAccountSelector(state);
|
||||||
const contactNameColorSelector = getContactNameColorSelector(state);
|
|
||||||
const selectedMessageIds = getSelectedMessageIds(state);
|
const selectedMessageIds = getSelectedMessageIds(state);
|
||||||
const defaultConversationColor = getDefaultConversationColor(state);
|
const defaultConversationColor = getDefaultConversationColor(state);
|
||||||
|
|
||||||
|
@ -63,7 +65,7 @@ export const getTimelineItem = (
|
||||||
regionCode,
|
regionCode,
|
||||||
targetedMessageId: targetedMessage?.id,
|
targetedMessageId: targetedMessage?.id,
|
||||||
targetedMessageCounter: targetedMessage?.counter,
|
targetedMessageCounter: targetedMessage?.counter,
|
||||||
contactNameColorSelector,
|
contactNameColors,
|
||||||
callSelector,
|
callSelector,
|
||||||
callHistorySelector,
|
callHistorySelector,
|
||||||
activeCall,
|
activeCall,
|
||||||
|
@ -72,3 +74,18 @@ export const getTimelineItem = (
|
||||||
defaultConversationColor,
|
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);
|
||||||
|
};
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { isEmpty, pick } from 'lodash';
|
import { isEmpty, pick } from 'lodash';
|
||||||
import type { RefObject } from 'react';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
@ -25,7 +24,7 @@ import {
|
||||||
} from '../selectors/conversations';
|
} from '../selectors/conversations';
|
||||||
import { selectAudioPlayerActive } from '../selectors/audioPlayer';
|
import { selectAudioPlayerActive } from '../selectors/audioPlayer';
|
||||||
|
|
||||||
import { SmartTimelineItem } from './TimelineItem';
|
import { SmartTimelineItem, type SmartTimelineItemProps } from './TimelineItem';
|
||||||
import { SmartCollidingAvatars } from './CollidingAvatars';
|
import { SmartCollidingAvatars } from './CollidingAvatars';
|
||||||
import type { PropsType as SmartCollidingAvatarsPropsType } from './CollidingAvatars';
|
import type { PropsType as SmartCollidingAvatarsPropsType } from './CollidingAvatars';
|
||||||
import { SmartContactSpoofingReviewDialog } from './ContactSpoofingReviewDialog';
|
import { SmartContactSpoofingReviewDialog } from './ContactSpoofingReviewDialog';
|
||||||
|
@ -40,8 +39,6 @@ import {
|
||||||
getCollisionsFromMemberships,
|
getCollisionsFromMemberships,
|
||||||
} from '../../util/groupMemberNameCollisions';
|
} from '../../util/groupMemberNameCollisions';
|
||||||
import { ContactSpoofingType } from '../../util/contactSpoofing';
|
import { ContactSpoofingType } from '../../util/contactSpoofing';
|
||||||
import type { UnreadIndicatorPlacement } from '../../util/timelineUtil';
|
|
||||||
import type { WidthBreakpoint } from '../../components/_util';
|
|
||||||
import { getPreferredBadgeSelector } from '../selectors/badges';
|
import { getPreferredBadgeSelector } from '../selectors/badges';
|
||||||
import { SmartMiniPlayer } from './MiniPlayer';
|
import { SmartMiniPlayer } from './MiniPlayer';
|
||||||
|
|
||||||
|
@ -58,16 +55,7 @@ function renderItem({
|
||||||
nextMessageId,
|
nextMessageId,
|
||||||
previousMessageId,
|
previousMessageId,
|
||||||
unreadIndicatorPlacement,
|
unreadIndicatorPlacement,
|
||||||
}: {
|
}: SmartTimelineItemProps): JSX.Element {
|
||||||
containerElementRef: RefObject<HTMLElement>;
|
|
||||||
containerWidthBreakpoint: WidthBreakpoint;
|
|
||||||
conversationId: string;
|
|
||||||
isOldestTimelineItem: boolean;
|
|
||||||
messageId: string;
|
|
||||||
nextMessageId: undefined | string;
|
|
||||||
previousMessageId: undefined | string;
|
|
||||||
unreadIndicatorPlacement: undefined | UnreadIndicatorPlacement;
|
|
||||||
}): JSX.Element {
|
|
||||||
return (
|
return (
|
||||||
<SmartTimelineItem
|
<SmartTimelineItem
|
||||||
containerElementRef={containerElementRef}
|
containerElementRef={containerElementRef}
|
||||||
|
|
|
@ -7,7 +7,6 @@ import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
import { TimelineItem } from '../../components/conversation/TimelineItem';
|
import { TimelineItem } from '../../components/conversation/TimelineItem';
|
||||||
import type { WidthBreakpoint } from '../../components/_util';
|
import type { WidthBreakpoint } from '../../components/_util';
|
||||||
import { useProxySelector } from '../../hooks/useProxySelector';
|
|
||||||
import { useConversationsActions } from '../ducks/conversations';
|
import { useConversationsActions } from '../ducks/conversations';
|
||||||
import { useComposerActions } from '../ducks/composer';
|
import { useComposerActions } from '../ducks/composer';
|
||||||
import { useGlobalModalActions } from '../ducks/globalModals';
|
import { useGlobalModalActions } from '../ducks/globalModals';
|
||||||
|
@ -23,7 +22,7 @@ import {
|
||||||
getPlatform,
|
getPlatform,
|
||||||
} from '../selectors/user';
|
} from '../selectors/user';
|
||||||
import { getTargetedMessage } from '../selectors/conversations';
|
import { getTargetedMessage } from '../selectors/conversations';
|
||||||
import { getTimelineItem } from '../selectors/timeline';
|
import { useTimelineItem } from '../selectors/timeline';
|
||||||
import {
|
import {
|
||||||
areMessagesInSameGroup,
|
areMessagesInSameGroup,
|
||||||
shouldCurrentMessageHideMetadata,
|
shouldCurrentMessageHideMetadata,
|
||||||
|
@ -37,7 +36,7 @@ import { renderAudioAttachment } from './renderAudioAttachment';
|
||||||
import { renderEmojiPicker } from './renderEmojiPicker';
|
import { renderEmojiPicker } from './renderEmojiPicker';
|
||||||
import { renderReactionPicker } from './renderReactionPicker';
|
import { renderReactionPicker } from './renderReactionPicker';
|
||||||
|
|
||||||
type ExternalProps = {
|
export type SmartTimelineItemProps = {
|
||||||
containerElementRef: RefObject<HTMLElement>;
|
containerElementRef: RefObject<HTMLElement>;
|
||||||
containerWidthBreakpoint: WidthBreakpoint;
|
containerWidthBreakpoint: WidthBreakpoint;
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
|
@ -55,8 +54,7 @@ function renderContact(contactId: string): JSX.Element {
|
||||||
function renderUniversalTimerNotification(): JSX.Element {
|
function renderUniversalTimerNotification(): JSX.Element {
|
||||||
return <SmartUniversalTimerNotification />;
|
return <SmartUniversalTimerNotification />;
|
||||||
}
|
}
|
||||||
|
export function SmartTimelineItem(props: SmartTimelineItemProps): JSX.Element {
|
||||||
export function SmartTimelineItem(props: ExternalProps): JSX.Element {
|
|
||||||
const {
|
const {
|
||||||
containerElementRef,
|
containerElementRef,
|
||||||
containerWidthBreakpoint,
|
containerWidthBreakpoint,
|
||||||
|
@ -73,10 +71,10 @@ export function SmartTimelineItem(props: ExternalProps): JSX.Element {
|
||||||
const interactionMode = useSelector(getInteractionMode);
|
const interactionMode = useSelector(getInteractionMode);
|
||||||
const theme = useSelector(getTheme);
|
const theme = useSelector(getTheme);
|
||||||
const platform = useSelector(getPlatform);
|
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 targetedMessage = useSelector(getTargetedMessage);
|
||||||
const isTargeted = Boolean(
|
const isTargeted = Boolean(
|
||||||
targetedMessage && messageId === targetedMessage.id
|
targetedMessage && messageId === targetedMessage.id
|
||||||
|
|
|
@ -7,9 +7,8 @@ import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
import { TypingBubble } from '../../components/conversation/TypingBubble';
|
import { TypingBubble } from '../../components/conversation/TypingBubble';
|
||||||
import { useGlobalModalActions } from '../ducks/globalModals';
|
import { useGlobalModalActions } from '../ducks/globalModals';
|
||||||
import { useProxySelector } from '../../hooks/useProxySelector';
|
|
||||||
import { getIntl, getTheme } from '../selectors/user';
|
import { getIntl, getTheme } from '../selectors/user';
|
||||||
import { getTimelineItem } from '../selectors/timeline';
|
import { useTimelineItem } from '../selectors/timeline';
|
||||||
import {
|
import {
|
||||||
getConversationSelector,
|
getConversationSelector,
|
||||||
getConversationMessagesSelector,
|
getConversationMessagesSelector,
|
||||||
|
@ -37,7 +36,7 @@ export function SmartTypingBubble({
|
||||||
conversationId
|
conversationId
|
||||||
);
|
);
|
||||||
const lastMessageId = last(conversationMessages.items);
|
const lastMessageId = last(conversationMessages.items);
|
||||||
const lastItem = useProxySelector(getTimelineItem, lastMessageId);
|
const lastItem = useTimelineItem(lastMessageId, conversationId);
|
||||||
let lastItemAuthorId: string | undefined;
|
let lastItemAuthorId: string | undefined;
|
||||||
let lastItemTimestamp: number | undefined;
|
let lastItemTimestamp: number | undefined;
|
||||||
if (lastItem?.data) {
|
if (lastItem?.data) {
|
||||||
|
|
|
@ -13,6 +13,7 @@ import type {
|
||||||
import {
|
import {
|
||||||
SendActionType,
|
SendActionType,
|
||||||
SendStatus,
|
SendStatus,
|
||||||
|
getHighestSuccessfulRecipientStatus,
|
||||||
isDelivered,
|
isDelivered,
|
||||||
isFailed,
|
isFailed,
|
||||||
isMessageJustForMe,
|
isMessageJustForMe,
|
||||||
|
@ -21,6 +22,7 @@ import {
|
||||||
isViewed,
|
isViewed,
|
||||||
maxStatus,
|
maxStatus,
|
||||||
sendStateReducer,
|
sendStateReducer,
|
||||||
|
someRecipientSendStatus,
|
||||||
someSendStatus,
|
someSendStatus,
|
||||||
} from '../../messages/MessageSendState';
|
} 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', () => {
|
it('returns false if there are no send states', () => {
|
||||||
const alwaysTrue = () => true;
|
const alwaysTrue = () => true;
|
||||||
assert.isFalse(someSendStatus(undefined, alwaysTrue));
|
assert.isFalse(
|
||||||
assert.isFalse(someSendStatus({}, alwaysTrue));
|
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 = {
|
const sendStateByConversationId: SendStateByConversationId = {
|
||||||
abc: {
|
abc: {
|
||||||
status: SendStatus.Sent,
|
status: SendStatus.Sent,
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
},
|
},
|
||||||
def: {
|
def: {
|
||||||
|
status: SendStatus.Delivered,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
},
|
||||||
|
[ourConversationId]: {
|
||||||
status: SendStatus.Read,
|
status: SendStatus.Read,
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
assert.isFalse(
|
assert.isFalse(
|
||||||
someSendStatus(
|
someRecipientSendStatus(
|
||||||
sendStateByConversationId,
|
sendStateByConversationId,
|
||||||
(status: SendStatus) => status === SendStatus.Delivered
|
ourConversationId,
|
||||||
|
(status: SendStatus) => status === SendStatus.Read
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -160,6 +170,67 @@ describe('message send state utilities', () => {
|
||||||
status: SendStatus.Read,
|
status: SendStatus.Read,
|
||||||
updatedAt: Date.now(),
|
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(
|
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', () => {
|
describe('isMessageJustForMe', () => {
|
||||||
const ourConversationId = uuid();
|
const ourConversationId = uuid();
|
||||||
|
|
||||||
it('returns false if the conversation has an empty send state', () => {
|
it('returns false if the conversation has an empty send state', () => {
|
||||||
assert.isFalse(isMessageJustForMe(undefined, ourConversationId));
|
|
||||||
assert.isFalse(isMessageJustForMe({}, ourConversationId));
|
assert.isFalse(isMessageJustForMe({}, ourConversationId));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -195,6 +299,22 @@ describe('message send state utilities', () => {
|
||||||
ourConversationId
|
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.
|
// This is an invalid state, but we still want to test the behavior.
|
||||||
assert.isFalse(
|
assert.isFalse(
|
||||||
isMessageJustForMe(
|
isMessageJustForMe(
|
||||||
|
|
|
@ -183,8 +183,10 @@ Bootstrap.benchmark(async (bootstrap: Bootstrap): Promise<void> => {
|
||||||
debug('waiting for timing from the app');
|
debug('waiting for timing from the app');
|
||||||
const { timestamp, delta } = await app.waitForMessageSend();
|
const { timestamp, delta } = await app.waitForMessageSend();
|
||||||
|
|
||||||
// Sleep to allow any receipts from previous rounds to be processed
|
if (GROUP_DELIVERY_RECEIPTS > 1) {
|
||||||
await sleep(1000);
|
// Sleep to allow any receipts from previous rounds to be processed
|
||||||
|
await sleep(1000);
|
||||||
|
}
|
||||||
|
|
||||||
debug('sending delivery receipts');
|
debug('sending delivery receipts');
|
||||||
receiptsFromPreviousMessage = await Promise.all(
|
receiptsFromPreviousMessage = await Promise.all(
|
||||||
|
|
Loading…
Add table
Reference in a new issue