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'; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 5 | import type { PendingMessage } from './pending'; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 6 | import { buildGroups, lastOwnSent, lastSenderIs, pendingToEvent } from './timeline-groups'; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 7 | import type { ImagePack, LinkPreview, Person, QuickReaction, RecentHistoryState, ReplyReference, TimelineEvent, TimelineItem } from './types'; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 8 | |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 9 | export let items: TimelineItem[]; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 10 | export let pending: PendingMessage[] = []; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 11 | export let joined: boolean; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 12 | export let self: string | null; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 13 | export let selfName: string | null = null; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 14 | export let people: Record<string, Person> = {}; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 15 | export let senderAvatarFor: (userId: string) => string | null; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 16 | export let reacting: boolean; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 17 | export let quickReactions: QuickReaction[] = []; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 18 | export let packs: ImagePack[] = []; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 19 | export let linkPreviews: Record<string, LinkPreview | null> = {}; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 20 | export let loadPreview: ((url: string) => void) | null = null; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 21 | export let editingEventId: string | null = null; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 22 | export let recentState: RecentHistoryState | undefined = undefined; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 23 | export let recentRetrying = false; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 24 | export let paginating = false; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 25 | export let fillingGap: string | null = null; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 26 | export let onReact: (eventId: string, key: string, shortcode?: string) => void; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 27 | export let onQuickChange: (list: QuickReaction[]) => void; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 28 | export let onUser: (userId: string, x: number, y: number, menu: boolean) => void; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 29 | export let onCloneSticker: (eventId: string, body: string, x: number, y: number) => void; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 30 | export let onMore: (event: TimelineEvent, x: number, y: number) => void; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 31 | export let onOpenThread: (rootEventId: string) => void; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 32 | export let onReply: (target: ReplyReference) => void; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 33 | export let onJump: (eventId: string) => void; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 34 | export let onEdit: (eventId: string, body: string) => void; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 35 | export let onPaginate: () => void; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 36 | export let onFillGap: (gapId: string) => void; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 37 | export let onRetryRecent: () => void; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 38 | export let onRetryPending: (id: string) => void; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 39 | export let onDiscardPending: (id: string) => void; |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 40 | |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 41 | $: hasStart = items.some((item) => item.kind === 'start'); |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 42 | $: grouped = buildGroups(items); |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 43 | $: lastSentId = lastOwnSent(items); |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 44 | $: pendingFollowsSelf = lastSenderIs(items, self); |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 45 | </script> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 46 | |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 47 | <div class="timeline-top"> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 48 | {#if recentState === 'Loading'} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 49 | <p class="side-note">Loading recent history…</p> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 50 | {:else if recentState && typeof recentState === 'object'} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 51 | <div class="history-gap"> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 52 | <span>Recent history could not be loaded</span> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 53 | <button type="button" class="ghost-btn" disabled={recentRetrying} on:click={onRetryRecent}> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 54 | {recentRetrying ? 'Loading…' : 'Retry'} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 55 | </button> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 56 | </div> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 57 | {/if} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 58 | {#if hasStart} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 59 | <p class="start-note">Beginning of history</p> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 60 | {:else} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 61 | <button type="button" class="ghost-btn" disabled={paginating || recentState === 'Loading'} on:click={onPaginate}> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 62 | {paginating ? 'Loading…' : 'Load older messages'} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 63 | </button> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 64 | {/if} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 65 | </div> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 66 | {#if grouped.length === 0} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 67 | <p class="side-note timeline-empty">No messages yet</p> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 68 | {:else} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 69 | {#each grouped as g (g.key)} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 70 | {#if g.item.kind === 'day'} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 71 | <div class="day-row"><span>{formatDay(g.item.timestamp_ms)}</span></div> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 72 | {:else if g.item.kind === 'readMarker'} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 73 | <div class="new-row"><span>New</span></div> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 74 | {:else if g.item.kind === 'start'} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 75 | <p class="start-note">Beginning of history</p> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 76 | {:else if g.item.kind === 'historyGap'} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 77 | {#if g.item.gap.continuity !== 'Verified'} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 78 | {@const gap = g.item.gap} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 79 | {@const filling = fillingGap === gap.id || gap.lifecycle === 'Fetching'} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 80 | <div class="history-gap"> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 81 | <span>{gap.failure ?? 'Some history is not loaded'}</span> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 82 | <button type="button" class="ghost-btn" disabled={!!fillingGap || gap.lifecycle === 'Fetching' || gap.lifecycle === 'Unavailable'} on:click={() => onFillGap(gap.id)}> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 83 | {filling ? 'Loading…' : gap.lifecycle === 'Unavailable' ? 'History unavailable' : 'Fetch older history'} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 84 | </button> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 85 | </div> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 86 | {/if} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 87 | {:else if g.item.kind === 'unknown'} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 88 | <p class="unknown-note">Unsupported event ({g.item.label})</p> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 89 | {:else} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 90 | <MessageRow |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 91 | itemId={g.key} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 92 | event={g.item.event} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 93 | showSender={g.showSender} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 94 | senderAvatar={senderAvatarFor(g.item.event.sender.user_id) ?? g.item.event.sender.avatar} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 95 | {reacting} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 96 | {onReact} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 97 | {quickReactions} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 98 | {packs} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 99 | {linkPreviews} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 100 | {loadPreview} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 101 | {onQuickChange} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 102 | {people} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 103 | {self} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 104 | {onUser} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 105 | {onCloneSticker} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 106 | {onMore} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 107 | onOpenThread={joined || g.item.event.thread_summary ? onOpenThread : null} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 108 | onReply={joined ? onReply : null} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 109 | {onJump} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 110 | onEdit={joined ? onEdit : null} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 111 | editing={editingEventId !== null && editingEventId === g.item.event.event_id} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 112 | showSent={g.item.event.item_id === lastSentId && pending.length === 0} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 113 | /> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 114 | {/if} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 115 | {/each} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 116 | {/if} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 117 | {#each pending as p, i (p.id)} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 118 | <MessageRow |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 119 | itemId={`pending-${p.id}`} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 120 | event={pendingToEvent(p, self, selfName)} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 121 | {onJump} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 122 | showSender={i === 0 && !pendingFollowsSelf} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 123 | senderAvatar={senderAvatarFor(self ?? '')} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 124 | {reacting} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 125 | {onReact} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 126 | {quickReactions} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 127 | {packs} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 128 | {linkPreviews} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 129 | {loadPreview} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 130 | {onQuickChange} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 131 | /> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 132 | {#if p.status === 'failed'} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 133 | <div class="pending-actions"> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 134 | <span class="pending-error" role="alert"><EmojiText text={p.error ?? ''} /></span> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 135 | <button type="button" class="ghost-btn" on:click={() => onRetryPending(p.id)}>Retry</button> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 136 | <button type="button" class="ghost-btn danger" on:click={() => onDiscardPending(p.id)}>Discard</button> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 137 | </div> |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 138 | {/if} |
f93c7e88ea05 | atagen | 2026-10-25 10:54 UTC | 139 | {/each} |