atagen

malice

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

Blame: web/src/lib/MentionsInbox.svelte

668ab5c30531atagen2026-10-25 03:13 UTC1
<script lang="ts">
668ab5c30531atagen2026-10-25 03:13 UTC2
  import { get } from 'svelte/store';
668ab5c30531atagen2026-10-25 03:13 UTC3
  import { onDestroy, onMount, tick } from 'svelte';
48af82777108atagen2026-10-26 10:41 UTC4
  import { getMembers, mentionContext, mentionsPage, recoverMentions, retryMentionContext, setMentionsRead, markMentionsReadThrough } from './api';
668ab5c30531atagen2026-10-25 03:13 UTC5
  import ContextTimeline from './ContextTimeline.svelte';
668ab5c30531atagen2026-10-25 03:13 UTC6
  import EmojiText from './EmojiText.svelte';
668ab5c30531atagen2026-10-25 03:13 UTC7
  import MessageRow from './MessageRow.svelte';
668ab5c30531atagen2026-10-25 03:13 UTC8
  import { mentionsSnapshot, publishMentionsPage, rooms, sessionUser } from './store';
668ab5c30531atagen2026-10-25 03:13 UTC9
  import { TimelineViewport } from './timeline-viewport';
668ab5c30531atagen2026-10-25 03:13 UTC10
  import { ApiError } from './transport';
668ab5c30531atagen2026-10-25 03:13 UTC11
  import type { TimelineItem } from './types';
668ab5c30531atagen2026-10-25 03:13 UTC12
  import { mentionKey } from './mentions-model';
48af82777108atagen2026-10-26 10:41 UTC13
  import { contextWithProfiles, roomsLackingProfiles, withProfiles } from './mention-profiles';
48af82777108atagen2026-10-26 10:41 UTC14
  import type { RoomProfiles } from './mention-profiles';
668ab5c30531atagen2026-10-25 03:13 UTC15
  import type { MentionEntry, MentionsPage, SavedMentionContext } from './mentions-model';
668ab5c30531atagen2026-10-25 03:13 UTC16
668ab5c30531atagen2026-10-25 03:13 UTC17
  export let self: string;
4f91f89d99faatagen2026-10-25 10:47 UTC18
  export let onLive: (roomId: string, eventId: string, threadRoot: string | null) => void;
668ab5c30531atagen2026-10-25 03:13 UTC19
668ab5c30531atagen2026-10-25 03:13 UTC20
  let page: MentionsPage | null = null;
668ab5c30531atagen2026-10-25 03:13 UTC21
  let entries: MentionEntry[] = [];
668ab5c30531atagen2026-10-25 03:13 UTC22
  let selected: MentionEntry | null = null;
668ab5c30531atagen2026-10-25 03:13 UTC23
  let saved: SavedMentionContext | null = null;
668ab5c30531atagen2026-10-25 03:13 UTC24
  let list: HTMLDivElement;
668ab5c30531atagen2026-10-25 03:13 UTC25
  let viewport: TimelineViewport | null = null;
668ab5c30531atagen2026-10-25 03:13 UTC26
  let observer: ResizeObserver | null = null;
668ab5c30531atagen2026-10-25 03:13 UTC27
  let error = '';
668ab5c30531atagen2026-10-25 03:13 UTC28
  let busy = false;
668ab5c30531atagen2026-10-25 03:13 UTC29
  let actionBusy = false;
668ab5c30531atagen2026-10-25 03:13 UTC30
  let loadingContext = false;
668ab5c30531atagen2026-10-25 03:13 UTC31
  let mounted = false;
668ab5c30531atagen2026-10-25 03:13 UTC32
  let alive = true;
668ab5c30531atagen2026-10-25 03:13 UTC33
  let generation = 0;
668ab5c30531atagen2026-10-25 03:13 UTC34
  let contextGeneration = 0;
668ab5c30531atagen2026-10-25 03:13 UTC35
  let depth = 1;
668ab5c30531atagen2026-10-25 03:13 UTC36
  let pendingRevision = 0;
668ab5c30531atagen2026-10-25 03:13 UTC37
  let refreshTimer: ReturnType<typeof setTimeout> | null = null;
668ab5c30531atagen2026-10-25 03:13 UTC38
  let renderedAccount = '';
668ab5c30531atagen2026-10-25 03:13 UTC39
  let selectedRevision = -1;
668ab5c30531atagen2026-10-25 03:13 UTC40
  let contextKey = '';
668ab5c30531atagen2026-10-25 03:13 UTC41
  let contextRequest: { key: string; revision: number } | null = null;
668ab5c30531atagen2026-10-25 03:13 UTC42
  let queuedContext: MentionEntry | null = null;
668ab5c30531atagen2026-10-25 03:13 UTC43
  let pendingExplicitOpen = false;
668ab5c30531atagen2026-10-25 03:13 UTC44
  let savedListTop = 0;
48af82777108atagen2026-10-26 10:41 UTC45
  let roomProfiles: RoomProfiles = {};
48af82777108atagen2026-10-26 10:41 UTC46
  let profileRequests = new Set<string>();
668ab5c30531atagen2026-10-25 03:13 UTC47
  $: unreadCount = $mentionsSnapshot?.boundary.account === self ? $mentionsSnapshot.unread_count : page?.unread_count ?? 0;
668ab5c30531atagen2026-10-25 03:13 UTC48
  $: joinedRooms = new Map($rooms.filter((room) => room.membership === 'Joined').map((room) => [room.room_id, room]));
668ab5c30531atagen2026-10-25 03:13 UTC49
  $: selectedRoom = selected ? joinedRooms.get(selected.key.room_id) : undefined;
668ab5c30531atagen2026-10-25 03:13 UTC50
  $: contextCanPaginate = !!selectedRoom;
48af82777108atagen2026-10-26 10:41 UTC51
  $: requestProfiles([
48af82777108atagen2026-10-26 10:41 UTC52
    ...entries.map((entry) => ({ roomId: entry.key.room_id, item: entry.event })),
48af82777108atagen2026-10-26 10:41 UTC53
    ...(selected && saved?.context ? [...saved.context.events_before, saved.context.event, ...saved.context.events_after].map((item) => ({ roomId: selected!.key.room_id, item })) : [])
48af82777108atagen2026-10-26 10:41 UTC54
  ], joinedRooms);
668ab5c30531atagen2026-10-25 03:13 UTC55
668ab5c30531atagen2026-10-25 03:13 UTC56
  function itemEvent(item: TimelineItem) {
668ab5c30531atagen2026-10-25 03:13 UTC57
    return item.kind === 'event' ? item.event : null;
668ab5c30531atagen2026-10-25 03:13 UTC58
  }
668ab5c30531atagen2026-10-25 03:13 UTC59
  function roomName(entry: MentionEntry): string {
668ab5c30531atagen2026-10-25 03:13 UTC60
    return entry.room_name || joinedRooms.get(entry.key.room_id)?.name || entry.key.room_id;
668ab5c30531atagen2026-10-25 03:13 UTC61
  }
