7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 1 | import type { 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; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 11 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 12 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 13 | function previewOf(event: TimelineEvent): string { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 14 | const content = event.content; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 15 | if (content.kind === 'Message') return content.message.media?.caption ?? content.message.body; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 16 | if (content.kind === 'Sticker') return content.body || 'Sticker'; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 17 | if (content.kind === 'Poll') return content.question; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 18 | return ''; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 19 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 20 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 21 | function entryFor(roomId: string, event: TimelineEvent, pinned: boolean): ThreadEntry | null { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 22 | if (!event.event_id || event.thread_root) return null; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 23 | return { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 24 | room_id: roomId, |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 25 | root_event_id: event.event_id, |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 26 | preview: previewOf(event), |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 27 | sender: event.sender.display_name ?? event.sender.user_id, |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 28 | reply_count: event.thread_summary?.reply_count ?? 0, |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 29 | latest_ms: event.thread_summary?.latest_event_timestamp_ms ?? event.timestamp_ms, |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 30 | pinned |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 31 | }; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 32 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 33 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 34 | /** Pinned threads first, then by latest activity. Pins whose root isn't loaded still get a row. */ |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 35 | export function roomThreads(roomId: string, items: TimelineItem[], pinnedRoots: string[]): ThreadEntry[] { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 36 | const pinned = new Set(pinnedRoots); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 37 | const found = new Map<string, ThreadEntry>(); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 38 | for (const item of items) { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 39 | if (item.kind !== 'event') continue; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 40 | const id = item.event.event_id; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 41 | if (!id || (!pinned.has(id) && (item.event.thread_summary?.reply_count ?? 0) === 0)) continue; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 42 | const entry = entryFor(roomId, item.event, pinned.has(id)); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 43 | if (entry) found.set(id, entry); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 44 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 45 | for (const root of pinnedRoots) { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 46 | if (!found.has(root)) { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 47 | found.set(root, { room_id: roomId, root_event_id: root, preview: '', sender: '', reply_count: 0, latest_ms: 0, pinned: true }); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 48 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 49 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 50 | 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 | 51 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 52 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 53 | export function threadMatches(entry: ThreadEntry, query: string): boolean { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 54 | if (!query) return true; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 55 | return entry.preview.toLowerCase().includes(query) || entry.sender.toLowerCase().includes(query); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 56 | } |