7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 1 | <script lang="ts"> |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 2 | import { slide } from 'svelte/transition'; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 3 | import EmojiText from './EmojiText.svelte'; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 4 | import PinButton from './PinButton.svelte'; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 5 | import { formatClock, formatDay } from './format'; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 6 | import { threadMatches } from './thread-index'; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 7 | import type { ThreadEntry } from './thread-index'; |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 8 | import type { ListHistoryState } from './types'; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 9 | import { togglePinnedThread } from './threads'; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 10 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 11 | export let roomId: string; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 12 | export let threads: ThreadEntry[]; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 13 | export let query = ''; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 14 | export let activeRoot: string | null = null; |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 15 | export let history: ListHistoryState | null = null; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 16 | export let onOpen: (rootEventId: string) => void; |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 17 | export let onOlder: () => void; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 18 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 19 | const COLLAPSED_LIMIT = 6; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 20 | let showAll = false; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 21 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 22 | $: matching = threads.filter((t) => threadMatches(t, query)); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 23 | $: shown = query || showAll ? matching : matching.slice(0, COLLAPSED_LIMIT); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 24 | $: hidden = matching.length - shown.length; |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 25 | $: canLoadOlder = !query && hidden === 0 && history !== null && history !== 'StartReached'; |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 26 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 27 | function label(t: ThreadEntry): string { |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 28 | return t.preview || (t.pinned ? 'Pinned thread' : 'Thread'); |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 29 | } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 30 | </script> |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 31 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 32 | <ul class="thread-fold" transition:slide={{ duration: 140 }} aria-label="Threads"> |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 33 | {#each shown as t (t.root_event_id)} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 34 | <li class="thread-item"> |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 35 | <button |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 36 | type="button" |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 37 | class="thread-row" |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 38 | class:active={activeRoot === t.root_event_id} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 39 | aria-current={activeRoot === t.root_event_id ? 'true' : undefined} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 40 | on:click={() => onOpen(t.root_event_id)} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 41 | > |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 42 | <span class="thread-preview"><EmojiText text={label(t)} /></span> |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 43 | {#if t.unread > 0}<span class="thread-unread" class:mention={t.mentions > 0} aria-label={`${t.unread} unread`}>{t.unread > 99 ? '99+' : t.unread}</span> |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 44 | {:else if t.reply_count > 0}<span class="thread-meta">{t.reply_count}</span>{/if} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 45 | {#if t.latest_ms}<time class="thread-meta" datetime={new Date(t.latest_ms).toISOString()}>{formatDay(t.latest_ms)} {formatClock(t.latest_ms)}</time>{/if} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 46 | </button> |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 47 | <PinButton pinned={t.pinned} label={`thread ${label(t)}`} onToggle={() => void togglePinnedThread(roomId, t.root_event_id)} /> |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 48 | </li> |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 49 | {/each} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 50 | {#if hidden > 0} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 51 | <li><button type="button" class="thread-more" on:click={() => (showAll = true)}>{hidden} more</button></li> |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 52 | {:else if canLoadOlder} |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 53 | <li><button type="button" class="thread-more" disabled={history === 'Loading'} on:click={onOlder}>{history === 'Loading' ? 'Loading…' : 'Older threads'}</button></li> |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 54 | {/if} |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 55 | </ul> |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 56 | |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 57 | <style> |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 58 | .thread-fold { list-style: none; margin: 0 0 2px 22px; padding: 0 0 0 8px; border-left: 1px solid var(--border); display: flex; flex-direction: column; gap: 1px; } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 59 | .thread-item { display: flex; align-items: center; gap: 2px; } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 60 | .thread-row { flex: 1; min-width: 0; display: flex; align-items: baseline; gap: 6px; padding: 3px 6px; border: none; border-radius: var(--r-sm); background: transparent; color: var(--muted); font: inherit; font-size: 12.5px; text-align: left; cursor: pointer; transition: background var(--pop), color var(--pop); } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 61 | .thread-row:hover { background: var(--panel-2); color: var(--text); } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 62 | .thread-row.active { background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--text); } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 63 | .thread-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 64 | .thread-preview { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 65 | .thread-meta { flex: none; font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 66 | .thread-item:hover :global(.pin-btn), .thread-item :global(.pin-btn.pinned) { opacity: 1; } |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 67 | @media (hover: none) { .thread-item:has(.active) :global(.pin-btn) { opacity: 1; } } |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 68 | @media (pointer: coarse) { .thread-row { padding: 9px 6px; } } |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 69 | .thread-unread { flex: none; min-width: 16px; padding: 0 4px; border-radius: var(--r-sm); background: var(--panel-2); color: var(--text); font-size: 10.5px; font-weight: 600; text-align: center; font-variant-numeric: tabular-nums; } |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 70 | .thread-unread.mention { background: var(--accent); color: var(--on-accent); } |
249e439c27e5 | atagen | 2026-10-25 13:26 UTC | 71 | .thread-more:disabled { color: var(--muted); cursor: default; } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 72 | .thread-more { border: none; background: transparent; color: var(--accent); font: inherit; font-size: 12px; padding: 2px 6px; cursor: pointer; } |
7ebc2472a9e3 | atagen | 2026-10-25 09:53 UTC | 73 | </style> |