249e439c27e5atagen2026-10-25 13:26 UTC62
  const GAP_LABELS: Record<string, string> = {
249e439c27e5atagen2026-10-25 13:26 UTC63
    SyncDiscontinuity: 'Sync was interrupted',
249e439c27e5atagen2026-10-25 13:26 UTC64
    LimitedTimeline: 'Too many messages arrived at once',
249e439c27e5atagen2026-10-25 13:26 UTC65
    RoomLeft: 'You left the room',
249e439c27e5atagen2026-10-25 13:26 UTC66
    ContextUnavailable: 'Context could not be loaded',
249e439c27e5atagen2026-10-25 13:26 UTC67
    NotificationHistoryUnavailable: 'Notification history unavailable',
249e439c27e5atagen2026-10-25 13:26 UTC68
    CandidateExpired: 'A message could not be read',
249e439c27e5atagen2026-10-25 13:26 UTC69
    StoreReset: 'Inbox was reset',
249e439c27e5atagen2026-10-25 13:26 UTC70
    StoreFailed: 'Inbox could not be saved',
249e439c27e5atagen2026-10-25 13:26 UTC71
    HookTimedOut: 'Checking a message timed out'
249e439c27e5atagen2026-10-25 13:26 UTC72
  };
249e439c27e5atagen2026-10-25 13:26 UTC73
249e439c27e5atagen2026-10-25 13:26 UTC74
  function gapLabel(reason: string): string {
249e439c27e5atagen2026-10-25 13:26 UTC75
    return GAP_LABELS[reason] ?? reason;
249e439c27e5atagen2026-10-25 13:26 UTC76
  }
249e439c27e5atagen2026-10-25 13:26 UTC77
668ab5c30531atagen2026-10-25 03:13 UTC78
  function reasonLabel(reason: string): string {
a4555a7eae2datagen2026-10-25 09:18 UTC79
    return reason === 'Direct' ? 'Direct mention' : reason === 'Reply' ? 'Reply' : reason === 'Keyword' ? 'Keyword' : 'Room mention';
668ab5c30531atagen2026-10-25 03:13 UTC80
  }
668ab5c30531atagen2026-10-25 03:13 UTC81
  function captureLabel(state: MentionEntry['capture']): string {
668ab5c30531atagen2026-10-25 03:13 UTC82
    if (state.kind === 'Failed') return state.message;
668ab5c30531atagen2026-10-25 03:13 UTC83
    return state.kind === 'Ready' ? 'Context saved' : state.kind === 'Partial' ? 'Partial context saved' : state.kind === 'Pending' ? 'Saving context' : 'Context unavailable';
668ab5c30531atagen2026-10-25 03:13 UTC84
  }
668ab5c30531atagen2026-10-25 03:13 UTC85
  function keyOf(entry: MentionEntry): string { return mentionKey(entry.key); }
48af82777108atagen2026-10-26 10:41 UTC86
  function requestProfiles(items: { roomId: string; item: TimelineItem }[], joined: Map<string, unknown>): void {
48af82777108atagen2026-10-26 10:41 UTC87
    const account = self;
48af82777108atagen2026-10-26 10:41 UTC88
    const requests = profileRequests;
48af82777108atagen2026-10-26 10:41 UTC89
    for (const roomId of roomsLackingProfiles(items)) {
48af82777108atagen2026-10-26 10:41 UTC90
      if (requests.has(roomId) || !joined.has(roomId)) continue;
48af82777108atagen2026-10-26 10:41 UTC91
      requests.add(roomId);
48af82777108atagen2026-10-26 10:41 UTC92
      getMembers(roomId).then(({ members }) => {
48af82777108atagen2026-10-26 10:41 UTC93
        if (currentAccount(account) && requests === profileRequests) roomProfiles = { ...roomProfiles, [roomId]: Object.fromEntries(members.map((member) => [member.user_id, member])) };
48af82777108atagen2026-10-26 10:41 UTC94
      }, () => requests.delete(roomId));
48af82777108atagen2026-10-26 10:41 UTC95
    }
48af82777108atagen2026-10-26 10:41 UTC96
  }
668ab5c30531atagen2026-10-25 03:13 UTC97
  function bindViewport(node: HTMLDivElement) {
668ab5c30531atagen2026-10-25 03:13 UTC98
    list = node;
668ab5c30531atagen2026-10-25 03:13 UTC99
    viewport?.releaseScroll();
668ab5c30531atagen2026-10-25 03:13 UTC100
    observer?.disconnect();
668ab5c30531atagen2026-10-25 03:13 UTC101
    node.scrollTop = savedListTop;
668ab5c30531atagen2026-10-25 03:13 UTC102
    viewport = new TimelineViewport(node, null);
668ab5c30531atagen2026-10-25 03:13 UTC103
    const content = node.querySelector<HTMLElement>('.entry-content');
668ab5c30531atagen2026-10-25 03:13 UTC104
    observer = new ResizeObserver(() => viewport?.restore());
668ab5c30531atagen2026-10-25 03:13 UTC105
    if (content) observer.observe(content);
668ab5c30531atagen2026-10-25 03:13 UTC106
    void tick().then(() => {
668ab5c30531atagen2026-10-25 03:13 UTC107
      if (!alive || selected || list !== node) return;
668ab5c30531atagen2026-10-25 03:13 UTC108
      node.scrollTop = savedListTop;
668ab5c30531atagen2026-10-25 03:13 UTC109
      viewport?.capture();
668ab5c30531atagen2026-10-25 03:13 UTC110
    });
668ab5c30531atagen2026-10-25 03:13 UTC111
    return {
668ab5c30531atagen2026-10-25 03:13 UTC112
      destroy() {
668ab5c30531atagen2026-10-25 03:13 UTC113
        if (!selected) savedListTop = node.scrollTop;
668ab5c30531atagen2026-10-25 03:13 UTC114
        observer?.disconnect();
668ab5c30531atagen2026-10-25 03:13 UTC115
        observer = null;
668ab5c30531atagen2026-10-25 03:13 UTC116
        viewport?.releaseScroll();
668ab5c30531atagen2026-10-25 03:13 UTC117
        viewport = null;
668ab5c30531atagen2026-10-25 03:13 UTC118
      }
668ab5c30531atagen2026-10-25 03:13 UTC119
    };
668ab5c30531atagen2026-10-25 03:13 UTC120
  }
668ab5c30531atagen2026-10-25 03:13 UTC121
  function openContext(entry: MentionEntry): void {
668ab5c30531atagen2026-10-25 03:13 UTC122
    if (!selected) savedListTop = list?.scrollTop ?? savedListTop;
668ab5c30531atagen2026-10-25 03:13 UTC123
    void loadContext(entry);
668ab5c30531atagen2026-10-25 03:13 UTC124
  }
668ab5c30531atagen2026-10-25 03:13 UTC125
  function currentAccount(account: string): boolean {
668ab5c30531atagen2026-10-25 03:13 UTC126
    return alive && account === self && get(sessionUser) === account;
668ab5c30531atagen2026-10-25 03:13 UTC127
  }
668ab5c30531atagen2026-10-25 03:13 UTC128
  function captureViewport(): void { viewport?.capture(); }
668ab5c30531atagen2026-10-25 03:13 UTC129
  async function restoreViewport(): Promise<void> {
668ab5c30531atagen2026-10-25 03:13 UTC130
    await tick();
668ab5c30531atagen2026-10-25 03:13 UTC131
    if (alive) viewport?.restore();
668ab5c30531atagen2026-10-25 03:13 UTC132
  }
668ab5c30531atagen2026-10-25 03:13 UTC133
  async function fetchPage(cursor: string | null): Promise<MentionsPage> {
668ab5c30531atagen2026-10-25 03:13 UTC134
    return mentionsPage(cursor, 50);
668ab5c30531atagen2026-10-25 03:13 UTC135
  }
