atagen

malice

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

Blame: web/src/lib/thread-index.ts

249e439c27e5atagen2026-10-25 13:26 UTC1
import type { ThreadListEntry, ThreadUnread, TimelineEvent, TimelineItem } from './types';
7ebc2472a9e3atagen2026-10-25 09:53 UTC2
7ebc2472a9e3atagen2026-10-25 09:53 UTC3
export interface ThreadEntry {
7ebc2472a9e3atagen2026-10-25 09:53 UTC4
  room_id: string;
7ebc2472a9e3atagen2026-10-25 09:53 UTC5
  root_event_id: string;
7ebc2472a9e3atagen2026-10-25 09:53 UTC6
  preview: string;
7ebc2472a9e3atagen2026-10-25 09:53 UTC7
  sender: string;
7ebc2472a9e3atagen2026-10-25 09:53 UTC8
  reply_count: number;
7ebc2472a9e3atagen2026-10-25 09:53 UTC9
  latest_ms: number;
7ebc2472a9e3atagen2026-10-25 09:53 UTC10
  pinned: boolean;
249e439c27e5atagen2026-10-25 13:26 UTC11
  unread: number;
249e439c27e5atagen2026-10-25 13:26 UTC12
  mentions: number;
7ebc2472a9e3atagen2026-10-25 09:53 UTC13
}
7ebc2472a9e3atagen2026-10-25 09:53 UTC14
7ebc2472a9e3atagen2026-10-25 09:53 UTC15
function previewOf(event: TimelineEvent): string {
7ebc2472a9e3atagen2026-10-25 09:53 UTC16
  const content = event.content;
7ebc2472a9e3atagen2026-10-25 09:53 UTC17
  if (content.kind === 'Message') return content.message.media?.caption ?? content.message.body;
7ebc2472a9e3atagen2026-10-25 09:53 UTC18
  if (content.kind === 'Sticker') return content.body || 'Sticker';
7ebc2472a9e3atagen2026-10-25 09:53 UTC19
  if (content.kind === 'Poll') return content.question;
7ebc2472a9e3atagen2026-10-25 09:53 UTC20
  return '';
7ebc2472a9e3atagen2026-10-25 09:53 UTC21
}
7ebc2472a9e3atagen2026-10-25 09:53 UTC22
7ebc2472a9e3atagen2026-10-25 09:53 UTC23
function entryFor(roomId: string, event: TimelineEvent, pinned: boolean): ThreadEntry | null {
7ebc2472a9e3atagen2026-10-25 09:53 UTC24
  if (!event.event_id || event.thread_root) return null;
7ebc2472a9e3atagen2026-10-25 09:53 UTC25
  return {
7ebc2472a9e3atagen2026-10-25 09:53 UTC26
    room_id: roomId,
7ebc2472a9e3atagen2026-10-25 09:53 UTC27
    root_event_id: event.event_id,
7ebc2472a9e3atagen2026-10-25 09:53 UTC28
    preview: previewOf(event),
7ebc2472a9e3atagen2026-10-25 09:53 UTC29
    sender: event.sender.display_name ?? event.sender.user_id,
7ebc2472a9e3atagen2026-10-25 09:53 UTC30
    reply_count: event.thread_summary?.reply_count ?? 0,
7ebc2472a9e3atagen2026-10-25 09:53 UTC31
    latest_ms: event.thread_summary?.latest_event_timestamp_ms ?? event.timestamp_ms,
249e439c27e5atagen2026-10-25 13:26 UTC32
    pinned,
249e439c27e5atagen2026-10-25 13:26 UTC33
    unread: 0,
249e439c27e5atagen2026-10-25 13:26 UTC34
    mentions: 0
7ebc2472a9e3atagen2026-10-25 09:53 UTC35
  };
7ebc2472a9e3atagen2026-10-25 09:53 UTC36
}
7ebc2472a9e3atagen2026-10-25 09:53 UTC37
249e439c27e5atagen2026-10-25 13:26 UTC38
/**
249e439c27e5atagen2026-10-25 13:26 UTC39
 * Pinned threads first, then by latest activity. The server's thread list wins over roots seen in the
249e439c27e5atagen2026-10-25 13:26 UTC40
 * loaded timeline, and pins or unread roots that neither has loaded still get a row.
249e439c27e5atagen2026-10-25 13:26 UTC41
 */
