diff --git a/desktop/src/features/messages/lib/timelineItems.test.mjs b/desktop/src/features/messages/lib/timelineItems.test.mjs
index 58ea21722b..54b6319f9d 100644
--- a/desktop/src/features/messages/lib/timelineItems.test.mjs
+++ b/desktop/src/features/messages/lib/timelineItems.test.mjs
@@ -2,7 +2,11 @@ import assert from "node:assert/strict";
import test from "node:test";
import { KIND_SYSTEM_MESSAGE } from "@/shared/constants/kinds";
-import { buildTimelineItems, getTimelineItemKey } from "./timelineItems.ts";
+import {
+ buildTimelineDayGroups,
+ buildTimelineItems,
+ getTimelineItemKey,
+} from "./timelineItems.ts";
function dayAt(year, month, day, hour = 12) {
return Math.floor(
@@ -100,3 +104,42 @@ test("getTimelineItemKey: keys are unique across the stream", () => {
const keys = items.map(getTimelineItemKey);
assert.equal(new Set(keys).size, keys.length);
});
+
+test("buildTimelineDayGroups: moves non-day rows under their day section", () => {
+ const entries = [
+ entry({ id: "d1a", createdAt: dayAt(2026, 6, 12) }),
+ entry({ id: "d1b", createdAt: dayAt(2026, 6, 12, 13) }),
+ entry({ id: "d2a", createdAt: dayAt(2026, 6, 13) }),
+ entry({ id: "d2b", createdAt: dayAt(2026, 6, 13, 13) }),
+ ];
+ const { items } = buildTimelineItems(entries, "d2b");
+
+ const groups = buildTimelineDayGroups(items);
+
+ assert.equal(groups.length, 2);
+ assert.deepEqual(
+ groups.map((group) => group.items.map((item) => item.kind)),
+ [
+ ["message", "message"],
+ ["message", "unread-divider", "message"],
+ ],
+ );
+ assert.ok(groups.every((group) => group.headingTimestamp !== null));
+});
+
+test("buildTimelineDayGroups: preserves leading rows without a day divider", () => {
+ const leadingRows = [
+ { kind: "unread-divider", key: "unread-a" },
+ { kind: "message", key: "a", entry: entry({ id: "a" }) },
+ ];
+
+ const groups = buildTimelineDayGroups(leadingRows);
+
+ assert.deepEqual(groups, [
+ {
+ key: "day-undated",
+ headingTimestamp: null,
+ items: leadingRows,
+ },
+ ]);
+});
diff --git a/desktop/src/features/messages/lib/timelineItems.ts b/desktop/src/features/messages/lib/timelineItems.ts
index bb20ac2163..a4cacd2b84 100644
--- a/desktop/src/features/messages/lib/timelineItems.ts
+++ b/desktop/src/features/messages/lib/timelineItems.ts
@@ -30,6 +30,14 @@ export type TimelineItemsResult = {
items: TimelineItem[];
};
+export type TimelineNonDayItem = Exclude
;
+
+export type TimelineDayGroup = {
+ key: string;
+ headingTimestamp: number | null;
+ items: TimelineNonDayItem[];
+};
+
/** Stable per-item key, unique across the flattened stream. */
export function getTimelineItemKey(item: TimelineItem): string {
return item.key;
@@ -84,3 +92,35 @@ export function buildTimelineItems(
return { items };
}
+
+export function buildTimelineDayGroups(
+ items: readonly TimelineItem[],
+): TimelineDayGroup[] {
+ const groups: TimelineDayGroup[] = [];
+ let currentGroup: TimelineDayGroup | null = null;
+
+ for (const item of items) {
+ if (item.kind === "day-divider") {
+ currentGroup = {
+ key: item.key,
+ headingTimestamp: item.headingTimestamp,
+ items: [],
+ };
+ groups.push(currentGroup);
+ continue;
+ }
+
+ if (!currentGroup) {
+ currentGroup = {
+ key: "day-undated",
+ headingTimestamp: null,
+ items: [],
+ };
+ groups.push(currentGroup);
+ }
+
+ currentGroup.items.push(item);
+ }
+
+ return groups;
+}
diff --git a/desktop/src/features/messages/ui/DayDivider.tsx b/desktop/src/features/messages/ui/DayDivider.tsx
index e5007a709b..8dff4b7a06 100644
--- a/desktop/src/features/messages/ui/DayDivider.tsx
+++ b/desktop/src/features/messages/ui/DayDivider.tsx
@@ -2,11 +2,11 @@ export function DayDivider({ label }: { label: string }) {
return (
diff --git a/desktop/src/features/messages/ui/MessageThreadPanel.tsx b/desktop/src/features/messages/ui/MessageThreadPanel.tsx
index a146c76cea..6df725a259 100644
--- a/desktop/src/features/messages/ui/MessageThreadPanel.tsx
+++ b/desktop/src/features/messages/ui/MessageThreadPanel.tsx
@@ -797,7 +797,7 @@ export function MessageThreadPanel({
const threadFooter = (
<>
{!isAtBottom ? (
-
+