Group disparate status together, but show metadata if different

This commit is contained in:
Scott Nonnenberg 2022-03-28 15:55:12 -07:00 committed by GitHub
parent 2602db97f0
commit 1ad284d22c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 282 additions and 146 deletions

View File

@ -18,8 +18,8 @@ const story = storiesOf('Components/Conversation/CallingNotification', module);
const getCommonProps = () => ({
conversationId: 'fake-conversation-id',
i18n,
isNextItemCallingNotification: false,
messageId: 'fake-message-id',
nextItem: undefined,
now: Date.now(),
returnToActiveCall: action('returnToActiveCall'),
startCallingLobby: action('startCallingLobby'),
@ -70,7 +70,7 @@ story.add('Two incoming direct calls back-to-back', () => {
<CallingNotification
{...getCommonProps()}
{...call1}
nextItem={{ type: 'callHistory', data: call2, timestamp: Date.now() }}
isNextItemCallingNotification
/>
<CallingNotification {...getCommonProps()} {...call2} />
</>
@ -99,7 +99,7 @@ story.add('Two outgoing direct calls back-to-back', () => {
<CallingNotification
{...getCommonProps()}
{...call1}
nextItem={{ type: 'callHistory', data: call2, timestamp: Date.now() }}
isNextItemCallingNotification
/>
<CallingNotification {...getCommonProps()} {...call2} />
</>

View File

@ -17,7 +17,6 @@ import {
} from '../../util/callingNotification';
import { missingCaseError } from '../../util/missingCaseError';
import { Tooltip, TooltipPlacement } from '../Tooltip';
import type { TimelineItemType } from './TimelineItem';
import * as log from '../../logging/log';
export type PropsActionsType = {
@ -31,7 +30,7 @@ export type PropsActionsType = {
type PropsHousekeeping = {
i18n: LocalizerType;
conversationId: string;
nextItem: undefined | TimelineItemType;
isNextItemCallingNotification: boolean;
};
type PropsType = CallingNotificationType & PropsActionsType & PropsHousekeeping;
@ -86,12 +85,12 @@ function renderCallingNotificationButton(
activeCallConversationId,
conversationId,
i18n,
nextItem,
isNextItemCallingNotification,
returnToActiveCall,
startCallingLobby,
} = props;
if (nextItem?.type === 'callHistory') {
if (isNextItemCallingNotification) {
return null;
}

View File

@ -171,6 +171,15 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
retryDeleteForEveryone: action('retryDeleteForEveryone'),
scrollToQuotedMessage: action('scrollToQuotedMessage'),
selectMessage: action('selectMessage'),
shouldCollapseAbove: isBoolean(overrideProps.shouldCollapseAbove)
? overrideProps.shouldCollapseAbove
: false,
shouldCollapseBelow: isBoolean(overrideProps.shouldCollapseBelow)
? overrideProps.shouldCollapseBelow
: false,
shouldHideMetadata: isBoolean(overrideProps.shouldHideMetadata)
? overrideProps.shouldHideMetadata
: false,
showContactDetail: action('showContactDetail'),
showContactModal: action('showContactModal'),
showExpiredIncomingTapToViewToast: action(
@ -202,9 +211,9 @@ const renderMany = (propsArray: ReadonlyArray<Props>) =>
<Message
key={message.text}
{...message}
previousItem={createTimelineItem(propsArray[index - 1])}
shouldCollapseAbove={Boolean(propsArray[index - 1])}
item={createTimelineItem(message)}
nextItem={createTimelineItem(propsArray[index + 1])}
shouldCollapseBelow={Boolean(propsArray[index + 1])}
/>
));

View File

@ -83,10 +83,6 @@ import { getCustomColorStyle } from '../../util/getCustomColorStyle';
import { offsetDistanceModifier } from '../../util/popperUtil';
import * as KeyboardLayout from '../../services/keyboardLayout';
import { StopPropagation } from '../StopPropagation';
import {
areMessagesInSameGroup,
UnreadIndicatorPlacement,
} from '../../util/timelineUtil';
type Trigger = {
handleContextClick: (event: React.MouseEvent<HTMLDivElement>) => void;
@ -269,14 +265,14 @@ export type PropsHousekeeping = {
i18n: LocalizerType;
interactionMode: InteractionModeType;
item?: TimelineItemType;
nextItem?: TimelineItemType;
previousItem?: TimelineItemType;
renderAudioAttachment: (props: AudioAttachmentProps) => JSX.Element;
renderReactionPicker: (
props: React.ComponentProps<typeof SmartReactionPicker>
) => JSX.Element;
shouldCollapseAbove: boolean;
shouldCollapseBelow: boolean;
shouldHideMetadata: boolean;
theme: ThemeType;
unreadIndicatorPlacement?: undefined | UnreadIndicatorPlacement;
};
export type PropsActions = {
@ -554,6 +550,7 @@ export class Message extends React.PureComponent<Props, State> {
attachments,
expirationLength,
expirationTimestamp,
shouldHideMetadata,
status,
text,
textDirection,
@ -565,7 +562,7 @@ export class Message extends React.PureComponent<Props, State> {
!expirationLength &&
!expirationTimestamp &&
(!status || SENT_STATUSES.has(status)) &&
this.isCollapsedBelow()
shouldHideMetadata
) {
return MetadataPlacement.NotRendered;
}
@ -688,34 +685,14 @@ export class Message extends React.PureComponent<Props, State> {
return isMessageRequestAccepted && !isBlocked;
}
private isCollapsedAbove(
{ item, previousItem, unreadIndicatorPlacement }: Readonly<Props> = this
.props
): boolean {
return areMessagesInSameGroup(
previousItem,
unreadIndicatorPlacement === UnreadIndicatorPlacement.JustAbove,
item
);
}
private isCollapsedBelow(
{ item, nextItem, unreadIndicatorPlacement }: Readonly<Props> = this.props
): boolean {
return areMessagesInSameGroup(
item,
unreadIndicatorPlacement === UnreadIndicatorPlacement.JustBelow,
nextItem
);
}
private shouldRenderAuthor(): boolean {
const { author, conversationType, direction } = this.props;
const { author, conversationType, direction, shouldCollapseAbove } =
this.props;
return Boolean(
direction === 'incoming' &&
conversationType === 'group' &&
author.title &&
!this.isCollapsedAbove()
!shouldCollapseAbove
);
}
@ -850,6 +827,8 @@ export class Message extends React.PureComponent<Props, State> {
renderingContext,
showMessageDetail,
showVisualAttachment,
shouldCollapseAbove,
shouldCollapseBelow,
status,
text,
textPending,
@ -925,10 +904,10 @@ export class Message extends React.PureComponent<Props, State> {
<ImageGrid
attachments={attachments}
withContentAbove={
isSticker || withContentAbove || this.isCollapsedAbove()
isSticker || withContentAbove || shouldCollapseAbove
}
withContentBelow={
isSticker || withContentBelow || this.isCollapsedBelow()
isSticker || withContentBelow || shouldCollapseBelow
}
isSticker={isSticker}
stickerSize={STICKER_SIZE}
@ -1223,6 +1202,7 @@ export class Message extends React.PureComponent<Props, State> {
id,
quote,
scrollToQuotedMessage,
shouldCollapseAbove,
} = this.props;
if (!quote) {
@ -1248,11 +1228,11 @@ export class Message extends React.PureComponent<Props, State> {
curveTopLeft = false;
curveTopRight = false;
} else if (isIncoming) {
curveTopLeft = !this.isCollapsedAbove();
curveTopLeft = !shouldCollapseAbove;
curveTopRight = true;
} else {
curveTopLeft = true;
curveTopRight = !this.isCollapsedAbove();
curveTopRight = !shouldCollapseAbove;
}
return (
@ -1285,6 +1265,7 @@ export class Message extends React.PureComponent<Props, State> {
direction,
i18n,
storyReplyContext,
shouldCollapseAbove,
} = this.props;
if (!storyReplyContext) {
@ -1299,11 +1280,11 @@ export class Message extends React.PureComponent<Props, State> {
curveTopLeft = false;
curveTopRight = false;
} else if (isIncoming) {
curveTopLeft = !this.isCollapsedAbove();
curveTopLeft = !shouldCollapseAbove;
curveTopRight = true;
} else {
curveTopLeft = true;
curveTopRight = !this.isCollapsedAbove();
curveTopRight = !shouldCollapseAbove;
}
return (
@ -1400,6 +1381,7 @@ export class Message extends React.PureComponent<Props, State> {
direction,
getPreferredBadge,
i18n,
shouldCollapseBelow,
showContactModal,
theme,
} = this.props;
@ -1415,7 +1397,7 @@ export class Message extends React.PureComponent<Props, State> {
this.hasReactions(),
})}
>
{this.isCollapsedBelow() ? (
{shouldCollapseBelow ? (
<AvatarSpacer size={GROUP_AVATAR_SIZE} />
) : (
<Avatar
@ -2660,8 +2642,16 @@ export class Message extends React.PureComponent<Props, State> {
}
public override render(): JSX.Element | null {
const { author, attachments, direction, id, isSticker, timestamp } =
this.props;
const {
author,
attachments,
direction,
id,
isSticker,
shouldCollapseAbove,
shouldCollapseBelow,
timestamp,
} = this.props;
const { expired, expiring, imageBroken, isSelected } = this.state;
// This id is what connects our triple-dot click with our associated pop-up menu.
@ -2681,8 +2671,8 @@ export class Message extends React.PureComponent<Props, State> {
className={classNames(
'module-message',
`module-message--${direction}`,
this.isCollapsedAbove() && 'module-message--collapsed-above',
this.isCollapsedBelow() && 'module-message--collapsed-below',
shouldCollapseAbove && 'module-message--collapsed-above',
shouldCollapseBelow && 'module-message--collapsed-below',
isSelected ? 'module-message--selected' : null,
expiring ? 'module-message--expired' : null
)}

View File

@ -345,6 +345,9 @@ export class MessageDetail extends React.Component<Props> {
replyToMessage={replyToMessage}
retryDeleteForEveryone={retryDeleteForEveryone}
retrySend={retrySend}
shouldCollapseAbove={false}
shouldCollapseBelow={false}
shouldHideMetadata={false}
showForwardMessageModal={showForwardMessageModal}
scrollToQuotedMessage={() => {
log.warn('MessageDetail: scrollToQuotedMessage called!');

View File

@ -82,6 +82,9 @@ const defaultMessageProps: MessagesProps = {
retryDeleteForEveryone: action('default--retryDeleteForEveryone'),
scrollToQuotedMessage: action('default--scrollToQuotedMessage'),
selectMessage: action('default--selectMessage'),
shouldCollapseAbove: false,
shouldCollapseBelow: false,
shouldHideMetadata: false,
showContactDetail: action('default--showContactDetail'),
showContactModal: action('default--showContactModal'),
showExpiredIncomingTapToViewToast: action(

View File

@ -423,25 +423,21 @@ const renderItem = ({
messageId,
containerElementRef,
containerWidthBreakpoint,
isOldestTimelineItem,
}: {
messageId: string;
containerElementRef: React.RefObject<HTMLElement>;
containerWidthBreakpoint: WidthBreakpoint;
isOldestTimelineItem: boolean;
}) => (
<TimelineItem
getPreferredBadge={() => undefined}
id=""
isOldestTimelineItem={isOldestTimelineItem}
isSelected={false}
renderEmojiPicker={() => <div />}
renderReactionPicker={() => <div />}
item={items[messageId]}
previousItem={undefined}
nextItem={undefined}
i18n={i18n}
interactionMode="keyboard"
isNextItemCallingNotification={false}
theme={ThemeType.light}
containerElementRef={containerElementRef}
containerWidthBreakpoint={containerWidthBreakpoint}
@ -451,6 +447,10 @@ const renderItem = ({
<div>*UniversalTimerNotification*</div>
)}
renderAudioAttachment={() => <div>*AudioAttachment*</div>}
shouldCollapseAbove={false}
shouldCollapseBelow={false}
shouldHideMetadata={false}
shouldRenderDateHeader={false}
{...actions()}
/>
);

View File

@ -53,7 +53,7 @@ const getDefaultProps = () => ({
conversationId: 'conversation-id',
getPreferredBadge: () => undefined,
id: 'asdf',
isOldestTimelineItem: false,
isNextItemCallingNotification: false,
isSelected: false,
interactionMode: 'keyboard' as const,
theme: ThemeType.light,
@ -94,8 +94,11 @@ const getDefaultProps = () => ({
showIdentity: action('showIdentity'),
startCallingLobby: action('startCallingLobby'),
returnToActiveCall: action('returnToActiveCall'),
previousItem: undefined,
nextItem: undefined,
shouldCollapseAbove: false,
shouldCollapseBelow: false,
shouldHideMetadata: false,
shouldRenderDateHeader: false,
now: Date.now(),
renderContact,

View File

@ -5,7 +5,6 @@ import type { ReactChild, RefObject } from 'react';
import React from 'react';
import type { LocalizerType, ThemeType } from '../../types/Util';
import { isSameDay } from '../../util/timestamp';
import type { InteractionModeType } from '../../state/ducks/conversations';
import { TimelineDateHeader } from './TimelineDateHeader';
@ -56,7 +55,6 @@ import type { SmartContactRendererType } from '../../groupChange';
import { ResetSessionNotification } from './ResetSessionNotification';
import type { PropsType as ProfileChangeNotificationPropsType } from './ProfileChangeNotification';
import { ProfileChangeNotification } from './ProfileChangeNotification';
import type { UnreadIndicatorPlacement } from '../../util/timelineUtil';
import type { FullJSXType } from '../Intl';
type CallHistoryType = {
@ -148,17 +146,15 @@ type PropsLocalType = {
conversationId: string;
item?: TimelineItemType;
id: string;
isNextItemCallingNotification: boolean;
isSelected: boolean;
selectMessage: (messageId: string, conversationId: string) => unknown;
shouldRenderDateHeader: boolean;
renderContact: SmartContactRendererType<FullJSXType>;
renderUniversalTimerNotification: () => JSX.Element;
i18n: LocalizerType;
interactionMode: InteractionModeType;
isOldestTimelineItem: boolean;
theme: ThemeType;
previousItem: undefined | TimelineItemType;
nextItem: undefined | TimelineItemType;
unreadIndicatorPlacement?: undefined | UnreadIndicatorPlacement;
};
type PropsActionsType = MessageActionsType &
@ -178,6 +174,9 @@ export type PropsType = PropsLocalType &
| 'renderEmojiPicker'
| 'renderAudioAttachment'
| 'renderReactionPicker'
| 'shouldCollapseAbove'
| 'shouldCollapseBelow'
| 'shouldHideMetadata'
>;
export class TimelineItem extends React.PureComponent<PropsType> {
@ -186,19 +185,20 @@ export class TimelineItem extends React.PureComponent<PropsType> {
containerElementRef,
conversationId,
getPreferredBadge,
i18n,
id,
isOldestTimelineItem,
isNextItemCallingNotification,
isSelected,
item,
i18n,
theme,
nextItem,
previousItem,
renderUniversalTimerNotification,
returnToActiveCall,
selectMessage,
shouldCollapseAbove,
shouldCollapseBelow,
shouldHideMetadata,
shouldRenderDateHeader,
startCallingLobby,
unreadIndicatorPlacement,
theme,
} = this.props;
if (!item) {
@ -217,12 +217,14 @@ export class TimelineItem extends React.PureComponent<PropsType> {
<Message
{...this.props}
{...item.data}
shouldCollapseAbove={shouldCollapseAbove}
shouldCollapseBelow={shouldCollapseBelow}
shouldHideMetadata={shouldHideMetadata}
containerElementRef={containerElementRef}
getPreferredBadge={getPreferredBadge}
i18n={i18n}
theme={theme}
renderingContext="conversation/TimelineItem"
unreadIndicatorPlacement={unreadIndicatorPlacement}
/>
);
} else {
@ -237,7 +239,7 @@ export class TimelineItem extends React.PureComponent<PropsType> {
<CallingNotification
conversationId={conversationId}
i18n={i18n}
nextItem={nextItem}
isNextItemCallingNotification={isNextItemCallingNotification}
returnToActiveCall={returnToActiveCall}
startCallingLobby={startCallingLobby}
{...item.data}
@ -340,14 +342,6 @@ export class TimelineItem extends React.PureComponent<PropsType> {
);
}
const shouldRenderDateHeader =
isOldestTimelineItem ||
Boolean(
previousItem &&
// This comparison avoids strange header behavior for out-of-order messages.
item.timestamp > previousItem.timestamp &&
!isSameDay(previousItem.timestamp, item.timestamp)
);
if (shouldRenderDateHeader) {
return (
<>

View File

@ -16,10 +16,15 @@ import {
getMessageSelector,
getSelectedMessage,
} from '../selectors/conversations';
import type { UnreadIndicatorPlacement } from '../../util/timelineUtil';
import {
areMessagesInSameGroup,
shouldCurrentMessageHideMetadata,
UnreadIndicatorPlacement,
} from '../../util/timelineUtil';
import { SmartContactName } from './ContactName';
import { SmartUniversalTimerNotification } from './UniversalTimerNotification';
import { isSameDay } from '../../util/timestamp';
type ExternalProps = {
containerElementRef: RefObject<HTMLElement>;
@ -65,24 +70,52 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
const conversation = getConversationSelector(state)(conversationId);
const isNextItemCallingNotification = nextItem?.type === 'callHistory';
const shouldCollapseAbove = areMessagesInSameGroup(
previousItem,
unreadIndicatorPlacement === UnreadIndicatorPlacement.JustAbove,
item
);
const shouldCollapseBelow = areMessagesInSameGroup(
item,
unreadIndicatorPlacement === UnreadIndicatorPlacement.JustBelow,
nextItem
);
const shouldHideMetadata = shouldCurrentMessageHideMetadata(
shouldCollapseBelow,
item,
nextItem
);
const shouldRenderDateHeader =
isOldestTimelineItem ||
Boolean(
item &&
previousItem &&
// This comparison avoids strange header behavior for out-of-order messages.
item.timestamp > previousItem.timestamp &&
!isSameDay(previousItem.timestamp, item.timestamp)
);
return {
item,
previousItem,
nextItem,
id: messageId,
containerElementRef,
conversationId,
conversationColor: conversation?.conversationColor,
customColor: conversation?.customColor,
getPreferredBadge: getPreferredBadgeSelector(state),
isOldestTimelineItem,
isNextItemCallingNotification,
isSelected,
renderContact,
renderUniversalTimerNotification,
shouldCollapseAbove,
shouldCollapseBelow,
shouldHideMetadata,
shouldRenderDateHeader,
i18n: getIntl(state),
interactionMode: getInteractionMode(state),
theme: getTheme(state),
unreadIndicatorPlacement,
};
};

View File

@ -4,12 +4,14 @@
import { assert } from 'chai';
import { times } from 'lodash';
import { v4 as uuid } from 'uuid';
import type { LastMessageStatus } from '../../model-types.d';
import { MINUTE, SECOND } from '../../util/durations';
import type { MaybeMessageTimelineItemType } from '../../util/timelineUtil';
import {
ScrollAnchor,
areMessagesInSameGroup,
getScrollAnchorBeforeUpdate,
shouldCurrentMessageHideMetadata,
TimelineMessageLoadingState,
} from '../../util/timelineUtil';
@ -118,60 +120,123 @@ describe('<Timeline> utilities', () => {
assert.isFalse(areMessagesInSameGroup(defaultOlder, true, defaultNewer));
});
it("returns false if they don't have matching sent status (and not delivered)", () => {
const older = {
...defaultOlder,
data: { ...defaultOlder.data, status: 'sent' as const },
};
assert.isFalse(areMessagesInSameGroup(older, false, defaultNewer));
});
it("returns false if newer is deletedForEveryone and older isn't", () => {
const newer = {
...defaultNewer,
data: { ...defaultNewer.data, deletedForEveryone: true },
};
assert.isFalse(areMessagesInSameGroup(defaultOlder, false, newer));
});
it("returns true if older is deletedForEveryone and newer isn't", () => {
const older = {
...defaultOlder,
data: { ...defaultOlder.data, deletedForEveryone: true },
};
assert.isTrue(areMessagesInSameGroup(older, false, defaultNewer));
});
it('returns true if both are deletedForEveryone', () => {
const older = {
...defaultOlder,
data: { ...defaultOlder.data, deletedForEveryone: true },
};
const newer = {
...defaultNewer,
data: { ...defaultNewer.data, deletedForEveryone: true },
};
assert.isTrue(areMessagesInSameGroup(older, false, newer));
});
it('returns true if they have delivered status or above', () => {
const older = {
...defaultOlder,
data: { ...defaultOlder.data, status: 'read' as const },
};
assert.isTrue(areMessagesInSameGroup(older, false, defaultNewer));
});
it('returns true if everything above works out', () => {
assert.isTrue(areMessagesInSameGroup(defaultOlder, false, defaultNewer));
});
});
describe('shouldCurrentMessageHideMetadata', () => {
const defaultNewer: MaybeMessageTimelineItemType = {
type: 'message' as const,
data: {
author: { id: uuid() },
timestamp: new Date(1998, 10, 21, 12, 34, 56, 123).valueOf(),
status: 'delivered',
},
};
const defaultCurrent: MaybeMessageTimelineItemType = {
type: 'message' as const,
data: {
author: { id: uuid() },
timestamp: defaultNewer.data.timestamp - MINUTE,
status: 'delivered',
},
};
it("returns false if messages aren't grouped", () => {
assert.isFalse(
shouldCurrentMessageHideMetadata(false, defaultCurrent, defaultNewer)
);
});
it('returns false if newer item is missing', () => {
assert.isFalse(
shouldCurrentMessageHideMetadata(true, defaultCurrent, undefined)
);
});
it('returns false if newer item is not a message', () => {
const linkNotification = {
type: 'linkNotification' as const,
data: null,
timestamp: Date.now(),
};
assert.isFalse(
shouldCurrentMessageHideMetadata(true, defaultCurrent, linkNotification)
);
});
it('returns false if newer is deletedForEveryone', () => {
const newer = {
...defaultNewer,
data: { ...defaultNewer.data, deletedForEveryone: true },
};
assert.isFalse(
shouldCurrentMessageHideMetadata(true, defaultCurrent, newer)
);
});
it('returns false if current message is unsent, even if its status matches the newer one', () => {
const statuses: ReadonlyArray<LastMessageStatus> = [
'paused',
'error',
'partial-sent',
'sending',
];
for (const status of statuses) {
const sameStatusNewer = {
...defaultNewer,
data: { ...defaultNewer.data, status },
};
const current = {
...defaultCurrent,
data: { ...defaultCurrent.data, status },
};
assert.isFalse(
shouldCurrentMessageHideMetadata(true, current, defaultNewer)
);
assert.isFalse(
shouldCurrentMessageHideMetadata(true, current, sameStatusNewer)
);
}
});
it('returns true if all messages are sent (but no higher)', () => {
const newer = {
...defaultNewer,
data: { ...defaultNewer.data, status: 'sent' as const },
};
const current = {
...defaultCurrent,
data: { ...defaultCurrent.data, status: 'sent' as const },
};
assert.isTrue(shouldCurrentMessageHideMetadata(true, current, newer));
});
it('returns true if all three have delivered status or above', () => {
assert.isTrue(
shouldCurrentMessageHideMetadata(true, defaultCurrent, defaultNewer)
);
});
it('returns true if both the current and next messages are deleted for everyone', () => {
const current = {
...defaultCurrent,
data: { ...defaultCurrent.data, deletedForEveryone: true },
};
const newer = {
...defaultNewer,
data: { ...defaultNewer.data, deletedForEveryone: true },
};
assert.isTrue(shouldCurrentMessageHideMetadata(true, current, newer));
});
});
describe('getScrollAnchorBeforeUpdate', () => {
const fakeItems = (count: number) => times(count, () => uuid());

View File

@ -2,6 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
import { isNumber } from 'lodash';
import * as log from '../logging/log';
import type { PropsType as TimelinePropsType } from '../components/conversation/Timeline';
import type { TimelineItemType } from '../components/conversation/TimelineItem';
import { WidthBreakpoint } from '../components/_util';
@ -54,8 +55,52 @@ const getMessageTimelineItemData = (
): undefined | MessageTimelineItemDataType =>
timelineItem?.type === 'message' ? timelineItem.data : undefined;
function isDelivered(status?: LastMessageStatus) {
return status === 'delivered' || status === 'read' || status === 'viewed';
export function shouldCurrentMessageHideMetadata(
areMessagesGrouped: boolean,
item: MaybeMessageTimelineItemType,
newerTimelineItem: MaybeMessageTimelineItemType
): boolean {
if (!areMessagesGrouped) {
return false;
}
const message = getMessageTimelineItemData(item);
if (!message) {
return false;
}
const newerMessage = getMessageTimelineItemData(newerTimelineItem);
if (!newerMessage) {
return false;
}
// If newer message is deleted, but current isn't, we'll show metadata.
if (newerMessage.deletedForEveryone && !message.deletedForEveryone) {
return false;
}
switch (message.status) {
case undefined:
return true;
case 'paused':
case 'error':
case 'partial-sent':
case 'sending':
return false;
case 'sent':
return newerMessage.status === 'sent';
case 'delivered':
case 'read':
case 'viewed':
return (
newerMessage.status === 'delivered' ||
newerMessage.status === 'read' ||
newerMessage.status === 'viewed'
);
default:
log.error(missingCaseError(message.status));
return false;
}
}
export function areMessagesInSameGroup(
@ -77,20 +122,12 @@ export function areMessagesInSameGroup(
return false;
}
// We definitely don't want to group if we transition from non-deleted to deleted, since
// deleted messages don't show status.
if (newerMessage.deletedForEveryone && !olderMessage.deletedForEveryone) {
return false;
}
return Boolean(
!olderMessage.reactions?.length &&
olderMessage.author.id === newerMessage.author.id &&
newerMessage.timestamp >= olderMessage.timestamp &&
newerMessage.timestamp - olderMessage.timestamp < COLLAPSE_WITHIN &&
isSameDay(olderMessage.timestamp, newerMessage.timestamp) &&
(olderMessage.status === newerMessage.status ||
(isDelivered(newerMessage.status) && isDelivered(olderMessage.status)))
isSameDay(olderMessage.timestamp, newerMessage.timestamp)
);
}