atagen

malice

public
Star0
Watch0
35a820422568f7c14365c307cb9fa13dcedce3da
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git

Blame: web/src/lib/MediaAttachmentView.svelte

973f8fbdaf80atagen2026-09-28 09:10 UTC1
<script lang="ts">
973f8fbdaf80atagen2026-09-28 09:10 UTC2
  import type { MediaAttachment } from './types';
973f8fbdaf80atagen2026-09-28 09:10 UTC3
  import { messageMediaUrl } from './api';
cf23dbf4e77catagen2026-10-23 06:36 UTC4
  import { lightbox } from './lightbox';
973f8fbdaf80atagen2026-09-28 09:10 UTC5
973f8fbdaf80atagen2026-09-28 09:10 UTC6
  export let attachment: MediaAttachment;
973f8fbdaf80atagen2026-09-28 09:10 UTC7
973f8fbdaf80atagen2026-09-28 09:10 UTC8
  const PREVIEWABLE = new Set(['Image', 'Sticker']);
973f8fbdaf80atagen2026-09-28 09:10 UTC9
973f8fbdaf80atagen2026-09-28 09:10 UTC10
  // Encrypted media often has no fetchable thumbnail, so request the bounded
973f8fbdaf80atagen2026-09-28 09:10 UTC11
  // full bytes directly instead of flashing a broken thumbnail first.
973f8fbdaf80atagen2026-09-28 09:10 UTC12
  $: fullUrl = messageMediaUrl(attachment.id, 'Full');
973f8fbdaf80atagen2026-09-28 09:10 UTC13
  $: thumbUrl = messageMediaUrl(attachment.id, 'Thumbnail');
973f8fbdaf80atagen2026-09-28 09:10 UTC14
  $: previewable = PREVIEWABLE.has(attachment.kind);
973f8fbdaf80atagen2026-09-28 09:10 UTC15
973f8fbdaf80atagen2026-09-28 09:10 UTC16
  let fullFailed = false;
