diff --git a/ts/components/conversation/CallingNotification.stories.tsx b/ts/components/conversation/CallingNotification.stories.tsx index de31dac0d1..14e8ef949c 100644 --- a/ts/components/conversation/CallingNotification.stories.tsx +++ b/ts/components/conversation/CallingNotification.stories.tsx @@ -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', () => { @@ -99,7 +99,7 @@ story.add('Two outgoing direct calls back-to-back', () => { diff --git a/ts/components/conversation/CallingNotification.tsx b/ts/components/conversation/CallingNotification.tsx index ba4e8a6b40..754d563b11 100644 --- a/ts/components/conversation/CallingNotification.tsx +++ b/ts/components/conversation/CallingNotification.tsx @@ -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; } diff --git a/ts/components/conversation/Message.stories.tsx b/ts/components/conversation/Message.stories.tsx index 452e46661a..eeddd4818c 100644 --- a/ts/components/conversation/Message.stories.tsx +++ b/ts/components/conversation/Message.stories.tsx @@ -171,6 +171,15 @@ const createProps = (overrideProps: Partial = {}): 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) => )); diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index e936311d02..24c5d600a8 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -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) => 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 ) => 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 { attachments, expirationLength, expirationTimestamp, + shouldHideMetadata, status, text, textDirection, @@ -565,7 +562,7 @@ export class Message extends React.PureComponent { !expirationLength && !expirationTimestamp && (!status || SENT_STATUSES.has(status)) && - this.isCollapsedBelow() + shouldHideMetadata ) { return MetadataPlacement.NotRendered; } @@ -688,34 +685,14 @@ export class Message extends React.PureComponent { return isMessageRequestAccepted && !isBlocked; } - private isCollapsedAbove( - { item, previousItem, unreadIndicatorPlacement }: Readonly = this - .props - ): boolean { - return areMessagesInSameGroup( - previousItem, - unreadIndicatorPlacement === UnreadIndicatorPlacement.JustAbove, - item - ); - } - - private isCollapsedBelow( - { item, nextItem, unreadIndicatorPlacement }: Readonly = 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 { renderingContext, showMessageDetail, showVisualAttachment, + shouldCollapseAbove, + shouldCollapseBelow, status, text, textPending, @@ -925,10 +904,10 @@ export class Message extends React.PureComponent { { id, quote, scrollToQuotedMessage, + shouldCollapseAbove, } = this.props; if (!quote) { @@ -1248,11 +1228,11 @@ export class Message extends React.PureComponent { 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 { direction, i18n, storyReplyContext, + shouldCollapseAbove, } = this.props; if (!storyReplyContext) { @@ -1299,11 +1280,11 @@ export class Message extends React.PureComponent { 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 { direction, getPreferredBadge, i18n, + shouldCollapseBelow, showContactModal, theme, } = this.props; @@ -1415,7 +1397,7 @@ export class Message extends React.PureComponent { this.hasReactions(), })} > - {this.isCollapsedBelow() ? ( + {shouldCollapseBelow ? ( ) : ( { } 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 { 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 )} diff --git a/ts/components/conversation/MessageDetail.tsx b/ts/components/conversation/MessageDetail.tsx index 19056150d3..fb1dec3c7e 100644 --- a/ts/components/conversation/MessageDetail.tsx +++ b/ts/components/conversation/MessageDetail.tsx @@ -345,6 +345,9 @@ export class MessageDetail extends React.Component { replyToMessage={replyToMessage} retryDeleteForEveryone={retryDeleteForEveryone} retrySend={retrySend} + shouldCollapseAbove={false} + shouldCollapseBelow={false} + shouldHideMetadata={false} showForwardMessageModal={showForwardMessageModal} scrollToQuotedMessage={() => { log.warn('MessageDetail: scrollToQuotedMessage called!'); diff --git a/ts/components/conversation/Quote.stories.tsx b/ts/components/conversation/Quote.stories.tsx index eb25a84a08..30b941e89c 100644 --- a/ts/components/conversation/Quote.stories.tsx +++ b/ts/components/conversation/Quote.stories.tsx @@ -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( diff --git a/ts/components/conversation/Timeline.stories.tsx b/ts/components/conversation/Timeline.stories.tsx index 4ecc521043..13b1650d68 100644 --- a/ts/components/conversation/Timeline.stories.tsx +++ b/ts/components/conversation/Timeline.stories.tsx @@ -421,25 +421,21 @@ const renderItem = ({ messageId, containerElementRef, containerWidthBreakpoint, - isOldestTimelineItem, }: { messageId: string; containerElementRef: React.RefObject; containerWidthBreakpoint: WidthBreakpoint; - isOldestTimelineItem: boolean; }) => ( undefined} id="" - isOldestTimelineItem={isOldestTimelineItem} isSelected={false} renderEmojiPicker={() =>
} renderReactionPicker={() =>
} item={items[messageId]} - previousItem={undefined} - nextItem={undefined} i18n={i18n} interactionMode="keyboard" + isNextItemCallingNotification={false} theme={ThemeType.light} containerElementRef={containerElementRef} containerWidthBreakpoint={containerWidthBreakpoint} @@ -449,6 +445,10 @@ const renderItem = ({
*UniversalTimerNotification*
)} renderAudioAttachment={() =>
*AudioAttachment*
} + shouldCollapseAbove={false} + shouldCollapseBelow={false} + shouldHideMetadata={false} + shouldRenderDateHeader={false} {...actions()} /> ); diff --git a/ts/components/conversation/TimelineItem.stories.tsx b/ts/components/conversation/TimelineItem.stories.tsx index f19c14745d..ade8bb02e8 100644 --- a/ts/components/conversation/TimelineItem.stories.tsx +++ b/ts/components/conversation/TimelineItem.stories.tsx @@ -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, diff --git a/ts/components/conversation/TimelineItem.tsx b/ts/components/conversation/TimelineItem.tsx index 90189789e1..35dd1b512e 100644 --- a/ts/components/conversation/TimelineItem.tsx +++ b/ts/components/conversation/TimelineItem.tsx @@ -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; 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 { @@ -186,19 +185,20 @@ export class TimelineItem extends React.PureComponent { 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 { ); } else { @@ -237,7 +239,7 @@ export class TimelineItem extends React.PureComponent { { ); } - 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 ( <> diff --git a/ts/state/smart/TimelineItem.tsx b/ts/state/smart/TimelineItem.tsx index 72e6671a6b..ef125b5e8e 100644 --- a/ts/state/smart/TimelineItem.tsx +++ b/ts/state/smart/TimelineItem.tsx @@ -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; @@ -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, }; }; diff --git a/ts/test-both/util/timelineUtil_test.ts b/ts/test-both/util/timelineUtil_test.ts index 646cd5dc95..a6ce61065d 100644 --- a/ts/test-both/util/timelineUtil_test.ts +++ b/ts/test-both/util/timelineUtil_test.ts @@ -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(' 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 = [ + '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()); diff --git a/ts/util/timelineUtil.ts b/ts/util/timelineUtil.ts index 3d40f76f32..cfd6a3f774 100644 --- a/ts/util/timelineUtil.ts +++ b/ts/util/timelineUtil.ts @@ -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) ); }