openvibers/openvibe.live Commits

Channel cards name the channel by its @handle (#779)

OP OpenVibers committed 3 days ago · 8b20340fd9 · 8 files, +64 −13

Kiosk stream cards, the live notify toast, the offline discover board, the
featured stream, the content feed channel line and the home rails all opened
/@username while showing a bare display name. Each now shows @username, as
its address is. Chat authors, rosters, bylines and page titles are not
handles being named and stay as they are.

Changes

ddiff --git a/public/js/app-home.js b/public/js/app-home.js
iindex 3ad8cbac..949222ea 100644
---- a/public/js/app-home.js
++++ b/public/js/app-home.js
@@@ -1398,8 +1398,8 @@ function _renderHomePulse(p) {
identity.className = 'pulse-fact-identity';
const face = document.createElement('span');
face.className = 'pulse-fact-face';
-- const name = String(person.display_name || person.username || person.name || 'OpenVibe');
-- face.textContent = name.charAt(0).toUpperCase();
++ const name = String(person.username ? '@' + person.username : (person.display_name || person.name || 'OpenVibe'));
++ face.textContent = String(person.display_name || person.username || person.name || 'O').charAt(0).toUpperCase();
if (person.avatar_url) {
const img = document.createElement('img');
img.src = person.avatar_url;
@@@ -1522,7 +1522,7 @@ function renderRecentlyOnline(containerId, streamers, append) {
img.onerror = () => img.remove(); face.append(img);
}
const who = el('span', 'recent-streamer-who');
-- const name = el('a', 'streamer-group-name', shown);
++ const name = el('a', 'streamer-group-name', s.username ? '@' + s.username : shown);
name.href = channelHref;
const when = el('span', 'streamer-group-last-online' + (s.live ? ' is-live' : ''), s.live ? 'Live now' : timeAgo(s.last_online_at));
if (!s.live) when.title = 'Last live ' + timeAgo(s.last_online_at);
@@@ -1867,7 +1867,7 @@ function streamCardHTML(s, isLive) {
${ovLabelChips(s)}
<div class="stream-card-streamer">
${_avatarSpan(s.avatar_url, s.username, s.profile_color)}
-- ${esc(s.username || 'Anonymous')}
++ ${esc(s.username ? '@' + s.username : 'Anonymous')}
${endedAgo}
</div>
${(isLive && s.description) ? `<div class="stream-card-desc" title="Click to expand" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('expanded')">${esc(s.description)}</div>` : ''}
@@@ -2412,7 +2412,7 @@ function _renderHomeStar(data) {
</div>
<div class="star-body">
<div class="star-kicker"><i class="fa-solid fa-star"></i> ${s.rotates ? 'Star of the day' : 'Featured streamer'}</div>
-- <h3 class="star-name"><a href="${esc(path)}" onclick="return handleLinkClick(event, '${esc(path)}')">${name}</a>${foreign && lang.code === 'ja' ? '<span class="star-jp">OpenVibeの看板配信者 — ようこそ!</span>' : ''}</h3>
++ <h3 class="star-name"><a href="${esc(path)}" onclick="return handleLinkClick(event, '${esc(path)}')">${s.username ? esc('@' + s.username) : name}</a>${foreign && lang.code === 'ja' ? '<span class="star-jp">OpenVibeの看板配信者 — ようこそ!</span>' : ''}</h3>
<div class="star-chips">${chips}</div>
</div>
<div class="star-more" id="star-more" hidden>
ddiff --git a/public/js/content-feed.js b/public/js/content-feed.js
iindex 927a5f2d..32973a8e 100644
---- a/public/js/content-feed.js
++++ b/public/js/content-feed.js
@@@ -135,7 +135,7 @@
function meta(it, extra) {
const row = el('div', { class: 'cf-meta' });
if (it.channel) {
-- const who = [avatar(it.channel), el('span', { class: 'cf-name', text: it.channel.display_name || it.channel.username })];
++ const who = [avatar(it.channel), el('span', { class: 'cf-name', text: '@' + (it.channel.username || it.channel.display_name) })];
row.append(it.channel.href ? spa(it.channel.href, { class: 'cf-chan' }, ...who) : el('span', { class: 'cf-chan' }, ...who));
}
const bits = el('span', { class: 'cf-facts' });
ddiff --git a/public/js/context-cards.js b/public/js/context-cards.js
iindex 0f4a67fe..84d1bf1c 100644
---- a/public/js/context-cards.js
++++ b/public/js/context-cards.js
@@@ -41,7 +41,7 @@
sections.push(`<section class="dsc-sec dsc-sec--live"><h4><i class="fa-solid fa-circle live-dot"></i> Live right now</h4><div class="dsc-live">${d.live.slice(0, 3).map(s => `
<a class="dsc-live-card" ${go(`/@${s.username}`)}>
<span class="dsc-live-thumb">${s.thumbnail_url ? `<img src="${esc(s.thumbnail_url)}" alt="" loading="lazy">` : '<span class="cc-clip-ph"><i class="fa-solid fa-tower-broadcast"></i></span>'}<span class="dsc-live-pill">LIVE</span><span class="dsc-live-viewers"><i class="fa-solid fa-eye"></i> ${n(s.viewer_count)}</span></span>
-- <span class="dsc-live-body">${av(s, 26)}<span><b>${esc(s.display_name || s.username)}</b><small>${esc(s.title || '')}</small></span></span>
++ <span class="dsc-live-body">${av(s, 26)}<span><b>${esc('@' + (s.username || s.display_name))}</b><small>${esc(s.title || '')}</small></span></span>
</a>`).join('')}</div></section>`);
}
if ((d.clips || []).length) {
@@@ -51,7 +51,7 @@
const star = d.star ? `<a class="dsc-star" ${go(`/@${d.star.username}`)}>
<span class="dsc-star-badge">⭐ Star of the day</span>
${av(d.star, 56)}
-- <span class="dsc-star-body"><b>${esc(d.star.display_name)}${d.star.live ? ' <span class="dsc-live-pill">LIVE</span>' : ''}</b>${d.star.headline ? `<span class="dsc-star-head">${esc(d.star.headline)}</span>` : ''}${d.star.reason ? `<small>${esc(d.star.reason)}</small>` : ''}</span>
++ <span class="dsc-star-body"><b>${esc('@' + (d.star.username || d.star.display_name))}${d.star.live ? ' <span class="dsc-live-pill">LIVE</span>' : ''}</b>${d.star.headline ? `<span class="dsc-star-head">${esc(d.star.headline)}</span>` : ''}${d.star.reason ? `<small>${esc(d.star.reason)}</small>` : ''}</span>
</a>` : '';
const recaps = (d.recaps || []).length ? `<div class="dsc-recaps"><div class="dsc-recaps-head"><i class="fa-solid fa-clipboard-list"></i> Fresh after-show reports</div>${d.recaps.slice(0, 4).map(r => `
<a class="dsc-recap" ${go(`/recap/${r.stream_id}`)}>
@@@ -62,7 +62,7 @@
}
if ((d.similar || []).length) {
sections.push(`<section class="dsc-sec dsc-sec--people"><h4><i class="fa-solid fa-people-group"></i> Streamers to check out</h4><div class="dsc-people">${d.similar.slice(0, 6).map(p => `
-- <a class="dsc-person" ${go(`/@${p.username}`)}>${av(p, 44)}<b>${esc(p.display_name || p.username)}</b><small>${p.live ? '<span class="dsc-live-dot"></span> live now' : (p.last_live_at ? `live ${esc(ago(p.last_live_at))}` : '')}${p.followers ? ` · ${n(p.followers)} follower${p.followers === 1 ? '' : 's'}` : ''}</small>${p.same_category && p.category ? `<em>${esc(p.category)}</em>` : ''}</a>`).join('')}</div></section>`);
++ <a class="dsc-person" ${go(`/@${p.username}`)}>${av(p, 44)}<b>${esc('@' + (p.username || p.display_name))}</b><small>${p.live ? '<span class="dsc-live-dot"></span> live now' : (p.last_live_at ? `live ${esc(ago(p.last_live_at))}` : '')}${p.followers ? ` · ${n(p.followers)} follower${p.followers === 1 ? '' : 's'}` : ''}</small>${p.same_category && p.category ? `<em>${esc(p.category)}</em>` : ''}</a>`).join('')}</div></section>`);
}
if (!sections.length && !side) { host.innerHTML = ''; return; }
ddiff --git a/public/js/home-featured.js b/public/js/home-featured.js
iindex deafb753..9f1be7b4 100644
---- a/public/js/home-featured.js
++++ b/public/js/home-featured.js
@@@ -144,7 +144,7 @@
<h3 class="hf-title">${escText(s.title || 'Untitled Stream')}</h3>
<a class="hf-who" href="${escText(chan)}" onclick="return handleLinkClick(event, '${escText(chan)}')">
${typeof _avatarSpan === 'function' ? _avatarSpan(s.avatar_url, s.username, s.profile_color) : ''}
-- <b>${escText(s.display_name || s.username)}</b>
++ <b>${escText('@' + (s.username || s.display_name || ''))}</b>
${s.category ? `<span class="hf-tag"${s.ai_category && s.ai_category === s.category ? ' title="Category inferred by OpenVibe&#39;s AI from the stream"' : ''}>${escText(s.category)}${s.ai_category && s.ai_category === s.category ? '<span class="cat-inferred"> · inferred</span>' : ''}</span>` : ''}
${up ? `<span class="muted"><i class="fa-solid fa-clock"></i> ${escText(up)}</span>` : ''}
</a>
ddiff --git a/public/kiosk.html b/public/kiosk.html
iindex 29512155..766baeec 100644
---- a/public/kiosk.html
++++ b/public/kiosk.html
@@@ -732,7 +732,7 @@ button{font:inherit}
</div>
<div class="info">
<div class="title">${esc(s.title||"Untitled stream")}</div>
-- <div class="meta">${esc(s.display_name||s.username||"OpenVibe.Live")}</div>
++ <div class="meta">${esc("@" + (s.username||s.display_name||"OpenVibe.Live"))}</div>
</div>
</a>`;
}
ddiff --git a/public/live-notify.js b/public/live-notify.js
iindex ff7b768c..fe87c670 100644
---- a/public/live-notify.js
++++ b/public/live-notify.js
@@@ -56,7 +56,7 @@
function show(d) {
injectCSS();
var c = ensureContainer();
-- var name = d.display_name || d.username || 'Someone';
++ var name = d.username ? '@' + d.username : (d.display_name || 'Someone');
var href = BASE + '/@' + encodeURIComponent(d.username || '');
var letter = (name[0] || '?').toUpperCase();
var av = d.avatar_url
ddiff --git a/test/at-handle.test.js b/test/at-handle.test.js
nnew file mode 100644
iindex 00000000..6cc8427c
---- /dev/null
++++ b/test/at-handle.test.js
@@@ -0,0 +1,51 @@
++'use strict';
++/**
++ * A card that opens /@name is named by its @handle (the owner, 2026-11-10): the offline
++ * discover board's live cards, star card and "Streamers to check out" rows. Display names
++ * alone would read as someone else's name while the address says @username.
++ *
++ * node test/at-handle.test.js
++ */
++const assert = require('node:assert/strict');
++const fs = require('node:fs');
++const vm = require('node:vm');
++const { parseHTML } = require('linkedom');
++
++const source = fs.readFileSync(require('node:path').join(__dirname, '../public/js/context-cards.js'), 'utf8');
++const { document } = parseHTML('<div id="ch-discover"></div>');
++
++const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
++const fixture = {
++ channel: null,
++ live: [{ username: 'alice', display_name: 'Alice Display', title: 'Night shoot', viewer_count: 7, thumbnail_url: null, avatar_url: null, profile_color: null }],
++ clips: [],
++ star: { username: 'starly', display_name: 'Star Display', headline: 'Still here', reason: 'Every day', live: false, avatar_url: null, profile_color: null },
++ recaps: [],
++ similar: [{ username: 'bob', display_name: 'Bob Display', followers: 3, live: false, last_live_at: null, avatar_url: null, profile_color: null }],
++};
++const context = {
++ document,
++ window: {},
++ esc,
++ timeAgo: () => '2h ago',
++ ovImgW: null,
++ videoHref: (kind, id) => `/${kind}/${id}`,
++ handleLinkClick: () => true,
++ api: async () => fixture,
++ encodeURIComponent,
++ Number, String, JSON, Promise,
++};
++context.window = context;
++vm.createContext(context);
++vm.runInContext(source, context);
++
++(async () => {
++const host = document.getElementById('ch-discover');
++await context.window.renderOfflineDiscover('x', host);
++const html = host.innerHTML;
++for (const handle of ['@alice', '@starly', '@bob']) {
++ assert.ok(html.includes(`<b>${handle}</b>`), `a card names the channel ${handle}`);
++}
++assert.ok(!html.includes('Alice Display') && !html.includes('Star Display') && !html.includes('Bob Display'), 'no bare display name names a channel');
++process.stdout.write('at-handle (discover board): all checks passed\n');
++})().catch((err) => { console.error(err); process.exit(1); });
ddiff --git a/test/home-pulse-rail.test.js b/test/home-pulse-rail.test.js
iindex f0209955..feaf5d04 100644
---- a/test/home-pulse-rail.test.js
++++ b/test/home-pulse-rail.test.js
@@@ -17,7 +17,7 @@ context.render({ goals: [{ title: 'Do not show', current_amount: 1, target_amoun
dailyFinished: 2 });
const cards = [...document.querySelectorAll('.pulse-fact-card')];
assert.equal(cards.length, 2);
--assert.match(cards[0].textContent, /LIVE NOW.*Ann.*12.*viewers watching/s);
++assert.match(cards[0].textContent, /LIVE NOW.*@ann.*12.*viewers watching/s);
assert.match(cards[1].textContent, /TODAY'S QUEST.*2.*people finished today/s);
assert.ok(!document.body.textContent.includes('Do not show'));
assert.ok(!document.body.textContent.includes('Ben'));