atagen

malice

public
Star0
Watch0

Commit e2b04d2f1db0

tarball

atagen <boss@atagen.co>2026-10-25 02:37 UTC

1 file changed+45-5
media: retry transient image preview failures
Modified file: web/src/lib/MediaAttachmentView.svelte+45 additions−5 deletions
@@ -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 bounded
1113 // 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 <img
5189 class="media-image"
5290 class:loaded
53- 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);