atagen

malice

public
Star0
Watch0
01351a71d24a2a93bdf1cb5282c9839a55856482
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git
web/src/lib/MediaAttachmentView.svelteSvelte140 lines5.3 KB
1
<script lang="ts">
2
  import type { MediaAttachment } from './types';
3
  import { messageMediaUrl } from './api';
4
  import { lightbox } from './lightbox';
5
6
  export let attachment: MediaAttachment;
7
8
  const PREVIEWABLE = new Set(['Image', 'Sticker']);
9
10
  // Encrypted media often has no fetchable thumbnail, so request the bounded
11
  // full bytes directly instead of flashing a broken thumbnail first.
12
  $: fullUrl = messageMediaUrl(attachment.id, 'Full');
13
  $: thumbUrl = messageMediaUrl(attachment.id, 'Thumbnail');
14
  $: previewable = PREVIEWABLE.has(attachment.kind);
15
16
  let fullFailed = false;
17
  let loaded = false;
18
19
  $: if (attachment.id) {
20
    fullFailed = false;
21
    loaded = false;
22
  }
23
24
  function openLightbox(ev: MouseEvent): void {
25
    if (ev.button !== 0 || ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey) return;
26
    ev.preventDefault();
27
    lightbox.set({ src: fullUrl, alt: attachment.caption || attachment.filename, filename: attachment.filename, mxc: attachment.mxc });
28
  }
29
30
  function sizeLabel(): string {
31
    if (typeof attachment.size_bytes !== 'number') return '';
32
    const bytes = attachment.size_bytes;
33
    if (bytes < 1024) return `${bytes} B`;
34
    if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
35
    return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
36
  }
37
38
  function durationLabel(): string {
39
    if (typeof attachment.duration_ms !== 'number') return '';
40
    const totalSeconds = Math.round(attachment.duration_ms / 1000);
41
    const minutes = Math.floor(totalSeconds / 60);
42
    const seconds = totalSeconds % 60;
43
    return `${minutes}:${seconds.toString().padStart(2, '0')}`;
44
  }
45
</script>
46
47
{#if previewable && !fullFailed}
48
  <figure class="media-figure">
49
    <a href={fullUrl} target="_blank" rel="noopener noreferrer" title={attachment.filename} on:click={openLightbox}>
50
      <img
51
        class="media-image"
52
        class:loaded
53
        src={fullUrl}
54
        alt={attachment.caption || attachment.filename}
55
        loading="lazy"
56
        on:load={() => (loaded = true)}
57
        on:error={() => (fullFailed = true)}
58
      />
59
    </a>
60
    {#if attachment.kind !== 'Sticker'}
61
      <figcaption class="media-caption">
62
        {#if attachment.caption && attachment.caption !== attachment.filename}
63
          <span>{attachment.caption}</span>
64
        {:else}
65
          <span>{attachment.filename}</span>
66
        {/if}
67
        {#if sizeLabel()}<span class="media-size"> · {sizeLabel()}</span>{/if}
68
      </figcaption>
69
    {/if}
70
  </figure>
71
{:else if previewable && fullFailed}
72
  <div class="media-file">
73
    <span class="file-icon" aria-hidden="true">🖼</span>
74
    <span>{attachment.caption || attachment.filename}</span>
75
    <span class="media-size">preview unavailable</span>
76
  </div>
77
{:else if attachment.kind === 'Video'}
78
  <div class="media-block">
79
    <!-- svelte-ignore a11y-media-has-caption -->
80
    <video class="media-video" controls preload="metadata" poster={attachment.has_thumbnail ? thumbUrl : undefined}>
81
      <source src={fullUrl} type={attachment.mimetype ?? undefined} />
82
      Your browser cannot play this video.
83
    </video>
84
    <div class="media-caption">
85
      <a href={fullUrl} target="_blank" rel="noopener noreferrer">{attachment.caption || attachment.filename}</a>
86
      {#if durationLabel()}<span class="media-size"> · {durationLabel()}</span>{/if}
87
      {#if sizeLabel()}<span class="media-size"> · {sizeLabel()}</span>{/if}
88
    </div>
89
  </div>
90
{:else if attachment.kind === 'Audio'}
91
  <div class="media-block">
92
    <!-- svelte-ignore a11y-media-has-caption -->
93
    <audio class="media-audio" controls preload="metadata" src={fullUrl}></audio>
94
    <div class="media-caption">
95
      <span>{attachment.caption || attachment.filename}</span>
96
      {#if durationLabel()}<span class="media-size"> · {durationLabel()}</span>{/if}
97
      {#if sizeLabel()}<span class="media-size"> · {sizeLabel()}</span>{/if}
98
    </div>
99
  </div>
100
{:else}
101
  <div class="media-file">
102
    <span class="file-icon" aria-hidden="true">📎</span>
103
    <a href={fullUrl} target="_blank" rel="noopener noreferrer" download={attachment.filename}>
104
      {attachment.caption || attachment.filename}
105
    </a>
106
    {#if sizeLabel()}<span class="media-size">{sizeLabel()}</span>{/if}
107
  </div>
108
{/if}
109
110
<style>
111
  .media-figure { margin: 6px 0 2px; max-width: 420px; }
112
  .media-image {
113
    display: block;
114
    max-width: 100%;
115
    max-height: 320px;
116
    border-radius: var(--r-md);
117
    border: 1px solid var(--border);
118
    background: var(--bg-soft);
119
  }
120
  .media-image.loaded { border-color: transparent; background: none; }
121
  .media-video { display: block; max-width: 100%; max-height: 320px; border-radius: var(--r-md); background: #000; }
122
  .media-audio { display: block; width: 100%; max-width: 420px; margin-top: 6px; }
123
  .media-block { margin: 6px 0 2px; max-width: 420px; }
124
  .media-caption { font-size: 12px; color: var(--muted); margin-top: 4px; display: flex; gap: 4px; flex-wrap: wrap; }
125
  .media-caption a { color: var(--text); }
126
  .media-size { opacity: 0.85; }
127
  .media-file {
128
    display: inline-flex;
129
    align-items: center;
130
    gap: 8px;
131
    margin: 6px 0 2px;
132
    padding: 6px 12px;
133
    border: 1px solid var(--border);
134
    border-radius: var(--r-md);
135
    background: var(--bg-soft);
136
    max-width: 100%;
137
  }
138
  .media-file a { color: var(--text); overflow-wrap: anywhere; }
139
  .file-icon { font-size: 15px; }
140
</style>