Skip to content
41 linesCodeBlameRaw
1/**
2 * What was said over a span, counted (`activity`, for Home). Pure, so the
3 * counting is tested apart from the service: the service reads one row per
4 * (conversation, author) with how many messages, over the conversations
5 * the viewer can read, and this adds them up.
6 */
7
8import type { ChatActivity } from "@g1t/contracts";
9
10/** One conversation's messages by one author in the span. */
11export type ActivityRow = { channel_id: string; author: string; n: number };
12
13/** Messages, conversations with any, and who said how much, most first. */
14export function chatActivity(rows: ActivityRow[], span: { from: string; until: string }): ChatActivity {
15 const channels = new Set<string>();
16 const byAuthor = new Map<string, number>();
17 let messages = 0;
18 for (const row of rows) {
19 const n = Math.max(0, Math.floor(Number(row.n) || 0));
20 if (n === 0) continue;
21 messages += n;
22 channels.add(row.channel_id);
23 byAuthor.set(row.author, (byAuthor.get(row.author) ?? 0) + n);
24 }
25 const authors = [...byAuthor]
26 .map(([key, count]) => ({ key, messages: count }))
27 .sort((a, b) => b.messages - a.messages || a.key.localeCompare(b.key));
28 return { from: span.from, until: span.until, messages, channels: channels.size, authors };
29}
30
31/**
32 * The span as a pair of RFC 3339 times, or null when either is not a time
33 * or the span is empty.
34 */
35export function activitySpan(from: unknown, until: unknown): { from: string; until: string; fromMs: number; untilMs: number } | null {
36 if (typeof from !== "string" || typeof until !== "string") return null;
37 const fromMs = Date.parse(from);
38 const untilMs = Date.parse(until);
39 if (!Number.isFinite(fromMs) || !Number.isFinite(untilMs) || untilMs <= fromMs) return null;
40 return { from, until, fromMs, untilMs };
41}