668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 1 | <script lang="ts"> |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 2 | import { get } from 'svelte/store'; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 3 | import { onDestroy, onMount, tick } from 'svelte'; |
fe847416ebd8 | atagen | 2026-10-27 03:27 UTC | 4 | import { mentionContext, mentionsPage, recoverMentions, retryMentionContext, setMentionsRead, markMentionsReadThrough } from './api'; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 5 | import ContextTimeline from './ContextTimeline.svelte'; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 6 | import EmojiText from './EmojiText.svelte'; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 7 | import MessageRow from './MessageRow.svelte'; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 8 | import { mentionsSnapshot, publishMentionsPage, rooms, sessionUser } from './store'; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 9 | import { TimelineViewport } from './timeline-viewport'; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 10 | import { ApiError } from './transport'; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 11 | import type { TimelineItem } from './types'; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 12 | import { mentionKey } from './mentions-model'; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 13 | import type { MentionEntry, MentionsPage, SavedMentionContext } from './mentions-model'; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 14 | |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 15 | export let self: string; |
4f91f89d99fa | atagen | 2026-10-25 10:47 UTC | 16 | export let onLive: (roomId: string, eventId: string, threadRoot: string | null) => void; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 17 | |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 18 | let page: MentionsPage | null = null; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 19 | let entries: MentionEntry[] = []; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 20 | let selected: MentionEntry | null = null; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 21 | let saved: SavedMentionContext | null = null; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 22 | let list: HTMLDivElement; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 23 | let viewport: TimelineViewport | null = null; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 24 | let observer: ResizeObserver | null = null; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 25 | let error = ''; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 26 | let busy = false; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 27 | let actionBusy = false; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 28 | let loadingContext = false; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 29 | let mounted = false; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 30 | let alive = true; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 31 | let generation = 0; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 32 | let contextGeneration = 0; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 33 | let depth = 1; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 34 | let pendingRevision = 0; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 35 | let refreshTimer: ReturnType<typeof setTimeout> | null = null; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 36 | let renderedAccount = ''; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 37 | let selectedRevision = -1; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 38 | let contextKey = ''; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 39 | let contextRequest: { key: string; revision: number } | null = null; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 40 | let queuedContext: MentionEntry | null = null; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 41 | let pendingExplicitOpen = false; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 42 | let savedListTop = 0; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 43 | $: unreadCount = $mentionsSnapshot?.boundary.account === self ? $mentionsSnapshot.unread_count : page?.unread_count ?? 0; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 44 | $: joinedRooms = new Map($rooms.filter((room) => room.membership === 'Joined').map((room) => [room.room_id, room])); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 45 | $: selectedRoom = selected ? joinedRooms.get(selected.key.room_id) : undefined; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 46 | $: contextCanPaginate = !!selectedRoom; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 47 | |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 48 | function itemEvent(item: TimelineItem) { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 49 | return item.kind === 'event' ? item.event : null; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 50 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 51 | function roomName(entry: MentionEntry): string { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 52 | return entry.room_name || joinedRooms.get(entry.key.room_id)?.name || entry.key.room_id; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 53 | } |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 54 | const GAP_LABELS: Record<string, string> = { |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 55 | SyncDiscontinuity: 'Sync was interrupted', |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 56 | LimitedTimeline: 'Too many messages arrived at once', |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 57 | RoomLeft: 'You left the room', |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 58 | ContextUnavailable: 'Context could not be loaded', |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 59 | NotificationHistoryUnavailable: 'Notification history unavailable', |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 60 | CandidateExpired: 'A message could not be read', |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 61 | StoreReset: 'Inbox was reset', |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 62 | StoreFailed: 'Inbox could not be saved', |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 63 | HookTimedOut: 'Checking a message timed out' |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 64 | }; |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 65 | |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 66 | function gapLabel(reason: string): string { |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 67 | return GAP_LABELS[reason] ?? reason; |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 68 | } |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 69 | |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 70 | function reasonLabel(reason: string): string { |
a4555a7eae2d | atagen | 2026-10-25 09:18 UTC | 71 | return reason === 'Direct' ? 'Direct mention' : reason === 'Reply' ? 'Reply' : reason === 'Keyword' ? 'Keyword' : 'Room mention'; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 72 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 73 | function captureLabel(state: MentionEntry['capture']): string { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 74 | if (state.kind === 'Failed') return state.message; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 75 | return state.kind === 'Ready' ? 'Context saved' : state.kind === 'Partial' ? 'Partial context saved' : state.kind === 'Pending' ? 'Saving context' : 'Context unavailable'; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 76 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 77 | function keyOf(entry: MentionEntry): string { return mentionKey(entry.key); } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 78 | function bindViewport(node: HTMLDivElement) { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 79 | list = node; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 80 | viewport?.releaseScroll(); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 81 | observer?.disconnect(); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 82 | node.scrollTop = savedListTop; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 83 | viewport = new TimelineViewport(node, null); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 84 | const content = node.querySelector<HTMLElement>('.entry-content'); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 85 | observer = new ResizeObserver(() => viewport?.restore()); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 86 | if (content) observer.observe(content); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 87 | void tick().then(() => { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 88 | if (!alive || selected || list !== node) return; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 89 | node.scrollTop = savedListTop; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 90 | viewport?.capture(); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 91 | }); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 92 | return { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 93 | destroy() { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 94 | if (!selected) savedListTop = node.scrollTop; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 95 | observer?.disconnect(); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 96 | observer = null; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 97 | viewport?.releaseScroll(); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 98 | viewport = null; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 99 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 100 | }; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 101 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 102 | function openContext(entry: MentionEntry): void { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 103 | if (!selected) savedListTop = list?.scrollTop ?? savedListTop; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 104 | void loadContext(entry); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 105 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 106 | function currentAccount(account: string): boolean { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 107 | return alive && account === self && get(sessionUser) === account; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 108 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 109 | function captureViewport(): void { viewport?.capture(); } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 110 | async function restoreViewport(): Promise<void> { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 111 | await tick(); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 112 | if (alive) viewport?.restore(); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 113 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 114 | async function fetchPage(cursor: string | null): Promise<MentionsPage> { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 115 | return mentionsPage(cursor, 50); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 116 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 117 | async function fetchRange(targetDepth: number, request: number, account: string): Promise<void> { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 118 | busy = true; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 119 | error = ''; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 120 | const anchorPage = page; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 121 | const anchorRevision = anchorPage?.revision ?? 0; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 122 | let succeeded = false; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 123 | try { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 124 | const first = await fetchPage(null); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 125 | if (!currentAccount(account) || request !== generation || first.boundary.account !== account) return; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 126 | const firstSnapshot = first; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 127 | const gathered = [...first.entries]; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 128 | let loadedDepth = 1; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 129 | let cursor = first.next_cursor; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 130 | let last = first; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 131 | for (let index = 1; index < targetDepth && cursor; index++) { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 132 | last = await fetchPage(cursor); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 133 | if (!currentAccount(account) || request !== generation || last.boundary.account !== account) return; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 134 | gathered.push(...last.entries); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 135 | loadedDepth++; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 136 | cursor = last.next_cursor; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 137 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 138 | const mirror = get(mentionsSnapshot); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 139 | if (mirror?.boundary.account === account && mirror.revision > firstSnapshot.revision) { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 140 | pendingRevision = Math.max(pendingRevision, mirror.revision); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 141 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 142 | if (firstSnapshot.revision < anchorRevision) return; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 143 | captureViewport(); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 144 | const unique = new Map<string, MentionEntry>(); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 145 | for (const entry of gathered) unique.set(keyOf(entry), entry); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 146 | if (mirror?.boundary.account === account) { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 147 | for (const update of mirror.entries) { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 148 | const key = keyOf(update); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 149 | const previous = unique.get(key); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 150 | if (previous && update.revision >= previous.revision) unique.set(key, update); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 151 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 152 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 153 | const replacement = [...unique.values()]; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 154 | page = { ...firstSnapshot, next_cursor: last.next_cursor, end_reached: last.end_reached, entries: replacement }; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 155 | entries = replacement; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 156 | if (selected) { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 157 | const latestSelected = replacement.find((entry) => keyOf(entry) === keyOf(selected!)); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 158 | if (latestSelected && latestSelected.revision > selected.revision) selected = latestSelected; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 159 | void loadContext(selected, false); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 160 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 161 | publishMentionsPage(firstSnapshot); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 162 | if (pendingRevision <= firstSnapshot.revision) pendingRevision = 0; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 163 | depth = loadedDepth; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 164 | succeeded = true; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 165 | await restoreViewport(); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 166 | } catch (cause) { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 167 | if (currentAccount(account) && request === generation) error = cause instanceof Error ? cause.message : 'Could not load mentions'; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 168 | } finally { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 169 | if (currentAccount(account) && request === generation) { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 170 | busy = false; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 171 | if (succeeded && pendingRevision > (page?.revision ?? 0)) scheduleRefresh(pendingRevision); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 172 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 173 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 174 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 175 | function scheduleRefresh(revision: number): void { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 176 | pendingRevision = Math.max(pendingRevision, revision); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 177 | if (busy || !mounted) return; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 178 | if (refreshTimer) clearTimeout(refreshTimer); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 179 | refreshTimer = setTimeout(() => { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 180 | refreshTimer = null; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 181 | if (!alive || busy) return; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 182 | const request = ++generation; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 183 | void fetchRange(Math.max(1, depth), request, self); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 184 | }, 200); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 185 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 186 | async function loadMore(): Promise<void> { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 187 | if (!page?.next_cursor || page.end_reached || busy) return; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 188 | const cursor = page.next_cursor; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 189 | const request = ++generation; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 190 | const account = self; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 191 | busy = true; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 192 | error = ''; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 193 | let succeeded = false; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 194 | try { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 195 | const next = await fetchPage(cursor); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 196 | if (!currentAccount(account) || request !== generation || next.boundary.account !== account) return; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 197 | const current = page; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 198 | if (!current || next.revision < current.revision) return; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 199 | captureViewport(); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 200 | const unique = new Map(entries.map((entry) => [keyOf(entry), entry])); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 201 | for (const entry of next.entries) unique.set(keyOf(entry), entry); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 202 | page = { ...current, next_cursor: next.next_cursor, end_reached: next.end_reached, entries: [...unique.values()] }; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 203 | entries = page.entries; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 204 | const mirror = get(mentionsSnapshot); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 205 | if (mirror?.boundary.account === account) { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 206 | for (const update of mirror.entries) { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 207 | const key = keyOf(update); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 208 | const previous = unique.get(key); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 209 | if (previous && update.revision >= previous.revision) unique.set(key, update); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 210 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 211 | entries = [...unique.values()]; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 212 | page = { ...page, entries }; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 213 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 214 | depth++; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 215 | succeeded = true; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 216 | await restoreViewport(); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 217 | } catch (cause) { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 218 | if (currentAccount(account) && request === generation) error = cause instanceof Error ? cause.message : 'Could not load mentions'; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 219 | } finally { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 220 | if (currentAccount(account) && request === generation) { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 221 | busy = false; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 222 | if (succeeded && pendingRevision > (page?.revision ?? 0)) scheduleRefresh(pendingRevision); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 223 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 224 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 225 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 226 | async function loadContext(entry: MentionEntry, explicit = true): Promise<void> { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 227 | const id = keyOf(entry); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 228 | if (explicit) pendingExplicitOpen = true; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 229 | if (contextRequest?.key === id) { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 230 | if (entry.revision > contextRequest.revision) queuedContext = entry; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 231 | return; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 232 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 233 | if (contextKey !== id) { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 234 | if (!selected) savedListTop = list?.scrollTop ?? savedListTop; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 235 | saved = null; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 236 | selectedRevision = -1; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 237 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 238 | contextKey = id; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 239 | selectedRevision = entry.revision; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 240 | selected = entry; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 241 | loadingContext = true; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 242 | error = ''; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 243 | const account = self; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 244 | const request = ++contextGeneration; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 245 | contextRequest = { key: id, revision: entry.revision }; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 246 | try { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 247 | const result = await mentionContext(entry.key.room_id, entry.key.event_id); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 248 | if (!currentAccount(account) || request !== contextGeneration || contextKey !== id || mentionKey(result.key) !== id) return; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 249 | saved = result; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 250 | if (pendingExplicitOpen) { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 251 | pendingExplicitOpen = false; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 252 | const current = entries.find((item) => keyOf(item) === id); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 253 | if (current && !current.read) { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 254 | await setMentionsRead([current.key], true); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 255 | if (currentAccount(account) && request === contextGeneration) scheduleRefresh(get(mentionsSnapshot)?.revision ?? page?.revision ?? 0); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 256 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 257 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 258 | } catch (cause) { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 259 | if (currentAccount(account) && request === contextGeneration) { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 260 | pendingExplicitOpen = false; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 261 | if (cause instanceof ApiError && cause.status === 404) { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 262 | backToList(); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 263 | error = 'This mention is no longer available'; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 264 | scheduleRefresh(page?.revision ?? 0); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 265 | } else { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 266 | error = cause instanceof Error ? cause.message : 'Could not load saved context'; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 267 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 268 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 269 | } finally { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 270 | if (currentAccount(account) && request === contextGeneration) { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 271 | contextRequest = null; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 272 | loadingContext = false; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 273 | const queued = queuedContext; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 274 | queuedContext = null; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 275 | if (queued && keyOf(queued) === id && queued.revision !== selectedRevision) void loadContext(queued, false); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 276 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 277 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 278 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 279 | async function setRead(entry: MentionEntry, read: boolean): Promise<void> { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 280 | if (actionBusy) return; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 281 | actionBusy = true; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 282 | const account = self; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 283 | try { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 284 | await setMentionsRead([entry.key], read); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 285 | if (!currentAccount(account)) return; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 286 | const mirror = get(mentionsSnapshot); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 287 | if (mirror?.boundary.account === account) scheduleRefresh(mirror.revision); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 288 | else scheduleRefresh((page?.revision ?? 0) + 1); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 289 | } catch (cause) { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 290 | if (currentAccount(account)) error = cause instanceof Error ? cause.message : 'Could not update mention'; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 291 | } finally { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 292 | if (currentAccount(account)) actionBusy = false; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 293 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 294 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 295 | async function markAllRead(): Promise<void> { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 296 | if (!page || actionBusy) return; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 297 | actionBusy = true; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 298 | const account = self; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 299 | const boundary = page.boundary; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 300 | try { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 301 | await markMentionsReadThrough(boundary); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 302 | if (!currentAccount(account)) return; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 303 | const mirror = get(mentionsSnapshot); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 304 | scheduleRefresh(mirror?.boundary.account === account ? mirror.revision : page?.revision ?? 0); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 305 | } catch (cause) { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 306 | if (currentAccount(account)) error = cause instanceof Error ? cause.message : 'Could not mark mentions read'; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 307 | } finally { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 308 | if (currentAccount(account)) actionBusy = false; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 309 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 310 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 311 | async function recover(): Promise<void> { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 312 | if (actionBusy) return; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 313 | actionBusy = true; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 314 | const account = self; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 315 | try { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 316 | await recoverMentions(); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 317 | if (!currentAccount(account)) return; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 318 | scheduleRefresh(get(mentionsSnapshot)?.revision ?? page?.revision ?? 0); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 319 | } catch (cause) { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 320 | if (currentAccount(account)) error = cause instanceof Error ? cause.message : 'Could not recover mention history'; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 321 | } finally { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 322 | if (currentAccount(account)) actionBusy = false; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 323 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 324 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 325 | async function retryCapture(): Promise<void> { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 326 | if (!selected || !selectedRoom || actionBusy || loadingContext) return; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 327 | actionBusy = true; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 328 | const request = contextGeneration; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 329 | const account = self; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 330 | try { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 331 | const result = await retryMentionContext(selected.key.room_id, selected.key.event_id); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 332 | if (!currentAccount(account) || request !== contextGeneration || mentionKey(result.key) !== contextKey) return; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 333 | saved = result; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 334 | scheduleRefresh(get(mentionsSnapshot)?.revision ?? page?.revision ?? 0); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 335 | } catch (cause) { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 336 | if (currentAccount(account) && request === contextGeneration) error = cause instanceof Error ? cause.message : 'Could not retry context capture'; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 337 | } finally { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 338 | if (currentAccount(account)) actionBusy = false; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 339 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 340 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 341 | async function retryEntry(entry: MentionEntry): Promise<void> { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 342 | if (selected?.revision !== entry.revision || keyOf(selected) !== keyOf(entry)) await loadContext(entry, false); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 343 | else await tick(); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 344 | await retryCapture(); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 345 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 346 | function backToList(): void { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 347 | contextGeneration++; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 348 | contextRequest = null; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 349 | queuedContext = null; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 350 | loadingContext = false; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 351 | pendingExplicitOpen = false; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 352 | selected = null; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 353 | saved = null; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 354 | contextKey = ''; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 355 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 356 | |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 357 | const unsubscribe = mentionsSnapshot.subscribe((snapshot) => { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 358 | if (!snapshot || snapshot.boundary.account !== self) return; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 359 | if (snapshot.revision < (page?.revision ?? -1)) return; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 360 | const updated = new Map(snapshot.entries.map((entry) => [keyOf(entry), entry])); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 361 | if (entries.length) { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 362 | captureViewport(); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 363 | entries = entries.map((entry) => { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 364 | const update = updated.get(keyOf(entry)); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 365 | return update && update.revision >= entry.revision ? update : entry; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 366 | }); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 367 | if (page) page = { ...page, entries }; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 368 | void restoreViewport(); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 369 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 370 | if (selected) { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 371 | const update = snapshot.entries.find((entry) => keyOf(entry) === keyOf(selected!)); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 372 | if (update && update.revision > selected.revision) selected = update; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 373 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 374 | if (busy) { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 375 | pendingRevision = Math.max(pendingRevision, snapshot.revision); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 376 | return; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 377 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 378 | if (!page || snapshot.revision > page.revision) scheduleRefresh(snapshot.revision); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 379 | }); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 380 | $: if (self && self !== renderedAccount) { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 381 | generation++; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 382 | contextGeneration++; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 383 | page = null; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 384 | entries = []; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 385 | error = ''; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 386 | selected = null; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 387 | saved = null; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 388 | depth = 1; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 389 | busy = false; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 390 | actionBusy = false; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 391 | loadingContext = false; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 392 | contextRequest = null; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 393 | queuedContext = null; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 394 | pendingExplicitOpen = false; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 395 | pendingRevision = 0; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 396 | savedListTop = 0; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 397 | if (refreshTimer) clearTimeout(refreshTimer); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 398 | renderedAccount = self; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 399 | if (mounted) void fetchRange(1, generation, self); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 400 | } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 401 | $: if (selected && selected.revision !== selectedRevision) void loadContext(selected, false); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 402 | onMount(() => { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 403 | mounted = true; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 404 | if (!page) void fetchRange(1, ++generation, self); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 405 | }); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 406 | onDestroy(() => { |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 407 | alive = false; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 408 | generation++; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 409 | contextGeneration++; |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 410 | unsubscribe(); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 411 | observer?.disconnect(); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 412 | viewport?.releaseScroll(); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 413 | if (refreshTimer) clearTimeout(refreshTimer); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 414 | }); |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 415 | </script> |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 416 | |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 417 | <section class="mentions-inbox"> |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 418 | {#if selected} |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 419 | {#if error}<div class="error" role="alert"><EmojiText text={error} /><button disabled={loadingContext} onclick={() => void loadContext(selected!)}>Retry</button></div>{/if} |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 420 | {#if saved?.context} |
fe847416ebd8 | atagen | 2026-10-27 03:27 UTC | 421 | <ContextTimeline context={saved.context} roomId={selected.key.room_id} eventId={selected.key.event_id} roomName={roomName(selected)} {self} canPaginate={contextCanPaginate} onClose={backToList} {onLive} /> |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 422 | {#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} |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 423 | {#if !contextCanPaginate}<p class="status">This room is no longer joined. Saved context remains available.</p>{/if} |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 424 | {#if saved.state.kind === 'Partial'}<p class="status">This saved context is partial.</p>{/if} |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 425 | {#if saved.state.kind === 'Pending'}<p class="status">Saving context…</p>{/if} |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 426 | {#if saved.state.kind === 'Failed'}<p class="status" role="alert"><EmojiText text={saved.state.message} /></p>{/if} |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 427 | {:else} |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 428 | <header><button onclick={backToList}>Back to mentions</button><strong><EmojiText text={roomName(selected)} /></strong></header> |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 429 | {#if loadingContext}<p class="status">Loading saved context…</p> |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 430 | {:else if saved?.state.kind === 'Failed'}<p class="status" role="alert"><EmojiText text={saved.state.message} /></p> |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 431 | {:else if saved?.state.kind === 'Unavailable'}<p class="status">Saved context is unavailable.</p> |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 432 | {:else if saved?.state.kind === 'Partial'}<p class="status">Saved context is partial and currently unavailable.</p> |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 433 | {:else}<p class="status">Saved context is not available yet.</p>{/if} |
fe847416ebd8 | atagen | 2026-10-27 03:27 UTC | 434 | {@const preview = itemEvent(selected.event)} |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 435 | {#if preview}<MessageRow itemId={preview.item_id} event={preview} showSender={true} reacting={false} onReact={() => {}} readOnly={true} showSent={false} {self} senderAvatar={preview.sender.avatar} />{/if} |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 436 | {#if selected.capture.kind === 'Failed' || selected.capture.kind === 'Partial' || selected.capture.kind === 'Unavailable'} |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 437 | <button disabled={!selectedRoom || actionBusy} onclick={retryCapture}>Retry context capture</button> |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 438 | {/if} |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 439 | {/if} |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 440 | {:else} |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 441 | <header><h2>Mentions {#if page}<span>({unreadCount} unread)</span>{/if}</h2><button disabled={!page || unreadCount === 0 || actionBusy} onclick={markAllRead}>Mark all read</button></header> |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 442 | <details class="coverage"> |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 443 | <summary>{page?.coverage.recovering ? 'Recovering mention history…' : 'History coverage'}{#if page?.coverage.gaps.length} · Incomplete{/if}</summary> |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 444 | <div class="coverage-body"> |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 445 | {#if page} |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 446 | {#if page.coverage.recovering}<p>History recovery is in progress.</p>{/if} |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 447 | {#if page.coverage.truncated}<p>Results are truncated.</p>{/if} |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 448 | {#if page.coverage.historical_supported === false}<p>Historical mention search is unsupported.</p>{/if} |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 449 | {#if page.coverage.unresolved_candidates}<p>{page.coverage.unresolved_candidates} possible mentions could not be resolved.</p>{/if} |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 450 | {#if page.coverage.gaps.length}<p>{page.coverage.gaps.length} history gaps remain.</p>{/if} |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 451 | {#if page.coverage.earliest_timestamp_ms}<p>Oldest observed event from {new Date(page.coverage.earliest_timestamp_ms).toLocaleString()}.</p>{/if} |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 452 | {#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} |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 453 | {:else}<p>Coverage is loading.</p>{/if} |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 454 | <button disabled={actionBusy || page?.coverage.recovering} onclick={recover}>Recover history</button> |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 455 | </div> |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 456 | </details> |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 457 | {#if error}<div class="error" role="alert"><EmojiText text={error} /><button disabled={busy} onclick={() => fetchRange(depth, ++generation, self)}>Retry</button></div>{/if} |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 458 | <div class="entries" use:bindViewport onscroll={() => { if (!viewport?.consumeScroll()) viewport?.capture(); }}> |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 459 | <div class="entry-content"> |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 460 | {#each entries as entry (keyOf(entry))} |
fe847416ebd8 | atagen | 2026-10-27 03:27 UTC | 461 | {@const event = itemEvent(entry.event)} |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 462 | <article class:unread={!entry.read}> |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 463 | <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> |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 464 | <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> |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 465 | {#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} |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 466 | <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> |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 467 | </article> |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 468 | {/each} |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 469 | </div> |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 470 | </div> |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 471 | <footer><button disabled={!page?.next_cursor || page.end_reached || busy} onclick={loadMore}>{busy ? 'Loading…' : 'Load older mentions'}</button></footer> |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 472 | {#if entries.length === 0 && !busy && !error}<p class="status">No mentions yet.</p>{/if} |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 473 | {/if} |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 474 | </section> |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 475 | |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 476 | <style> |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 477 | .mentions-inbox { display:flex; flex-direction:column; min-width:0; min-height:0; height:100%; color:var(--text); } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 478 | header { display:flex; align-items:center; gap:.6rem; padding:.6rem; border-bottom:1px solid var(--border); } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 479 | header h2 { flex:1; margin:0; font-size:1rem; } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 480 | header h2 span { color:var(--muted); font-size:.85em; } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 481 | button { color:var(--text); background:var(--panel); border:1px solid var(--border); border-radius:.4rem; padding:.4rem .65rem; cursor:pointer; } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 482 | button:disabled { opacity:.55; cursor:default; } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 483 | .entries { flex:1; min-height:0; overflow:auto; overflow-anchor:none; padding:.5rem; } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 484 | article { border:1px solid var(--border); border-radius:.6rem; padding:.5rem; margin-bottom:.6rem; } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 485 | article.unread { border-color:var(--accent); } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 486 | .entry-head,.entry-actions,.labels { display:flex; align-items:center; gap:.5rem; flex-wrap:wrap; } |
20fe182f3204 | atagen | 2026-10-26 05:50 UTC | 487 | .entry-head .room { font-weight:600; text-align:left; } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 488 | .entry-head time,.entry-head span { color:var(--muted); font-size:.8rem; } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 489 | .entry-head time { margin-left:auto; } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 490 | .labels { margin:.35rem 0; color:var(--muted); font-size:.78rem; } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 491 | .labels span { border:1px solid var(--border); border-radius:1rem; padding:.1rem .4rem; } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 492 | .entry-actions { justify-content:flex-end; margin-top:.4rem; } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 493 | footer { display:flex; justify-content:center; padding:.45rem; } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 494 | .coverage { padding:.55rem .75rem; background:var(--panel); border-bottom:1px solid var(--border); } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 495 | .coverage-body { max-height:min(15rem, 35vh); overflow:auto; } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 496 | .coverage p,.status { margin:.25rem 0; color:var(--muted); } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 497 | .error { display:flex; align-items:center; gap:.5rem; padding:.5rem; color:var(--danger); } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 498 | .capture-retry { align-self:flex-start; margin:.4rem; } |
668ab5c30531 | atagen | 2026-10-25 03:13 UTC | 499 | </style> |