668ab5c30531atagen2026-10-25 03:13 UTC136
  async function fetchRange(targetDepth: number, request: number, account: string): Promise<void> {
668ab5c30531atagen2026-10-25 03:13 UTC137
    busy = true;
668ab5c30531atagen2026-10-25 03:13 UTC138
    error = '';
668ab5c30531atagen2026-10-25 03:13 UTC139
    const anchorPage = page;
668ab5c30531atagen2026-10-25 03:13 UTC140
    const anchorRevision = anchorPage?.revision ?? 0;
668ab5c30531atagen2026-10-25 03:13 UTC141
    let succeeded = false;
668ab5c30531atagen2026-10-25 03:13 UTC142
    try {
668ab5c30531atagen2026-10-25 03:13 UTC143
      const first = await fetchPage(null);
668ab5c30531atagen2026-10-25 03:13 UTC144
      if (!currentAccount(account) || request !== generation || first.boundary.account !== account) return;
668ab5c30531atagen2026-10-25 03:13 UTC145
      const firstSnapshot = first;
668ab5c30531atagen2026-10-25 03:13 UTC146
      const gathered = [...first.entries];
668ab5c30531atagen2026-10-25 03:13 UTC147
      let loadedDepth = 1;
668ab5c30531atagen2026-10-25 03:13 UTC148
      let cursor = first.next_cursor;
668ab5c30531atagen2026-10-25 03:13 UTC149
      let last = first;
668ab5c30531atagen2026-10-25 03:13 UTC150
      for (let index = 1; index < targetDepth && cursor; index++) {
668ab5c30531atagen2026-10-25 03:13 UTC151
        last = await fetchPage(cursor);
668ab5c30531atagen2026-10-25 03:13 UTC152
        if (!currentAccount(account) || request !== generation || last.boundary.account !== account) return;
668ab5c30531atagen2026-10-25 03:13 UTC153
        gathered.push(...last.entries);
668ab5c30531atagen2026-10-25 03:13 UTC154
        loadedDepth++;
668ab5c30531atagen2026-10-25 03:13 UTC155
        cursor = last.next_cursor;
668ab5c30531atagen2026-10-25 03:13 UTC156
      }
668ab5c30531atagen2026-10-25 03:13 UTC157
      const mirror = get(mentionsSnapshot);
668ab5c30531atagen2026-10-25 03:13 UTC158
      if (mirror?.boundary.account === account && mirror.revision > firstSnapshot.revision) {
668ab5c30531atagen2026-10-25 03:13 UTC159
        pendingRevision = Math.max(pendingRevision, mirror.revision);
668ab5c30531atagen2026-10-25 03:13 UTC160
      }
668ab5c30531atagen2026-10-25 03:13 UTC161
      if (firstSnapshot.revision < anchorRevision) return;
668ab5c30531atagen2026-10-25 03:13 UTC162
      captureViewport();
668ab5c30531atagen2026-10-25 03:13 UTC163
      const unique = new Map<string, MentionEntry>();
668ab5c30531atagen2026-10-25 03:13 UTC164
      for (const entry of gathered) unique.set(keyOf(entry), entry);
668ab5c30531atagen2026-10-25 03:13 UTC165
      if (mirror?.boundary.account === account) {
668ab5c30531atagen2026-10-25 03:13 UTC166
        for (const update of mirror.entries) {
668ab5c30531atagen2026-10-25 03:13 UTC167
          const key = keyOf(update);
668ab5c30531atagen2026-10-25 03:13 UTC168
          const previous = unique.get(key);
668ab5c30531atagen2026-10-25 03:13 UTC169
          if (previous && update.revision >= previous.revision) unique.set(key, update);
668ab5c30531atagen2026-10-25 03:13 UTC170
        }
668ab5c30531atagen2026-10-25 03:13 UTC171
      }
668ab5c30531atagen2026-10-25 03:13 UTC172
      const replacement = [...unique.values()];
668ab5c30531atagen2026-10-25 03:13 UTC173
      page = { ...firstSnapshot, next_cursor: last.next_cursor, end_reached: last.end_reached, entries: replacement };
668ab5c30531atagen2026-10-25 03:13 UTC174
      entries = replacement;
668ab5c30531atagen2026-10-25 03:13 UTC175
      if (selected) {
668ab5c30531atagen2026-10-25 03:13 UTC176
        const latestSelected = replacement.find((entry) => keyOf(entry) === keyOf(selected!));
668ab5c30531atagen2026-10-25 03:13 UTC177
        if (latestSelected && latestSelected.revision > selected.revision) selected = latestSelected;
668ab5c30531atagen2026-10-25 03:13 UTC178
        void loadContext(selected, false);
668ab5c30531atagen2026-10-25 03:13 UTC179
      }
668ab5c30531atagen2026-10-25 03:13 UTC180
      publishMentionsPage(firstSnapshot);
668ab5c30531atagen2026-10-25 03:13 UTC181
      if (pendingRevision <= firstSnapshot.revision) pendingRevision = 0;
668ab5c30531atagen2026-10-25 03:13 UTC182
      depth = loadedDepth;
668ab5c30531atagen2026-10-25 03:13 UTC183
      succeeded = true;
668ab5c30531atagen2026-10-25 03:13 UTC184
      await restoreViewport();
668ab5c30531atagen2026-10-25 03:13 UTC185
    } catch (cause) {
668ab5c30531atagen2026-10-25 03:13 UTC186
      if (currentAccount(account) && request === generation) error = cause instanceof Error ? cause.message : 'Could not load mentions';
668ab5c30531atagen2026-10-25 03:13 UTC187
    } finally {
668ab5c30531atagen2026-10-25 03:13 UTC188
      if (currentAccount(account) && request === generation) {
668ab5c30531atagen2026-10-25 03:13 UTC189
        busy = false;
668ab5c30531atagen2026-10-25 03:13 UTC190
        if (succeeded && pendingRevision > (page?.revision ?? 0)) scheduleRefresh(pendingRevision);
668ab5c30531atagen2026-10-25 03:13 UTC191
      }
668ab5c30531atagen2026-10-25 03:13 UTC192
    }
668ab5c30531atagen2026-10-25 03:13 UTC193
  }
668ab5c30531atagen2026-10-25 03:13 UTC194
  function scheduleRefresh(revision: number): void {
668ab5c30531atagen2026-10-25 03:13 UTC195
    pendingRevision = Math.max(pendingRevision, revision);
668ab5c30531atagen2026-10-25 03:13 UTC196
    if (busy || !mounted) return;
668ab5c30531atagen2026-10-25 03:13 UTC197
    if (refreshTimer) clearTimeout(refreshTimer);
668ab5c30531atagen2026-10-25 03:13 UTC198
    refreshTimer = setTimeout(() => {
668ab5c30531atagen2026-10-25 03:13 UTC199
      refreshTimer = null;
668ab5c30531atagen2026-10-25 03:13 UTC200
      if (!alive || busy) return;
668ab5c30531atagen2026-10-25 03:13 UTC201
      const request = ++generation;
668ab5c30531atagen2026-10-25 03:13 UTC202
      void fetchRange(Math.max(1, depth), request, self);
668ab5c30531atagen2026-10-25 03:13 UTC203
    }, 200);
668ab5c30531atagen2026-10-25 03:13 UTC204
  }