249e439c27e5atagen2026-10-25 13:26 UTC42
export function roomThreads(
249e439c27e5atagen2026-10-25 13:26 UTC43
  roomId: string,
249e439c27e5atagen2026-10-25 13:26 UTC44
  items: TimelineItem[],
249e439c27e5atagen2026-10-25 13:26 UTC45
  pinnedRoots: string[],
249e439c27e5atagen2026-10-25 13:26 UTC46
  listed: ThreadListEntry[] = [],
249e439c27e5atagen2026-10-25 13:26 UTC47
  unreads: Record<string, ThreadUnread> = {}
249e439c27e5atagen2026-10-25 13:26 UTC48
): ThreadEntry[] {
7ebc2472a9e3atagen2026-10-25 09:53 UTC49
  const pinned = new Set(pinnedRoots);
7ebc2472a9e3atagen2026-10-25 09:53 UTC50
  const found = new Map<string, ThreadEntry>();
7ebc2472a9e3atagen2026-10-25 09:53 UTC51
  for (const item of items) {
7ebc2472a9e3atagen2026-10-25 09:53 UTC52
    if (item.kind !== 'event') continue;
7ebc2472a9e3atagen2026-10-25 09:53 UTC53
    const id = item.event.event_id;
7ebc2472a9e3atagen2026-10-25 09:53 UTC54
    if (!id || (!pinned.has(id) && (item.event.thread_summary?.reply_count ?? 0) === 0)) continue;
7ebc2472a9e3atagen2026-10-25 09:53 UTC55
    const entry = entryFor(roomId, item.event, pinned.has(id));
7ebc2472a9e3atagen2026-10-25 09:53 UTC56
    if (entry) found.set(id, entry);
7ebc2472a9e3atagen2026-10-25 09:53 UTC57
  }
249e439c27e5atagen2026-10-25 13:26 UTC58
  for (const listedEntry of listed) {
249e439c27e5atagen2026-10-25 13:26 UTC59
    const entry = entryFor(roomId, listedEntry.root, pinned.has(listedEntry.root_event_id));
249e439c27e5atagen2026-10-25 13:26 UTC60
    if (!entry) continue;
945233a9ddf4atagen2026-10-25 14:01 UTC61
    found.set(listedEntry.root_event_id, { ...entry, reply_count: listedEntry.reply_count, latest_ms: listedEntry.latest_activity_ms });
249e439c27e5atagen2026-10-25 13:26 UTC62
  }
249e439c27e5atagen2026-10-25 13:26 UTC63
  for (const root of new Set([...pinnedRoots, ...Object.keys(unreads)])) {
7ebc2472a9e3atagen2026-10-25 09:53 UTC64
    if (!found.has(root)) {
249e439c27e5atagen2026-10-25 13:26 UTC65
      found.set(root, { room_id: roomId, root_event_id: root, preview: '', sender: '', reply_count: 0, latest_ms: 0, pinned: pinned.has(root), unread: 0, mentions: 0 });
7ebc2472a9e3atagen2026-10-25 09:53 UTC66
    }
7ebc2472a9e3atagen2026-10-25 09:53 UTC67
  }
249e439c27e5atagen2026-10-25 13:26 UTC68
  for (const [root, unread] of Object.entries(unreads)) {
249e439c27e5atagen2026-10-25 13:26 UTC69
    const entry = found.get(root);
249e439c27e5atagen2026-10-25 13:26 UTC70
    if (entry) found.set(root, { ...entry, unread: unread.messages, mentions: unread.mentions });
249e439c27e5atagen2026-10-25 13:26 UTC71
  }
7ebc2472a9e3atagen2026-10-25 09:53 UTC72
  return [...found.values()].sort((a, b) => Number(b.pinned) - Number(a.pinned) || b.latest_ms - a.latest_ms);
7ebc2472a9e3atagen2026-10-25 09:53 UTC73
}
7ebc2472a9e3atagen2026-10-25 09:53 UTC74
7ebc2472a9e3atagen2026-10-25 09:53 UTC75
export function threadMatches(entry: ThreadEntry, query: string): boolean {
7ebc2472a9e3atagen2026-10-25 09:53 UTC76
  if (!query) return true;
7ebc2472a9e3atagen2026-10-25 09:53 UTC77
  return entry.preview.toLowerCase().includes(query) || entry.sender.toLowerCase().includes(query);
7ebc2472a9e3atagen2026-10-25 09:53 UTC78
}