Skip to content
60 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Chat and workspace agents: channels, DMs and named agents you talk to1/**
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(
27 rows: UnreadRow[],
28 member: string,
29 username: string,
30 lastRead: Map<string, string | null>,
31): Map<string, Counts> {
32 const counts = new Map<string, Counts>();
33 for (const row of rows) {
34 if (!isUnread(row, member, lastRead.get(row.channel_id) ?? null)) continue;
35 const count = counts.get(row.channel_id) ?? { unread: 0, mentions: 0 };
36 count.unread += 1;
37 if (mentions(row.mentions, username)) count.mentions += 1;
38 counts.set(row.channel_id, count);
39 }
40 return counts;
41}
42
43/** Starred first, then the most recently active, then by title. */
44export function sidebarOrder<T extends { starred: boolean; title: string; channel: { last_message_at: string | null; created_at: string } }>(
45 entries: T[],
46): T[] {
47 const active = (entry: T) => entry.channel.last_message_at ?? entry.channel.created_at;
48 return [...entries].sort((a, b) => {
49 if (a.starred !== b.starred) return a.starred ? -1 : 1;
50 const at = active(b).localeCompare(active(a));
51 return at || a.title.localeCompare(b.title);
52 });
53}
54
55/** A direct message's title: the others' names, or yours for notes to yourself. */
56export function dmTitle(others: { display_name: string }[], self: { display_name: string } | null): string {
57 if (!others.length) return self ? `${self.display_name} (you)` : "Notes";
58 const names = others.map((o) => o.display_name);
59 return names.length <= 3 ? names.join(", ") : `${names.slice(0, 3).join(", ")} and ${names.length - 3} more`;
60}