openvibers/openvibe.network Commits

On OpenVibe now: one row per thing, the window said once (#783)

OP OpenVibers committed 3 days ago · 0e61ebe4a2 · 3 files, +28 −8

Rows fold by title and person, not by address: a channel's streams each
have their own page, so Trending showed one channel four times. A ranked
card says "most watched this week" once in its head instead of on every
row, and a quiet card's line sits muted beside the site.

Changes

ddiff --git a/public/css/my.css b/public/css/my.css
iindex 6cef0a0b..f66f0480 100644
---- a/public/css/my.css
++++ b/public/css/my.css
@@@ -616,7 +616,8 @@ button.td-stat:hover span { color: var(--accent-light); }
.td-net-h { display: flex; align-items: center; gap: 10px; padding-bottom: 6px; }
.td-net-ht { display: grid; flex: 1; min-width: 0; }
.td-net-ht b { font-size: 15px; font-weight: 800; }
--.td-net-ht small { font-size: 12px; color: var(--text-muted); }
++.td-net-ht small { font-size: 12px; color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
++.td-net-quiet { opacity: .75; font-style: italic; }
.td-net-card[data-site="media-hub"] .td-site-ic { color: #f472b6; background: color-mix(in srgb, #ec4899 16%, transparent); }
.td-net-card[data-site="games"] .td-site-ic { color: #4ade80; background: color-mix(in srgb, #22c55e 16%, transparent); }
.td-net-card[data-site="live"] .td-site-ic { color: #f87171; background: color-mix(in srgb, #ef4444 16%, transparent); }
ddiff --git a/public/js/my/today.js b/public/js/my/today.js
iindex 23952fed..4021beb6 100644
---- a/public/js/my/today.js
++++ b/public/js/my/today.js
@@@ -167,29 +167,38 @@ function renderNet(frame) {
}).sort((a, b) => b.rank - a.rank).slice(0, 3);
wrap.replaceChildren(...cards.map((c) => {
const site = TD_SITES[c.site] || [c.site || 'OpenVibe', 'fa-globe', null];
++ // One row per thing: the same title by the same person folds into one, whatever address each copy has (a
++ // channel's streams each have their own page: "iZoR_One's Stream" four times was one channel, 2026-11-10).
const grouped = new Map();
for (const row of c.rows.sort((a, b) => (a.ageMs ?? Infinity) - (b.ageMs ?? Infinity))) {
-- const key = `${row.title}\u0000${row.person || row.author || row.url}`;
++ const key = `${String(row.title || '').trim().toLowerCase()}\u0000${row.person || row.author || ''}`;
const old = grouped.get(key);
if (old) old.count++;
else grouped.set(key, { ...row, count: 1 });
}
const stale = c.newest !== Infinity && c.newest > 7 * 86400000;
const visibleRows = [...grouped.values()].slice(0, stale ? 2 : 5);
++ // A ranked card (no row has an age: "most watched this week") says its window once, in its head, not on every row.
++ const ranked = c.newest === Infinity;
++ const streams = /^(live|media-hub|video)$/.test(String(c.site || ''));
const list = tdEl('ol', 'td-net-list', visibleRows.map((it, i) => {
const src = it.image && tdSafeUrl(it.image.src);
-- const when = it.ageMs == null ? (c.window || 'most watched this week') : String(it.meta || '').match(/\d+\s*(?:m|min|h|d|w)\s*ago/i)?.[0] || 'recently';
-- const detail = [it.count > 1 ? `${it.count} streams this week` : '', when].filter(Boolean).join(' · ');
++ const when = ranked ? '' : String(it.meta || '').match(/\d+\s*(?:m|min|h|d|w)\s*ago/i)?.[0] || 'recently';
++ const times = it.count > 1 ? (streams ? `${it.count} streams this week` : `${it.count} times`) : '';
++ const detail = [times, when].filter(Boolean).join(' · ');
return tdEl('li', '', [tdEl('a', 'td-net-row', [
tdEl('span', 'td-net-i', [String(i + 1)]),
src ? tdEl('img', 'td-net-thumb', null, { src, alt: '', width: 64, height: 36, loading: 'lazy', decoding: 'async' }) : null,
-- tdEl('span', 'td-net-t', [tdEl('b', '', [String(it.title || '')]), tdEl('small', '', [detail])]),
++ tdEl('span', 'td-net-t', [tdEl('b', '', [String(it.title || '')]), detail ? tdEl('small', '', [detail]) : null]),
], { href: tdSafeUrl(it.url) })]);
}));
const more = c.more && tdSafeUrl(c.more.url) ? tdEl('a', 'td-more', [String(c.more.label || 'More')], { href: tdSafeUrl(c.more.url) }) : null;
return tdEl('section', 'td-box td-net-card', [
-- tdEl('div', 'td-net-h', [tdEl('span', 'td-site-ic', [tdIcon(site[1])]), tdEl('span', 'td-net-ht', [tdEl('b', '', [String(c.title || site[0])]), tdEl('small', '', [site[0]])]), more]),
-- stale ? tdEl('small', '', [`quiet for ${Math.floor(c.newest / 86400000)} days`]) : null,
++ // The head's second line: the site, then the ranking window, or for a card nothing new reached in a week how long
++ // it has been quiet (muted: it explains why the card is short; it is not news).
++ tdEl('div', 'td-net-h', [tdEl('span', 'td-site-ic', [tdIcon(site[1])]), tdEl('span', 'td-net-ht', [tdEl('b', '', [String(c.title || site[0])]),
++ tdEl('small', '', [site[0], ranked ? tdEl('span', 'td-net-when', [` · ${c.window || 'most watched this week'}`]) : null,
++ stale ? tdEl('span', 'td-net-quiet', [` · quiet for ${Math.floor(c.newest / 86400000)} days`]) : null])]), more]),
list,
], { 'data-site': String(c.site || '') });
}));
ddiff --git a/test/my-today.test.js b/test/my-today.test.js
iindex 98df0c18..8ce16b10 100644
---- a/test/my-today.test.js
++++ b/test/my-today.test.js
@@@ -121,13 +121,23 @@ assert.match(CSS, /@media \(min-width:1000px\)\{[\s\S]*\.settings\{display:grid;
const ordered = p.ids['td-net-cards'].children;
assert.deepStrictEqual(ordered.map((c) => c.attrs['data-site']), ['live', 'media-hub', 'community'], 'live leads, then fresh video, then stale forum');
assert.match(ordered[1].textContent, /1d ago/, 'a row states its age');
-- assert.match(ordered[2].textContent, /quiet for 18 days/, 'stale cards say why they are quiet');
++ assert.match(ordered[2].textContent, /quiet for 18 days/, 'stale cards say why they are quiet, in the head');
++ assert.strictEqual(ordered[2].all((c) => c.className === 'td-net-quiet').length, 1, 'muted, beside the site');
assert.strictEqual(ordered[2].all((c) => c.tagName === 'LI').length, 1, 'stale card keeps its newest row');
p.sandbox.renderNet({ cards: [{ id: 'streams', site: 'live', title: 'Most watched', items: [
{ title: 'Same stream', url: 'https://openvibe.live/@a', meta: '1d ago' },
{ title: 'Same stream', url: 'https://openvibe.live/@a', meta: '2d ago' },
] }] });
assert.match(p.ids['td-net-cards'].textContent, /2 streams this week/, 'same title and person fold into one row');
++ // A ranked card (no ages): each copy of a stream has its own address and still folds; the window is said once, in the head.
++ p.sandbox.renderNet({ cards: [{ id: 'trend', site: 'media-hub', title: 'Trending', window: 'most watched this week', items: [
++ { title: "iZoR_One's Stream", url: 'https://openvibe.video/v/1' }, { title: "iZoR_One's Stream", url: 'https://openvibe.video/v/2' },
++ { title: 'LOTFX Live Trading', url: 'https://openvibe.video/v/3' }, { title: "iZoR_One's Stream", url: 'https://openvibe.video/v/4' },
++ ] }] });
++ const trend = p.ids['td-net-cards'].children[0];
++ assert.strictEqual(trend.all((c) => c.tagName === 'LI').length, 2, 'four rows of two things: two rows');
++ assert.strictEqual((trend.textContent.match(/most watched this week/g) || []).length, 1, 'the window once, in the head');
++ assert.match(trend.textContent, /3 streams this week/, 'the folded row says how many');
p.sandbox.renderNet(null);
assert.strictEqual(p.ids['td-net'].hidden, true, 'no cards: the block stays out');