atagen

malice

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