atagen

malice

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