openvibers/openvibe.network Commits

openvibe.my/@handle: date each video; leave out avatar-upload screenshots (#680)

OP OpenVibers committed 5 days ago · 3f0fa4e6fc · 2 files, +18 −7

On the live page JapaneseOldGuy's three recordings all read "JapaneseOldGuy's Stream" and two "Avatar upload"
screenshot pastes showed as shared pastes. Each video now says its day (Video · Nov 7, 2026), and a screenshot paste
an avatar upload left behind with its default title is not listed (the fetch reads ten to still show three).

Changes

ddiff --git a/server/profile/public.js b/server/profile/public.js
iindex 74e33bae..b2c4f6e4 100644
---- a/server/profile/public.js
++++ b/server/profile/public.js
@@@ -227,13 +227,17 @@ function createPublicProfiles({ db, selfToken = () => null, inventoryUrl = 'http
// Video's watch page by its public id (Track U); /v/<number> 301s there for a recording without one yet.
title: String(v.title || v.name).slice(0, 100), url: /^[A-Za-z0-9_-]{11}$/.test(String(v.watch_id || ''))
? `https://openvibe.video/watch?v=${v.watch_id}` : `https://openvibe.video/v/${encodeURIComponent(v.id || v.vod_id)}`,
++ // The date tells apart recordings that share a title ("JapaneseOldGuy's Stream" three times).
++ date: Number.isFinite(Date.parse(v.created_at)) ? new Date(v.created_at).toISOString().slice(0, 10) : null,
})),
};
}
async function communityPlaces(username) {
-- const body = await publicJson(`${communityBase}/api/pastes/by-user/${encodeURIComponent(username)}?limit=3`);
-- return (Array.isArray(body.pastes) ? body.pastes : []).filter((p) => p && p.visibility === 'public' && typeof p.slug === 'string' && /^[A-Za-z0-9_-]{1,120}$/.test(p.slug) && typeof p.title === 'string' && p.title).slice(0, 3).map((p) => ({
++ const body = await publicJson(`${communityBase}/api/pastes/by-user/${encodeURIComponent(username)}?limit=10`);
++ return (Array.isArray(body.pastes) ? body.pastes : []).filter((p) => p && p.visibility === 'public' && typeof p.slug === 'string' && /^[A-Za-z0-9_-]{1,120}$/.test(p.slug) && typeof p.title === 'string' && p.title
++ // A screenshot an avatar upload left behind keeps its default title: not something the person shared.
++ && !(p.type === 'screenshot' && p.title === 'Avatar upload')).slice(0, 3).map((p) => ({
title: String(p.title).slice(0, 100), url: `${COMMUNITY_SITE}/p/${encodeURIComponent(p.slug)}`,
}));
}
@@@ -375,10 +379,17 @@ function art(i, size = 'md') {
}
const rarityBadge = (r) => `<span class="pf-rarity r-${esc(r)}">${esc(RARITY_LABEL[r] || r)}</span>`;
++/** "Nov 7, 2026" from an ISO day, in the page's own words (no locale surprises on the server). */
++function monthDay(day) {
++ const [y, m, d] = String(day).split('-').map(Number);
++ const names = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
++ return names[m - 1] ? `${names[m - 1]} ${d}, ${y}` : '';
++}
++
function publicPlacesHtml(places) {
const cards = [];
if (places.live) cards.push(`<li><a href="${esc(places.live.url)}"><strong>${esc(places.live.title)}</strong>${places.live.is_live ? '<span class="pf-live">Live now</span>' : '<span>Live channel</span>'}</a></li>`);
-- for (const video of places.videos || []) cards.push(`<li><a href="${esc(video.url)}"><strong>${esc(video.title)}</strong><span>Video</span></a></li>`);
++ for (const video of places.videos || []) cards.push(`<li><a href="${esc(video.url)}"><strong>${esc(video.title)}</strong><span>Video${video.date ? ` · ${esc(monthDay(video.date))}` : ''}</span></a></li>`);
for (const space of places.spaces || []) cards.push(`<li><a href="${esc(space.url)}"><strong>${esc(space.title)}</strong><span>Published site</span></a></li>`);
for (const paste of places.community || []) cards.push(`<li><a href="${esc(paste.url)}"><strong>${esc(paste.title)}</strong><span>Community paste</span></a></li>`);
if (!cards.length) return '';
ddiff --git a/test/public-profile.test.js b/test/public-profile.test.js
iindex 86444339..2ac9c407 100644
---- a/test/public-profile.test.js
++++ b/test/public-profile.test.js
@@@ -62,8 +62,8 @@ const INSTANCES = [
const json = (s, o) => { res.writeHead(s, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(o)); };
if (down) return json(503, { code: 'down' });
if (u.pathname === '/api/streams/channel/Ana/live') return json(200, { channel: { username: 'Ana', display_name: 'Ana' }, streams: [{ id: 9 }] });
-- if (u.pathname === '/api/streams/channel/Ana') return json(200, { channel: { username: 'Ana', display_name: 'Ana' }, vods: [{ id: 'vod_public', watch_id: 'tdLVs5wad4A', title: 'First video', visibility: 'public' }, { id: 'vod_private', title: 'Private video', visibility: 'private' }] });
-- if (u.pathname === '/api/pastes/by-user/Ana') return json(200, { pastes: [{ slug: 'first-paste', title: 'A public note', visibility: 'public' }, { slug: 'hidden-paste', title: 'Hidden note', visibility: 'unlisted' }] });
++ if (u.pathname === '/api/streams/channel/Ana') return json(200, { channel: { username: 'Ana', display_name: 'Ana' }, vods: [{ id: 'vod_public', watch_id: 'tdLVs5wad4A', title: 'First video', visibility: 'public', created_at: '2026-11-07T20:15:00Z' }, { id: 'vod_private', title: 'Private video', visibility: 'private' }] });
++ if (u.pathname === '/api/pastes/by-user/Ana') return json(200, { pastes: [{ slug: 'avatar-shot', title: 'Avatar upload', type: 'screenshot', visibility: 'public' }, { slug: 'first-paste', title: 'A public note', visibility: 'public' }, { slug: 'hidden-paste', title: 'Hidden note', visibility: 'unlisted' }] });
if (u.pathname === '/spaces') {
res.writeHead(200, { 'Content-Type': 'text/html' });
return res.end('<article class="sp-card"><a class="sp-card-main" href="/spaces/ana-site"><span class="sp-card-badge is-live">Published</span><h3>Ana Site</h3><p>Public site</p></a><footer><span>by @Ana</span></footer></article><article class="sp-card"><a class="sp-card-main" href="/spaces/hidden-site"><span class="sp-card-badge is-live">Unlisted</span><h3>Hidden Site</h3><p>Private site</p></a><footer><span>by @Ana</span></footer></article>');
@@@ -99,7 +99,7 @@ const INSTANCES = [
assert.ok(/<span class="pf-hat" title="Royal Crown" aria-hidden="true">👑<\/span>/.test(r.text), 'her hat sits on her picture');
assert.ok(!r.text.includes('<script>alert(1)</script>') && r.text.includes('Hello there'), 'the saved public-page bio is shown as text');
assert.ok(r.text.includes('href="https://example.com/"'), 'HTTPS public links render');
-- assert.ok(r.text.includes('aria-labelledby="pf-places"') && r.text.includes('Live now') && r.text.includes('First video') && r.text.includes('A public note') && r.text.includes('Ana Site'));
++ assert.ok(r.text.includes('aria-labelledby="pf-places"') && r.text.includes('Live now') && r.text.includes('First video') && r.text.includes('Video · Nov 7, 2026') && r.text.includes('A public note') && r.text.includes('Ana Site'));
assert.ok(!r.text.includes('Private video') && !r.text.includes('Hidden note') && !r.text.includes('Hidden Site'), 'private or unlisted places are omitted');
assert.ok(r.text.includes('Sparkles &lt;b&gt;') && !r.text.includes('Sparkles <b>'), 'item names are escaped');
assert.ok(r.text.includes('id="pf-wearing"') && /pf-worn r-legendary[\s\S]*Royal Crown/.test(r.text), 'the crown is in the showcase');
@@@ -162,7 +162,7 @@ const INSTANCES = [
assert.strictEqual(JSON.parse(r.text).person.places.live.is_live, true);
assert.deepStrictEqual(JSON.parse(r.text).person.places.videos.map((v) => v.title), ['First video']);
assert.deepStrictEqual(JSON.parse(r.text).person.places.videos.map((v) => v.url), ['https://openvibe.video/watch?v=tdLVs5wad4A'], 'a video links to its watch page');
-- assert.deepStrictEqual(JSON.parse(r.text).person.places.community.map((v) => v.title), ['A public note']);
++ assert.deepStrictEqual(JSON.parse(r.text).person.places.community.map((v) => v.title), ['A public note'], 'an avatar upload screenshot is not a shared paste');
assert.deepStrictEqual(JSON.parse(r.text).person.places.spaces.map((v) => v.title), ['Ana Site']);
const person = JSON.parse(r.text).person;
r = await get('/api/v1/people/@ana/page');