f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 1 | <script lang="ts"> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 2 | import EmojiText from './EmojiText.svelte'; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 3 | import MessageRow from './MessageRow.svelte'; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 4 | import { formatDay } from './format'; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 5 | import { expand, motionActive, rowIn, rowOut, setRowMotion } from './motion'; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 6 | import type { PendingMessage } from './pending'; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 7 | import { buildGroups, type Group, lastOwnSent, lastSenderIs, pendingToEvent } from './timeline-groups'; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 8 | import type { ImagePack, LinkPreview, Person, QuickReaction, RecentHistoryState, ReplyReference, TimelineEvent, TimelineItem } from './types'; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 9 | |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 10 | export let items: TimelineItem[]; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 11 | export let pending: PendingMessage[] = []; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 12 | export let joined: boolean; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 13 | export let self: string | null; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 14 | export let selfName: string | null = null; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 15 | export let people: Record<string, Person> = {}; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 16 | export let senderAvatarFor: (userId: string) => string | null; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 17 | export let reacting: boolean; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 18 | export let quickReactions: QuickReaction[] = []; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 19 | export let packs: ImagePack[] = []; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 20 | export let linkPreviews: Record<string, LinkPreview | null> = {}; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 21 | export let loadPreview: ((url: string) => void) | null = null; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 22 | export let editingEventId: string | null = null; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 23 | export let recentState: RecentHistoryState | undefined = undefined; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 24 | export let recentRetrying = false; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 25 | export let paginating = false; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 26 | export let fillingGap: string | null = null; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 27 | export let following = true; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 28 | export let onReact: (eventId: string, key: string, shortcode?: string) => void; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 29 | export let onQuickChange: (list: QuickReaction[]) => void; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 30 | export let onUser: (userId: string, x: number, y: number, menu: boolean) => void; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 31 | export let onCloneSticker: (eventId: string, body: string, x: number, y: number) => void; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 32 | export let onMore: (event: TimelineEvent, x: number, y: number) => void; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 33 | export let onOpenThread: (rootEventId: string) => void; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 34 | export let onReply: (target: ReplyReference) => void; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 35 | export let onJump: (eventId: string) => void; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 36 | export let onEdit: (eventId: string, body: string) => void; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 37 | export let onPaginate: () => void; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 38 | export let onGrew: () => void; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 39 | export let onFillGap: (gapId: string) => void; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 40 | export let onRetryRecent: () => void; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 41 | export let onRetryPending: (id: string) => void; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 42 | export let onDiscardPending: (id: string) => void; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 43 | |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 44 | const INITIAL_GROUPS = 150; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 45 | const READ_CAP = 400; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 46 | const READ_CONTEXT = 20; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 47 | const STEP = 30; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 48 | const MAX_GROUPS = 300; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 49 | const JUMP_HALF = 60; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 50 | const LIVE_MAX = 8; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 51 | |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 52 | let ready = false; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 53 | let start = 0; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 54 | let end = 0; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 55 | let tail = true; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 56 | let startKey: string | null = null; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 57 | let endKey: string | null = null; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 58 | let quiet = false; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 59 | let mounted = false; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 60 | let shownKeys: string[] = []; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 61 | let shownPending = new Map<string, PendingMessage['status']>(); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 62 | let firstKey: string | undefined; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 63 | |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 64 | function eventKey(list: Group[], from: number, step: 1 | -1): string | null { |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 65 | for (let i = from; i >= 0 && i < list.length; i += step) if (list[i].item.kind === 'event') return list[i].key; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 66 | return null; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 67 | } |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 68 | |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 69 | function settle(list: Group[]): void { |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 70 | startKey = eventKey(list, start, 1); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 71 | endKey = eventKey(list, end - 1, -1); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 72 | tail = end >= list.length; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 73 | } |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 74 | |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 75 | function reconcile(list: Group[], follow: boolean): void { |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 76 | if (!ready) { |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 77 | if (list.length === 0) return; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 78 | start = Math.max(0, list.length - INITIAL_GROUPS); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 79 | const marker = list.findIndex((g) => g.item.kind === 'readMarker'); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 80 | if (marker >= 0 && marker < start) start = Math.max(0, marker - READ_CONTEXT, list.length - READ_CAP); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 81 | end = list.length; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 82 | ready = true; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 83 | settle(list); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 84 | return; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 85 | } |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 86 | const oldest = start === 0; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 87 | let s = startKey === null ? start : list.findIndex((g) => g.key === startKey); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 88 | if (s < 0) s = start; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 89 | if (oldest) s = 0; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 90 | let e = list.length; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 91 | if (!tail && !follow) { |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 92 | const at = endKey === null ? -1 : list.findIndex((g) => g.key === endKey); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 93 | e = at >= 0 ? at + 1 : Math.min(end, list.length); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 94 | } |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 95 | s = Math.min(s, e); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 96 | if (e - s > MAX_GROUPS) { |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 97 | if (follow && e === list.length) s = e - MAX_GROUPS; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 98 | else if (oldest) e = s + MAX_GROUPS; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 99 | else s = e - MAX_GROUPS; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 100 | } |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 101 | start = s; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 102 | end = e; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 103 | settle(list); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 104 | } |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 105 | |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 106 | export function atTail(): boolean { |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 107 | return tail; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 108 | } |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 109 | |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 110 | export function has(eventId: string): boolean { |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 111 | return grouped.some((g) => g.item.kind === 'event' && g.item.event.event_id === eventId); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 112 | } |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 113 | |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 114 | export function reveal(eventId: string): boolean { |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 115 | const at = grouped.findIndex((g) => g.item.kind === 'event' && g.item.event.event_id === eventId); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 116 | if (at < 0 || (at >= start && at < end)) return false; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 117 | quiet = true; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 118 | start = Math.max(0, at - JUMP_HALF); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 119 | end = Math.min(grouped.length, at + JUMP_HALF); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 120 | settle(grouped); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 121 | return true; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 122 | } |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 123 | |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 124 | export function resetToTail(): boolean { |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 125 | if (tail) return false; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 126 | quiet = true; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 127 | start = Math.max(0, grouped.length - INITIAL_GROUPS); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 128 | end = grouped.length; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 129 | settle(grouped); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 130 | return true; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 131 | } |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 132 | |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 133 | export function grow(direction: 'up' | 'down'): boolean { |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 134 | if (direction === 'up') { |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 135 | if (start === 0) return false; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 136 | quiet = true; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 137 | start = Math.max(0, start - STEP); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 138 | if (end - start > MAX_GROUPS) end = start + MAX_GROUPS; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 139 | } else { |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 140 | if (end >= grouped.length) return false; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 141 | quiet = true; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 142 | end = Math.min(grouped.length, end + STEP); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 143 | if (end - start > MAX_GROUPS) start = end - MAX_GROUPS; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 144 | } |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 145 | settle(grouped); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 146 | return true; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 147 | } |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 148 | |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 149 | function loadOlder(): void { |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 150 | if (grow('up')) onGrew(); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 151 | else onPaginate(); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 152 | } |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 153 | |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 154 | function trackMotion(vis: Group[], list: Group[], pend: PendingMessage[]): void { |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 155 | const keys = vis.map((g) => g.key); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 156 | const pendingNow = new Map<string, PendingMessage['status']>(pend.map((p) => [`pending-${p.id}`, p.status])); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 157 | const enter = new Set<string>(); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 158 | const leave = new Set<string>(); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 159 | const prepended = firstKey !== list[0]?.key; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 160 | if (mounted && !quiet && !prepended && shownKeys.length > 0) { |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 161 | const old = new Set(shownKeys); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 162 | const now = new Set(keys); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 163 | const listKeys = new Set(list.map((g) => g.key)); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 164 | const added = keys.filter((k) => !old.has(k)); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 165 | const gone = shownKeys.filter((k) => !now.has(k) && !listKeys.has(k)); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 166 | if (added.length <= LIVE_MAX && gone.length <= LIVE_MAX) { |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 167 | const byKey = new Map(vis.map((g) => [g.key, g.item] as const)); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 168 | const echoed = shownPending.size > 0 && !motionActive(); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 169 | const lastOld = keys.lastIndexOf(shownKeys[shownKeys.length - 1]); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 170 | for (const k of added) { |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 171 | const item = byKey.get(k); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 172 | const appended = lastOld >= 0 && keys.indexOf(k) > lastOld; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 173 | if ((appended || k === 'readMarker') && !(echoed && item?.kind === 'event' && item.event.own)) enter.add(k); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 174 | } |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 175 | for (const k of gone) leave.add(k); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 176 | } |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 177 | for (const id of pendingNow.keys()) if (!shownPending.has(id)) enter.add(id); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 178 | for (const [id, status] of shownPending) if (!pendingNow.has(id) && status === 'failed') leave.add(id); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 179 | } |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 180 | mounted = true; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 181 | quiet = false; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 182 | shownKeys = keys; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 183 | shownPending = pendingNow; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 184 | firstKey = list[0]?.key; |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 185 | setRowMotion(enter, leave); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 186 | } |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 187 | |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 188 | $: hasStart = items.some((item) => item.kind === 'start'); |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 189 | $: grouped = buildGroups(items); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 190 | $: reconcile(grouped, following); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 191 | $: visible = grouped.slice(start, end); |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 192 | $: trackMotion(visible, grouped, tail ? pending : []); |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 193 | $: lastSentId = lastOwnSent(items); |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 194 | $: pendingFollowsSelf = lastSenderIs(items, self); |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 195 | </script> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 196 | |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 197 | <div class="timeline-top"> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 198 | {#if recentState === 'Loading'} |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 199 | <p class="side-note" transition:expand>Loading recent history…</p> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 200 | {:else if recentState && typeof recentState === 'object'} |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 201 | <div class="history-gap" transition:expand> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 202 | <span>Recent history could not be loaded</span> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 203 | <button type="button" class="ghost-btn" disabled={recentRetrying} on:click={onRetryRecent}> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 204 | {recentRetrying ? 'Loading…' : 'Retry'} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 205 | </button> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 206 | </div> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 207 | {/if} |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 208 | {#if hasStart && start === 0} |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 209 | <p class="start-note" transition:expand>Beginning of history</p> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 210 | {:else} |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 211 | <button type="button" class="ghost-btn" transition:expand disabled={paginating || recentState === 'Loading'} on:click={loadOlder}> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 212 | {paginating ? 'Loading…' : 'Load older messages'} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 213 | </button> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 214 | {/if} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 215 | </div> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 216 | {#if grouped.length === 0} |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 217 | <p class="side-note timeline-empty" transition:expand>No messages yet</p> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 218 | {:else} |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 219 | {#each visible as g (g.key)} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 220 | {#if g.item.kind === 'day'} |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 221 | <div class="day-row" data-row={g.key} in:rowIn|global out:rowOut|global><span>{formatDay(g.item.timestamp_ms)}</span></div> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 222 | {:else if g.item.kind === 'readMarker'} |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 223 | <div class="new-row" data-row={g.key} in:rowIn|global out:rowOut|global><span>New</span></div> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 224 | {:else if g.item.kind === 'start'} |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 225 | <p class="start-note" data-row={g.key} in:rowIn|global out:rowOut|global>Beginning of history</p> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 226 | {:else if g.item.kind === 'historyGap'} |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 227 | {@const gap = g.item.gap} |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 228 | <div class="history-gap" data-row={g.key} in:rowIn|global out:rowOut|global> |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 229 | <span>{gap.failure ?? 'Some history is not loaded'}</span> |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 230 | <button type="button" class="ghost-btn" disabled={!!fillingGap || gap.lifecycle === 'Unavailable'} on:click={() => onFillGap(gap.id)}> |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 231 | {fillingGap === gap.id ? 'Loading…' : gap.lifecycle === 'Unavailable' ? 'History unavailable' : 'Fetch older history'} |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 232 | </button> |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 233 | </div> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 234 | {:else if g.item.kind === 'unknown'} |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 235 | <p class="unknown-note" data-row={g.key} in:rowIn|global out:rowOut|global>Unsupported event ({g.item.label})</p> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 236 | {:else} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 237 | <MessageRow |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 238 | itemId={g.key} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 239 | event={g.item.event} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 240 | showSender={g.showSender} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 241 | senderAvatar={senderAvatarFor(g.item.event.sender.user_id) ?? g.item.event.sender.avatar} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 242 | {reacting} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 243 | {onReact} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 244 | {quickReactions} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 245 | {packs} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 246 | {linkPreviews} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 247 | {loadPreview} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 248 | {onQuickChange} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 249 | {people} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 250 | {self} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 251 | {onUser} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 252 | {onCloneSticker} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 253 | {onMore} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 254 | onOpenThread={joined || g.item.event.thread_summary ? onOpenThread : null} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 255 | onReply={joined ? onReply : null} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 256 | {onJump} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 257 | onEdit={joined ? onEdit : null} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 258 | editing={editingEventId !== null && editingEventId === g.item.event.event_id} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 259 | showSent={g.item.event.item_id === lastSentId && pending.length === 0} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 260 | /> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 261 | {/if} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 262 | {/each} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 263 | {/if} |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 264 | {#each tail ? pending : [] as p, i (p.id)} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 265 | <MessageRow |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 266 | itemId={`pending-${p.id}`} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 267 | event={pendingToEvent(p, self, selfName)} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 268 | {onJump} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 269 | showSender={i === 0 && !pendingFollowsSelf} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 270 | senderAvatar={senderAvatarFor(self ?? '')} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 271 | {reacting} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 272 | {onReact} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 273 | {quickReactions} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 274 | {packs} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 275 | {linkPreviews} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 276 | {loadPreview} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 277 | {onQuickChange} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 278 | /> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 279 | {#if p.status === 'failed'} |
d07bca5da4ae | atagen | 2026-10-26 03:48 UTC | 280 | <div class="pending-actions" transition:expand> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 281 | <span class="pending-error" role="alert"><EmojiText text={p.error ?? ''} /></span> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 282 | <button type="button" class="ghost-btn" on:click={() => onRetryPending(p.id)}>Retry</button> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 283 | <button type="button" class="ghost-btn danger" on:click={() => onDiscardPending(p.id)}>Discard</button> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 284 | </div> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 285 | {/if} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 286 | {/each} |