668ab5c30531atagen2026-10-25 03:13 UTC205
  async function loadMore(): Promise<void> {
668ab5c30531atagen2026-10-25 03:13 UTC206
    if (!page?.next_cursor || page.end_reached || busy) return;
668ab5c30531atagen2026-10-25 03:13 UTC207
    const cursor = page.next_cursor;
668ab5c30531atagen2026-10-25 03:13 UTC208
    const request = ++generation;
668ab5c30531atagen2026-10-25 03:13 UTC209
    const account = self;
668ab5c30531atagen2026-10-25 03:13 UTC210
    busy = true;
668ab5c30531atagen2026-10-25 03:13 UTC211
    error = '';
668ab5c30531atagen2026-10-25 03:13 UTC212
    let succeeded = false;
668ab5c30531atagen2026-10-25 03:13 UTC213
    try {
668ab5c30531atagen2026-10-25 03:13 UTC214
      const next = await fetchPage(cursor);
668ab5c30531atagen2026-10-25 03:13 UTC215
      if (!currentAccount(account) || request !== generation || next.boundary.account !== account) return;
668ab5c30531atagen2026-10-25 03:13 UTC216
      const current = page;
668ab5c30531atagen2026-10-25 03:13 UTC217
      if (!current || next.revision < current.revision) return;
668ab5c30531atagen2026-10-25 03:13 UTC218
      captureViewport();
668ab5c30531atagen2026-10-25 03:13 UTC219
      const unique = new Map(entries.map((entry) => [keyOf(entry), entry]));
668ab5c30531atagen2026-10-25 03:13 UTC220
      for (const entry of next.entries) unique.set(keyOf(entry), entry);
668ab5c30531atagen2026-10-25 03:13 UTC221
      page = { ...current, next_cursor: next.next_cursor, end_reached: next.end_reached, entries: [...unique.values()] };
668ab5c30531atagen2026-10-25 03:13 UTC222
      entries = page.entries;
668ab5c30531atagen2026-10-25 03:13 UTC223
      const mirror = get(mentionsSnapshot);
668ab5c30531atagen2026-10-25 03:13 UTC224
      if (mirror?.boundary.account === account) {
668ab5c30531atagen2026-10-25 03:13 UTC225
        for (const update of mirror.entries) {
668ab5c30531atagen2026-10-25 03:13 UTC226
          const key = keyOf(update);
668ab5c30531atagen2026-10-25 03:13 UTC227
          const previous = unique.get(key);
668ab5c30531atagen2026-10-25 03:13 UTC228
          if (previous && update.revision >= previous.revision) unique.set(key, update);
668ab5c30531atagen2026-10-25 03:13 UTC229
        }
668ab5c30531atagen2026-10-25 03:13 UTC230
        entries = [...unique.values()];
668ab5c30531atagen2026-10-25 03:13 UTC231
        page = { ...page, entries };
668ab5c30531atagen2026-10-25 03:13 UTC232
      }
668ab5c30531atagen2026-10-25 03:13 UTC233
      depth++;
668ab5c30531atagen2026-10-25 03:13 UTC234
      succeeded = true;
668ab5c30531atagen2026-10-25 03:13 UTC235
      await restoreViewport();
668ab5c30531atagen2026-10-25 03:13 UTC236
    } catch (cause) {
668ab5c30531atagen2026-10-25 03:13 UTC237
      if (currentAccount(account) && request === generation) error = cause instanceof Error ? cause.message : 'Could not load mentions';
668ab5c30531atagen2026-10-25 03:13 UTC238
    } finally {
668ab5c30531atagen2026-10-25 03:13 UTC239
      if (currentAccount(account) && request === generation) {
668ab5c30531atagen2026-10-25 03:13 UTC240
        busy = false;
668ab5c30531atagen2026-10-25 03:13 UTC241
        if (succeeded && pendingRevision > (page?.revision ?? 0)) scheduleRefresh(pendingRevision);
668ab5c30531atagen2026-10-25 03:13 UTC242
      }
668ab5c30531atagen2026-10-25 03:13 UTC243
    }
668ab5c30531atagen2026-10-25 03:13 UTC244
  }
668ab5c30531atagen2026-10-25 03:13 UTC245
  async function loadContext(entry: MentionEntry, explicit = true): Promise<void> {
668ab5c30531atagen2026-10-25 03:13 UTC246
    const id = keyOf(entry);
668ab5c30531atagen2026-10-25 03:13 UTC247
    if (explicit) pendingExplicitOpen = true;
668ab5c30531atagen2026-10-25 03:13 UTC248
    if (contextRequest?.key === id) {
668ab5c30531atagen2026-10-25 03:13 UTC249
      if (entry.revision > contextRequest.revision) queuedContext = entry;
668ab5c30531atagen2026-10-25 03:13 UTC250
      return;
668ab5c30531atagen2026-10-25 03:13 UTC251
    }
668ab5c30531atagen2026-10-25 03:13 UTC252
    if (contextKey !== id) {
668ab5c30531atagen2026-10-25 03:13 UTC253
      if (!selected) savedListTop = list?.scrollTop ?? savedListTop;
668ab5c30531atagen2026-10-25 03:13 UTC254
      saved = null;
668ab5c30531atagen2026-10-25 03:13 UTC255
      selectedRevision = -1;
668ab5c30531atagen2026-10-25 03:13 UTC256
    }
668ab5c30531atagen2026-10-25 03:13 UTC257
    contextKey = id;
668ab5c30531atagen2026-10-25 03:13 UTC258
    selectedRevision = entry.revision;
668ab5c30531atagen2026-10-25 03:13 UTC259
    selected = entry;
668ab5c30531atagen2026-10-25 03:13 UTC260
    loadingContext = true;
668ab5c30531atagen2026-10-25 03:13 UTC261
    error = '';
668ab5c30531atagen2026-10-25 03:13 UTC262
    const account = self;
668ab5c30531atagen2026-10-25 03:13 UTC263
    const request = ++contextGeneration;
668ab5c30531atagen2026-10-25 03:13 UTC264
    contextRequest = { key: id, revision: entry.revision };
668ab5c30531atagen2026-10-25 03:13 UTC265
    try {
668ab5c30531atagen2026-10-25 03:13 UTC266
      const result = await mentionContext(entry.key.room_id, entry.key.event_id);
668ab5c30531atagen2026-10-25 03:13 UTC267
      if (!currentAccount(account) || request !== contextGeneration || contextKey !== id || mentionKey(result.key) !== id) return;
668ab5c30531atagen2026-10-25 03:13 UTC268
      saved = result;
668ab5c30531atagen2026-10-25 03:13 UTC269
      if (pendingExplicitOpen) {
668ab5c30531atagen2026-10-25 03:13 UTC270
        pendingExplicitOpen = false;
668ab5c30531atagen2026-10-25 03:13 UTC271
        const current = entries.find((item) => keyOf(item) === id);
668ab5c30531atagen2026-10-25 03:13 UTC272
        if (current && !current.read) {
668ab5c30531atagen2026-10-25 03:13 UTC273
          await setMentionsRead([current.key], true);
668ab5c30531atagen2026-10-25 03:13 UTC274
          if (currentAccount(account) && request === contextGeneration) scheduleRefresh(get(mentionsSnapshot)?.revision ?? page?.revision ?? 0);
668ab5c30531atagen2026-10-25 03:13 UTC275
        }
668ab5c30531atagen2026-10-25 03:13 UTC276
      }
668ab5c30531atagen2026-10-25 03:13 UTC277
    } catch (cause) {
668ab5c30531atagen2026-10-25 03:13 UTC278
      if (currentAccount(account) && request === contextGeneration) {
668ab5c30531atagen2026-10-25 03:13 UTC279
        pendingExplicitOpen = false;
668ab5c30531atagen2026-10-25 03:13 UTC280
        if (cause instanceof ApiError && cause.status === 404) {
668ab5c30531atagen2026-10-25 03:13 UTC281
          backToList();
668ab5c30531atagen2026-10-25 03:13 UTC282
          error = 'This mention is no longer available';
668ab5c30531atagen2026-10-25 03:13 UTC283
          scheduleRefresh(page?.revision ?? 0);
668ab5c30531atagen2026-10-25 03:13 UTC284
        } else {
668ab5c30531atagen2026-10-25 03:13 UTC285
          error = cause instanceof Error ? cause.message : 'Could not load saved context';
668ab5c30531atagen2026-10-25 03:13 UTC286
        }
668ab5c30531atagen2026-10-25 03:13 UTC287
      }
668ab5c30531atagen2026-10-25 03:13 UTC288
    } finally {
668ab5c30531atagen2026-10-25 03:13 UTC289
      if (currentAccount(account) && request === contextGeneration) {
668ab5c30531atagen2026-10-25 03:13 UTC290
        contextRequest = null;
668ab5c30531atagen2026-10-25 03:13 UTC291
        loadingContext = false;
668ab5c30531atagen2026-10-25 03:13 UTC292
        const queued = queuedContext;
668ab5c30531atagen2026-10-25 03:13 UTC293
        queuedContext = null;
668ab5c30531atagen2026-10-25 03:13 UTC294
        if (queued && keyOf(queued) === id && queued.revision !== selectedRevision) void loadContext(queued, false);
668ab5c30531atagen2026-10-25 03:13 UTC295
      }
668ab5c30531atagen2026-10-25 03:13 UTC296
    }
668ab5c30531atagen2026-10-25 03:13 UTC297
  }
