feat: chat
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user