atagen

malice

public
Star0
Watch0
b7913beddfde83544e4d9e6230a4e505b8a00f71
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git
web/src/lib/RoomTimelineRows.svelteSvelte139 lines5.6 KB
1
<script lang="ts">
2
  import EmojiText from './EmojiText.svelte';
3
  import MessageRow from './MessageRow.svelte';
4
  import { formatDay } from './format';
5
  import type { PendingMessage } from './pending';
6
  import { buildGroups, lastOwnSent, lastSenderIs, pendingToEvent } from './timeline-groups';
7
  import type { ImagePack, LinkPreview, Person, QuickReaction, RecentHistoryState, ReplyReference, TimelineEvent, TimelineItem } from './types';
8
9
  export let items: TimelineItem[];
10
  export let pending: PendingMessage[] = [];
11
  export let joined: boolean;
12
  export let self: string | null;
13
  export let selfName: string | null = null;
14
  export let people: Record<string, Person> = {};
15
  export let senderAvatarFor: (userId: string) => string | null;
16
  export let reacting: boolean;
17
  export let quickReactions: QuickReaction[] = [];
18
  export let packs: ImagePack[] = [];
19
  export let linkPreviews: Record<string, LinkPreview | null> = {};
20
  export let loadPreview: ((url: string) => void) | null = null;
21
  export let editingEventId: string | null = null;
22
  export let recentState: RecentHistoryState | undefined = undefined;
23
  export let recentRetrying = false;
24
  export let paginating = false;
25
  export let fillingGap: string | null = null;
26
  export let onReact: (eventId: string, key: string, shortcode?: string) => void;
27
  export let onQuickChange: (list: QuickReaction[]) => void;
28
  export let onUser: (userId: string, x: number, y: number, menu: boolean) => void;
29
  export let onCloneSticker: (eventId: string, body: string, x: number, y: number) => void;
30
  export let onMore: (event: TimelineEvent, x: number, y: number) => void;
31
  export let onOpenThread: (rootEventId: string) => void;
32
  export let onReply: (target: ReplyReference) => void;
33
  export let onJump: (eventId: string) => void;
34
  export let onEdit: (eventId: string, body: string) => void;
35
  export let onPaginate: () => void;
36
  export let onFillGap: (gapId: string) => void;
37
  export let onRetryRecent: () => void;
38
  export let onRetryPending: (id: string) => void;
39
  export let onDiscardPending: (id: string) => void;
40
41
  $: hasStart = items.some((item) => item.kind === 'start');
42
  $: grouped = buildGroups(items);
43
  $: lastSentId = lastOwnSent(items);
44
  $: pendingFollowsSelf = lastSenderIs(items, self);
45
</script>
46
47
<div class="timeline-top">
48
  {#if recentState === 'Loading'}
49
    <p class="side-note">Loading recent history…</p>
50
  {:else if recentState && typeof recentState === 'object'}
51
    <div class="history-gap">
52
      <span>Recent history could not be loaded</span>
53
      <button type="button" class="ghost-btn" disabled={recentRetrying} on:click={onRetryRecent}>
54
        {recentRetrying ? 'Loading…' : 'Retry'}
55
      </button>
56
    </div>
57
  {/if}
58
  {#if hasStart}
59
    <p class="start-note">Beginning of history</p>
60
  {:else}
61
    <button type="button" class="ghost-btn" disabled={paginating || recentState === 'Loading'} on:click={onPaginate}>
62
      {paginating ? 'Loading…' : 'Load older messages'}
63
    </button>
64
  {/if}
65
</div>
66
{#if grouped.length === 0}
67
  <p class="side-note timeline-empty">No messages yet</p>
68
{:else}
69
  {#each grouped as g (g.key)}
70
    {#if g.item.kind === 'day'}
71
      <div class="day-row"><span>{formatDay(g.item.timestamp_ms)}</span></div>
72
    {:else if g.item.kind === 'readMarker'}
73
      <div class="new-row"><span>New</span></div>
74
    {:else if g.item.kind === 'start'}
75
      <p class="start-note">Beginning of history</p>
76
    {:else if g.item.kind === 'historyGap'}
77
      {#if g.item.gap.continuity !== 'Verified'}
78
        {@const gap = g.item.gap}
79
        {@const filling = fillingGap === gap.id || gap.lifecycle === 'Fetching'}
80
        <div class="history-gap">
81
          <span>{gap.failure ?? 'Some history is not loaded'}</span>
82
          <button type="button" class="ghost-btn" disabled={!!fillingGap || gap.lifecycle === 'Fetching' || gap.lifecycle === 'Unavailable'} on:click={() => onFillGap(gap.id)}>
83
            {filling ? 'Loading…' : gap.lifecycle === 'Unavailable' ? 'History unavailable' : 'Fetch older history'}
84
          </button>
85
        </div>
86
      {/if}
87
    {:else if g.item.kind === 'unknown'}
88
      <p class="unknown-note">Unsupported event ({g.item.label})</p>
89
    {:else}
90
      <MessageRow
91
        itemId={g.key}
92
        event={g.item.event}
93
        showSender={g.showSender}
94
        senderAvatar={senderAvatarFor(g.item.event.sender.user_id) ?? g.item.event.sender.avatar}
95
        {reacting}
96
        {onReact}
97
        {quickReactions}
98
        {packs}
99
        {linkPreviews}
100
        {loadPreview}
101
        {onQuickChange}
102
        {people}
103
        {self}
104
        {onUser}
105
        {onCloneSticker}
106
        {onMore}
107
        onOpenThread={joined || g.item.event.thread_summary ? onOpenThread : null}
108
        onReply={joined ? onReply : null}
109
        {onJump}
110
        onEdit={joined ? onEdit : null}
111
        editing={editingEventId !== null && editingEventId === g.item.event.event_id}
112
        showSent={g.item.event.item_id === lastSentId && pending.length === 0}
113
      />
114
    {/if}
115
  {/each}
116
{/if}
117
{#each pending as p, i (p.id)}
118
  <MessageRow
119
    itemId={`pending-${p.id}`}
120
    event={pendingToEvent(p, self, selfName)}
121
    {onJump}
122
    showSender={i === 0 && !pendingFollowsSelf}
123
    senderAvatar={senderAvatarFor(self ?? '')}
124
    {reacting}
125
    {onReact}
126
    {quickReactions}
127
    {packs}
128
    {linkPreviews}
129
    {loadPreview}
130
    {onQuickChange}
131
  />
132
  {#if p.status === 'failed'}
133
    <div class="pending-actions">
134
      <span class="pending-error" role="alert"><EmojiText text={p.error ?? ''} /></span>
135
      <button type="button" class="ghost-btn" on:click={() => onRetryPending(p.id)}>Retry</button>
136
      <button type="button" class="ghost-btn danger" on:click={() => onDiscardPending(p.id)}>Discard</button>
137
    </div>
138
  {/if}
139
{/each}