668ab5c30531atagen2026-10-25 03:13 UTC298
  async function setRead(entry: MentionEntry, read: boolean): Promise<void> {
668ab5c30531atagen2026-10-25 03:13 UTC299
    if (actionBusy) return;
668ab5c30531atagen2026-10-25 03:13 UTC300
    actionBusy = true;
668ab5c30531atagen2026-10-25 03:13 UTC301
    const account = self;
668ab5c30531atagen2026-10-25 03:13 UTC302
    try {
668ab5c30531atagen2026-10-25 03:13 UTC303
      await setMentionsRead([entry.key], read);
668ab5c30531atagen2026-10-25 03:13 UTC304
      if (!currentAccount(account)) return;
668ab5c30531atagen2026-10-25 03:13 UTC305
      const mirror = get(mentionsSnapshot);
668ab5c30531atagen2026-10-25 03:13 UTC306
      if (mirror?.boundary.account === account) scheduleRefresh(mirror.revision);
668ab5c30531atagen2026-10-25 03:13 UTC307
      else scheduleRefresh((page?.revision ?? 0) + 1);
668ab5c30531atagen2026-10-25 03:13 UTC308
    } catch (cause) {
668ab5c30531atagen2026-10-25 03:13 UTC309
      if (currentAccount(account)) error = cause instanceof Error ? cause.message : 'Could not update mention';
668ab5c30531atagen2026-10-25 03:13 UTC310
    } finally {
668ab5c30531atagen2026-10-25 03:13 UTC311
      if (currentAccount(account)) actionBusy = false;
668ab5c30531atagen2026-10-25 03:13 UTC312
    }
668ab5c30531atagen2026-10-25 03:13 UTC313
  }
668ab5c30531atagen2026-10-25 03:13 UTC314
  async function markAllRead(): Promise<void> {
668ab5c30531atagen2026-10-25 03:13 UTC315
    if (!page || actionBusy) return;
668ab5c30531atagen2026-10-25 03:13 UTC316
    actionBusy = true;
668ab5c30531atagen2026-10-25 03:13 UTC317
    const account = self;
668ab5c30531atagen2026-10-25 03:13 UTC318
    const boundary = page.boundary;
668ab5c30531atagen2026-10-25 03:13 UTC319
    try {
668ab5c30531atagen2026-10-25 03:13 UTC320
      await markMentionsReadThrough(boundary);
668ab5c30531atagen2026-10-25 03:13 UTC321
      if (!currentAccount(account)) return;
668ab5c30531atagen2026-10-25 03:13 UTC322
      const mirror = get(mentionsSnapshot);
668ab5c30531atagen2026-10-25 03:13 UTC323
      scheduleRefresh(mirror?.boundary.account === account ? mirror.revision : page?.revision ?? 0);
668ab5c30531atagen2026-10-25 03:13 UTC324
    } catch (cause) {
668ab5c30531atagen2026-10-25 03:13 UTC325
      if (currentAccount(account)) error = cause instanceof Error ? cause.message : 'Could not mark mentions read';
668ab5c30531atagen2026-10-25 03:13 UTC326
    } finally {
668ab5c30531atagen2026-10-25 03:13 UTC327
      if (currentAccount(account)) actionBusy = false;
668ab5c30531atagen2026-10-25 03:13 UTC328
    }
668ab5c30531atagen2026-10-25 03:13 UTC329
  }
668ab5c30531atagen2026-10-25 03:13 UTC330
  async function recover(): Promise<void> {
668ab5c30531atagen2026-10-25 03:13 UTC331
    if (actionBusy) return;
668ab5c30531atagen2026-10-25 03:13 UTC332
    actionBusy = true;
668ab5c30531atagen2026-10-25 03:13 UTC333
    const account = self;
668ab5c30531atagen2026-10-25 03:13 UTC334
    try {
668ab5c30531atagen2026-10-25 03:13 UTC335
      await recoverMentions();
668ab5c30531atagen2026-10-25 03:13 UTC336
      if (!currentAccount(account)) return;
668ab5c30531atagen2026-10-25 03:13 UTC337
      scheduleRefresh(get(mentionsSnapshot)?.revision ?? page?.revision ?? 0);
668ab5c30531atagen2026-10-25 03:13 UTC338
    } catch (cause) {
668ab5c30531atagen2026-10-25 03:13 UTC339
      if (currentAccount(account)) error = cause instanceof Error ? cause.message : 'Could not recover mention history';
668ab5c30531atagen2026-10-25 03:13 UTC340
    } finally {
668ab5c30531atagen2026-10-25 03:13 UTC341
      if (currentAccount(account)) actionBusy = false;
668ab5c30531atagen2026-10-25 03:13 UTC342
    }
668ab5c30531atagen2026-10-25 03:13 UTC343
  }
