atagen

malice

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