atagen

malice

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

Blame: web/src/lib/RoomTimelineRows.svelte

f93c7e88ea05atagen2026-10-25 10:54 UTC1
<script lang="ts">
f93c7e88ea05atagen2026-10-25 10:54 UTC2
  import EmojiText from './EmojiText.svelte';
f93c7e88ea05atagen2026-10-25 10:54 UTC3
  import MessageRow from './MessageRow.svelte';
f93c7e88ea05atagen2026-10-25 10:54 UTC4
  import { formatDay } from './format';
f93c7e88ea05atagen2026-10-25 10:54 UTC5
  import type { PendingMessage } from './pending';
f93c7e88ea05atagen2026-10-25 10:54 UTC6
  import { buildGroups, lastOwnSent, lastSenderIs, pendingToEvent } from './timeline-groups';
f93c7e88ea05atagen2026-10-25 10:54 UTC7
  import type { ImagePack, LinkPreview, Person, QuickReaction, RecentHistoryState, ReplyReference, TimelineEvent, TimelineItem } from './types';
f93c7e88ea05atagen2026-10-25 10:54 UTC8
f93c7e88ea05atagen2026-10-25 10:54 UTC9
  export let items: TimelineItem[];
f93c7e88ea05atagen2026-10-25 10:54 UTC10
  export let pending: PendingMessage[] = [];
f93c7e88ea05atagen2026-10-25 10:54 UTC11
  export let joined: boolean;
f93c7e88ea05atagen2026-10-25 10:54 UTC12
  export let self: string | null;
f93c7e88ea05atagen2026-10-25 10:54 UTC13
  export let selfName: string | null = null;
f93c7e88ea05atagen2026-10-25 10:54 UTC14
  export let people: Record<string, Person> = {};
f93c7e88ea05atagen2026-10-25 10:54 UTC15
  export let senderAvatarFor: (userId: string) => string | null;
f93c7e88ea05atagen2026-10-25 10:54 UTC16
  export let reacting: boolean;
f93c7e88ea05atagen2026-10-25 10:54 UTC17
  export let quickReactions: QuickReaction[] = [];
f93c7e88ea05atagen2026-10-25 10:54 UTC18
  export let packs: ImagePack[] = [];
f93c7e88ea05atagen2026-10-25 10:54 UTC19
  export let linkPreviews: Record<string, LinkPreview | null> = {};
f93c7e88ea05atagen2026-10-25 10:54 UTC20
  export let loadPreview: ((url: string) => void) | null = null;
f93c7e88ea05atagen2026-10-25 10:54 UTC21
  export let editingEventId: string | null = null;
f93c7e88ea05atagen2026-10-25 10:54 UTC22
  export let recentState: RecentHistoryState | undefined = undefined;
f93c7e88ea05atagen2026-10-25 10:54 UTC23
  export let recentRetrying = false;
f93c7e88ea05atagen2026-10-25 10:54 UTC24
  export let paginating = false;
f93c7e88ea05atagen2026-10-25 10:54 UTC25
  export let fillingGap: string | null = null;
f93c7e88ea05atagen2026-10-25 10:54 UTC26
  export let onReact: (eventId: string, key: string, shortcode?: string) => void;
f93c7e88ea05atagen2026-10-25 10:54 UTC27
  export let onQuickChange: (list: QuickReaction[]) => void;
f93c7e88ea05atagen2026-10-25 10:54 UTC28
  export let onUser: (userId: string, x: number, y: number, menu: boolean) => void;
f93c7e88ea05atagen2026-10-25 10:54 UTC29
  export let onCloneSticker: (eventId: string, body: string, x: number, y: number) => void;
f93c7e88ea05atagen2026-10-25 10:54 UTC30
  export let onMore: (event: TimelineEvent, x: number, y: number) => void;
f93c7e88ea05atagen2026-10-25 10:54 UTC31
  export let onOpenThread: (rootEventId: string) => void;
