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