Commit e2b04d2f1db0
tarball1 file changed+45-5
@@ -1,4 +1,5 @@
11
<script lang="ts">2
+ import { onDestroy } from 'svelte';23
import type { MediaAttachment } from './types';34
import { messageMediaUrl } from './api';45
import { lightbox } from './lightbox';@@ -6,6 +7,7 @@
67
export let attachment: MediaAttachment;78
89
const PREVIEWABLE = new Set(['Image', 'Sticker']);10
+ const RETRY_DELAYS = [1000, 3000, 10000];911
1012
// Encrypted media often has no fetchable thumbnail, so request the bounded1113
// full bytes directly instead of flashing a broken thumbnail first.@@ -15,12 +17,46 @@
1517
1618
let fullFailed = false;1719
let loaded = false;20
+ let mediaId: string | null = null;21
+ let retryCount = 0;22
+ let retryToken = '';23
+ let retryTimer: ReturnType<typeof setTimeout> | null = null;1824
19
- $: if (attachment.id) {25
+ $: previewUrl = retryToken ? `${fullUrl}&retry=${retryToken}` : fullUrl;26
+ $: if (attachment.id !== mediaId) resetPreview(attachment.id);27
+28
+ function clearRetry(): void {29
+ if (retryTimer !== null) clearTimeout(retryTimer);30
+ retryTimer = null;31
+ }32
+33
+ function resetPreview(id: string): void {34
+ clearRetry();35
+ mediaId = id;36
+ retryCount = 0;37
+ retryToken = '';38
+ fullFailed = false;39
+ loaded = false;40
+ }41
+42
+ function retryPreview(resetBudget = false): void {43
+ clearRetry();44
+ if (resetBudget) retryCount = 0;45
+ retryCount += 1;46
+ retryToken = `${Date.now()}-${retryCount}`;2047
fullFailed = false;2148
loaded = false;2249
}2350
51
+ function previewFailed(): void {52
+ fullFailed = true;53
+ loaded = false;54
+ if (retryTimer !== null || retryCount >= RETRY_DELAYS.length) return;55
+ retryTimer = setTimeout(() => retryPreview(), RETRY_DELAYS[retryCount]);56
+ }57
+58
+ onDestroy(clearRetry);59
+2460
function openLightbox(ev: MouseEvent): void {2561
if (ev.button !== 0 || ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey) return;2662
ev.preventDefault();@@ -44,17 +80,19 @@
4480
}4581
</script>4682
83
+<svelte:window on:online={() => { if (fullFailed) retryPreview(true); }} />84
+4785
{#if previewable && !fullFailed}4886
<figure class="media-figure">4987
<a href={fullUrl} target="_blank" rel="noopener noreferrer" title={attachment.filename} on:click={openLightbox}>5088
<img5189
class="media-image"5290
class:loaded53
- src={fullUrl}91
+ src={previewUrl}5492
alt={attachment.caption || attachment.filename}5593
loading="lazy"5694
on:load={() => (loaded = true)}57
- on:error={() => (fullFailed = true)}95
+ on:error={previewFailed}5896
/>5997
</a>6098
{#if attachment.kind !== 'Sticker'}@@ -71,8 +109,9 @@
71109
{:else if previewable && fullFailed}72110
<div class="media-file">73111
<span class="file-icon" aria-hidden="true">🖼</span>74
- <span>{attachment.caption || attachment.filename}</span>75
- <span class="media-size">preview unavailable</span>112
+ <a href={fullUrl} target="_blank" rel="noopener noreferrer">{attachment.caption || attachment.filename}</a>113
+ <span class="media-size">{retryTimer !== null ? 'Retrying preview…' : 'Preview unavailable'}</span>114
+ <button type="button" class="ghost-btn" on:click={() => retryPreview(true)}>Retry</button>76115
</div>77116
{:else if attachment.kind === 'Video'}78117
<div class="media-block">@@ -128,6 +167,7 @@
128167
display: inline-flex;129168
align-items: center;130169
gap: 8px;170
+ flex-wrap: wrap;131171
margin: 6px 0 2px;132172
padding: 6px 12px;133173
border: 1px solid var(--border);