| 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 | } |