diff options
| author | Adam Soltys <[email protected]> | 2026-10-22 10:12:49 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-22 10:12:49 -0700 |
| commit | cc84dd0c6c852b46b309079a5a90a54080fc049c (patch) | |
| tree | af524ce1d63d2d62b987cc456553c563391ebea5 /src/features | |
| parent | 7947be155d4cc4f43dd89d743c5e09bfac4b635b (diff) | |
| download | coinosv3-cc84dd0c6c852b46b309079a5a90a54080fc049c.tar.gz coinosv3-cc84dd0c6c852b46b309079a5a90a54080fc049c.zip | |
Images grid in justified rows too: tiles in each picture's own shape (the tile thumbnail is no longer a square crop: 360px tall, its middle cut to 1:2–2:1; vmedia pic.webp?s=row for hosts without CORS; tile cache v2)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/features')
| -rw-r--r-- | src/features/messages.js | 31 |
1 files changed, 24 insertions, 7 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index 34bc815..c8b5683 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -13723,12 +13723,17 @@ export function messagesFeature(ctx) { // <img> of the original (or the 1100px column copy) to fetch and decode, // and the browser's lazy margin is a screen or two. These are local and a // few KB, so every tile loads at once and decodes in no time. - const TILE_CACHE = 'coinos-tile-thumbs-v1', TILE_PX = 360, TILE_MAX = 1200; + const TILE_CACHE = 'coinos-tile-thumbs-v2', TILE_PX = 360, TILE_MAX = 1200; const tileThumbs = new Map(); // url -> object URL const tileKey = (url) => 'https://tile-thumb.invalid/?u=' + encodeURIComponent(url); const tileQueue = [], tileTried = new Set(), tileLooked = new Set(); let tileBusy = 0, tileRepaint = null; - const tileStore = () => caches.open(TILE_CACHE); + let tileOldGone = false; + const tileStore = () => { + // the square tiles of before (v1) go: the grid's rows want each shape + if (!tileOldGone) { tileOldGone = true; caches.delete('coinos-tile-thumbs-v1').catch(() => {}); } + return caches.open(TILE_CACHE); + }; async function warmTileThumbs(urls) { if (typeof caches === 'undefined') return; let found = 0; @@ -13748,7 +13753,7 @@ export function messagesFeature(ctx) { const kept = mediaReady.get(url)?.thumb; if (!kept && (noCorsHost(url) || corsRefused.has(url))) { // the host keeps its bytes: the server cuts the tile - const tile = await serverPicBlob(url, 'tile'); + const tile = await serverPicBlob(url, 'row'); if (tile) await saveTileThumb(url, tile); return; } @@ -13758,7 +13763,7 @@ export function messagesFeature(ctx) { if (!res.ok) return; blob = await res.blob(); } catch (e) { - if (e && e.corsRefused) { corsRefused.add(url); const tile = await serverPicBlob(url, 'tile'); if (tile) await saveTileThumb(url, tile); } + if (e && e.corsRefused) { corsRefused.add(url); const tile = await serverPicBlob(url, 'row'); if (tile) await saveTileThumb(url, tile); } return; } if (/gif/i.test(blob.type)) return; // animated: the tile shows the original @@ -13931,6 +13936,17 @@ export function messagesFeature(ctx) { const posterAr = new Map(); const clampAr = (r) => Math.min(2, Math.max(0.5, r)); const clipRatio = (url) => { const z = clipSize(url); return z ? clampAr(z.w / z.h) : posterAr.get(url) || 1; }; + // a grid item's: the file's measured size, the post's word for it (NIP-92 + // dim), or its still once that has loaded + function itemRatio(it) { + if (clipSize(it.url)) return clipRatio(it.url); + for (const tag of it.ev.tags || []) { + if (tag[0] !== 'imeta' || !tag.includes('url ' + it.url)) continue; + const m = /^(\d+)(?:\.0+)?x(\d+)(?:\.0+)?$/.exec(imetaFields(tag).dim || ''); + if (m && +m[1] && +m[2]) return clampAr(+m[1] / +m[2]); + } + return posterAr.get(it.url) || 1; + } function arFromStill(img, url) { if (clipSize(url) || !img.naturalWidth || !img.naturalHeight) return; const r = clampAr(img.naturalWidth / img.naturalHeight); @@ -14226,7 +14242,8 @@ export function messagesFeature(ctx) { // rows, each tile its clip's own proportions and each row as tall as it // takes to fill the width; tall ones only (a phone held upright) keep // the tall tiles three across. - const shape = mediaMode !== 'videos' ? '' : clipsAll() ? ' justified' : screenTall() ? '' : ' wide'; + // Pictures go in justified rows too, in their own shapes. + const shape = mediaMode === 'images' ? ' justified' : mediaMode !== 'videos' ? '' : clipsAll() ? ' justified' : screenTall() ? '' : ' wide'; return h('div', { class: 'media-grid' + shape, 'data-key': 'media-grid:' + mediaMode }, ...items.map((it) => { const k = mediaKey(it); @@ -14240,14 +14257,14 @@ export function messagesFeature(ctx) { const fromServer = mediaMode === 'videos' && poster && poster === serverThumb(it.url); return h('button', { class: 'media-tile' + (mediaMode === 'videos' ? ' video' : ''), type: 'button', 'data-key': 'tile:' + k, - style: shape === ' justified' ? '--ar:' + clipRatio(it.url).toFixed(3) : undefined, + style: shape === ' justified' ? '--ar:' + itemRatio(it).toFixed(3) : undefined, 'aria-label': displayName(it.ev.pubkey), onClick: () => openMediaPager(mediaMode, k), onPointerDown: mediaMode === 'videos' ? () => earlyPlayer(it.url) : undefined, }, mediaMode === 'images' || poster ? h('img', { src: poster || tileThumbs.get(it.url) || mediaSrc(it.url), alt: '', draggable: 'false', loading: poster || tileThumbs.has(it.url) ? 'eager' : 'lazy', decoding: 'async', - onLoad: mediaMode === 'videos' ? (e) => arFromStill(e.currentTarget, it.url) : undefined, + onLoad: (e) => arFromStill(e.currentTarget, it.url), onError: (e) => { if (fromServer) { serverThumbFailed.add(it.url); render(); return; } // the frame grab's turn const b = e.target.closest('.media-tile'); if (b) b.classList.add('broken'); |
