Skip to content
44 linesCodeBlameRaw
1/**
2 * Unread counts as the feed keeps them: per conversation, summed per
3 * workspace the way the rail's badge sums them (muted conversations leave
4 * their unread out; mentions count everywhere). Pure, so it is tested
5 * apart from the feed.
6 */
7import type { ChannelCounts, FeedCounts } from "@g1t/contracts";
8
9/** The counts after a delivery: added, or (`set`) replaced. Never below zero. */
10export function applyCounts(
11 before: ChannelCounts | null,
12 change: { channel_id: string; unread: number; mentions: number; muted?: boolean | null; set?: boolean },
13): ChannelCounts {
14 const n = (v: unknown) => (Number.isFinite(v) ? Math.max(0, Math.floor(Number(v))) : 0);
15 const d = (v: unknown) => (Number.isFinite(v) ? Math.floor(Number(v)) : 0);
16 const muted = typeof change.muted === "boolean" ? change.muted : (before?.muted ?? false);
17 if (change.set || !before) {
18 return {
19 channel_id: change.channel_id,
20 unread: change.set ? n(change.unread) : n(d(change.unread)),
21 mentions: change.set ? n(change.mentions) : n(d(change.mentions)),
22 muted,
23 };
24 }
25 return {
26 channel_id: change.channel_id,
27 unread: Math.max(0, before.unread + d(change.unread)),
28 mentions: Math.max(0, before.mentions + d(change.mentions)),
29 muted,
30 };
31}
32
33/** A workspace's counts as the socket sends them. */
34export function totals(workspace: string, rows: ChannelCounts[], inboxUnread: number, complete: boolean): FeedCounts {
35 let unread = 0;
36 let mentions = 0;
37 const perChannel: FeedCounts["per_channel"] = [];
38 for (const row of rows) {
39 mentions += row.mentions;
40 if (!row.muted) unread += row.unread;
41 if (row.unread > 0 || row.mentions > 0) perChannel.push({ channel_id: row.channel_id, unread: row.unread, mentions: row.mentions });
42 }
43 return { workspace, chat_unread: unread, chat_mentions: mentions, inbox_unread: Math.max(0, inboxUnread), per_channel: perChannel, complete };
44}