openvibers/openvibe.network Commits
The home page fetches card pictures at the size it draws them (#500)
Changes
@@@ -115,7 +115,16 @@ const fmtCount = (n) => Number(n).toLocaleString('en-US');// A picture that fails to load (a stream's first thumbnail is written a few seconds after it goes live) falls back to// the avatar when there is one, else to an empty tile, never the browser's broken-image icon.const BLANK = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';--const img = (src, fallback, w, h) => `<img src="${esc(src)}" alt="" loading="lazy" decoding="async" width="${w}" height="${h}" data-fallback="${esc(fallback || BLANK)}" onerror="this.onerror=null;this.src=this.dataset.fallback">`;++// A Media object is stored at full size: ask its resize ladder for the width drawn here (96 for the 36 px face, 640++// for a 320 px card picture). Any other picture address — a Live thumbnail, an avatar from another host, one already++// sized — is used unchanged. The full-size address stays in data-full: if Media cannot resize it, the picture retries++// the original once before the existing fallback.++const mediaW = (src, w) => (/^https:\/\/openvibe\.media\/o\/med_[0-9A-Za-z]+$/.test(String(src || '')) ? `${src}?w=${w >= 288 ? 640 : w >= 72 ? 160 : 96}` : src);++const img = (src, fallback, w, h) => {++ const sized = mediaW(src, w);++ const full = sized === src ? '' : ` data-full="${esc(src)}"`;++ return `<img src="${esc(sized)}" alt="" loading="lazy" decoding="async" width="${w}" height="${h}"${full} data-fallback="${esc(fallback || BLANK)}" onerror="if(this.dataset.full&&this.src!==this.dataset.full){this.src=this.dataset.full;return}this.src=this.dataset.fallback">`;++};function rightNow(a) {// Rich cards (the owner, 2026-10-28: "more compact, bigger, like YouTube… a preview… view count… the uploader and// avatar… rich content"): a 16:9 picture with its length or LIVE badge, a play button and what happens in it on hover,@@@ -133,6 +133,18 @@ assert.ok(html.includes('LIVE · 1.5k') && html.includes('Al<b>ice') && htassert.ok(html.includes('data-fallback="https://openvibe.media/a"'), 'a broken thumbnail falls back to the avatar');assert.ok(html.includes('href="https://openvibe.community/p/one"') && html.includes('href="https://openvibe.blog/@a/b"'));assert.ok(html.includes('href="https://openvibe.video/v/9"') && html.includes('<code class="now-code">FALL10</code>') && html.includes('Remote · Berlin · via Arbeitnow'), 'recordings, codes and jobs render');++// A Media object is asked for the width drawn (a 36 px face: 96; a 320 px card picture: 640), keeps the full-size++// address and retries it once before the existing fallback. Other picture hosts are used unchanged.++{++ const faceObj = 'https://openvibe.media/o/med_01M644DY8BFX4H7MTRKPGG052R';++ const shotObj = 'https://openvibe.media/o/med_01M6ACHAZPBW24T4624DD7MVFJ';++ activity._set({ live: [{ name: 'Alice', title: 'Hi', url: 'https://openvibe.live/@alice', thumb: 'https://openvibe.live/t.jpg', avatar: faceObj, viewers: 1 }], pastes: [{ title: 'Shot', url: 'https://openvibe.community/p/one', kind: 'Screenshot', shot: shotObj, avatar: '', at: null }], posts: [] });++ const page = home.render().html;++ assert.ok(page.includes(`src="${faceObj}?w=96"`) && page.includes(`data-full="${faceObj}"`), 'a 36 px face asks for 96 and keeps the original');++ assert.ok(page.includes(`src="${shotObj}?w=640"`) && page.includes(`data-full="${shotObj}"`), 'a 320 px card picture asks for 640 and keeps the original');++ assert.ok(page.includes('onerror="if(this.dataset.full&&this.src!==this.dataset.full){this.src=this.dataset.full;return}this.src=this.dataset.fallback"'), 'the original is retried once, then the fallback');++ assert.ok(page.includes('src="https://openvibe.live/t.jpg"') && !page.includes('https://openvibe.live/t.jpg?w='), 'a Live thumbnail is not a Media object and is used unchanged');++}// A phone shows one of what is new at a time (the short-pages rule, 2026-10-30): radio chips and :has(), no script, every// part still in the page; the first part present is picked, and a wider screen or a browser without :has() shows them all.{
@@ -115,7 +115,16 @@ const fmtCount = (n) => Number(n).toLocaleString('en-US');
@@ -115,7 +115,16 @@ const fmtCount = (n) => Number(n).toLocaleString('en-US');
// A picture that fails to load (a stream's first thumbnail is written a few seconds after it goes live) falls back to
// A picture that fails to load (a stream's first thumbnail is written a few seconds after it goes live) falls back to
// the avatar when there is one, else to an empty tile, never the browser's broken-image icon.
// the avatar when there is one, else to an empty tile, never the browser's broken-image icon.
const BLANK = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
const BLANK = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
-const img = (src, fallback, w, h) => `<img src="${esc(src)}" alt="" loading="lazy" decoding="async" width="${w}" height="${h}" data-fallback="${esc(fallback || BLANK)}" onerror="this.onerror=null;this.src=this.dataset.fallback">`;
+// A Media object is stored at full size: ask its resize ladder for the width drawn here (96 for the 36 px face, 640
+// for a 320 px card picture). Any other picture address — a Live thumbnail, an avatar from another host, one already
+// sized — is used unchanged. The full-size address stays in data-full: if Media cannot resize it, the picture retries
+// the original once before the existing fallback.
+const mediaW = (src, w) => (/^https:\/\/openvibe\.media\/o\/med_[0-9A-Za-z]+$/.test(String(src || '')) ? `${src}?w=${w >= 288 ? 640 : w >= 72 ? 160 : 96}` : src);
+const img = (src, fallback, w, h) => {
+ const sized = mediaW(src, w);
+ const full = sized === src ? '' : ` data-full="${esc(src)}"`;
+ return `<img src="${esc(sized)}" alt="" loading="lazy" decoding="async" width="${w}" height="${h}"${full} data-fallback="${esc(fallback || BLANK)}" onerror="if(this.dataset.full&&this.src!==this.dataset.full){this.src=this.dataset.full;return}this.src=this.dataset.fallback">`;
+};
function rightNow(a) {
function rightNow(a) {
// Rich cards (the owner, 2026-10-28: "more compact, bigger, like YouTube… a preview… view count… the uploader and
// Rich cards (the owner, 2026-10-28: "more compact, bigger, like YouTube… a preview… view count… the uploader and
// avatar… rich content"): a 16:9 picture with its length or LIVE badge, a play button and what happens in it on hover,
// avatar… rich content"): a 16:9 picture with its length or LIVE badge, a play button and what happens in it on hover,
@@ -133,6 +133,18 @@ assert.ok(html.includes('LIVE · 1.5k') && html.includes('Al<b>ice') && ht
@@ -133,6 +133,18 @@ assert.ok(html.includes('LIVE · 1.5k') && html.includes('Al<b>ice') && ht
assert.ok(html.includes('data-fallback="https://openvibe.media/a"'), 'a broken thumbnail falls back to the avatar');
assert.ok(html.includes('data-fallback="https://openvibe.media/a"'), 'a broken thumbnail falls back to the avatar');
assert.ok(html.includes('href="https://openvibe.community/p/one"') && html.includes('href="https://openvibe.blog/@a/b"'));
assert.ok(html.includes('href="https://openvibe.community/p/one"') && html.includes('href="https://openvibe.blog/@a/b"'));
assert.ok(html.includes('href="https://openvibe.video/v/9"') && html.includes('<code class="now-code">FALL10</code>') && html.includes('Remote · Berlin · via Arbeitnow'), 'recordings, codes and jobs render');
assert.ok(html.includes('href="https://openvibe.video/v/9"') && html.includes('<code class="now-code">FALL10</code>') && html.includes('Remote · Berlin · via Arbeitnow'), 'recordings, codes and jobs render');
+// A Media object is asked for the width drawn (a 36 px face: 96; a 320 px card picture: 640), keeps the full-size
+// address and retries it once before the existing fallback. Other picture hosts are used unchanged.
+{
+ const faceObj = 'https://openvibe.media/o/med_01M644DY8BFX4H7MTRKPGG052R';
+ const shotObj = 'https://openvibe.media/o/med_01M6ACHAZPBW24T4624DD7MVFJ';
+ activity._set({ live: [{ name: 'Alice', title: 'Hi', url: 'https://openvibe.live/@alice', thumb: 'https://openvibe.live/t.jpg', avatar: faceObj, viewers: 1 }], pastes: [{ title: 'Shot', url: 'https://openvibe.community/p/one', kind: 'Screenshot', shot: shotObj, avatar: '', at: null }], posts: [] });
+ const page = home.render().html;
+ assert.ok(page.includes(`src="${faceObj}?w=96"`) && page.includes(`data-full="${faceObj}"`), 'a 36 px face asks for 96 and keeps the original');
+ assert.ok(page.includes(`src="${shotObj}?w=640"`) && page.includes(`data-full="${shotObj}"`), 'a 320 px card picture asks for 640 and keeps the original');
+ assert.ok(page.includes('onerror="if(this.dataset.full&&this.src!==this.dataset.full){this.src=this.dataset.full;return}this.src=this.dataset.fallback"'), 'the original is retried once, then the fallback');
+ assert.ok(page.includes('src="https://openvibe.live/t.jpg"') && !page.includes('https://openvibe.live/t.jpg?w='), 'a Live thumbnail is not a Media object and is used unchanged');
+}
// A phone shows one of what is new at a time (the short-pages rule, 2026-10-30): radio chips and :has(), no script, every
// A phone shows one of what is new at a time (the short-pages rule, 2026-10-30): radio chips and :has(), no script, every
// part still in the page; the first part present is picked, and a wider screen or a browser without :has() shows them all.
// part still in the page; the first part present is picked, and a wider screen or a browser without :has() shows them all.
{
{