atagen

malice

public
Star0
Watch0
6f8d26e8d14c7991087fe7565acdf3807c3eb6b1
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git

Blame: web/src/lib/threads.ts

7ebc2472a9e3atagen2026-10-25 09:53 UTC1
import { derived, get, writable } from 'svelte/store';
7ebc2472a9e3atagen2026-10-25 09:53 UTC2
import { getAccountData, runCommand, setAccountData } from './api';
7ebc2472a9e3atagen2026-10-25 09:53 UTC3
import { pushBanner, sessionUser, timelines } from './store';
7ebc2472a9e3atagen2026-10-25 09:53 UTC4
import { roomThreads } from './thread-index';
7ebc2472a9e3atagen2026-10-25 09:53 UTC5
import type { ThreadEntry } from './thread-index';
7ebc2472a9e3atagen2026-10-25 09:53 UTC6
7ebc2472a9e3atagen2026-10-25 09:53 UTC7
const PINNED_TYPE = 'dev.malice.pinned_threads';
7ebc2472a9e3atagen2026-10-25 09:53 UTC8
7ebc2472a9e3atagen2026-10-25 09:53 UTC9
export interface OpenThread {
7ebc2472a9e3atagen2026-10-25 09:53 UTC10
  room_id: string;
7ebc2472a9e3atagen2026-10-25 09:53 UTC11
  root_event_id: string;
4f91f89d99faatagen2026-10-25 10:47 UTC12
  focus_event_id?: string;
7ebc2472a9e3atagen2026-10-25 09:53 UTC13
}
7ebc2472a9e3atagen2026-10-25 09:53 UTC14
7ebc2472a9e3atagen2026-10-25 09:53 UTC15
export const openThread = writable<OpenThread | null>(null);
7ebc2472a9e3atagen2026-10-25 09:53 UTC16
export const pinnedThreads = writable<Record<string, string[]>>({});
7ebc2472a9e3atagen2026-10-25 09:53 UTC17
7ebc2472a9e3atagen2026-10-25 09:53 UTC18
export const threadsByRoom = derived([timelines, pinnedThreads], ([$timelines, $pinned]) => {
7ebc2472a9e3atagen2026-10-25 09:53 UTC19
  const out: Record<string, ThreadEntry[]> = {};
7ebc2472a9e3atagen2026-10-25 09:53 UTC20
  for (const roomId of new Set([...Object.keys($timelines), ...Object.keys($pinned)])) {
7ebc2472a9e3atagen2026-10-25 09:53 UTC21
    const list = roomThreads(roomId, $timelines[roomId] ?? [], $pinned[roomId] ?? []);
7ebc2472a9e3atagen2026-10-25 09:53 UTC22
    if (list.length) out[roomId] = list;
7ebc2472a9e3atagen2026-10-25 09:53 UTC23
  }
7ebc2472a9e3atagen2026-10-25 09:53 UTC24
  return out;
7ebc2472a9e3atagen2026-10-25 09:53 UTC25
});
7ebc2472a9e3atagen2026-10-25 09:53 UTC26
7ebc2472a9e3atagen2026-10-25 09:53 UTC27
function parsePinned(raw: unknown): Record<string, string[]> {
7ebc2472a9e3atagen2026-10-25 09:53 UTC28
  if (typeof raw !== 'object' || raw === null) return {};
7ebc2472a9e3atagen2026-10-25 09:53 UTC29
  const rooms = (raw as Record<string, unknown>)['rooms'];
7ebc2472a9e3atagen2026-10-25 09:53 UTC30
  if (typeof rooms !== 'object' || rooms === null) return {};
7ebc2472a9e3atagen2026-10-25 09:53 UTC31
  const out: Record<string, string[]> = {};
7ebc2472a9e3atagen2026-10-25 09:53 UTC32
  for (const [roomId, list] of Object.entries(rooms)) {
7ebc2472a9e3atagen2026-10-25 09:53 UTC33
    if (Array.isArray(list)) out[roomId] = list.filter((id): id is string => typeof id === 'string');
7ebc2472a9e3atagen2026-10-25 09:53 UTC34
  }
7ebc2472a9e3atagen2026-10-25 09:53 UTC35
  return out;
7ebc2472a9e3atagen2026-10-25 09:53 UTC36
}
7ebc2472a9e3atagen2026-10-25 09:53 UTC37
7ebc2472a9e3atagen2026-10-25 09:53 UTC38
export async function loadPinnedThreads(): Promise<void> {
7ebc2472a9e3atagen2026-10-25 09:53 UTC39
  try {
7ebc2472a9e3atagen2026-10-25 09:53 UTC40
    pinnedThreads.set(parsePinned(await getAccountData(PINNED_TYPE)));
7ebc2472a9e3atagen2026-10-25 09:53 UTC41
  } catch {
7ebc2472a9e3atagen2026-10-25 09:53 UTC42
    pinnedThreads.set({});
7ebc2472a9e3atagen2026-10-25 09:53 UTC43
  }
7ebc2472a9e3atagen2026-10-25 09:53 UTC44
}
7ebc2472a9e3atagen2026-10-25 09:53 UTC45
7ebc2472a9e3atagen2026-10-25 09:53 UTC46
export async function togglePinnedThread(roomId: string, rootEventId: string): Promise<void> {
7ebc2472a9e3atagen2026-10-25 09:53 UTC47
  const before = get(pinnedThreads);
7ebc2472a9e3atagen2026-10-25 09:53 UTC48
  const list = before[roomId] ?? [];
7ebc2472a9e3atagen2026-10-25 09:53 UTC49
  const nextList = list.includes(rootEventId) ? list.filter((id) => id !== rootEventId) : [...list, rootEventId];
7ebc2472a9e3atagen2026-10-25 09:53 UTC50
  const next = { ...before, [roomId]: nextList };
7ebc2472a9e3atagen2026-10-25 09:53 UTC51
  if (nextList.length === 0) delete next[roomId];
7ebc2472a9e3atagen2026-10-25 09:53 UTC52
  pinnedThreads.set(next);
7ebc2472a9e3atagen2026-10-25 09:53 UTC53
  try {
7ebc2472a9e3atagen2026-10-25 09:53 UTC54
    await setAccountData(PINNED_TYPE, { rooms: next });
7ebc2472a9e3atagen2026-10-25 09:53 UTC55
  } catch (e) {
7ebc2472a9e3atagen2026-10-25 09:53 UTC56
    pinnedThreads.set(before);
7ebc2472a9e3atagen2026-10-25 09:53 UTC57
    pushBanner(e instanceof Error ? e.message : 'Pinning the thread failed');
7ebc2472a9e3atagen2026-10-25 09:53 UTC58
  }
7ebc2472a9e3atagen2026-10-25 09:53 UTC59
}
7ebc2472a9e3atagen2026-10-25 09:53 UTC60
7ebc2472a9e3atagen2026-10-25 09:53 UTC61
let opened: OpenThread | null = null;
7ebc2472a9e3atagen2026-10-25 09:53 UTC62
7ebc2472a9e3atagen2026-10-25 09:53 UTC63
/** The room must already be open, since macltok builds thread timelines inside it. */
7ebc2472a9e3atagen2026-10-25 09:53 UTC64
export async function showThread(target: OpenThread | null): Promise<void> {
7ebc2472a9e3atagen2026-10-25 09:53 UTC65
  const previous = opened;
7ebc2472a9e3atagen2026-10-25 09:53 UTC66
  opened = target;
7ebc2472a9e3atagen2026-10-25 09:53 UTC67
  openThread.set(target);
4f91f89d99faatagen2026-10-25 10:47 UTC68
  if (previous && target && previous.room_id === target.room_id && previous.root_event_id === target.root_event_id) return;
4f91f89d99faatagen2026-10-25 10:47 UTC69
  if (previous) runCommand({ op: 'close_thread', room_id: previous.room_id, root_event_id: previous.root_event_id }).catch(() => undefined);
7ebc2472a9e3atagen2026-10-25 09:53 UTC70
  if (!target) return;
7ebc2472a9e3atagen2026-10-25 09:53 UTC71
  try {
4f91f89d99faatagen2026-10-25 10:47 UTC72
    await runCommand({ op: 'open_thread', room_id: target.room_id, root_event_id: target.root_event_id });
7ebc2472a9e3atagen2026-10-25 09:53 UTC73
  } catch (e) {
7ebc2472a9e3atagen2026-10-25 09:53 UTC74
    if (opened === target) {
7ebc2472a9e3atagen2026-10-25 09:53 UTC75
      opened = null;
7ebc2472a9e3atagen2026-10-25 09:53 UTC76
      openThread.set(null);
7ebc2472a9e3atagen2026-10-25 09:53 UTC77
    }
7ebc2472a9e3atagen2026-10-25 09:53 UTC78
    pushBanner(e instanceof Error ? e.message : 'Opening the thread failed');
7ebc2472a9e3atagen2026-10-25 09:53 UTC79
  }
7ebc2472a9e3atagen2026-10-25 09:53 UTC80
}
7ebc2472a9e3atagen2026-10-25 09:53 UTC81
7ebc2472a9e3atagen2026-10-25 09:53 UTC82
let account: string | null | undefined;
7ebc2472a9e3atagen2026-10-25 09:53 UTC83
sessionUser.subscribe((user) => {
7ebc2472a9e3atagen2026-10-25 09:53 UTC84
  if (user === account) return;
7ebc2472a9e3atagen2026-10-25 09:53 UTC85
  account = user;
7ebc2472a9e3atagen2026-10-25 09:53 UTC86
  opened = null;
7ebc2472a9e3atagen2026-10-25 09:53 UTC87
  openThread.set(null);
7ebc2472a9e3atagen2026-10-25 09:53 UTC88
  pinnedThreads.set({});
7ebc2472a9e3atagen2026-10-25 09:53 UTC89
});