81 lines
2.2 KiB
TypeScript
81 lines
2.2 KiB
TypeScript
/**
|
|
* Groups a thread's messages the way Instagram does: consecutive messages from
|
|
* one sender, sent close together, render as a stack with a single avatar and
|
|
* one timestamp, and a date separator marks each new day.
|
|
*
|
|
* Pure and message-shaped so it can be reasoned about (and checked) without a
|
|
* DOM.
|
|
*/
|
|
|
|
import type { ChatMessage } from "@/lib/chat";
|
|
|
|
/** How long a pause before the same sender starts a new stack. */
|
|
export const GROUP_GAP_MS = 5 * 60 * 1000;
|
|
|
|
export type ChatRow =
|
|
| { kind: "date"; key: string; at: string }
|
|
| {
|
|
kind: "group";
|
|
key: string;
|
|
senderId: string | null;
|
|
messages: ChatMessage[];
|
|
};
|
|
|
|
function dayKey(iso: string): string {
|
|
const d = new Date(iso);
|
|
return `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
|
|
}
|
|
|
|
/**
|
|
* `messages` must be ascending by seq (oldest first) — the order the thread is
|
|
* rendered in.
|
|
*/
|
|
export function groupMessages(
|
|
messages: ChatMessage[],
|
|
gapMs: number = GROUP_GAP_MS,
|
|
): ChatRow[] {
|
|
const rows: ChatRow[] = [];
|
|
let current: Extract<ChatRow, { kind: "group" }> | null = null;
|
|
let currentDay = "";
|
|
|
|
for (const m of messages) {
|
|
const day = dayKey(m.created_at);
|
|
if (day !== currentDay) {
|
|
rows.push({ kind: "date", key: `date-${day}`, at: m.created_at });
|
|
currentDay = day;
|
|
current = null; // a new day always starts a new stack
|
|
}
|
|
|
|
const last = current?.messages[current.messages.length - 1];
|
|
const gap = last
|
|
? new Date(m.created_at).getTime() - new Date(last.created_at).getTime()
|
|
: Infinity;
|
|
|
|
// System notices stand alone — they belong to no one and shouldn't be
|
|
// absorbed into a neighbouring stack.
|
|
const standalone = m.kind === "system";
|
|
const continues =
|
|
current !== null &&
|
|
!standalone &&
|
|
current.senderId === (m.sender_id ?? null) &&
|
|
current.messages[0]?.kind !== "system" &&
|
|
gap <= gapMs;
|
|
|
|
if (continues && current) {
|
|
current.messages.push(m);
|
|
continue;
|
|
}
|
|
|
|
current = {
|
|
kind: "group",
|
|
key: `group-${m.id}`,
|
|
senderId: m.sender_id ?? null,
|
|
messages: [m],
|
|
};
|
|
rows.push(current);
|
|
if (standalone) current = null;
|
|
}
|
|
|
|
return rows;
|
|
}
|