668ab5c30531atagen2026-10-25 03:13 UTC344
  async function retryCapture(): Promise<void> {
668ab5c30531atagen2026-10-25 03:13 UTC345
    if (!selected || !selectedRoom || actionBusy || loadingContext) return;
668ab5c30531atagen2026-10-25 03:13 UTC346
    actionBusy = true;
668ab5c30531atagen2026-10-25 03:13 UTC347
    const request = contextGeneration;
668ab5c30531atagen2026-10-25 03:13 UTC348
    const account = self;
668ab5c30531atagen2026-10-25 03:13 UTC349
    try {
668ab5c30531atagen2026-10-25 03:13 UTC350
      const result = await retryMentionContext(selected.key.room_id, selected.key.event_id);
668ab5c30531atagen2026-10-25 03:13 UTC351
      if (!currentAccount(account) || request !== contextGeneration || mentionKey(result.key) !== contextKey) return;
668ab5c30531atagen2026-10-25 03:13 UTC352
      saved = result;
668ab5c30531atagen2026-10-25 03:13 UTC353
      scheduleRefresh(get(mentionsSnapshot)?.revision ?? page?.revision ?? 0);
668ab5c30531atagen2026-10-25 03:13 UTC354
    } catch (cause) {
668ab5c30531atagen2026-10-25 03:13 UTC355
      if (currentAccount(account) && request === contextGeneration) error = cause instanceof Error ? cause.message : 'Could not retry context capture';
668ab5c30531atagen2026-10-25 03:13 UTC356
    } finally {
668ab5c30531atagen2026-10-25 03:13 UTC357
      if (currentAccount(account)) actionBusy = false;
668ab5c30531atagen2026-10-25 03:13 UTC358
    }
668ab5c30531atagen2026-10-25 03:13 UTC359
  }
668ab5c30531atagen2026-10-25 03:13 UTC360
  async function retryEntry(entry: MentionEntry): Promise<void> {
668ab5c30531atagen2026-10-25 03:13 UTC361
    if (selected?.revision !== entry.revision || keyOf(selected) !== keyOf(entry)) await loadContext(entry, false);
668ab5c30531atagen2026-10-25 03:13 UTC362
    else await tick();
668ab5c30531atagen2026-10-25 03:13 UTC363
    await retryCapture();
668ab5c30531atagen2026-10-25 03:13 UTC364
  }
668ab5c30531atagen2026-10-25 03:13 UTC365
  function backToList(): void {
668ab5c30531atagen2026-10-25 03:13 UTC366
    contextGeneration++;
668ab5c30531atagen2026-10-25 03:13 UTC367
    contextRequest = null;
668ab5c30531atagen2026-10-25 03:13 UTC368
    queuedContext = null;
668ab5c30531atagen2026-10-25 03:13 UTC369
    loadingContext = false;
668ab5c30531atagen2026-10-25 03:13 UTC370
    pendingExplicitOpen = false;
668ab5c30531atagen2026-10-25 03:13 UTC371
    selected = null;
668ab5c30531atagen2026-10-25 03:13 UTC372
    saved = null;
668ab5c30531atagen2026-10-25 03:13 UTC373
    contextKey = '';
668ab5c30531atagen2026-10-25 03:13 UTC374
  }
668ab5c30531atagen2026-10-25 03:13 UTC375
668ab5c30531atagen2026-10-25 03:13 UTC376
  const unsubscribe = mentionsSnapshot.subscribe((snapshot) => {
668ab5c30531atagen2026-10-25 03:13 UTC377
    if (!snapshot || snapshot.boundary.account !== self) return;
668ab5c30531atagen2026-10-25 03:13 UTC378
    if (snapshot.revision < (page?.revision ?? -1)) return;
668ab5c30531atagen2026-10-25 03:13 UTC379
    const updated = new Map(snapshot.entries.map((entry) => [keyOf(entry), entry]));
668ab5c30531atagen2026-10-25 03:13 UTC380
    if (entries.length) {
668ab5c30531atagen2026-10-25 03:13 UTC381
      captureViewport();
668ab5c30531atagen2026-10-25 03:13 UTC382
      entries = entries.map((entry) => {
668ab5c30531atagen2026-10-25 03:13 UTC383
        const update = updated.get(keyOf(entry));
668ab5c30531atagen2026-10-25 03:13 UTC384
        return update && update.revision >= entry.revision ? update : entry;
668ab5c30531atagen2026-10-25 03:13 UTC385
      });
668ab5c30531atagen2026-10-25 03:13 UTC386
      if (page) page = { ...page, entries };
668ab5c30531atagen2026-10-25 03:13 UTC387
      void restoreViewport();
668ab5c30531atagen2026-10-25 03:13 UTC388
    }
668ab5c30531atagen2026-10-25 03:13 UTC389
    if (selected) {
668ab5c30531atagen2026-10-25 03:13 UTC390
      const update = snapshot.entries.find((entry) => keyOf(entry) === keyOf(selected!));
668ab5c30531atagen2026-10-25 03:13 UTC391
      if (update && update.revision > selected.revision) selected = update;
668ab5c30531atagen2026-10-25 03:13 UTC392
    }
668ab5c30531atagen2026-10-25 03:13 UTC393
    if (busy) {
668ab5c30531atagen2026-10-25 03:13 UTC394
      pendingRevision = Math.max(pendingRevision, snapshot.revision);
668ab5c30531atagen2026-10-25 03:13 UTC395
      return;
668ab5c30531atagen2026-10-25 03:13 UTC396
    }
668ab5c30531atagen2026-10-25 03:13 UTC397
    if (!page || snapshot.revision > page.revision) scheduleRefresh(snapshot.revision);
668ab5c30531atagen2026-10-25 03:13 UTC398
  });
668ab5c30531atagen2026-10-25 03:13 UTC399
  $: if (self && self !== renderedAccount) {
668ab5c30531atagen2026-10-25 03:13 UTC400
    generation++;
668ab5c30531atagen2026-10-25 03:13 UTC401
    contextGeneration++;
668ab5c30531atagen2026-10-25 03:13 UTC402
    page = null;
668ab5c30531atagen2026-10-25 03:13 UTC403
    entries = [];
668ab5c30531atagen2026-10-25 03:13 UTC404
    error = '';
668ab5c30531atagen2026-10-25 03:13 UTC405
    selected = null;
668ab5c30531atagen2026-10-25 03:13 UTC406
    saved = null;
668ab5c30531atagen2026-10-25 03:13 UTC407
    depth = 1;
668ab5c30531atagen2026-10-25 03:13 UTC408
    busy = false;
668ab5c30531atagen2026-10-25 03:13 UTC409
    actionBusy = false;
668ab5c30531atagen2026-10-25 03:13 UTC410
    loadingContext = false;
668ab5c30531atagen2026-10-25 03:13 UTC411
    contextRequest = null;
668ab5c30531atagen2026-10-25 03:13 UTC412
    queuedContext = null;
668ab5c30531atagen2026-10-25 03:13 UTC413
    pendingExplicitOpen = false;
668ab5c30531atagen2026-10-25 03:13 UTC414
    pendingRevision = 0;
668ab5c30531atagen2026-10-25 03:13 UTC415
    savedListTop = 0;
48af82777108atagen2026-10-26 10:41 UTC416
    roomProfiles = {};
48af82777108atagen2026-10-26 10:41 UTC417
    profileRequests = new Set();
668ab5c30531atagen2026-10-25 03:13 UTC418
    if (refreshTimer) clearTimeout(refreshTimer);
668ab5c30531atagen2026-10-25 03:13 UTC419
    renderedAccount = self;
668ab5c30531atagen2026-10-25 03:13 UTC420
    if (mounted) void fetchRange(1, generation, self);
668ab5c30531atagen2026-10-25 03:13 UTC421
  }
668ab5c30531atagen2026-10-25 03:13 UTC422
  $: if (selected && selected.revision !== selectedRevision) void loadContext(selected, false);
