Files
spsays-training/lib/chat-grouping.ts
T
2026-08-29 03:49:58 +05:30

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