aboutsummaryrefslogtreecommitdiffstats
path: root/src/features/messages.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-09 01:25:04 -0700
committerAdam Soltys <[email protected]>2026-10-09 01:25:04 -0700
commitff1a8a72c398c77fbac005f1fde25ad8f43503ef (patch)
tree43b8f1d87239193667d59ac6071c488065f44cb2 /src/features/messages.js
parent817bcc3ba7b2aaad388bc5b2e5cda5ccf7b27766 (diff)
downloadcoinosv3-ff1a8a72c398c77fbac005f1fde25ad8f43503ef.tar.gz
coinosv3-ff1a8a72c398c77fbac005f1fde25ad8f43503ef.zip
Feed images: the column copy and the grid tile are decoded, resized and WebP-encoded in a worker, one picture at a time, instead of on the page thread between scroll frames (a profile of the live feed showed 1.8 s of convertToBlob there, in tasks up to 383 ms). A picture that hangs or crashes the worker is given up and the worker restarts for the next; three in a row and originals paint for the session, as they do wherever workers can't run
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Diffstat (limited to 'src/features/messages.js')
-rw-r--r--src/features/messages.js49
1 files changed, 12 insertions, 37 deletions
diff --git a/src/features/messages.js b/src/features/messages.js
index 5cdf09b..e9e238a 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -33,6 +33,7 @@ import { loadMediaIndex, saveMediaIndex, wipeMediaIndex } from '../media-cache.j
import { mergeFeedWindow } from '../feed-window.js';
import { createThreadStore } from '../thread-cache.js';
import { createThreadPrefetch } from '../thread-prefetch.js';
+import { makeImageThumbnails } from '../image-thumbnails.js';
import { createFeedCache, FEED_CACHE_POSTS, FEED_SEED_POSTS } from '../feed-cache.js';
import { POPULAR_RELAYS, FIREHOSE_RELAYS, popularCandidates } from '../popular-feed.js';
import { makeSearcher, resultRows, fallbackAvatar, warmSearch, punkImageUrl, punkSmallUrl, primalCache } from '../recipient-search.js';
@@ -5732,28 +5733,14 @@ export function messagesFeature(ctx) {
if (e && e.corsRefused) noteNoCors(url); // refused, as corsFetch confirmed
return null;
}
- let bmp;
- try { bmp = await createImageBitmap(blob); } catch { return null; }
- const dims = { width: bmp.width, height: bmp.height };
- // the Images grid's square comes from the same decode: scrolling the
- // feed fills the grid's cache, no second download
- if (!/gif/i.test(blob.type) && !tileThumbs.has(url)) await tileFromBitmap(url, bmp).catch(() => {});
- // small enough already, or animated (a GIF's copy would be one frame):
- // remember the shape and paint the original
- const keep = bmp.width <= MEDIA_THUMB_W * 1.25 || /gif/i.test(blob.type);
- let out = null;
- if (!keep) {
- try {
- const h = Math.max(1, Math.round(bmp.height * MEDIA_THUMB_W / bmp.width));
- const c = new OffscreenCanvas(MEDIA_THUMB_W, h);
- const g = c.getContext('2d');
- g.imageSmoothingQuality = 'high';
- g.drawImage(bmp, 0, 0, MEDIA_THUMB_W, h);
- out = await c.convertToBlob({ type: 'image/webp', quality: 0.82 });
- if (!/webp/.test(out.type)) out = await c.convertToBlob({ type: 'image/jpeg', quality: 0.82 });
- } catch { out = null; }
- }
- try { bmp.close(); } catch {}
+ const result = await makeImageThumbnails(blob, {
+ width: MEDIA_THUMB_W, tileSize: tileThumbs.has(url) ? 0 : TILE_PX,
+ });
+ if (!result) return null;
+ const dims = { width: result.width, height: result.height }, out = result.media;
+ // Both sizes share one decode in the worker; only cache writes and object
+ // URLs come back to the page. The lightbox still uses the original.
+ if (result.tile) await saveTileThumb(url, result.tile).catch(() => {});
try {
const store = await thumbStore();
await store.put(url, new Response(out || new Blob(), { headers: { 'x-dims': JSON.stringify(dims), 'x-original': out ? '0' : '1' } }));
@@ -10561,22 +10548,10 @@ export function messagesFeature(ctx) {
blob = await res.blob();
} catch { return; }
if (/gif/i.test(blob.type)) return; // animated: the tile shows the original
- let bmp;
- try { bmp = await createImageBitmap(blob); } catch { return; }
- try { await tileFromBitmap(url, bmp); } catch {} finally { try { bmp.close(); } catch {} }
+ const result = await makeImageThumbnails(blob, { tileSize: TILE_PX });
+ if (result?.tile) await saveTileThumb(url, result.tile);
}
- // A tile's square, cut from a decoded picture and kept (the caller closes it).
- async function tileFromBitmap(url, bmp) {
- if (typeof OffscreenCanvas === 'undefined' || typeof caches === 'undefined') return;
- const side = Math.min(bmp.width, bmp.height);
- const px = Math.min(TILE_PX, side);
- const sq = await createImageBitmap(bmp, (bmp.width - side) / 2, (bmp.height - side) / 2, side, side,
- { resizeWidth: px, resizeHeight: px, resizeQuality: 'high' });
- const c = new OffscreenCanvas(px, px);
- c.getContext('2d').drawImage(sq, 0, 0);
- try { sq.close(); } catch {}
- let out = await c.convertToBlob({ type: 'image/webp', quality: 0.8 });
- if (!/webp/.test(out.type)) out = await c.convertToBlob({ type: 'image/jpeg', quality: 0.8 });
+ async function saveTileThumb(url, out) {
tileThumbs.set(url, URL.createObjectURL(out));
const store = await tileStore();
await store.put(tileKey(url), new Response(out, { headers: { 'content-type': out.type } }));