249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 1 | import type { ThreadListEntry, ThreadUnread, TimelineEvent, TimelineItem } from './types'; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 2 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 3 | export interface ThreadEntry { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 4 | room_id: string; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 5 | root_event_id: string; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 6 | preview: string; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 7 | sender: string; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 8 | reply_count: number; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 9 | latest_ms: number; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 10 | pinned: boolean; |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 11 | unread: number; |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 12 | mentions: number; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 13 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 14 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 15 | function previewOf(event: TimelineEvent): string { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 16 | const content = event.content; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 17 | if (content.kind === 'Message') return content.message.media?.caption ?? content.message.body; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 18 | if (content.kind === 'Sticker') return content.body || 'Sticker'; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 19 | if (content.kind === 'Poll') return content.question; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 20 | return ''; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 21 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 22 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 23 | function entryFor(roomId: string, event: TimelineEvent, pinned: boolean): ThreadEntry | null { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 24 | if (!event.event_id || event.thread_root) return null; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 25 | return { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 26 | room_id: roomId, |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 27 | root_event_id: event.event_id, |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 28 | preview: previewOf(event), |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 29 | sender: event.sender.display_name ?? event.sender.user_id, |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 30 | reply_count: event.thread_summary?.reply_count ?? 0, |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 31 | latest_ms: event.thread_summary?.latest_event_timestamp_ms ?? event.timestamp_ms, |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 32 | pinned, |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 33 | unread: 0, |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 34 | mentions: 0 |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 35 | }; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 36 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 37 | |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 38 | /** |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 39 | * Pinned threads first, then by latest activity. The server's thread list wins over roots seen in the |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 40 | * loaded timeline, and pins or unread roots that neither has loaded still get a row. |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 41 | */ |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 42 | export function roomThreads( |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 43 | roomId: string, |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 44 | items: TimelineItem[], |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 45 | pinnedRoots: string[], |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 46 | listed: ThreadListEntry[] = [], |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 47 | unreads: Record<string, ThreadUnread> = {} |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 48 | ): ThreadEntry[] { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 49 | const pinned = new Set(pinnedRoots); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 50 | const found = new Map<string, ThreadEntry>(); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 51 | for (const item of items) { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 52 | if (item.kind !== 'event') continue; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 53 | const id = item.event.event_id; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 54 | if (!id || (!pinned.has(id) && (item.event.thread_summary?.reply_count ?? 0) === 0)) continue; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 55 | const entry = entryFor(roomId, item.event, pinned.has(id)); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 56 | if (entry) found.set(id, entry); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 57 | } |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 58 | for (const listedEntry of listed) { |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 59 | const entry = entryFor(roomId, listedEntry.root, pinned.has(listedEntry.root_event_id)); |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 60 | if (!entry) continue; |
945233a9ddf4 | atagen | 2026-10-25 14:01 UTC | 61 | found.set(listedEntry.root_event_id, { ...entry, reply_count: listedEntry.reply_count, latest_ms: listedEntry.latest_activity_ms }); |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 62 | } |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 63 | for (const root of new Set([...pinnedRoots, ...Object.keys(unreads)])) { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 64 | if (!found.has(root)) { |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 65 | found.set(root, { room_id: roomId, root_event_id: root, preview: '', sender: '', reply_count: 0, latest_ms: 0, pinned: pinned.has(root), unread: 0, mentions: 0 }); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 66 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 67 | } |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 68 | for (const [root, unread] of Object.entries(unreads)) { |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 69 | const entry = found.get(root); |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 70 | if (entry) found.set(root, { ...entry, unread: unread.messages, mentions: unread.mentions }); |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 71 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 72 | return [...found.values()].sort((a, b) => Number(b.pinned) - Number(a.pinned) || b.latest_ms - a.latest_ms); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 73 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 74 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 75 | export function threadMatches(entry: ThreadEntry, query: string): boolean { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 76 | if (!query) return true; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 77 | return entry.preview.toLowerCase().includes(query) || entry.sender.toLowerCase().includes(query); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 78 | } |