dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 1 | <script lang="ts" context="module"> |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 2 | import { sessionUser } from './store'; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 3 | import { releaseStaged as release } from './staging'; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 4 | import type { Mention } from './mentions'; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 5 | import type { StagedAttachment } from './staging'; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 6 | |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 7 | interface ThreadDraft { |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 8 | text: string; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 9 | picked: Mention[]; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 10 | staged: StagedAttachment[]; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 11 | } |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 12 | |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 13 | const drafts = new Map<string, ThreadDraft>(); |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 14 | let draftAccount: string | null | undefined; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 15 | sessionUser.subscribe((user) => { |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 16 | if (user === draftAccount) return; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 17 | draftAccount = user; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 18 | for (const saved of drafts.values()) release(saved.staged); |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 19 | drafts.clear(); |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 20 | }); |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 21 | </script> |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 22 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 23 | <script lang="ts"> |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 24 | import { tick } from 'svelte'; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 25 | import MessageRow from './MessageRow.svelte'; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 26 | import EmojiText from './EmojiText.svelte'; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 27 | import Composer from './Composer.svelte'; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 28 | import { editMessage, markRead, paginateRoom, reactToMessage, sendMessage, uploadAttachment } from './api'; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 29 | import { parseCommand, unescapeCommand } from './commands'; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 30 | import { customEmotes, resolveEmotes } from './emotes'; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 31 | import { mentionsFromHtml, resolveMentions } from './mentions'; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 32 | import { releaseStaged, stageFiles } from './staging'; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 33 | import { pushBanner, threadKey, threadTimelines, timelines } from './store'; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 34 | import { pinnedThreads, togglePinnedThread } from './threads'; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 35 | import type { ImagePack, LinkPreview, Person, QuickReaction, ReplyReference, TimelineEvent, TimelineItem } from './types'; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 36 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 37 | export let roomId: string; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 38 | export let rootEventId: string; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 39 | export let roomName: string; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 40 | export let joined: boolean; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 41 | export let self: string | null; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 42 | export let people: Record<string, Person> = {}; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 43 | export let senderAvatarFor: (userId: string) => string | null = () => null; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 44 | export let quickReactions: QuickReaction[] = []; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 45 | export let packs: ImagePack[] = []; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 46 | export let linkPreviews: Record<string, LinkPreview | null> = {}; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 47 | export let loadPreview: ((url: string) => void) | null = null; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 48 | export let onQuickChange: (list: QuickReaction[]) => void = () => {}; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 49 | export let onUser: ((userId: string, x: number, y: number, menu: boolean) => void) | null = null; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 50 | export let onMore: ((event: TimelineEvent, x: number, y: number) => void) | null = null; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 51 | export let onMentionStart: () => void = () => {}; |
4f91f89d99fa | atagen | 2026-10-25 10:47 UTC | 52 | export let focusEventId: string | null = null; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 53 | export let onClose: () => void; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 54 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 55 | const GROUP_GAP_MS = 5 * 60 * 1000; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 56 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 57 | let list: HTMLDivElement | null = null; |
4f91f89d99fa | atagen | 2026-10-25 10:47 UTC | 58 | let thread: HTMLDivElement | null = null; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 59 | let composerRef: Composer | null = null; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 60 | let draft = ''; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 61 | let picked: Mention[] = []; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 62 | let staged: StagedAttachment[] = []; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 63 | let replyingTo: ReplyReference | null = null; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 64 | let editing: { eventId: string; original: string; stash: string } | null = null; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 65 | let sending = false; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 66 | let uploadName: string | null = null; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 67 | let loadedKey = ''; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 68 | let paginating = false; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 69 | let reacting = false; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 70 | let stickToBottom = true; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 71 | let lastRead = ''; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 72 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 73 | $: key = threadKey(roomId, rootEventId); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 74 | $: items = $threadTimelines[key] ?? []; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 75 | $: roomRoot = ($timelines[roomId] ?? []).find((item) => item.kind === 'event' && item.event.event_id === rootEventId) ?? null; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 76 | $: events = threadEvents(items, roomRoot); |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 77 | $: hasStart = items.some((item) => item.kind === 'start' || (item.kind === 'event' && item.event.event_id === rootEventId)); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 78 | $: pinned = ($pinnedThreads[roomId] ?? []).includes(rootEventId); |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 79 | $: emotes = customEmotes(packs); |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 80 | $: switchTo(key); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 81 | $: void follow(events); |
4f91f89d99fa | atagen | 2026-10-25 10:47 UTC | 82 | let focused: string | null = null; |
4f91f89d99fa | atagen | 2026-10-25 10:47 UTC | 83 | $: void focusOn(focusEventId, events); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 84 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 85 | function threadEvents(list: TimelineItem[], root: TimelineItem | null): TimelineEvent[] { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 86 | const out = list.flatMap((item) => (item.kind === 'event' ? [item.event] : [])); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 87 | if (root?.kind === 'event' && !out.some((event) => event.event_id === rootEventId)) out.unshift(root.event); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 88 | return out; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 89 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 90 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 91 | function showSender(index: number): boolean { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 92 | if (index === 0) return true; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 93 | const prev = events[index - 1]; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 94 | const cur = events[index]; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 95 | return prev.sender.user_id !== cur.sender.user_id || Math.abs(cur.timestamp_ms - prev.timestamp_ms) > GROUP_GAP_MS; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 96 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 97 | |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 98 | function switchTo(next: string): void { |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 99 | if (next === loadedKey) return; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 100 | if (loadedKey) drafts.set(loadedKey, { text: editing ? editing.stash : draft, picked, staged }); |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 101 | loadedKey = next; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 102 | const saved = drafts.get(next); |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 103 | draft = saved?.text ?? ''; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 104 | picked = saved?.picked ?? []; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 105 | staged = saved?.staged ?? []; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 106 | replyingTo = null; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 107 | editing = null; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 108 | stickToBottom = true; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 109 | lastRead = ''; |
4f91f89d99fa | atagen | 2026-10-25 10:47 UTC | 110 | focused = null; |
4f91f89d99fa | atagen | 2026-10-25 10:47 UTC | 111 | } |
4f91f89d99fa | atagen | 2026-10-25 10:47 UTC | 112 | |
4f91f89d99fa | atagen | 2026-10-25 10:47 UTC | 113 | async function focusOn(target: string | null, list: TimelineEvent[]): Promise<void> { |
4f91f89d99fa | atagen | 2026-10-25 10:47 UTC | 114 | if (!target || target === focused || !list.some((event) => event.event_id === target)) return; |
4f91f89d99fa | atagen | 2026-10-25 10:47 UTC | 115 | focused = target; |
4f91f89d99fa | atagen | 2026-10-25 10:47 UTC | 116 | stickToBottom = false; |
4f91f89d99fa | atagen | 2026-10-25 10:47 UTC | 117 | await tick(); |
4f91f89d99fa | atagen | 2026-10-25 10:47 UTC | 118 | const row = list.length && thread ? thread.querySelector<HTMLElement>(`[data-event="${CSS.escape(target)}"]`) : null; |
4f91f89d99fa | atagen | 2026-10-25 10:47 UTC | 119 | if (!row) return; |
4f91f89d99fa | atagen | 2026-10-25 10:47 UTC | 120 | row.scrollIntoView({ block: 'center', behavior: 'smooth' }); |
4f91f89d99fa | atagen | 2026-10-25 10:47 UTC | 121 | row.classList.remove('flash'); |
4f91f89d99fa | atagen | 2026-10-25 10:47 UTC | 122 | void row.offsetWidth; |
4f91f89d99fa | atagen | 2026-10-25 10:47 UTC | 123 | row.classList.add('flash'); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 124 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 125 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 126 | async function follow(_events: TimelineEvent[]): Promise<void> { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 127 | await tick(); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 128 | if (list && stickToBottom) list.scrollTop = list.scrollHeight; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 129 | sendReceipt(); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 130 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 131 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 132 | function sendReceipt(): void { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 133 | if (document.hidden || !document.hasFocus() || !stickToBottom) return; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 134 | const latest = [...events].reverse().find((event) => event.event_id); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 135 | if (!latest?.event_id || latest.event_id === lastRead) return; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 136 | lastRead = latest.event_id; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 137 | markRead(roomId, latest.event_id, rootEventId).catch(() => undefined); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 138 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 139 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 140 | function onScroll(): void { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 141 | if (!list) return; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 142 | const wasAtBottom = stickToBottom; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 143 | stickToBottom = list.scrollHeight - list.scrollTop - list.clientHeight < 2; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 144 | if (stickToBottom && !wasAtBottom) sendReceipt(); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 145 | if (list.scrollTop < 60 && !hasStart) void paginate(); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 146 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 147 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 148 | async function paginate(): Promise<void> { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 149 | if (paginating) return; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 150 | paginating = true; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 151 | try { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 152 | await paginateRoom(roomId, rootEventId); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 153 | } catch (e) { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 154 | pushBanner(e instanceof Error ? e.message : 'Loading older replies failed'); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 155 | } finally { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 156 | paginating = false; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 157 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 158 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 159 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 160 | async function react(eventId: string, emoji: string, shortcode?: string): Promise<void> { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 161 | if (reacting) return; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 162 | reacting = true; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 163 | try { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 164 | await reactToMessage(roomId, eventId, emoji, shortcode, rootEventId); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 165 | } catch (e) { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 166 | pushBanner(e instanceof Error ? e.message : 'Reaction failed'); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 167 | } finally { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 168 | reacting = false; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 169 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 170 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 171 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 172 | function startReply(target: ReplyReference): void { |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 173 | if (editing) cancelEdit(); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 174 | replyingTo = target; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 175 | composerRef?.focus(); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 176 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 177 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 178 | function startEdit(eventId: string, body: string): void { |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 179 | const stash = editing ? editing.stash : draft; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 180 | replyingTo = null; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 181 | editing = { eventId, original: body, stash }; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 182 | const target = events.find((event) => event.event_id === eventId); |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 183 | if (target?.content.kind === 'Message') picked = [...picked, ...mentionsFromHtml(target.content.message.formatted_body)]; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 184 | draft = body; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 185 | void composerRef?.focusEnd(); |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 186 | } |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 187 | |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 188 | function cancelEdit(): void { |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 189 | if (!editing) return; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 190 | draft = editing.stash; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 191 | editing = null; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 192 | composerRef?.focus(); |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 193 | } |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 194 | |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 195 | function editLastOwn(): boolean { |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 196 | for (let i = events.length - 1; i >= 0; i -= 1) { |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 197 | const event = events[i]; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 198 | if (!event.own) continue; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 199 | if (!event.editable || !event.event_id || event.content.kind !== 'Message' || event.content.message.kind !== 'Text') return false; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 200 | startEdit(event.event_id, event.content.message.body); |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 201 | return true; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 202 | } |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 203 | return false; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 204 | } |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 205 | |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 206 | function stage(files: File[]): void { |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 207 | const accepted = stageFiles(files, pushBanner); |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 208 | if (accepted.length) staged = [...staged, ...accepted]; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 209 | composerRef?.focus(); |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 210 | } |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 211 | |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 212 | function unstage(id: number): void { |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 213 | releaseStaged(staged.filter((a) => a.id === id)); |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 214 | staged = staged.filter((a) => a.id !== id); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 215 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 216 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 217 | async function submit(): Promise<void> { |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 218 | if (sending) return; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 219 | const body = draft.trim(); |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 220 | if (editing) { |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 221 | const { eventId, original } = editing; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 222 | cancelEdit(); |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 223 | draft = ''; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 224 | if (!body || body === original.trim()) return; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 225 | editMessage(roomId, eventId, body, resolveMentions(body, picked), resolveEmotes(body, emotes), rootEventId).catch((e) => |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 226 | pushBanner(e instanceof Error ? e.message : 'Edit failed') |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 227 | ); |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 228 | picked = []; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 229 | return; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 230 | } |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 231 | const files = staged; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 232 | if (!body && files.length === 0) return; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 233 | const command = parseCommand(body); |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 234 | if (command && command.name !== 'me' && command.name !== 'notice') { |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 235 | pushBanner(`/${command.name} only works in the room composer`); |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 236 | return; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 237 | } |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 238 | const text = command ? command.args : unescapeCommand(body); |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 239 | if (command && (!text || files.length)) { |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 240 | pushBanner(`/${command.name} needs something to say and no attachments`); |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 241 | return; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 242 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 243 | const reply = replyingTo; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 244 | const mentions = resolveMentions(text, picked); |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 245 | const used = resolveEmotes(text, emotes); |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 246 | draft = ''; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 247 | picked = []; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 248 | staged = []; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 249 | replyingTo = null; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 250 | stickToBottom = true; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 251 | sending = true; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 252 | try { |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 253 | if (files.length === 0) { |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 254 | await sendMessage(roomId, text, reply?.event_id ?? null, mentions, command ? (command.name === 'me' ? 'emote' : 'notice') : 'text', used, rootEventId); |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 255 | } else { |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 256 | let first = true; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 257 | for (const attachment of files) { |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 258 | uploadName = attachment.file.name || 'attachment'; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 259 | try { |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 260 | if (first) await uploadAttachment(roomId, attachment.file, text || null, reply?.event_id ?? null, mentions, used, rootEventId); |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 261 | else await uploadAttachment(roomId, attachment.file, null, null, [], [], rootEventId); |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 262 | first = false; |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 263 | } catch (e) { |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 264 | pushBanner(e instanceof Error ? e.message : 'Upload failed'); |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 265 | } |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 266 | } |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 267 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 268 | } catch (e) { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 269 | pushBanner(e instanceof Error ? e.message : 'Sending to the thread failed'); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 270 | } finally { |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 271 | releaseStaged(files); |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 272 | uploadName = null; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 273 | sending = false; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 274 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 275 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 276 | </script> |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 277 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 278 | <svelte:window on:focus={sendReceipt} /> |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 279 | <svelte:document on:visibilitychange={sendReceipt} /> |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 280 | |
4f91f89d99fa | atagen | 2026-10-25 10:47 UTC | 281 | <div class="thread-pane" bind:this={thread}> |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 282 | <header class="thread-head"> |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 283 | <h2>Thread</h2> |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 284 | <span class="thread-room"><EmojiText text={roomName} /></span> |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 285 | <span class="spacer"></span> |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 286 | <button |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 287 | type="button" |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 288 | class="icon-btn" |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 289 | class:active={pinned} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 290 | aria-pressed={pinned} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 291 | aria-label={pinned ? 'Unpin thread' : 'Pin thread'} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 292 | title={pinned ? 'Unpin thread' : 'Pin thread'} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 293 | on:click={() => void togglePinnedThread(roomId, rootEventId)} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 294 | > |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 295 | <svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path d="M9 3h6l-1 6 4 4v2h-5v6l-1 1-1-1v-6H6v-2l4-4z" fill={pinned ? 'currentColor' : 'none'} stroke="currentColor" stroke-width="1.8" stroke-linejoin="round" /></svg> |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 296 | </button> |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 297 | <button type="button" class="icon-btn" aria-label="Close thread" on:click={onClose}> |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 298 | <span aria-hidden="true">✕</span> |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 299 | </button> |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 300 | </header> |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 301 | <div class="thread-list" bind:this={list} on:scroll={onScroll}> |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 302 | {#if !hasStart && events.length > 1} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 303 | <button type="button" class="ghost-btn older" disabled={paginating} on:click={() => void paginate()}> |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 304 | {paginating ? 'Loading…' : 'Load older replies'} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 305 | </button> |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 306 | {/if} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 307 | {#if events.length === 0} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 308 | <p class="side-note">Loading thread…</p> |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 309 | {/if} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 310 | {#each events as event, i (event.item_id || event.event_id)} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 311 | <div class:root={event.event_id === rootEventId}> |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 312 | <MessageRow |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 313 | itemId={`thread-${event.item_id || event.event_id}`} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 314 | {event} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 315 | showSender={showSender(i) || event.event_id === rootEventId} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 316 | senderAvatar={senderAvatarFor(event.sender.user_id) ?? event.sender.avatar} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 317 | {reacting} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 318 | onReact={(eventId, emoji, shortcode) => void react(eventId, emoji, shortcode)} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 319 | {quickReactions} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 320 | {packs} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 321 | {linkPreviews} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 322 | {loadPreview} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 323 | {onQuickChange} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 324 | {people} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 325 | {self} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 326 | {onUser} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 327 | {onMore} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 328 | onReply={joined ? startReply : null} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 329 | onEdit={joined ? startEdit : null} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 330 | editing={editing?.eventId === event.event_id} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 331 | showSent={false} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 332 | /> |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 333 | </div> |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 334 | {/each} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 335 | </div> |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 336 | {#if joined} |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 337 | <div class="thread-composer"> |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 338 | <Composer |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 339 | bind:this={composerRef} |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 340 | bind:value={draft} |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 341 | inputId={`thread-composer-${rootEventId}`} |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 342 | label="Reply in thread" |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 343 | placeholder="Reply in thread" |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 344 | {picked} |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 345 | onPick={(mention) => (picked = [...picked, mention])} |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 346 | {staged} |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 347 | onStage={stage} |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 348 | onUnstage={unstage} |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 349 | uploadBusy={uploadName !== null} |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 350 | {uploadName} |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 351 | editing={editing ? { original: editing.original } : null} |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 352 | onCancelEdit={cancelEdit} |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 353 | {replyingTo} |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 354 | onCancelReply={() => (replyingTo = null)} |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 355 | {people} |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 356 | {self} |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 357 | {emotes} |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 358 | {packs} |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 359 | onSubmit={() => void submit()} |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 360 | onEditLast={editLastOwn} |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 361 | {onMentionStart} |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 362 | /> |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 363 | </div> |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 364 | {/if} |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 365 | </div> |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 366 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 367 | <style> |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 368 | .thread-pane { display: flex; flex-direction: column; min-width: 0; min-height: 0; height: 100%; animation: pop-in var(--pop); } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 369 | .thread-head { display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-bottom: 1px solid var(--border); } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 370 | .thread-head h2 { margin: 0; font-size: 14px; font-weight: 650; } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 371 | .thread-room { color: var(--muted); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 372 | .spacer { flex: 1; } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 373 | .icon-btn.active { color: var(--accent); } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 374 | .thread-list { flex: 1; min-height: 0; overflow-y: auto; padding: 8px 10px; } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 375 | .older { display: block; margin: 0 auto 8px; } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 376 | .root { padding-bottom: 6px; margin-bottom: 6px; border-bottom: 1px solid var(--border); } |
dda384a360e0 | atagen | 2026-10-25 10:27 UTC | 377 | .thread-composer { flex: none; } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 378 | </style> |