atagen

malice

public
Star0
Watch0
9ad1d568a721b95f4c11c07b836f1ee0be6a81ae
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git

Blame: web/src/lib/RoomThreads.svelte

7ebc2472a9e3atagen2026-10-25 09:53 UTC1
<script lang="ts">
7ebc2472a9e3atagen2026-10-25 09:53 UTC2
  import { slide } from 'svelte/transition';
7ebc2472a9e3atagen2026-10-25 09:53 UTC3
  import EmojiText from './EmojiText.svelte';
7ebc2472a9e3atagen2026-10-25 09:53 UTC4
  import PinButton from './PinButton.svelte';
7ebc2472a9e3atagen2026-10-25 09:53 UTC5
  import { formatClock, formatDay } from './format';
7ebc2472a9e3atagen2026-10-25 09:53 UTC6
  import { threadMatches } from './thread-index';
7ebc2472a9e3atagen2026-10-25 09:53 UTC7
  import type { ThreadEntry } from './thread-index';
249e439c27e5atagen2026-10-25 13:26 UTC8
  import type { ListHistoryState } from './types';
7ebc2472a9e3atagen2026-10-25 09:53 UTC9
  import { togglePinnedThread } from './threads';
7ebc2472a9e3atagen2026-10-25 09:53 UTC10
7ebc2472a9e3atagen2026-10-25 09:53 UTC11
  export let roomId: string;
7ebc2472a9e3atagen2026-10-25 09:53 UTC12
  export let threads: ThreadEntry[];
7ebc2472a9e3atagen2026-10-25 09:53 UTC13
  export let query = '';
7ebc2472a9e3atagen2026-10-25 09:53 UTC14
  export let activeRoot: string | null = null;
249e439c27e5atagen2026-10-25 13:26 UTC15
  export let history: ListHistoryState | null = null;
7ebc2472a9e3atagen2026-10-25 09:53 UTC16
  export let onOpen: (rootEventId: string) => void;
249e439c27e5atagen2026-10-25 13:26 UTC17
  export let onOlder: () => void;
7ebc2472a9e3atagen2026-10-25 09:53 UTC18
7ebc2472a9e3atagen2026-10-25 09:53 UTC19
  const COLLAPSED_LIMIT = 6;
7ebc2472a9e3atagen2026-10-25 09:53 UTC20
  let showAll = false;
7ebc2472a9e3atagen2026-10-25 09:53 UTC21
7ebc2472a9e3atagen2026-10-25 09:53 UTC22
  $: matching = threads.filter((t) => threadMatches(t, query));
7ebc2472a9e3atagen2026-10-25 09:53 UTC23
  $: shown = query || showAll ? matching : matching.slice(0, COLLAPSED_LIMIT);
7ebc2472a9e3atagen2026-10-25 09:53 UTC24
  $: hidden = matching.length - shown.length;
249e439c27e5atagen2026-10-25 13:26 UTC25
  $: canLoadOlder = !query && hidden === 0 && history !== null && history !== 'StartReached';
7ebc2472a9e3atagen2026-10-25 09:53 UTC26
7ebc2472a9e3atagen2026-10-25 09:53 UTC27
  function label(t: ThreadEntry): string {
7ebc2472a9e3atagen2026-10-25 09:53 UTC28
    return t.preview || (t.pinned ? 'Pinned thread' : 'Thread');
7ebc2472a9e3atagen2026-10-25 09:53 UTC29
  }
