atagen

malice

public
Star0
Watch0
8909d39b777b6223ad52240456fc8b82edc70be3
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git
web/src/lib/RoomTimelineRows.svelteSvelte136 lines5.5 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
      {@const gap = g.item.gap}
78
      <div class="history-gap">
79
        <span>{gap.failure ?? 'Some history is not loaded'}</span>
80
        <button type="button" class="ghost-btn" disabled={!!fillingGap || gap.lifecycle === 'Unavailable'} on:click={() => onFillGap(gap.id)}>
81
          {fillingGap === gap.id ? 'Loading…' : gap.lifecycle === 'Unavailable' ? 'History unavailable' : 'Fetch older history'}
82
        </button>
83
      </div>
84
    {:else if g.item.kind === 'unknown'}
85
      <p class="unknown-note">Unsupported event ({g.item.label})</p>
86
    {:else}
87
      <MessageRow
88
        itemId={g.key}
89
        event={g.item.event}
90
        showSender={g.showSender}
91
        senderAvatar={senderAvatarFor(g.item.event.sender.user_id) ?? g.item.event.sender.avatar}
92
        {reacting}
93
        {onReact}
94
        {quickReactions}
95
        {packs}
96
        {linkPreviews}
97
        {loadPreview}
98
        {onQuickChange}
99
        {people}
100
        {self}
101
        {onUser}
102
        {onCloneSticker}
103
        {onMore}
104
        onOpenThread={joined || g.item.event.thread_summary ? onOpenThread : null}
105
        onReply={joined ? onReply : null}
106
        {onJump}
107
        onEdit={joined ? onEdit : null}
108
        editing={editingEventId !== null && editingEventId === g.item.event.event_id}
109
        showSent={g.item.event.item_id === lastSentId && pending.length === 0}
110
      />
111
    {/if}
112
  {/each}
113
{/if}
114
{#each pending as p, i (p.id)}
115
  <MessageRow
116
    itemId={`pending-${p.id}`}
117
    event={pendingToEvent(p, self, selfName)}
118
    {onJump}
119
    showSender={i === 0 && !pendingFollowsSelf}
120
    senderAvatar={senderAvatarFor(self ?? '')}
121
    {reacting}
122
    {onReact}
123
    {quickReactions}
124
    {packs}
125
    {linkPreviews}
126
    {loadPreview}
127
    {onQuickChange}
128
  />
129
  {#if p.status === 'failed'}
130
    <div class="pending-actions">
131
      <span class="pending-error" role="alert"><EmojiText text={p.error ?? ''} /></span>
132
      <button type="button" class="ghost-btn" on:click={() => onRetryPending(p.id)}>Retry</button>
133
      <button type="button" class="ghost-btn danger" on:click={() => onDiscardPending(p.id)}>Discard</button>
134
    </div>
135
  {/if}
136
{/each}