atagen

malice

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