atagen

malice

public
Star0
Watch0
b7913beddfde83544e4d9e6230a4e505b8a00f71
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git
web/src/lib/thread-index.tsTypescript56 lines2.1 KB
1
import type { TimelineEvent, TimelineItem } from './types';
2
3
export interface ThreadEntry {
4
  room_id: string;
5
  root_event_id: string;
6
  preview: string;
7
  sender: string;
8
  reply_count: number;
9
  latest_ms: number;
10
  pinned: boolean;
11
}
12
13
function previewOf(event: TimelineEvent): string {
14
  const content = event.content;
15
  if (content.kind === 'Message') return content.message.media?.caption ?? content.message.body;
16
  if (content.kind === 'Sticker') return content.body || 'Sticker';
17
  if (content.kind === 'Poll') return content.question;
18
  return '';
19
}
20
21
function entryFor(roomId: string, event: TimelineEvent, pinned: boolean): ThreadEntry | null {
22
  if (!event.event_id || event.thread_root) return null;
23
  return {
24
    room_id: roomId,
25
    root_event_id: event.event_id,
26
    preview: previewOf(event),
27
    sender: event.sender.display_name ?? event.sender.user_id,
28
    reply_count: event.thread_summary?.reply_count ?? 0,
29
    latest_ms: event.thread_summary?.latest_event_timestamp_ms ?? event.timestamp_ms,
30
    pinned
31
  };
32
}
33
34
/** Pinned threads first, then by latest activity. Pins whose root isn't loaded still get a row. */
35
export function roomThreads(roomId: string, items: TimelineItem[], pinnedRoots: string[]): ThreadEntry[] {
36
  const pinned = new Set(pinnedRoots);
37
  const found = new Map<string, ThreadEntry>();
38
  for (const item of items) {
39
    if (item.kind !== 'event') continue;
40
    const id = item.event.event_id;
41
    if (!id || (!pinned.has(id) && (item.event.thread_summary?.reply_count ?? 0) === 0)) continue;
42
    const entry = entryFor(roomId, item.event, pinned.has(id));
43
    if (entry) found.set(id, entry);
44
  }
45
  for (const root of pinnedRoots) {
46
    if (!found.has(root)) {
47
      found.set(root, { room_id: roomId, root_event_id: root, preview: '', sender: '', reply_count: 0, latest_ms: 0, pinned: true });
48
    }
49
  }
50
  return [...found.values()].sort((a, b) => Number(b.pinned) - Number(a.pinned) || b.latest_ms - a.latest_ms);
51
}
52
53
export function threadMatches(entry: ThreadEntry, query: string): boolean {
54
  if (!query) return true;
55
  return entry.preview.toLowerCase().includes(query) || entry.sender.toLowerCase().includes(query);
56
}