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
|
||||
// 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 }
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Add table
Reference in a new issue