Collapsing Items: A few improvements

This commit is contained in:
Scott Nonnenberg
2026-03-25 07:00:02 +10:00
committed by GitHub
parent 97cf9a90fb
commit c353d41794
11 changed files with 185 additions and 49 deletions

View File

@@ -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}}",

View File

@@ -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<HTMLElement | null>(),
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 <CollapseSetViewer {...props} />;
}
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 },

View File

@@ -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<HTMLElement | null>;
@@ -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 {
<CollapseSetButton
{...props}
count={collapsedCount}
dayCount={collapsedDayCount}
isExpanded={isExpanded}
onClick={() => {
setIsExpanded(value => !value);
}}
onDelete={() => {
toggleDeleteMessagesModal({
conversationId,
messageIds: collapsedMessages.map(item => item.id),
});
}}
/>
</div>
) : 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 ? (
<AxoSymbol.InlineGlyph
symbol="chevron-up"
@@ -314,10 +343,40 @@ function CollapseSetButton(
);
return (
<AxoButton.Root size="lg" variant="secondary" onClick={onClick}>
<div className={tw('font-semibold text-label-secondary')}>
{leadingIcon} {text} {trailingIcon}
</div>
</AxoButton.Root>
<MessageContextMenu
renderer="AxoContextMenu"
disabled={props.isSelectMode}
i18n={i18n}
onDeleteMessage={onDelete}
shouldShowAdditional={false}
onDebugMessage={null}
onDownload={null}
onEdit={null}
onReplyToMessage={null}
onReact={null}
onEndPoll={null}
onRetryMessageSend={null}
onRetryDeleteForEveryone={null}
onCopy={null}
onSelect={null}
onForward={null}
onMoreInfo={null}
onPinMessage={null}
onUnpinMessage={null}
>
<AxoButton.Root size="md" variant="secondary" onClick={onClick}>
<div className={tw('font-semibold text-label-secondary')}>
<I18n
id="icu:collapsedContainer"
i18n={i18n}
components={{
leadingIcon,
text,
trailingIcon,
}}
/>
</div>
</AxoButton.Root>
</MessageContextMenu>
);
}

View File

@@ -360,6 +360,7 @@ const renderItem = ({
isTargeted={false}
isBlocked={false}
isGroup={false}
isSelectMode={false}
i18n={i18n}
interactivity={MessageInteractivity.Normal}
interactionMode="keyboard"

View File

@@ -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 &&

View File

@@ -43,6 +43,7 @@ const getDefaultProps = () => ({
id: 'asdf',
isNextItemCallingNotification: false,
isPinned: false,
isSelectMode: false,
isTargeted: false,
isBlocked: false,
isGroup: false,

View File

@@ -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}
/>
);

View File

@@ -139,7 +139,7 @@ export const SmartTimeline = memo(function SmartTimeline({
React.useMemo(() => {
const result = mapItemsIntoCollapseSets({
activeCall,
allowMultidayDaySets: true,
allowMultidaySets: false,
callHistorySelector,
callSelector,
getCallIdFromEra,

View File

@@ -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}

View File

@@ -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,
});

View File

@@ -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<CollapsedMessage>;
endingState: DurationInSeconds | undefined;
messages: Array<CollapsedMessage>;
}
| {
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,