f93c7e88ea05atagen2026-10-25 10:54 UTC32
  export let onReply: (target: ReplyReference) => void;
f93c7e88ea05atagen2026-10-25 10:54 UTC33
  export let onJump: (eventId: string) => void;
f93c7e88ea05atagen2026-10-25 10:54 UTC34
  export let onEdit: (eventId: string, body: string) => void;
f93c7e88ea05atagen2026-10-25 10:54 UTC35
  export let onPaginate: () => void;
f93c7e88ea05atagen2026-10-25 10:54 UTC36
  export let onFillGap: (gapId: string) => void;
f93c7e88ea05atagen2026-10-25 10:54 UTC37
  export let onRetryRecent: () => void;
f93c7e88ea05atagen2026-10-25 10:54 UTC38
  export let onRetryPending: (id: string) => void;
f93c7e88ea05atagen2026-10-25 10:54 UTC39
  export let onDiscardPending: (id: string) => void;
f93c7e88ea05atagen2026-10-25 10:54 UTC40
f93c7e88ea05atagen2026-10-25 10:54 UTC41
  $: hasStart = items.some((item) => item.kind === 'start');
f93c7e88ea05atagen2026-10-25 10:54 UTC42
  $: grouped = buildGroups(items);
f93c7e88ea05atagen2026-10-25 10:54 UTC43
  $: lastSentId = lastOwnSent(items);
f93c7e88ea05atagen2026-10-25 10:54 UTC44
  $: pendingFollowsSelf = lastSenderIs(items, self);
