atagen

malice

public
Star0
Watch0
b7913beddfde83544e4d9e6230a4e505b8a00f71
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git
web/src/lib/RoomThreads.svelteSvelte61 lines3.2 KB
1
<script lang="ts">
2
  import { slide } from 'svelte/transition';
3
  import EmojiText from './EmojiText.svelte';
4
  import PinButton from './PinButton.svelte';
5
  import { formatClock, formatDay } from './format';
6
  import { threadMatches } from './thread-index';
7
  import type { ThreadEntry } from './thread-index';
8
  import { togglePinnedThread } from './threads';
9
10
  export let roomId: string;
11
  export let threads: ThreadEntry[];
12
  export let query = '';
13
  export let activeRoot: string | null = null;
14
  export let onOpen: (rootEventId: string) => void;
15
16
  const COLLAPSED_LIMIT = 6;
17
  let showAll = false;
18
19
  $: matching = threads.filter((t) => threadMatches(t, query));
20
  $: shown = query || showAll ? matching : matching.slice(0, COLLAPSED_LIMIT);
21
  $: hidden = matching.length - shown.length;
22
23
  function label(t: ThreadEntry): string {
24
    return t.preview || (t.pinned ? 'Pinned thread' : 'Thread');
25
  }
26
</script>
27
28
<ul class="thread-fold" transition:slide={{ duration: 140 }} aria-label="Threads">
29
  {#each shown as t (t.root_event_id)}
30
    <li class="thread-item">
31
      <button
32
        type="button"
33
        class="thread-row"
34
        class:active={activeRoot === t.root_event_id}
35
        aria-current={activeRoot === t.root_event_id ? 'true' : undefined}
36
        on:click={() => onOpen(t.root_event_id)}
37
      >
38
        <span class="thread-preview"><EmojiText text={label(t)} /></span>
39
        {#if t.reply_count > 0}<span class="thread-meta">{t.reply_count}</span>{/if}
40
        {#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}
41
      </button>
42
      <PinButton pinned={t.pinned} label={`thread ${label(t)}`} onToggle={() => void togglePinnedThread(roomId, t.root_event_id)} />
43
    </li>
44
  {/each}
45
  {#if hidden > 0}
46
    <li><button type="button" class="thread-more" on:click={() => (showAll = true)}>{hidden} more</button></li>
47
  {/if}
48
</ul>
49
50
<style>
51
  .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; }
52
  .thread-item { display: flex; align-items: center; gap: 2px; }
53
  .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); }
54
  .thread-row:hover { background: var(--panel-2); color: var(--text); }
55
  .thread-row.active { background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--text); }
56
  .thread-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
57
  .thread-preview { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
58
  .thread-meta { flex: none; font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; }
59
  .thread-item:hover :global(.pin-btn), .thread-item :global(.pin-btn.pinned) { opacity: 1; }
60
  .thread-more { border: none; background: transparent; color: var(--accent); font: inherit; font-size: 12px; padding: 2px 6px; cursor: pointer; }
61
</style>