7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 1 | import { derived, get, writable } from 'svelte/store'; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 2 | import { getAccountData, runCommand, setAccountData } from './api'; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 3 | import { pushBanner, sessionUser, timelines } from './store'; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 4 | import { roomThreads } from './thread-index'; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 5 | import type { ThreadEntry } from './thread-index'; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 6 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 7 | const PINNED_TYPE = 'dev.malice.pinned_threads'; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 8 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 9 | export interface OpenThread { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 10 | room_id: string; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 11 | root_event_id: string; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 12 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 13 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 14 | export const openThread = writable<OpenThread | null>(null); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 15 | export const pinnedThreads = writable<Record<string, string[]>>({}); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 16 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 17 | export const threadsByRoom = derived([timelines, pinnedThreads], ([$timelines, $pinned]) => { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 18 | const out: Record<string, ThreadEntry[]> = {}; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 19 | for (const roomId of new Set([...Object.keys($timelines), ...Object.keys($pinned)])) { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 20 | const list = roomThreads(roomId, $timelines[roomId] ?? [], $pinned[roomId] ?? []); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 21 | if (list.length) out[roomId] = list; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 22 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 23 | return out; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 24 | }); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 25 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 26 | function parsePinned(raw: unknown): Record<string, string[]> { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 27 | if (typeof raw !== 'object' || raw === null) return {}; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 28 | const rooms = (raw as Record<string, unknown>)['rooms']; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 29 | if (typeof rooms !== 'object' || rooms === null) return {}; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 30 | const out: Record<string, string[]> = {}; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 31 | for (const [roomId, list] of Object.entries(rooms)) { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 32 | if (Array.isArray(list)) out[roomId] = list.filter((id): id is string => typeof id === 'string'); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 33 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 34 | return out; |
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 | export async function loadPinnedThreads(): Promise<void> { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 38 | try { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 39 | pinnedThreads.set(parsePinned(await getAccountData(PINNED_TYPE))); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 40 | } catch { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 41 | pinnedThreads.set({}); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 42 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 43 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 44 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 45 | export async function togglePinnedThread(roomId: string, rootEventId: string): Promise<void> { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 46 | const before = get(pinnedThreads); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 47 | const list = before[roomId] ?? []; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 48 | const nextList = list.includes(rootEventId) ? list.filter((id) => id !== rootEventId) : [...list, rootEventId]; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 49 | const next = { ...before, [roomId]: nextList }; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 50 | if (nextList.length === 0) delete next[roomId]; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 51 | pinnedThreads.set(next); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 52 | try { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 53 | await setAccountData(PINNED_TYPE, { rooms: next }); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 54 | } catch (e) { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 55 | pinnedThreads.set(before); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 56 | pushBanner(e instanceof Error ? e.message : 'Pinning the thread failed'); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 57 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 58 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 59 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 60 | let opened: OpenThread | null = null; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 61 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 62 | /** The room must already be open, since macltok builds thread timelines inside it. */ |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 63 | export async function showThread(target: OpenThread | null): Promise<void> { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 64 | const previous = opened; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 65 | if (previous && target && previous.room_id === target.room_id && previous.root_event_id === target.root_event_id) return; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 66 | opened = target; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 67 | openThread.set(target); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 68 | if (previous) runCommand({ op: 'close_thread', ...previous }).catch(() => undefined); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 69 | if (!target) return; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 70 | try { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 71 | await runCommand({ op: 'open_thread', ...target }); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 72 | } catch (e) { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 73 | if (opened === target) { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 74 | opened = null; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 75 | openThread.set(null); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 76 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 77 | pushBanner(e instanceof Error ? e.message : 'Opening the thread failed'); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 78 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 79 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 80 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 81 | let account: string | null | undefined; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 82 | sessionUser.subscribe((user) => { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 83 | if (user === account) return; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 84 | account = user; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 85 | opened = null; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 86 | openThread.set(null); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 87 | pinnedThreads.set({}); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 88 | }); |