Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

55 linesCodeBlameRaw
1/**
2 * Unread and mention counts for the sidebar, and its order. Pure, so they
3 * are tested apart from the service.
4 */
5
6import { mentions } from "./mentions.ts";
7
8/** A message the counts are made from: one after the member's last read. */
9export type UnreadRow = { channel_id: string; id: string; author: string; mentions: string | null };
10
11export type Counts = { unread: number; mentions: number };
12
13/**
14 * Whether a message is unread for a member: newer than the last one they
15 * read, and not their own (what you wrote, you have read). Top-level
16 * messages and thread replies count alike.
17 */
18export function isUnread(row: { id: string; author: string }, member: string, lastReadId: string | null): boolean {
19 return row.author !== member && (lastReadId == null || row.id > lastReadId);
20}
21
22/**
23 * Each channel's counts for `member` (`user:<id>`), whose username is
24 * `username`, from the rows after their last read in each channel.
25 */
26export function tally(rows: UnreadRow[], member: string, username: string, lastRead: Map<string, string | null>): Map<string, Counts> {
27 const counts = new Map<string, Counts>();
28 for (const row of rows) {
29 if (!isUnread(row, member, lastRead.get(row.channel_id) ?? null)) continue;
30 const count = counts.get(row.channel_id) ?? { unread: 0, mentions: 0 };
31 count.unread += 1;
32 if (mentions(row.mentions, username)) count.mentions += 1;
33 counts.set(row.channel_id, count);
34 }
35 return counts;
36}
37
38/** Starred first, then the most recently active, then by title. */
39export function sidebarOrder<T extends { starred: boolean; title: string; channel: { last_message_at: string | null; created_at: string } }>(
40 entries: T[],
41): T[] {
42 const active = (entry: T) => entry.channel.last_message_at ?? entry.channel.created_at;
43 return [...entries].sort((a, b) => {
44 if (a.starred !== b.starred) return a.starred ? -1 : 1;
45 const at = active(b).localeCompare(active(a));
46 return at || a.title.localeCompare(b.title);
47 });
48}
49
50/** A direct message's title: the others' names, or yours for notes to yourself. */
51export function dmTitle(others: { display_name: string }[], self: { display_name: string } | null): string {
52 if (!others.length) return self ? `${self.display_name} (you)` : "Notes";
53 const names = others.map((o) => o.display_name);
54 return names.length <= 3 ? names.join(", ") : `${names.slice(0, 3).join(", ")} and ${names.length - 3} more`;
55}