7ebc2472a9e3atagen2026-10-25 09:53 UTC30
</script>
7ebc2472a9e3atagen2026-10-25 09:53 UTC31
7ebc2472a9e3atagen2026-10-25 09:53 UTC32
<ul class="thread-fold" transition:slide={{ duration: 140 }} aria-label="Threads">
7ebc2472a9e3atagen2026-10-25 09:53 UTC33
  {#each shown as t (t.root_event_id)}
7ebc2472a9e3atagen2026-10-25 09:53 UTC34
    <li class="thread-item">
7ebc2472a9e3atagen2026-10-25 09:53 UTC35
      <button
7ebc2472a9e3atagen2026-10-25 09:53 UTC36
        type="button"
7ebc2472a9e3atagen2026-10-25 09:53 UTC37
        class="thread-row"
7ebc2472a9e3atagen2026-10-25 09:53 UTC38
        class:active={activeRoot === t.root_event_id}
7ebc2472a9e3atagen2026-10-25 09:53 UTC39
        aria-current={activeRoot === t.root_event_id ? 'true' : undefined}
7ebc2472a9e3atagen2026-10-25 09:53 UTC40
        on:click={() => onOpen(t.root_event_id)}
7ebc2472a9e3atagen2026-10-25 09:53 UTC41
      >
7ebc2472a9e3atagen2026-10-25 09:53 UTC42
        <span class="thread-preview"><EmojiText text={label(t)} /></span>
249e439c27e5atagen2026-10-25 13:26 UTC43
        {#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>
249e439c27e5atagen2026-10-25 13:26 UTC44
        {:else if t.reply_count > 0}<span class="thread-meta">{t.reply_count}</span>{/if}
7ebc2472a9e3atagen2026-10-25 09:53 UTC45
        {#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}
7ebc2472a9e3atagen2026-10-25 09:53 UTC46
      </button>
7ebc2472a9e3atagen2026-10-25 09:53 UTC47
      <PinButton pinned={t.pinned} label={`thread ${label(t)}`} onToggle={() => void togglePinnedThread(roomId, t.root_event_id)} />
7ebc2472a9e3atagen2026-10-25 09:53 UTC48
    </li>
7ebc2472a9e3atagen2026-10-25 09:53 UTC49
  {/each}
7ebc2472a9e3atagen2026-10-25 09:53 UTC50
  {#if hidden > 0}
7ebc2472a9e3atagen2026-10-25 09:53 UTC51
    <li><button type="button" class="thread-more" on:click={() => (showAll = true)}>{hidden} more</button></li>
249e439c27e5atagen2026-10-25 13:26 UTC52
  {:else if canLoadOlder}
249e439c27e5atagen2026-10-25 13:26 UTC53
    <li><button type="button" class="thread-more" disabled={history === 'Loading'} on:click={onOlder}>{history === 'Loading' ? 'Loading…' : 'Older threads'}</button></li>
7ebc2472a9e3atagen2026-10-25 09:53 UTC54
  {/if}
7ebc2472a9e3atagen2026-10-25 09:53 UTC55
</ul>
7ebc2472a9e3atagen2026-10-25 09:53 UTC56
7ebc2472a9e3atagen2026-10-25 09:53 UTC57
<style>
7ebc2472a9e3atagen2026-10-25 09:53 UTC58
  .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; }
7ebc2472a9e3atagen2026-10-25 09:53 UTC59
  .thread-item { display: flex; align-items: center; gap: 2px; }
7ebc2472a9e3atagen2026-10-25 09:53 UTC60
  .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); }
7ebc2472a9e3atagen2026-10-25 09:53 UTC61
  .thread-row:hover { background: var(--panel-2); color: var(--text); }
7ebc2472a9e3atagen2026-10-25 09:53 UTC62
  .thread-row.active { background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--text); }
7ebc2472a9e3atagen2026-10-25 09:53 UTC63
  .thread-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
7ebc2472a9e3atagen2026-10-25 09:53 UTC64
  .thread-preview { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
7ebc2472a9e3atagen2026-10-25 09:53 UTC65
  .thread-meta { flex: none; font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; }
7ebc2472a9e3atagen2026-10-25 09:53 UTC66
  .thread-item:hover :global(.pin-btn), .thread-item :global(.pin-btn.pinned) { opacity: 1; }
249e439c27e5atagen2026-10-25 13:26 UTC67
  .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; }
249e439c27e5atagen2026-10-25 13:26 UTC68
  .thread-unread.mention { background: var(--accent); color: var(--on-accent); }
249e439c27e5atagen2026-10-25 13:26 UTC69
  .thread-more:disabled { color: var(--muted); cursor: default; }
7ebc2472a9e3atagen2026-10-25 09:53 UTC70
  .thread-more { border: none; background: transparent; color: var(--accent); font: inherit; font-size: 12px; padding: 2px 6px; cursor: pointer; }
7ebc2472a9e3atagen2026-10-25 09:53 UTC71
</style>