668ab5c30531atagen2026-10-25 03:13 UTC423
  onMount(() => {
668ab5c30531atagen2026-10-25 03:13 UTC424
    mounted = true;
668ab5c30531atagen2026-10-25 03:13 UTC425
    if (!page) void fetchRange(1, ++generation, self);
668ab5c30531atagen2026-10-25 03:13 UTC426
  });
668ab5c30531atagen2026-10-25 03:13 UTC427
  onDestroy(() => {
668ab5c30531atagen2026-10-25 03:13 UTC428
    alive = false;
668ab5c30531atagen2026-10-25 03:13 UTC429
    generation++;
668ab5c30531atagen2026-10-25 03:13 UTC430
    contextGeneration++;
668ab5c30531atagen2026-10-25 03:13 UTC431
    unsubscribe();
668ab5c30531atagen2026-10-25 03:13 UTC432
    observer?.disconnect();
668ab5c30531atagen2026-10-25 03:13 UTC433
    viewport?.releaseScroll();
668ab5c30531atagen2026-10-25 03:13 UTC434
    if (refreshTimer) clearTimeout(refreshTimer);
668ab5c30531atagen2026-10-25 03:13 UTC435
  });
668ab5c30531atagen2026-10-25 03:13 UTC436
</script>
668ab5c30531atagen2026-10-25 03:13 UTC437
668ab5c30531atagen2026-10-25 03:13 UTC438
<section class="mentions-inbox">
668ab5c30531atagen2026-10-25 03:13 UTC439
  {#if selected}
668ab5c30531atagen2026-10-25 03:13 UTC440
    {#if error}<div class="error" role="alert"><EmojiText text={error} /><button disabled={loadingContext} onclick={() => void loadContext(selected!)}>Retry</button></div>{/if}
668ab5c30531atagen2026-10-25 03:13 UTC441
    {#if saved?.context}
48af82777108atagen2026-10-26 10:41 UTC442
      <ContextTimeline context={contextWithProfiles(saved.context, roomProfiles[selected.key.room_id])} roomId={selected.key.room_id} eventId={selected.key.event_id} roomName={roomName(selected)} {self} canPaginate={contextCanPaginate} onClose={backToList} {onLive} />
668ab5c30531atagen2026-10-25 03:13 UTC443
      {#if selected.capture.kind === 'Partial' || selected.capture.kind === 'Failed' || selected.capture.kind === 'Unavailable' || saved.state.kind === 'Partial' || saved.state.kind === 'Failed'}<button class="capture-retry" disabled={!selectedRoom || actionBusy} onclick={retryCapture}>Retry context capture</button>{/if}
668ab5c30531atagen2026-10-25 03:13 UTC444
      {#if !contextCanPaginate}<p class="status">This room is no longer joined. Saved context remains available.</p>{/if}
668ab5c30531atagen2026-10-25 03:13 UTC445
      {#if saved.state.kind === 'Partial'}<p class="status">This saved context is partial.</p>{/if}
668ab5c30531atagen2026-10-25 03:13 UTC446
      {#if saved.state.kind === 'Pending'}<p class="status">Saving context…</p>{/if}
668ab5c30531atagen2026-10-25 03:13 UTC447
      {#if saved.state.kind === 'Failed'}<p class="status" role="alert"><EmojiText text={saved.state.message} /></p>{/if}
668ab5c30531atagen2026-10-25 03:13 UTC448
    {:else}
668ab5c30531atagen2026-10-25 03:13 UTC449
      <header><button onclick={backToList}>Back to mentions</button><strong><EmojiText text={roomName(selected)} /></strong></header>
668ab5c30531atagen2026-10-25 03:13 UTC450
      {#if loadingContext}<p class="status">Loading saved context…</p>
668ab5c30531atagen2026-10-25 03:13 UTC451
      {:else if saved?.state.kind === 'Failed'}<p class="status" role="alert"><EmojiText text={saved.state.message} /></p>
668ab5c30531atagen2026-10-25 03:13 UTC452
      {:else if saved?.state.kind === 'Unavailable'}<p class="status">Saved context is unavailable.</p>
668ab5c30531atagen2026-10-25 03:13 UTC453
      {:else if saved?.state.kind === 'Partial'}<p class="status">Saved context is partial and currently unavailable.</p>
668ab5c30531atagen2026-10-25 03:13 UTC454
      {:else}<p class="status">Saved context is not available yet.</p>{/if}
48af82777108atagen2026-10-26 10:41 UTC455
      {@const preview = itemEvent(withProfiles(selected.event, roomProfiles[selected.key.room_id]))}
668ab5c30531atagen2026-10-25 03:13 UTC456
      {#if preview}<MessageRow itemId={preview.item_id} event={preview} showSender={true} reacting={false} onReact={() => {}} readOnly={true} showSent={false} {self} senderAvatar={preview.sender.avatar} />{/if}
668ab5c30531atagen2026-10-25 03:13 UTC457
      {#if selected.capture.kind === 'Failed' || selected.capture.kind === 'Partial' || selected.capture.kind === 'Unavailable'}
668ab5c30531atagen2026-10-25 03:13 UTC458
        <button disabled={!selectedRoom || actionBusy} onclick={retryCapture}>Retry context capture</button>
668ab5c30531atagen2026-10-25 03:13 UTC459
      {/if}
668ab5c30531atagen2026-10-25 03:13 UTC460
    {/if}
668ab5c30531atagen2026-10-25 03:13 UTC461
  {:else}
668ab5c30531atagen2026-10-25 03:13 UTC462
    <header><h2>Mentions {#if page}<span>({unreadCount} unread)</span>{/if}</h2><button disabled={!page || unreadCount === 0 || actionBusy} onclick={markAllRead}>Mark all read</button></header>
668ab5c30531atagen2026-10-25 03:13 UTC463
    <details class="coverage">
249e439c27e5atagen2026-10-25 13:26 UTC464
      <summary>{page?.coverage.recovering ? 'Recovering mention history…' : 'History coverage'}{#if page?.coverage.gaps.length} · Incomplete{/if}</summary>
668ab5c30531atagen2026-10-25 03:13 UTC465
      <div class="coverage-body">
668ab5c30531atagen2026-10-25 03:13 UTC466
        {#if page}
668ab5c30531atagen2026-10-25 03:13 UTC467
          {#if page.coverage.recovering}<p>History recovery is in progress.</p>{/if}
668ab5c30531atagen2026-10-25 03:13 UTC468
          {#if page.coverage.truncated}<p>Results are truncated.</p>{/if}
668ab5c30531atagen2026-10-25 03:13 UTC469
          {#if page.coverage.historical_supported === false}<p>Historical mention search is unsupported.</p>{/if}
668ab5c30531atagen2026-10-25 03:13 UTC470
          {#if page.coverage.unresolved_candidates}<p>{page.coverage.unresolved_candidates} possible mentions could not be resolved.</p>{/if}
668ab5c30531atagen2026-10-25 03:13 UTC471
          {#if page.coverage.gaps.length}<p>{page.coverage.gaps.length} history gaps remain.</p>{/if}
668ab5c30531atagen2026-10-25 03:13 UTC472
          {#if page.coverage.earliest_timestamp_ms}<p>Oldest observed event from {new Date(page.coverage.earliest_timestamp_ms).toLocaleString()}.</p>{/if}
249e439c27e5atagen2026-10-25 13:26 UTC473
          {#each page.coverage.gaps as gap}<p><EmojiText text={gap.room_id ? joinedRooms.get(gap.room_id)?.name ?? gap.room_id : 'Unknown room'} /> · {gapLabel(gap.reason)}</p>{/each}
668ab5c30531atagen2026-10-25 03:13 UTC474
        {:else}<p>Coverage is loading.</p>{/if}
668ab5c30531atagen2026-10-25 03:13 UTC475
        <button disabled={actionBusy || page?.coverage.recovering} onclick={recover}>Recover history</button>
668ab5c30531atagen2026-10-25 03:13 UTC476
      </div>
668ab5c30531atagen2026-10-25 03:13 UTC477
    </details>
668ab5c30531atagen2026-10-25 03:13 UTC478
    {#if error}<div class="error" role="alert"><EmojiText text={error} /><button disabled={busy} onclick={() => fetchRange(depth, ++generation, self)}>Retry</button></div>{/if}
668ab5c30531atagen2026-10-25 03:13 UTC479
    <div class="entries" use:bindViewport onscroll={() => { if (!viewport?.consumeScroll()) viewport?.capture(); }}>
668ab5c30531atagen2026-10-25 03:13 UTC480
      <div class="entry-content">
668ab5c30531atagen2026-10-25 03:13 UTC481
      {#each entries as entry (keyOf(entry))}
48af82777108atagen2026-10-26 10:41 UTC482
        {@const event = itemEvent(withProfiles(entry.event, roomProfiles[entry.key.room_id]))}
668ab5c30531atagen2026-10-25 03:13 UTC483
        <article class:unread={!entry.read}>
668ab5c30531atagen2026-10-25 03:13 UTC484
          <div class="entry-head"><button class="room" onclick={() => openContext(entry)}><EmojiText text={roomName(entry)} /></button><time>{new Date(entry.timestamp_ms).toLocaleString()}</time><span>{entry.read ? 'Read' : 'Unread'}</span></div>
668ab5c30531atagen2026-10-25 03:13 UTC485
          <div class="labels">{#each entry.reasons as reason}<span>{reasonLabel(reason)}</span>{/each}<span>{captureLabel(entry.capture)}</span>{#if entry.notification_suppressed}<span>Notifications suppressed</span>{/if}</div>
668ab5c30531atagen2026-10-25 03:13 UTC486
          {#if event}<MessageRow itemId={keyOf(entry)} {event} showSender={true} reacting={false} onReact={() => {}} readOnly={true} showSent={false} {self} senderAvatar={event.sender.avatar} />{:else}<p>Mention message unavailable</p>{/if}
668ab5c30531atagen2026-10-25 03:13 UTC487
          <div class="entry-actions"><button onclick={() => openContext(entry)}>Open saved context</button><button disabled={actionBusy} onclick={() => void setRead(entry, !entry.read)}>{entry.read ? 'Mark unread' : 'Mark read'}</button>{#if entry.capture.kind === 'Failed' || entry.capture.kind === 'Partial' || entry.capture.kind === 'Unavailable'}<button disabled={!joinedRooms.has(entry.key.room_id) || actionBusy} onclick={() => void retryEntry(entry)}>Retry capture</button>{/if}</div>
668ab5c30531atagen2026-10-25 03:13 UTC488
        </article>
668ab5c30531atagen2026-10-25 03:13 UTC489
      {/each}
668ab5c30531atagen2026-10-25 03:13 UTC490
      </div>
668ab5c30531atagen2026-10-25 03:13 UTC491
    </div>
668ab5c30531atagen2026-10-25 03:13 UTC492
    <footer><button disabled={!page?.next_cursor || page.end_reached || busy} onclick={loadMore}>{busy ? 'Loading…' : 'Load older mentions'}</button></footer>
668ab5c30531atagen2026-10-25 03:13 UTC493
    {#if entries.length === 0 && !busy && !error}<p class="status">No mentions yet.</p>{/if}
668ab5c30531atagen2026-10-25 03:13 UTC494
  {/if}
668ab5c30531atagen2026-10-25 03:13 UTC495
</section>
668ab5c30531atagen2026-10-25 03:13 UTC496
668ab5c30531atagen2026-10-25 03:13 UTC497
<style>
668ab5c30531atagen2026-10-25 03:13 UTC498
  .mentions-inbox { display:flex; flex-direction:column; min-width:0; min-height:0; height:100%; color:var(--text); }
668ab5c30531atagen2026-10-25 03:13 UTC499
  header { display:flex; align-items:center; gap:.6rem; padding:.6rem; border-bottom:1px solid var(--border); }
668ab5c30531atagen2026-10-25 03:13 UTC500
  header h2 { flex:1; margin:0; font-size:1rem; }
668ab5c30531atagen2026-10-25 03:13 UTC501
  header h2 span { color:var(--muted); font-size:.85em; }
668ab5c30531atagen2026-10-25 03:13 UTC502
  button { color:var(--text); background:var(--panel); border:1px solid var(--border); border-radius:.4rem; padding:.4rem .65rem; cursor:pointer; }
668ab5c30531atagen2026-10-25 03:13 UTC503
  button:disabled { opacity:.55; cursor:default; }
668ab5c30531atagen2026-10-25 03:13 UTC504
  .entries { flex:1; min-height:0; overflow:auto; overflow-anchor:none; padding:.5rem; }
668ab5c30531atagen2026-10-25 03:13 UTC505
  article { border:1px solid var(--border); border-radius:.6rem; padding:.5rem; margin-bottom:.6rem; }
668ab5c30531atagen2026-10-25 03:13 UTC506
  article.unread { border-color:var(--accent); }
668ab5c30531atagen2026-10-25 03:13 UTC507
  .entry-head,.entry-actions,.labels { display:flex; align-items:center; gap:.5rem; flex-wrap:wrap; }
20fe182f3204atagen2026-10-26 05:50 UTC508
  .entry-head .room { font-weight:600; text-align:left; }
668ab5c30531atagen2026-10-25 03:13 UTC509
  .entry-head time,.entry-head span { color:var(--muted); font-size:.8rem; }
668ab5c30531atagen2026-10-25 03:13 UTC510
  .entry-head time { margin-left:auto; }
668ab5c30531atagen2026-10-25 03:13 UTC511
  .labels { margin:.35rem 0; color:var(--muted); font-size:.78rem; }
668ab5c30531atagen2026-10-25 03:13 UTC512
  .labels span { border:1px solid var(--border); border-radius:1rem; padding:.1rem .4rem; }
668ab5c30531atagen2026-10-25 03:13 UTC513
  .entry-actions { justify-content:flex-end; margin-top:.4rem; }
668ab5c30531atagen2026-10-25 03:13 UTC514
  footer { display:flex; justify-content:center; padding:.45rem; }
668ab5c30531atagen2026-10-25 03:13 UTC515
  .coverage { padding:.55rem .75rem; background:var(--panel); border-bottom:1px solid var(--border); }
668ab5c30531atagen2026-10-25 03:13 UTC516
  .coverage-body { max-height:min(15rem, 35vh); overflow:auto; }
668ab5c30531atagen2026-10-25 03:13 UTC517
  .coverage p,.status { margin:.25rem 0; color:var(--muted); }
668ab5c30531atagen2026-10-25 03:13 UTC518
  .error { display:flex; align-items:center; gap:.5rem; padding:.5rem; color:var(--danger); }
668ab5c30531atagen2026-10-25 03:13 UTC519
  .capture-retry { align-self:flex-start; margin:.4rem; }
668ab5c30531atagen2026-10-25 03:13 UTC520
</style>