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 to | 1 | /** |
| 2 | * Unread and mention counts for the sidebar, and its order. Pure, so they | |
| 3 | * are tested apart from the service. | |
| 4 | */ | |
| 5 | ||
| 6 | import { mentions } from "./mentions.ts"; | |
| 7 | ||
| 8 | /** A message the counts are made from: one after the member's last read. */ | |
| 9 | export type UnreadRow = { channel_id: string; id: string; author: string; mentions: string | null }; | |
| 10 | ||
| 11 | export 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 | */ | |
| 18 | export 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 | */ | |
| 26 | export 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. */ | |
| 44 | export 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. */ | |
| 56 | export 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 | } |