f93c7e88ea05atagen2026-10-25 10:54 UTC45
</script>
f93c7e88ea05atagen2026-10-25 10:54 UTC46
f93c7e88ea05atagen2026-10-25 10:54 UTC47
<div class="timeline-top">
f93c7e88ea05atagen2026-10-25 10:54 UTC48
  {#if recentState === 'Loading'}
f93c7e88ea05atagen2026-10-25 10:54 UTC49
    <p class="side-note">Loading recent history…</p>
f93c7e88ea05atagen2026-10-25 10:54 UTC50
  {:else if recentState && typeof recentState === 'object'}
f93c7e88ea05atagen2026-10-25 10:54 UTC51
    <div class="history-gap">
f93c7e88ea05atagen2026-10-25 10:54 UTC52
      <span>Recent history could not be loaded</span>
f93c7e88ea05atagen2026-10-25 10:54 UTC53
      <button type="button" class="ghost-btn" disabled={recentRetrying} on:click={onRetryRecent}>
f93c7e88ea05atagen2026-10-25 10:54 UTC54
        {recentRetrying ? 'Loading…' : 'Retry'}
f93c7e88ea05atagen2026-10-25 10:54 UTC55
      </button>
f93c7e88ea05atagen2026-10-25 10:54 UTC56
    </div>
f93c7e88ea05atagen2026-10-25 10:54 UTC57
  {/if}
f93c7e88ea05atagen2026-10-25 10:54 UTC58
  {#if hasStart}
f93c7e88ea05atagen2026-10-25 10:54 UTC59
    <p class="start-note">Beginning of history</p>
f93c7e88ea05atagen2026-10-25 10:54 UTC60
  {:else}
f93c7e88ea05atagen2026-10-25 10:54 UTC61
    <button type="button" class="ghost-btn" disabled={paginating || recentState === 'Loading'} on:click={onPaginate}>
f93c7e88ea05atagen2026-10-25 10:54 UTC62
      {paginating ? 'Loading…' : 'Load older messages'}
f93c7e88ea05atagen2026-10-25 10:54 UTC63
    </button>
f93c7e88ea05atagen2026-10-25 10:54 UTC64
  {/if}
f93c7e88ea05atagen2026-10-25 10:54 UTC65
</div>
f93c7e88ea05atagen2026-10-25 10:54 UTC66
{#if grouped.length === 0}
f93c7e88ea05atagen2026-10-25 10:54 UTC67
  <p class="side-note timeline-empty">No messages yet</p>
f93c7e88ea05atagen2026-10-25 10:54 UTC68
{:else}
f93c7e88ea05atagen2026-10-25 10:54 UTC69
  {#each grouped as g (g.key)}
f93c7e88ea05atagen2026-10-25 10:54 UTC70
    {#if g.item.kind === 'day'}
f93c7e88ea05atagen2026-10-25 10:54 UTC71
      <div class="day-row"><span>{formatDay(g.item.timestamp_ms)}</span></div>
f93c7e88ea05atagen2026-10-25 10:54 UTC72
    {:else if g.item.kind === 'readMarker'}
f93c7e88ea05atagen2026-10-25 10:54 UTC73
      <div class="new-row"><span>New</span></div>
f93c7e88ea05atagen2026-10-25 10:54 UTC74
    {:else if g.item.kind === 'start'}
f93c7e88ea05atagen2026-10-25 10:54 UTC75
      <p class="start-note">Beginning of history</p>
f93c7e88ea05atagen2026-10-25 10:54 UTC76
    {:else if g.item.kind === 'historyGap'}
249e439c27e5atagen2026-10-25 13:26 UTC77
      {@const gap = g.item.gap}
249e439c27e5atagen2026-10-25 13:26 UTC78
      <div class="history-gap">
249e439c27e5atagen2026-10-25 13:26 UTC79
        <span>{gap.failure ?? 'Some history is not loaded'}</span>
249e439c27e5atagen2026-10-25 13:26 UTC80
        <button type="button" class="ghost-btn" disabled={!!fillingGap || gap.lifecycle === 'Unavailable'} on:click={() => onFillGap(gap.id)}>
249e439c27e5atagen2026-10-25 13:26 UTC81
          {fillingGap === gap.id ? 'Loading…' : gap.lifecycle === 'Unavailable' ? 'History unavailable' : 'Fetch older history'}
249e439c27e5atagen2026-10-25 13:26 UTC82
        </button>
249e439c27e5atagen2026-10-25 13:26 UTC83
      </div>
f93c7e88ea05atagen2026-10-25 10:54 UTC84
    {:else if g.item.kind === 'unknown'}
f93c7e88ea05atagen2026-10-25 10:54 UTC85
      <p class="unknown-note">Unsupported event ({g.item.label})</p>
f93c7e88ea05atagen2026-10-25 10:54 UTC86
    {:else}
f93c7e88ea05atagen2026-10-25 10:54 UTC87
      <MessageRow
f93c7e88ea05atagen2026-10-25 10:54 UTC88
        itemId={g.key}
f93c7e88ea05atagen2026-10-25 10:54 UTC89
        event={g.item.event}
f93c7e88ea05atagen2026-10-25 10:54 UTC90
        showSender={g.showSender}
f93c7e88ea05atagen2026-10-25 10:54 UTC91
        senderAvatar={senderAvatarFor(g.item.event.sender.user_id) ?? g.item.event.sender.avatar}
f93c7e88ea05atagen2026-10-25 10:54 UTC92
        {reacting}
f93c7e88ea05atagen2026-10-25 10:54 UTC93
        {onReact}
f93c7e88ea05atagen2026-10-25 10:54 UTC94
        {quickReactions}
f93c7e88ea05atagen2026-10-25 10:54 UTC95
        {packs}
f93c7e88ea05atagen2026-10-25 10:54 UTC96
        {linkPreviews}
f93c7e88ea05atagen2026-10-25 10:54 UTC97
        {loadPreview}
f93c7e88ea05atagen2026-10-25 10:54 UTC98
        {onQuickChange}
f93c7e88ea05atagen2026-10-25 10:54 UTC99
        {people}
f93c7e88ea05atagen2026-10-25 10:54 UTC100
        {self}
f93c7e88ea05atagen2026-10-25 10:54 UTC101
        {onUser}
f93c7e88ea05atagen2026-10-25 10:54 UTC102
        {onCloneSticker}
f93c7e88ea05atagen2026-10-25 10:54 UTC103
        {onMore}
f93c7e88ea05atagen2026-10-25 10:54 UTC104
        onOpenThread={joined || g.item.event.thread_summary ? onOpenThread : null}
f93c7e88ea05atagen2026-10-25 10:54 UTC105
        onReply={joined ? onReply : null}
f93c7e88ea05atagen2026-10-25 10:54 UTC106
        {onJump}
f93c7e88ea05atagen2026-10-25 10:54 UTC107
        onEdit={joined ? onEdit : null}
f93c7e88ea05atagen2026-10-25 10:54 UTC108
        editing={editingEventId !== null && editingEventId === g.item.event.event_id}
f93c7e88ea05atagen2026-10-25 10:54 UTC109
        showSent={g.item.event.item_id === lastSentId && pending.length === 0}
f93c7e88ea05atagen2026-10-25 10:54 UTC110
      />
f93c7e88ea05atagen2026-10-25 10:54 UTC111
    {/if}
f93c7e88ea05atagen2026-10-25 10:54 UTC112
  {/each}
f93c7e88ea05atagen2026-10-25 10:54 UTC113
{/if}
f93c7e88ea05atagen2026-10-25 10:54 UTC114
{#each pending as p, i (p.id)}
f93c7e88ea05atagen2026-10-25 10:54 UTC115
  <MessageRow
f93c7e88ea05atagen2026-10-25 10:54 UTC116
    itemId={`pending-${p.id}`}
f93c7e88ea05atagen2026-10-25 10:54 UTC117
    event={pendingToEvent(p, self, selfName)}
f93c7e88ea05atagen2026-10-25 10:54 UTC118
    {onJump}
f93c7e88ea05atagen2026-10-25 10:54 UTC119
    showSender={i === 0 && !pendingFollowsSelf}
f93c7e88ea05atagen2026-10-25 10:54 UTC120
    senderAvatar={senderAvatarFor(self ?? '')}
f93c7e88ea05atagen2026-10-25 10:54 UTC121
    {reacting}
f93c7e88ea05atagen2026-10-25 10:54 UTC122
    {onReact}
f93c7e88ea05atagen2026-10-25 10:54 UTC123
    {quickReactions}
f93c7e88ea05atagen2026-10-25 10:54 UTC124
    {packs}
f93c7e88ea05atagen2026-10-25 10:54 UTC125
    {linkPreviews}
f93c7e88ea05atagen2026-10-25 10:54 UTC126
    {loadPreview}
f93c7e88ea05atagen2026-10-25 10:54 UTC127
    {onQuickChange}
f93c7e88ea05atagen2026-10-25 10:54 UTC128
  />
f93c7e88ea05atagen2026-10-25 10:54 UTC129
  {#if p.status === 'failed'}
f93c7e88ea05atagen2026-10-25 10:54 UTC130
    <div class="pending-actions">
f93c7e88ea05atagen2026-10-25 10:54 UTC131
      <span class="pending-error" role="alert"><EmojiText text={p.error ?? ''} /></span>
f93c7e88ea05atagen2026-10-25 10:54 UTC132
      <button type="button" class="ghost-btn" on:click={() => onRetryPending(p.id)}>Retry</button>
f93c7e88ea05atagen2026-10-25 10:54 UTC133
      <button type="button" class="ghost-btn danger" on:click={() => onDiscardPending(p.id)}>Discard</button>
f93c7e88ea05atagen2026-10-25 10:54 UTC134
    </div>
f93c7e88ea05atagen2026-10-25 10:54 UTC135
  {/if}
f93c7e88ea05atagen2026-10-25 10:54 UTC136
{/each}