From c353d41794052d4afcc9422a43e49ca8eb820842 Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Wed, 25 Mar 2026 07:00:02 +1000 Subject: [PATCH] Collapsing Items: A few improvements --- _locales/en/messages.json | 14 ++- .../conversation/CollapseSet.dom.stories.tsx | 47 +++++++--- .../conversation/CollapseSet.dom.tsx | 89 +++++++++++++++---- .../conversation/Timeline.dom.stories.tsx | 1 + ts/components/conversation/Timeline.dom.tsx | 12 ++- .../conversation/TimelineItem.dom.stories.tsx | 1 + .../conversation/TimelineItem.dom.tsx | 4 + ts/state/smart/Timeline.preload.tsx | 2 +- ts/state/smart/TimelineItem.preload.tsx | 3 + ts/test-node/util/CollapseSet_test.std.ts | 33 ++++++- ts/util/CollapseSet.std.ts | 28 +++--- 11 files changed, 185 insertions(+), 49 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 85c0f820ee..fd3b6f6b6f 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -3384,9 +3384,21 @@ "messageformat": "You updated the group.", "description": "Shown in the conversation history when you update a group" }, + "icu:collapsedContainer": { + "messageformat": "{leadingIcon} {text} {trailingIcon}", + "description": "This is how all the elements will be assembled: leading icon will be a timer or person, text is the text summary, and trailingIcon is the up/down chevron showing whether the area is expanded or collapsed" + }, "icu:collapsedGroupUpdates": { "messageformat": "{count, plural, one {# group update} other {# group updates}}", - "description": "Label for a button giving access to a collection of group updates (count will always be 2+)" + "description": "Label for a button giving access to a collection of group updates (count will always be 2+" + }, + "icu:multidayCollapse__container": { + "messageformat": "{containerDescription} ยท {dayCountSummary}", + "description": "For collapsed multiday sets, this is how the overall summary will be combined with the day count summary." + }, + "icu:multidayCollapse__dayCountSummary": { + "messageformat": "{dayCount, plural, one {# day} other {# days}}", + "description": "Will be appended to the end of the other collapse strings. dayCount will always be 2+" }, "icu:collapsedChatUpdates": { "messageformat": "{count, plural, one {# chat update} other {# chat updates}}", diff --git a/ts/components/conversation/CollapseSet.dom.stories.tsx b/ts/components/conversation/CollapseSet.dom.stories.tsx index 2a84e218b2..09d5ec504e 100644 --- a/ts/components/conversation/CollapseSet.dom.stories.tsx +++ b/ts/components/conversation/CollapseSet.dom.stories.tsx @@ -3,14 +3,15 @@ import * as React from 'react'; import type { Meta } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import { DurationInSeconds } from '../../util/durations/duration-in-seconds.std.js'; +import { WidthBreakpoint } from '../_util.std.js'; +import { tw } from '../../axo/tw.dom.js'; import { CollapseSetViewer } from './CollapseSet.dom.js'; import type { Props } from './CollapseSet.dom.js'; import type { RenderItemProps } from '../../state/smart/TimelineItem.preload.js'; -import { DurationInSeconds } from '../../util/durations/duration-in-seconds.std.js'; -import { WidthBreakpoint } from '../_util.std.js'; -import { tw } from '../../axo/tw.dom.js'; const { i18n } = window.SignalContext; @@ -27,17 +28,21 @@ function renderItem({ item }: RenderItemProps) { } const defaultProps: Props = { + // CollapseSet + id: 'id1', + type: 'none', + messages: undefined, + // The rest containerElementRef: React.createRef(), containerWidthBreakpoint: WidthBreakpoint.Wide, conversationId: 'c1', i18n, - id: 'id1', isBlocked: false, isGroup: true, - messages: undefined, + isSelectMode: false, renderItem, targetedMessage: undefined, - type: 'none', + toggleDeleteMessagesModal: action('toggleDeleteMessagesModal'), }; export function GroupWithTwo(): React.JSX.Element { @@ -97,11 +102,11 @@ export function GroupWithTen(): React.JSX.Element { { id: 'id1', isUnseen: false }, { id: 'id2', isUnseen: false }, { id: 'id3', isUnseen: false }, - { id: 'id4', isUnseen: false }, + { id: 'id4', isUnseen: false, atDateBoundary: true }, { id: 'id5', isUnseen: false }, { id: 'id6', isUnseen: false }, { id: 'id7', isUnseen: false }, - { id: 'id8', isUnseen: false }, + { id: 'id8', isUnseen: false, atDateBoundary: true }, { id: 'id9', isUnseen: false }, { id: 'id10', isUnseen: false }, ], @@ -138,7 +143,21 @@ export function TimerWithTwoZero(): React.JSX.Element { type: 'timer-changes', messages: [ { id: 'id1', isUnseen: false }, - { id: 'id2', isUnseen: false }, + { id: 'id2', isUnseen: false, atDateBoundary: true }, + ], + endingState: DurationInSeconds.fromSeconds(0), + }; + return ; +} + +export function TimerWithTwoZeroInSelectMode(): React.JSX.Element { + const props: Props = { + ...defaultProps, + type: 'timer-changes', + isSelectMode: true, + messages: [ + { id: 'id1', isUnseen: false }, + { id: 'id2', isUnseen: false, atDateBoundary: true }, ], endingState: DurationInSeconds.fromSeconds(0), }; @@ -166,11 +185,11 @@ export function TimerWithTenAt1Hr(): React.JSX.Element { { id: 'id1', isUnseen: false }, { id: 'id2', isUnseen: false }, { id: 'id3', isUnseen: false }, - { id: 'id4', isUnseen: false }, + { id: 'id4', isUnseen: false, atDateBoundary: true }, { id: 'id5', isUnseen: false }, { id: 'id6', isUnseen: false }, { id: 'id7', isUnseen: false }, - { id: 'id8', isUnseen: false }, + { id: 'id8', isUnseen: false, atDateBoundary: true }, { id: 'id9', isUnseen: false }, { id: 'id10', isUnseen: false }, ], @@ -198,7 +217,7 @@ export function GroupWithFourTwoUnseen(): React.JSX.Element { messages: [ { id: 'id1', isUnseen: false }, { id: 'id2', isUnseen: false }, - { id: 'id3', isUnseen: true }, + { id: 'id3', isUnseen: true, atDateBoundary: true }, { id: 'id4', isUnseen: true }, ], }; @@ -225,7 +244,7 @@ export function GroupWithWithUpdateAfterDelay(): React.JSX.Element { type: 'group-updates', messages: [ { id: 'id1', isUnseen: false }, - { id: 'id2', isUnseen: false }, + { id: 'id2', isUnseen: false, atDateBoundary: true }, { id: 'id3', isUnseen: true }, { id: 'id4', isUnseen: true }, ], @@ -237,7 +256,7 @@ export function GroupWithWithUpdateAfterDelay(): React.JSX.Element { type: 'group-updates', messages: [ { id: 'id1', isUnseen: false }, - { id: 'id2', isUnseen: false }, + { id: 'id2', isUnseen: false, atDateBoundary: true }, { id: 'id3', isUnseen: false }, { id: 'id4', isUnseen: false }, { id: 'id5', isUnseen: true }, diff --git a/ts/components/conversation/CollapseSet.dom.tsx b/ts/components/conversation/CollapseSet.dom.tsx index 88ee5f4cf9..e089ad679a 100644 --- a/ts/components/conversation/CollapseSet.dom.tsx +++ b/ts/components/conversation/CollapseSet.dom.tsx @@ -13,6 +13,7 @@ import { missingCaseError } from '../../util/missingCaseError.std.js'; import { AxoSymbol } from '../../axo/AxoSymbol.dom.js'; import { tw } from '../../axo/tw.dom.js'; import { AxoButton } from '../../axo/AxoButton.dom.js'; +import { MessageContextMenu } from './MessageContextMenu.dom.js'; import type { WidthBreakpoint } from '../_util.std.js'; import type { @@ -22,6 +23,8 @@ import type { import type { RenderItemProps } from '../../state/smart/TimelineItem.preload.js'; import type { LocalizerType } from '../../types/I18N.std.js'; import type { TargetedMessageType } from '../../state/selectors/conversations.dom.js'; +import type { DeleteMessagesPropsType } from '../../state/ducks/globalModals.preload.js'; +import { I18n } from '../I18n.dom.js'; export type Props = CollapseSet & { containerElementRef: RefObject; @@ -30,8 +33,10 @@ export type Props = CollapseSet & { i18n: LocalizerType; isBlocked: boolean; isGroup: boolean; + isSelectMode: boolean; renderItem: (props: RenderItemProps) => React.JSX.Element; targetedMessage: TargetedMessageType | undefined; + toggleDeleteMessagesModal: (props: DeleteMessagesPropsType) => void; }; export function CollapseSetViewer(props: Props): React.JSX.Element { @@ -49,6 +54,7 @@ export function CollapseSetViewer(props: Props): React.JSX.Element { messages, renderItem, targetedMessage, + toggleDeleteMessagesModal, } = props; const [isExpanded, setIsExpanded] = useState(false); const [messageCache, setMessageCache] = useState< @@ -99,16 +105,23 @@ export function CollapseSetViewer(props: Props): React.JSX.Element { let oldestOriginallyUnseenIndex; const max = messages?.length; + let collapsedCount = 0; + let collapsedDayCount = 1; + for (let i = 0; i < max; i += 1) { const message = messages[i]; strictAssert( message, 'CollapseSet finding oldestOriginallyUnseenIndex in messages' ); + if (messageCache[message.id]?.isUnseen) { oldestOriginallyUnseenIndex = i; break; } + + collapsedCount += 1 + (message.extraItems ?? 0); + collapsedDayCount += message.atDateBoundary ? 1 : 0; } // We only want to show the button if we have at least two items @@ -123,11 +136,6 @@ export function CollapseSetViewer(props: Props): React.JSX.Element { 0, !shouldShowButton ? 0 : oldestOriginallyUnseenIndex ); - let collapsedCount = collapsedMessages.length; - collapsedMessages.forEach(message => { - collapsedCount += message.extraItems ?? 0; - }); - const passThroughMessages = messages.slice( !shouldShowButton ? 0 : oldestOriginallyUnseenIndex ); @@ -141,10 +149,17 @@ export function CollapseSetViewer(props: Props): React.JSX.Element { { setIsExpanded(value => !value); }} + onDelete={() => { + toggleDeleteMessagesModal({ + conversationId, + messageIds: collapsedMessages.map(item => item.id), + }); + }} /> ) : undefined} @@ -188,6 +203,7 @@ export function CollapseSetViewer(props: Props): React.JSX.Element { const indexItem = { type: 'none' as const, id: child.id, + dayCount: undefined, messages: undefined, }; @@ -226,6 +242,7 @@ export function CollapseSetViewer(props: Props): React.JSX.Element { const indexItem = { type: 'none' as const, id: child.id, + dayCount: undefined, messages: undefined, }; @@ -255,22 +272,25 @@ export function CollapseSetViewer(props: Props): React.JSX.Element { function CollapseSetButton( props: CollapseSet & { count: number; + dayCount: number; isExpanded: boolean; isGroup: boolean; + isSelectMode: boolean; i18n: LocalizerType; onClick: () => unknown; + onDelete: () => unknown; } ): React.JSX.Element { - const { count, i18n, isExpanded, onClick, type } = props; - - let leadingIcon; - let text; + const { count, dayCount, i18n, isExpanded, onClick, onDelete, type } = props; strictAssert( type !== 'none', - "CollapseSetViewer should never render a 'none' set" + "CollapseSetButton should never render a 'none' set" ); + let leadingIcon; + let text; + // Note: no need for labels for these icons, since they have full text descriptions if (type === 'group-updates') { if (props.isGroup) { @@ -301,6 +321,15 @@ function CollapseSetButton( throw missingCaseError(type); } + if (dayCount > 1) { + text = i18n('icu:multidayCollapse__container', { + containerDescription: text, + dayCountSummary: i18n('icu:multidayCollapse__dayCountSummary', { + dayCount, + }), + }); + } + const trailingIcon = isExpanded ? ( -
- {leadingIcon} {text} {trailingIcon} -
- + + +
+ +
+
+
); } diff --git a/ts/components/conversation/Timeline.dom.stories.tsx b/ts/components/conversation/Timeline.dom.stories.tsx index 074dc5196a..2994d365e6 100644 --- a/ts/components/conversation/Timeline.dom.stories.tsx +++ b/ts/components/conversation/Timeline.dom.stories.tsx @@ -360,6 +360,7 @@ const renderItem = ({ isTargeted={false} isBlocked={false} isGroup={false} + isSelectMode={false} i18n={i18n} interactivity={MessageInteractivity.Normal} interactionMode="keyboard" diff --git a/ts/components/conversation/Timeline.dom.tsx b/ts/components/conversation/Timeline.dom.tsx index 269c367a6b..1caf110e9f 100644 --- a/ts/components/conversation/Timeline.dom.tsx +++ b/ts/components/conversation/Timeline.dom.tsx @@ -554,12 +554,18 @@ export class Timeline extends React.Component< messageIdToMarkRead && (itemId || lastIndex === newestBottomVisibleItemIndex) ) { - markMessageRead(id, messageIdToMarkRead); - return; + const item = items[newestBottomVisibleItemIndex]; + if (!item || item.type === 'none') { + markMessageRead(id, messageIdToMarkRead); + return; + } } // We can return early if the newest partially-visible item is not a CollapseSet - const newestPartiallyVisibleIndex = newestBottomVisibleItemIndex + 1; + const newestPartiallyVisibleIndex = Math.min( + lastIndex, + newestBottomVisibleItemIndex + 1 + ); const newestPartiallyVisibleItem = items[newestPartiallyVisibleIndex]; if ( newestPartiallyVisibleItem && diff --git a/ts/components/conversation/TimelineItem.dom.stories.tsx b/ts/components/conversation/TimelineItem.dom.stories.tsx index e950057b21..5fb3259f11 100644 --- a/ts/components/conversation/TimelineItem.dom.stories.tsx +++ b/ts/components/conversation/TimelineItem.dom.stories.tsx @@ -43,6 +43,7 @@ const getDefaultProps = () => ({ id: 'asdf', isNextItemCallingNotification: false, isPinned: false, + isSelectMode: false, isTargeted: false, isBlocked: false, isGroup: false, diff --git a/ts/components/conversation/TimelineItem.dom.tsx b/ts/components/conversation/TimelineItem.dom.tsx index 84a204ce4e..7435b05477 100644 --- a/ts/components/conversation/TimelineItem.dom.tsx +++ b/ts/components/conversation/TimelineItem.dom.tsx @@ -215,6 +215,7 @@ type PropsLocalType = { isBlocked: boolean; isGroup: boolean; isNextItemCallingNotification: boolean; + isSelectMode: boolean; isTargeted: boolean; scrollToPinnedMessage: (pinMessage: PinMessageData) => void; scrollToPollMessage: ( @@ -265,6 +266,7 @@ export const TimelineItem = memo(function TimelineItem({ isBlocked, isGroup, isNextItemCallingNotification, + isSelectMode, isTargeted, item, onOpenEditNicknameAndNoteModal, @@ -326,8 +328,10 @@ export const TimelineItem = memo(function TimelineItem({ conversationId={conversationId} isBlocked={isBlocked} isGroup={isGroup} + isSelectMode={isSelectMode} renderItem={renderItem} targetedMessage={targetedMessage} + toggleDeleteMessagesModal={reducedProps.toggleDeleteMessagesModal} i18n={i18n} /> ); diff --git a/ts/state/smart/Timeline.preload.tsx b/ts/state/smart/Timeline.preload.tsx index a1d064b6b7..4912c69b98 100644 --- a/ts/state/smart/Timeline.preload.tsx +++ b/ts/state/smart/Timeline.preload.tsx @@ -139,7 +139,7 @@ export const SmartTimeline = memo(function SmartTimeline({ React.useMemo(() => { const result = mapItemsIntoCollapseSets({ activeCall, - allowMultidayDaySets: true, + allowMultidaySets: false, callHistorySelector, callSelector, getCallIdFromEra, diff --git a/ts/state/smart/TimelineItem.preload.tsx b/ts/state/smart/TimelineItem.preload.tsx index 44a4583775..58f617efa1 100644 --- a/ts/state/smart/TimelineItem.preload.tsx +++ b/ts/state/smart/TimelineItem.preload.tsx @@ -22,6 +22,7 @@ import { getPlatform, } from '../selectors/user.std.js'; import { + getSelectedMessageIds, getTargetedMessage, getTargetedMessageSource, } from '../selectors/conversations.dom.js'; @@ -95,6 +96,7 @@ export const SmartTimelineItem = memo(function SmartTimelineItem( const interactionMode = useSelector(getInteractionMode); const theme = useSelector(getTheme); const platform = useSelector(getPlatform); + const selectedMessageIds = useSelector(getSelectedMessageIds); const itemFromSelector = useTimelineItem(messageId, conversationId); const previousItem = useTimelineItem(previousMessageId, conversationId); @@ -244,6 +246,7 @@ export const SmartTimelineItem = memo(function SmartTimelineItem( getSharedGroupNames={getSharedGroupNames} isNextItemCallingNotification={isNextItemCallingNotification} isTargeted={isTargeted} + isSelectMode={selectedMessageIds != null} renderAudioAttachment={renderAudioAttachment} renderContact={renderContact} renderReactionPicker={renderReactionPicker} diff --git a/ts/test-node/util/CollapseSet_test.std.ts b/ts/test-node/util/CollapseSet_test.std.ts index f323a29889..b16db261e9 100644 --- a/ts/test-node/util/CollapseSet_test.std.ts +++ b/ts/test-node/util/CollapseSet_test.std.ts @@ -32,7 +32,7 @@ describe('util/CollapseSets', () => { const yesterday = now - DAY; const defaultParams = { activeCall: undefined, - allowMultidayDaySets: true, + allowMultidaySets: true, callHistorySelector: () => undefined, callSelector: () => undefined, getCallIdFromEra: (eraId: string) => eraId, @@ -140,11 +140,13 @@ describe('util/CollapseSets', () => { id: 'id1', isUnseen: false, extraItems: 1, + atDateBoundary: false, }, { id: 'id2', isUnseen: true, extraItems: undefined, + atDateBoundary: false, }, ], }, @@ -208,10 +210,12 @@ describe('util/CollapseSets', () => { { id: 'id1', isUnseen: true, + atDateBoundary: false, }, { id: 'id2', isUnseen: true, + atDateBoundary: false, }, ], }, @@ -302,10 +306,12 @@ describe('util/CollapseSets', () => { { id: 'id1', isUnseen: false, + atDateBoundary: false, }, { id: 'id2', isUnseen: true, + atDateBoundary: false, }, ], }, @@ -440,6 +446,7 @@ describe('util/CollapseSets', () => { { id: 'id1', isUnseen: false, + atDateBoundary: false, }, ], }, @@ -471,6 +478,7 @@ describe('util/CollapseSets', () => { { id: 'id5', isUnseen: false, + atDateBoundary: false, }, ], }, @@ -567,6 +575,7 @@ describe('util/CollapseSets', () => { id: 'id1', isUnseen: false, extraItems: 1, + atDateBoundary: false, }, ], }, @@ -583,6 +592,7 @@ describe('util/CollapseSets', () => { id: 'id3', isUnseen: false, extraItems: undefined, + atDateBoundary: false, }, ], }, @@ -653,6 +663,7 @@ describe('util/CollapseSets', () => { { id: 'id1', isUnseen: false, + atDateBoundary: false, }, ], }, @@ -664,10 +675,12 @@ describe('util/CollapseSets', () => { { id: 'id2', isUnseen: false, + atDateBoundary: false, }, { id: 'id3', isUnseen: false, + atDateBoundary: false, }, ], }, @@ -794,6 +807,7 @@ describe('util/CollapseSets', () => { id: 'id2', isUnseen: false, extraItems: undefined, + atDateBoundary: false, }, ], }, @@ -810,21 +824,25 @@ describe('util/CollapseSets', () => { id: 'id4', isUnseen: false, extraItems: undefined, + atDateBoundary: false, }, { id: 'id5', isUnseen: false, extraItems: undefined, + atDateBoundary: false, }, { id: 'id6', isUnseen: false, extraItems: undefined, + atDateBoundary: true, }, { id: 'id7', isUnseen: false, extraItems: undefined, + atDateBoundary: false, }, ], }, @@ -836,11 +854,13 @@ describe('util/CollapseSets', () => { id: 'id8', isUnseen: false, extraItems: undefined, + atDateBoundary: false, }, { id: 'id9', isUnseen: false, extraItems: undefined, + atDateBoundary: false, }, ], }, @@ -947,6 +967,7 @@ describe('util/CollapseSets', () => { id: 'id5', isUnseen: false, extraItems: undefined, + atDateBoundary: true, }, ], }, @@ -1071,6 +1092,7 @@ describe('util/CollapseSets', () => { id: 'id4', isUnseen: false, extraItems: 1, + atDateBoundary: false, }, ], }, @@ -1166,6 +1188,7 @@ describe('util/CollapseSets', () => { id: 'id1', isUnseen: false, extraItems: undefined, + atDateBoundary: false, }, ], }, @@ -1182,6 +1205,7 @@ describe('util/CollapseSets', () => { id: 'id3', isUnseen: false, extraItems: undefined, + atDateBoundary: false, }, ], }, @@ -1307,6 +1331,7 @@ describe('util/CollapseSets', () => { id: 'id2', isUnseen: false, extraItems: undefined, + atDateBoundary: false, }, ], }, @@ -1323,11 +1348,13 @@ describe('util/CollapseSets', () => { id: 'id4', isUnseen: false, extraItems: undefined, + atDateBoundary: false, }, { id: 'id5', isUnseen: false, extraItems: undefined, + atDateBoundary: false, }, ], }, @@ -1344,6 +1371,7 @@ describe('util/CollapseSets', () => { id: 'id7', isUnseen: false, extraItems: undefined, + atDateBoundary: false, }, ], }, @@ -1360,6 +1388,7 @@ describe('util/CollapseSets', () => { id: 'id9', isUnseen: false, extraItems: undefined, + atDateBoundary: false, }, ], }, @@ -1372,7 +1401,7 @@ describe('util/CollapseSets', () => { const { resultSets, resultScrollToIndex, resultUnseenIndex } = mapItemsIntoCollapseSets({ ...defaultParams, - allowMultidayDaySets: false, + allowMultidaySets: false, items, messages, }); diff --git a/ts/util/CollapseSet.std.ts b/ts/util/CollapseSet.std.ts index cb299d3da4..414f88874d 100644 --- a/ts/util/CollapseSet.std.ts +++ b/ts/util/CollapseSet.std.ts @@ -25,6 +25,7 @@ export type CollapsedMessage = { isUnseen: boolean; // A single group-v2-change message can have more than one change in it extraItems?: number; + atDateBoundary?: boolean; }; export type CollapseSet = @@ -41,8 +42,8 @@ export type CollapseSet = | { type: 'timer-changes'; id: string; - messages: Array; endingState: DurationInSeconds | undefined; + messages: Array; } | { type: 'call-events'; @@ -119,13 +120,7 @@ export function canCollapseForCallSet( conversationCall?.peekInfo?.eraId != null && options.getCallIdFromEra(conversationCall.peekInfo.eraId); - const deviceCount = conversationCall?.peekInfo?.deviceCount ?? 0; - - // Don't group if current call in the converasation, or there are devices in the call - if ( - callHistory.mode === CallMode.Group && - (callId === conversationCallId || deviceCount > 0) - ) { + if (callHistory.mode === CallMode.Group && callId === conversationCallId) { return false; } @@ -134,7 +129,7 @@ export function canCollapseForCallSet( export function mapItemsIntoCollapseSets({ activeCall, - allowMultidayDaySets, + allowMultidaySets, callHistorySelector, callSelector, getCallIdFromEra, @@ -145,7 +140,7 @@ export function mapItemsIntoCollapseSets({ scrollToIndex, }: { activeCall: CallStateType | undefined; - allowMultidayDaySets: boolean; + allowMultidaySets: boolean; callHistorySelector: CallHistorySelectorType; callSelector: CallSelectorType; getCallIdFromEra: (eraId: string) => string; @@ -244,13 +239,13 @@ export function mapItemsIntoCollapseSets({ } const canContinueSet = !atDateBoundary || - (allowMultidayDaySets && !isToday && havePreviousCompleteDay); + (allowMultidaySets && !isToday && havePreviousCompleteDay); // Start a new set if we just crossed the last seen indicator if (i === oldestUnseenIndex) { haveCompleteDay &&= atDateBoundary; - if (allowMultidayDaySets) { + if (allowMultidaySets) { // If we've just terminated a multiday set, we need to split it; everything from // the current day needs to be in its own set. const didSplit = maybeSplitLastCollapseSet({ @@ -299,6 +294,7 @@ export function mapItemsIntoCollapseSets({ id: currentId, isUnseen: currentMessage.seenStatus === SeenStatus.Unseen, extraItems, + atDateBoundary, }); } else if (lastCollapseSet.type === 'none') { resultSets.pop(); @@ -315,6 +311,7 @@ export function mapItemsIntoCollapseSets({ id: currentId, isUnseen: currentMessage.seenStatus === SeenStatus.Unseen, extraItems, + atDateBoundary, }, ], }); @@ -348,6 +345,7 @@ export function mapItemsIntoCollapseSets({ lastCollapseSet.messages.push({ id: currentId, isUnseen: currentMessage.seenStatus === SeenStatus.Unseen, + atDateBoundary, }); lastCollapseSet.endingState = currentMessage.expirationTimerUpdate?.expireTimer; @@ -365,6 +363,7 @@ export function mapItemsIntoCollapseSets({ { id: currentId, isUnseen: currentMessage.seenStatus === SeenStatus.Unseen, + atDateBoundary, }, ], }); @@ -408,6 +407,7 @@ export function mapItemsIntoCollapseSets({ lastCollapseSet.messages.push({ id: currentId, isUnseen: currentMessage.seenStatus === SeenStatus.Unseen, + atDateBoundary, }); } else if (lastCollapseSet.type === 'none') { resultSets.pop(); @@ -422,6 +422,7 @@ export function mapItemsIntoCollapseSets({ { id: currentId, isUnseen: currentMessage.seenStatus === SeenStatus.Unseen, + atDateBoundary, }, ], }); @@ -441,7 +442,7 @@ export function mapItemsIntoCollapseSets({ haveCompleteDay &&= atDateBoundary; - if (allowMultidayDaySets) { + if (allowMultidaySets) { // If we've just terminated a multiday set, we need to split it; everything from // the current day needs to be in its own set. const didSplit = maybeSplitLastCollapseSet({ @@ -561,6 +562,7 @@ export function maybeSplitLastCollapseSet({ currentDayMessages.length > 1 || (firstCurrentMessage.extraItems ?? 0) > 0 ) { + firstCurrentMessage.atDateBoundary = false; const currentDaySet: CollapseSet = { ...lastCollapseSet, id: firstCurrentMessage.id,