aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-02 10:09:28 -0700
committerAdam Soltys <[email protected]>2026-10-02 10:09:28 -0700
commit8249fbe964ae63fa5a34b150b2e5819972d93e5c (patch)
tree93892b6c66818596abff896a12c378f703196aad /tools
parent98a1cb08eff9958d87c6d84462669d2a1990b21e (diff)
downloadcoinosv3-8249fbe964ae63fa5a34b150b2e5819972d93e5c.tar.gz
coinosv3-8249fbe964ae63fa5a34b150b2e5819972d93e5c.zip
Pictures paint with the page: column-sized copies, and CORS that works
Measured in the live tab (screencast frames): back from a profile, the feed's text was on screen ~200ms before its photo. The <img> was loaded and complete on the first frame — a 3024px, 12-megapixel original that Chrome re-decodes whenever the page is rebuilt. - Post photos wider than the column at 2x are painted from an 1100px WebP copy made once (fetch, createImageBitmap, OffscreenCanvas) and kept in Cache Storage; it decodes in a few ms, decoding=sync, inside the same frame as the text. GIFs and small pictures keep the original; the lightbox opens the original. Profile banners use the same copies, warmed on prefetch. - corsFetch: primal's CDN caches its no-Origin answer without Vary: Origin, so a CORS fetch after the <img> got that header-less copy and failed — which had written blossom.primal.net, m.primal.net and coinos.io off as 'no CORS', so their avatars never got local thumbnails and painted late from the originals. A refusal is now retried once past the cache before a host is blamed; the old list is dropped (nocors-v2). - DM list: the unread dot sits centred at the row's end, like the communities' below, instead of beside the time. - Balance card gear: the icon alone, no circle. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Dmcrt9KyNDKVWFKnb8BREo
Diffstat (limited to 'tools')
-rw-r--r--tools/feed-video-aspect-test.js17
1 files changed, 17 insertions, 0 deletions
diff --git a/tools/feed-video-aspect-test.js b/tools/feed-video-aspect-test.js
index 6f646cb..2a678bd 100644
--- a/tools/feed-video-aspect-test.js
+++ b/tools/feed-video-aspect-test.js
@@ -2,6 +2,7 @@
// bands either side: from the post's imeta `dim` when it says, else from the
// clip's metadata fetched before the row is admitted. A clip with no known
// shape still gets the 16:9 box.
+// Photos too: a big one paints from a column-sized copy (see makeMediaThumb).
// Run: bun tools/feed-video-aspect-test.js (CHROME=<path> to pick a browser)
import puppeteer from 'puppeteer-core';
import { cacheKeyFor } from '../src/wallet.js';
@@ -19,6 +20,12 @@ const clip = (name, size) => {
if (p.exitCode !== 0) throw new Error('ffmpeg failed');
};
clip('tall.mp4', '360x480'); clip('wide.mp4', '640x360'); clip('told.mp4', '360x640');
+// photos: a camera-sized one (painted from a column-sized copy) and a small one
+const photo = (name, size) => {
+ const p = Bun.spawnSync(['ffmpeg', '-y', '-loglevel', 'error', '-f', 'lavfi', '-i', `testsrc=size=${size}`, '-frames:v', '1', `${dir}/${name}`]);
+ if (p.exitCode !== 0) throw new Error('ffmpeg failed');
+};
+photo('big.jpg', '3000x2000'); photo('small.jpg', '800x600');
const html = await buildHtml({ minify: true, pwa: false });
const server = Bun.serve({ port: 0, idleTimeout: 30, fetch: async (req) => {
@@ -26,6 +33,7 @@ const server = Bun.serve({ port: 0, idleTimeout: 30, fetch: async (req) => {
// this one's metadata arrives too late for a probe: only the imeta dim can shape it
if (path === '/v/told.mp4') await sleep(6000);
if (path.startsWith('/v/')) return new Response(Bun.file(dir + path.slice(2)), { headers: { 'content-type': 'video/mp4' } });
+ if (path.startsWith('/i/')) return new Response(Bun.file(dir + path.slice(2)), { headers: { 'content-type': 'image/jpeg' } });
if (path === '/broken.mp4') return new Response('nope', { status: 404 });
if (path.startsWith('/punks') || path === '/verify-worker.js') return new Response(Bun.file('dist' + path));
return new Response(html, { headers: { 'content-type': 'text/html' } });
@@ -40,6 +48,8 @@ const notes = [
// imeta says 9:16; the file is too, but the probe must not be needed
post(2, 'told clip ' + base + '/v/told.mp4', [['imeta', 'url ' + base + '/v/told.mp4', 'm video/mp4', 'dim 1080x1920']]),
post(3, 'broken clip ' + base + '/broken.mp4'),
+ post(4, 'big photo ' + base + '/i/big.jpg'),
+ post(5, 'small photo ' + base + '/i/small.jpg'),
];
const browser = await puppeteer.launch({ executablePath: process.env.CHROME || '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox', '--autoplay-policy=no-user-gesture-required'] });
@@ -105,6 +115,13 @@ try {
check('a clip whose post gives imeta dim paints at that shape, without waiting on the file', Math.abs(ratio(b['told.mp4']) - 0.56) < 0.02 && /aspect-ratio:1080\/1920/.test(b['told.mp4'].style || ''), JSON.stringify(b['told.mp4']));
check('...full width, no taller than the screen', b['told.mp4'] && b['told.mp4'].w > 300 && b['told.mp4'].h <= 845, JSON.stringify(b['told.mp4']));
check('a clip that will not load keeps the 16:9 box', /aspect-ratio:16\/9/.test(b['broken.mp4']?.style || ''), JSON.stringify(b['broken.mp4']));
+ // a camera-sized photo paints from a column-sized copy, decoded with the page
+ const imgs = await page.evaluate(() => Object.fromEntries([...document.querySelectorAll('.notes-feed img.note-img')].map((i) => [
+ i.closest('.row').innerText.includes('big photo') ? 'big' : 'small', { src: i.src.slice(0, 5), nw: i.naturalWidth, dec: i.decoding, ar: i.style.aspectRatio }])));
+ check('a 3000px photo paints from an 1100px copy', imgs.big && imgs.big.src === 'blob:' && imgs.big.nw === 1100 && imgs.big.dec === 'sync', JSON.stringify(imgs.big));
+ check('...keeping the original shape', imgs.big && imgs.big.ar.replace(/\s/g, '') === '3000/2000', JSON.stringify(imgs.big));
+ check('a small photo is its own original', imgs.small && imgs.small.src === 'http:' && imgs.small.nw === 800, JSON.stringify(imgs.small));
+ check('the copy is kept on disk for the next visit', await page.evaluate(async () => !!(await (await caches.open('coinos-media-thumbs-v1')).match(location.origin + '/i/big.jpg'))));
await sleep(1500);
check('shapes are remembered on the device', await page.evaluate(() => (JSON.parse(localStorage.getItem('coinos-video-dims') || '[]')).length >= 3));
check('no page errors', errs.length === 0, errs.join('; ').slice(0, 300));