atagen

malice

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