973f8fbdaf80atagen2026-09-28 09:10 UTC17
973f8fbdaf80atagen2026-09-28 09:10 UTC18
  $: if (attachment.id) {
973f8fbdaf80atagen2026-09-28 09:10 UTC19
    fullFailed = false;
973f8fbdaf80atagen2026-09-28 09:10 UTC20
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC21
cf23dbf4e77catagen2026-10-23 06:36 UTC22
  function openLightbox(ev: MouseEvent): void {
cf23dbf4e77catagen2026-10-23 06:36 UTC23
    if (ev.button !== 0 || ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey) return;
cf23dbf4e77catagen2026-10-23 06:36 UTC24
    ev.preventDefault();
7cca1d5628d1atagen2026-10-24 07:36 UTC25
    lightbox.set({ src: fullUrl, alt: attachment.caption || attachment.filename, filename: attachment.filename, mxc: attachment.mxc });
cf23dbf4e77catagen2026-10-23 06:36 UTC26
  }
cf23dbf4e77catagen2026-10-23 06:36 UTC27
973f8fbdaf80atagen2026-09-28 09:10 UTC28
  function sizeLabel(): string {
973f8fbdaf80atagen2026-09-28 09:10 UTC29
    if (typeof attachment.size_bytes !== 'number') return '';
973f8fbdaf80atagen2026-09-28 09:10 UTC30
    const bytes = attachment.size_bytes;
973f8fbdaf80atagen2026-09-28 09:10 UTC31
    if (bytes < 1024) return `${bytes} B`;
973f8fbdaf80atagen2026-09-28 09:10 UTC32
    if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
973f8fbdaf80atagen2026-09-28 09:10 UTC33
    return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
973f8fbdaf80atagen2026-09-28 09:10 UTC34
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC35
973f8fbdaf80atagen2026-09-28 09:10 UTC36
  function durationLabel(): string {
973f8fbdaf80atagen2026-09-28 09:10 UTC37
    if (typeof attachment.duration_ms !== 'number') return '';
973f8fbdaf80atagen2026-09-28 09:10 UTC38
    const totalSeconds = Math.round(attachment.duration_ms / 1000);
973f8fbdaf80atagen2026-09-28 09:10 UTC39
    const minutes = Math.floor(totalSeconds / 60);
973f8fbdaf80atagen2026-09-28 09:10 UTC40
    const seconds = totalSeconds % 60;
973f8fbdaf80atagen2026-09-28 09:10 UTC41
    return `${minutes}:${seconds.toString().padStart(2, '0')}`;
973f8fbdaf80atagen2026-09-28 09:10 UTC42
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC43
</script>
973f8fbdaf80atagen2026-09-28 09:10 UTC44
973f8fbdaf80atagen2026-09-28 09:10 UTC45
{#if previewable && !fullFailed}
973f8fbdaf80atagen2026-09-28 09:10 UTC46
  <figure class="media-figure">
cf23dbf4e77catagen2026-10-23 06:36 UTC47
    <a href={fullUrl} target="_blank" rel="noopener noreferrer" title={attachment.filename} on:click={openLightbox}>
973f8fbdaf80atagen2026-09-28 09:10 UTC48
      <img
973f8fbdaf80atagen2026-09-28 09:10 UTC49
        class="media-image"
973f8fbdaf80atagen2026-09-28 09:10 UTC50
        src={fullUrl}
973f8fbdaf80atagen2026-09-28 09:10 UTC51
        alt={attachment.caption || attachment.filename}
973f8fbdaf80atagen2026-09-28 09:10 UTC52
        loading="lazy"
973f8fbdaf80atagen2026-09-28 09:10 UTC53
        on:error={() => (fullFailed = true)}
973f8fbdaf80atagen2026-09-28 09:10 UTC54
      />
973f8fbdaf80atagen2026-09-28 09:10 UTC55
    </a>
973f8fbdaf80atagen2026-09-28 09:10 UTC56
    <figcaption class="media-caption">
973f8fbdaf80atagen2026-09-28 09:10 UTC57
      {#if attachment.caption && attachment.caption !== attachment.filename}
973f8fbdaf80atagen2026-09-28 09:10 UTC58
        <span>{attachment.caption}</span>
973f8fbdaf80atagen2026-09-28 09:10 UTC59
      {:else}
973f8fbdaf80atagen2026-09-28 09:10 UTC60
        <span>{attachment.filename}</span>
973f8fbdaf80atagen2026-09-28 09:10 UTC61
      {/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC62
      {#if sizeLabel()}<span class="media-size"> · {sizeLabel()}</span>{/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC63
    </figcaption>
973f8fbdaf80atagen2026-09-28 09:10 UTC64
  </figure>
973f8fbdaf80atagen2026-09-28 09:10 UTC65
{:else if previewable && fullFailed}
973f8fbdaf80atagen2026-09-28 09:10 UTC66
  <div class="media-file">
973f8fbdaf80atagen2026-09-28 09:10 UTC67
    <span class="file-icon" aria-hidden="true">🖼</span>
973f8fbdaf80atagen2026-09-28 09:10 UTC68
    <span>{attachment.caption || attachment.filename}</span>
973f8fbdaf80atagen2026-09-28 09:10 UTC69
    <span class="media-size">preview unavailable</span>
973f8fbdaf80atagen2026-09-28 09:10 UTC70
  </div>
973f8fbdaf80atagen2026-09-28 09:10 UTC71
{:else if attachment.kind === 'Video'}
973f8fbdaf80atagen2026-09-28 09:10 UTC72
  <div class="media-block">
973f8fbdaf80atagen2026-09-28 09:10 UTC73
    <!-- svelte-ignore a11y-media-has-caption -->
973f8fbdaf80atagen2026-09-28 09:10 UTC74
    <video class="media-video" controls preload="metadata" poster={attachment.has_thumbnail ? thumbUrl : undefined}>
973f8fbdaf80atagen2026-09-28 09:10 UTC75
      <source src={fullUrl} type={attachment.mimetype ?? undefined} />
973f8fbdaf80atagen2026-09-28 09:10 UTC76
      Your browser cannot play this video.
973f8fbdaf80atagen2026-09-28 09:10 UTC77
    </video>
973f8fbdaf80atagen2026-09-28 09:10 UTC78
    <div class="media-caption">
973f8fbdaf80atagen2026-09-28 09:10 UTC79
      <a href={fullUrl} target="_blank" rel="noopener noreferrer">{attachment.caption || attachment.filename}</a>
973f8fbdaf80atagen2026-09-28 09:10 UTC80
      {#if durationLabel()}<span class="media-size"> · {durationLabel()}</span>{/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC81
      {#if sizeLabel()}<span class="media-size"> · {sizeLabel()}</span>{/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC82
    </div>
973f8fbdaf80atagen2026-09-28 09:10 UTC83
  </div>
973f8fbdaf80atagen2026-09-28 09:10 UTC84
{:else if attachment.kind === 'Audio'}
973f8fbdaf80atagen2026-09-28 09:10 UTC85
  <div class="media-block">
973f8fbdaf80atagen2026-09-28 09:10 UTC86
    <!-- svelte-ignore a11y-media-has-caption -->
973f8fbdaf80atagen2026-09-28 09:10 UTC87
    <audio class="media-audio" controls preload="metadata" src={fullUrl}></audio>
973f8fbdaf80atagen2026-09-28 09:10 UTC88
    <div class="media-caption">
973f8fbdaf80atagen2026-09-28 09:10 UTC89
      <span>{attachment.caption || attachment.filename}</span>
973f8fbdaf80atagen2026-09-28 09:10 UTC90
      {#if durationLabel()}<span class="media-size"> · {durationLabel()}</span>{/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC91
      {#if sizeLabel()}<span class="media-size"> · {sizeLabel()}</span>{/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC92
    </div>
973f8fbdaf80atagen2026-09-28 09:10 UTC93
  </div>
973f8fbdaf80atagen2026-09-28 09:10 UTC94
{:else}
973f8fbdaf80atagen2026-09-28 09:10 UTC95
  <div class="media-file">
973f8fbdaf80atagen2026-09-28 09:10 UTC96
    <span class="file-icon" aria-hidden="true">📎</span>
973f8fbdaf80atagen2026-09-28 09:10 UTC97
    <a href={fullUrl} target="_blank" rel="noopener noreferrer" download={attachment.filename}>
973f8fbdaf80atagen2026-09-28 09:10 UTC98
      {attachment.caption || attachment.filename}
973f8fbdaf80atagen2026-09-28 09:10 UTC99
    </a>
973f8fbdaf80atagen2026-09-28 09:10 UTC100
    {#if sizeLabel()}<span class="media-size">{sizeLabel()}</span>{/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC101
  </div>
973f8fbdaf80atagen2026-09-28 09:10 UTC102
{/if}
973f8fbdaf80atagen2026-09-28 09:10 UTC103
973f8fbdaf80atagen2026-09-28 09:10 UTC104
<style>
973f8fbdaf80atagen2026-09-28 09:10 UTC105
  .media-figure { margin: 6px 0 2px; max-width: 420px; }
973f8fbdaf80atagen2026-09-28 09:10 UTC106
  .media-image {
973f8fbdaf80atagen2026-09-28 09:10 UTC107
    display: block;
973f8fbdaf80atagen2026-09-28 09:10 UTC108
    max-width: 100%;
973f8fbdaf80atagen2026-09-28 09:10 UTC109
    max-height: 320px;
05a48e9e7942atagen2026-10-24 09:32 UTC110
    border-radius: var(--r-md);
973f8fbdaf80atagen2026-09-28 09:10 UTC111
    border: 1px solid var(--border);
973f8fbdaf80atagen2026-09-28 09:10 UTC112
    background: var(--bg-soft);
973f8fbdaf80atagen2026-09-28 09:10 UTC113
  }
05a48e9e7942atagen2026-10-24 09:32 UTC114
  .media-video { display: block; max-width: 100%; max-height: 320px; border-radius: var(--r-md); background: #000; }
973f8fbdaf80atagen2026-09-28 09:10 UTC115
  .media-audio { display: block; width: 100%; max-width: 420px; margin-top: 6px; }
973f8fbdaf80atagen2026-09-28 09:10 UTC116
  .media-block { margin: 6px 0 2px; max-width: 420px; }
973f8fbdaf80atagen2026-09-28 09:10 UTC117
  .media-caption { font-size: 12px; color: var(--muted); margin-top: 4px; display: flex; gap: 4px; flex-wrap: wrap; }
973f8fbdaf80atagen2026-09-28 09:10 UTC118
  .media-caption a { color: var(--text); }
973f8fbdaf80atagen2026-09-28 09:10 UTC119
  .media-size { opacity: 0.85; }
973f8fbdaf80atagen2026-09-28 09:10 UTC120
  .media-file {
973f8fbdaf80atagen2026-09-28 09:10 UTC121
    display: inline-flex;
973f8fbdaf80atagen2026-09-28 09:10 UTC122
    align-items: center;
973f8fbdaf80atagen2026-09-28 09:10 UTC123
    gap: 8px;
973f8fbdaf80atagen2026-09-28 09:10 UTC124
    margin: 6px 0 2px;
973f8fbdaf80atagen2026-09-28 09:10 UTC125
    padding: 6px 12px;
973f8fbdaf80atagen2026-09-28 09:10 UTC126
    border: 1px solid var(--border);
05a48e9e7942atagen2026-10-24 09:32 UTC127
    border-radius: var(--r-md);
973f8fbdaf80atagen2026-09-28 09:10 UTC128
    background: var(--bg-soft);
973f8fbdaf80atagen2026-09-28 09:10 UTC129
    max-width: 100%;
973f8fbdaf80atagen2026-09-28 09:10 UTC130
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC131
  .media-file a { color: var(--text); overflow-wrap: anywhere; }
973f8fbdaf80atagen2026-09-28 09:10 UTC132
  .file-icon { font-size: 15px; }
973f8fbdaf80atagen2026-09-28 09:10 UTC133
</style>