openvibers/openvibe.chat Commits
A live presence link opens the channel, not a section under it (#491)
Changes
@@@ -67,9 +67,19 @@};function presenceState(p) { if (!p) return null; var s = p.live ? 'live' : String(p.state || ''); return PRESENCE[s] ? s : null; }function presenceLine(p) { var s = presenceState(p); return s ? PRESENCE[s].label + (p.status_text ? ' \u00b7 ' + String(p.status_text).slice(0, 80) : '') : ''; }++ // A channel link never carries a section: openvibe.live/@name#about is the channel page (ask 285).++ function channelHref(href) {++ if (typeof href !== 'string') return href;++ var u; try { u = new URL(href, 'https://openvibe.live'); } catch (_) { return href; }++ if (u.origin !== 'https://openvibe.live' || !/^\/@[A-Za-z0-9_]{3,24}\/?$/.test(u.pathname)) return href;++ var clean = '/@' + u.pathname.slice(2).replace(/\/$/, '');++ return /^https:\/\//i.test(href) ? 'https://openvibe.live' + clean : clean;++ }function presenceHref(p) {var href = p && (p.channel_url || p.url);-- return typeof href === 'string' && (/^https:\/\//i.test(href) || /^\/(?!\/)/.test(href)) ? href : null;++ if (typeof href !== 'string') return null;++ href = channelHref(href);++ return (/^https:\/\//i.test(href) || /^\/(?!\/)/.test(href)) ? href : null;}function setPresenceLine(box, p, username) {if (!box) return;@@@ -66,12 +66,26 @@ function presenceOrder(presence) {const state = presenceState(presence);return state === 'online' || state === 'live' ? 0 : state === 'away' || state === 'dnd' ? 1 : 2;}++/**++ * A channel link never carries a section: openvibe.live/@name#about is the channel page, and the address a live++ * presence link hands over is /@name (ask 285). A relative /@name stays relative; any other link is untouched.++ */++function channelHref(href) {++ if (typeof href !== 'string') return href;++ let u;++ try { u = new URL(href, 'https://openvibe.live'); } catch { return href; }++ if (u.origin !== 'https://openvibe.live' || !/^\/@[A-Za-z0-9_]{3,24}\/?$/.test(u.pathname)) return href;++ const clean = `/@${u.pathname.slice(2).replace(/\/$/, '')}`;++ return /^https:\/\//i.test(href) ? `https://openvibe.live${clean}` : clean;++}++/** "Online", "Live now" and — when there is one — " · their status line". Nothing for offline. */function presenceLine(presence, { liveHref = null } = {}) {const state = presenceState(presence);if (!state) return '';const label = esc(PRESENCE[state].label);-- const href = typeof liveHref === 'string' && (/^https:\/\//i.test(liveHref) || /^\/(?!\/)/.test(liveHref)) ? liveHref : null;++ const cleaned = channelHref(liveHref);++ const href = typeof cleaned === 'string' && (/^https:\/\//i.test(cleaned) || /^\/(?!\/)/.test(cleaned)) ? cleaned : null;const shown = state === 'live' && href ? `<a href="${esc(href)}">${label}</a>` : label;return `${shown}${presence.status_text ? ` · ${esc(String(presence.status_text).slice(0, 80))}` : ''}`;}@@@ -41,6 +41,10 @@ t('the state line and the member-list order', () => {assert.strictEqual(ui.presenceLine({ state: 'dnd' }), 'Do not disturb');assert.strictEqual(ui.presenceLine({ state: 'online', live: true }), 'Live now');assert.strictEqual(ui.presenceLine({ state: 'online', live: true }, { liveHref: 'https://openvibe.live/@alice' }), '<a href="https://openvibe.live/@alice">Live now</a>');++ assert.strictEqual(ui.presenceLine({ state: 'online', live: true }, { liveHref: 'https://openvibe.live/@alice#about' }), '<a href="https://openvibe.live/@alice">Live now</a>', 'a section is not the channel');++ assert.strictEqual(ui.presenceLine({ state: 'online', live: true }, { liveHref: 'https://openvibe.live/@alice?section=videos' }), '<a href="https://openvibe.live/@alice">Live now</a>', 'nor a section query');++ assert.strictEqual(ui.presenceLine({ state: 'online', live: true }, { liveHref: '/@alice#about' }), '<a href="/@alice">Live now</a>', 'a relative channel link stays relative');++ assert.strictEqual(ui.presenceLine({ state: 'online', live: true }, { liveHref: 'https://openvibe.live/vod/5#t=10' }), '<a href="https://openvibe.live/vod/5#t=10">Live now</a>', 'other Live pages keep their fragment');assert.strictEqual(ui.presenceLine({ state: 'online' }, { liveHref: 'javascript:alert(1)' }), 'Online');assert.strictEqual(ui.presenceLine({ state: 'offline' }), '');assert.strictEqual(ui.presenceLine({ state: 'invisible' }), '');@@@ -50,6 +54,17 @@ t('the state line and the member-list order', () => {);});++t('the client draws the same section-free live channel link', () => {++ const chat = fs.readFileSync(path.join(__dirname, '..', 'public', 'web', 'chat.js'), 'utf8');++ const fn = /function channelHref\(href\) \{[\s\S]*?\n \}/.exec(chat);++ assert.ok(fn, 'channelHref is in public/web/chat.js');++ const run = new Function('URL', `${fn[0]}; return channelHref;`)(URL);++ assert.strictEqual(run('https://openvibe.live/@alice#about'), 'https://openvibe.live/@alice');++ assert.strictEqual(run('https://openvibe.live/@alice?section=videos'), 'https://openvibe.live/@alice');++ assert.strictEqual(run('/@alice#clips'), '/@alice');++ assert.strictEqual(run('https://openvibe.live/vod/5#t=10'), 'https://openvibe.live/vod/5#t=10');++});++t('the dot CSS copies Shared\'s .ovp-dot', () => {const css = fs.readFileSync(path.join(__dirname, '..', 'public', 'web', 'chat.css'), 'utf8');for (const rule of [
@@ -67,9 +67,19 @@
@@ -67,9 +67,19 @@
};
};
function presenceState(p) { if (!p) return null; var s = p.live ? 'live' : String(p.state || ''); return PRESENCE[s] ? s : null; }
function presenceState(p) { if (!p) return null; var s = p.live ? 'live' : String(p.state || ''); return PRESENCE[s] ? s : null; }
function presenceLine(p) { var s = presenceState(p); return s ? PRESENCE[s].label + (p.status_text ? ' \u00b7 ' + String(p.status_text).slice(0, 80) : '') : ''; }
function presenceLine(p) { var s = presenceState(p); return s ? PRESENCE[s].label + (p.status_text ? ' \u00b7 ' + String(p.status_text).slice(0, 80) : '') : ''; }
+ // A channel link never carries a section: openvibe.live/@name#about is the channel page (ask 285).
+ function channelHref(href) {
+ if (typeof href !== 'string') return href;
+ var u; try { u = new URL(href, 'https://openvibe.live'); } catch (_) { return href; }
+ if (u.origin !== 'https://openvibe.live' || !/^\/@[A-Za-z0-9_]{3,24}\/?$/.test(u.pathname)) return href;
+ var clean = '/@' + u.pathname.slice(2).replace(/\/$/, '');
+ return /^https:\/\//i.test(href) ? 'https://openvibe.live' + clean : clean;
+ }
function presenceHref(p) {
function presenceHref(p) {
var href = p && (p.channel_url || p.url);
var href = p && (p.channel_url || p.url);
- return typeof href === 'string' && (/^https:\/\//i.test(href) || /^\/(?!\/)/.test(href)) ? href : null;
+ if (typeof href !== 'string') return null;
+ href = channelHref(href);
+ return (/^https:\/\//i.test(href) || /^\/(?!\/)/.test(href)) ? href : null;
}
}
function setPresenceLine(box, p, username) {
function setPresenceLine(box, p, username) {
if (!box) return;
if (!box) return;
@@ -66,12 +66,26 @@ function presenceOrder(presence) {
@@ -66,12 +66,26 @@ function presenceOrder(presence) {
const state = presenceState(presence);
const state = presenceState(presence);
return state === 'online' || state === 'live' ? 0 : state === 'away' || state === 'dnd' ? 1 : 2;
return state === 'online' || state === 'live' ? 0 : state === 'away' || state === 'dnd' ? 1 : 2;
}
}
+/**
+ * A channel link never carries a section: openvibe.live/@name#about is the channel page, and the address a live
+ * presence link hands over is /@name (ask 285). A relative /@name stays relative; any other link is untouched.
+ */
+function channelHref(href) {
+ if (typeof href !== 'string') return href;
+ let u;
+ try { u = new URL(href, 'https://openvibe.live'); } catch { return href; }
+ if (u.origin !== 'https://openvibe.live' || !/^\/@[A-Za-z0-9_]{3,24}\/?$/.test(u.pathname)) return href;
+ const clean = `/@${u.pathname.slice(2).replace(/\/$/, '')}`;
+ return /^https:\/\//i.test(href) ? `https://openvibe.live${clean}` : clean;
+}
+
/** "Online", "Live now" and — when there is one — " · their status line". Nothing for offline. */
/** "Online", "Live now" and — when there is one — " · their status line". Nothing for offline. */
function presenceLine(presence, { liveHref = null } = {}) {
function presenceLine(presence, { liveHref = null } = {}) {
const state = presenceState(presence);
const state = presenceState(presence);
if (!state) return '';
if (!state) return '';
const label = esc(PRESENCE[state].label);
const label = esc(PRESENCE[state].label);
- const href = typeof liveHref === 'string' && (/^https:\/\//i.test(liveHref) || /^\/(?!\/)/.test(liveHref)) ? liveHref : null;
+ const cleaned = channelHref(liveHref);
+ const href = typeof cleaned === 'string' && (/^https:\/\//i.test(cleaned) || /^\/(?!\/)/.test(cleaned)) ? cleaned : null;
const shown = state === 'live' && href ? `<a href="${esc(href)}">${label}</a>` : label;
const shown = state === 'live' && href ? `<a href="${esc(href)}">${label}</a>` : label;
return `${shown}${presence.status_text ? ` · ${esc(String(presence.status_text).slice(0, 80))}` : ''}`;
return `${shown}${presence.status_text ? ` · ${esc(String(presence.status_text).slice(0, 80))}` : ''}`;
}
}
@@ -41,6 +41,10 @@ t('the state line and the member-list order', () => {
@@ -41,6 +41,10 @@ t('the state line and the member-list order', () => {
assert.strictEqual(ui.presenceLine({ state: 'dnd' }), 'Do not disturb');
assert.strictEqual(ui.presenceLine({ state: 'dnd' }), 'Do not disturb');
assert.strictEqual(ui.presenceLine({ state: 'online', live: true }), 'Live now');
assert.strictEqual(ui.presenceLine({ state: 'online', live: true }), 'Live now');
assert.strictEqual(ui.presenceLine({ state: 'online', live: true }, { liveHref: 'https://openvibe.live/@alice' }), '<a href="https://openvibe.live/@alice">Live now</a>');
assert.strictEqual(ui.presenceLine({ state: 'online', live: true }, { liveHref: 'https://openvibe.live/@alice' }), '<a href="https://openvibe.live/@alice">Live now</a>');
+ assert.strictEqual(ui.presenceLine({ state: 'online', live: true }, { liveHref: 'https://openvibe.live/@alice#about' }), '<a href="https://openvibe.live/@alice">Live now</a>', 'a section is not the channel');
+ assert.strictEqual(ui.presenceLine({ state: 'online', live: true }, { liveHref: 'https://openvibe.live/@alice?section=videos' }), '<a href="https://openvibe.live/@alice">Live now</a>', 'nor a section query');
+ assert.strictEqual(ui.presenceLine({ state: 'online', live: true }, { liveHref: '/@alice#about' }), '<a href="/@alice">Live now</a>', 'a relative channel link stays relative');
+ assert.strictEqual(ui.presenceLine({ state: 'online', live: true }, { liveHref: 'https://openvibe.live/vod/5#t=10' }), '<a href="https://openvibe.live/vod/5#t=10">Live now</a>', 'other Live pages keep their fragment');
assert.strictEqual(ui.presenceLine({ state: 'online' }, { liveHref: 'javascript:alert(1)' }), 'Online');
assert.strictEqual(ui.presenceLine({ state: 'online' }, { liveHref: 'javascript:alert(1)' }), 'Online');
assert.strictEqual(ui.presenceLine({ state: 'offline' }), '');
assert.strictEqual(ui.presenceLine({ state: 'offline' }), '');
assert.strictEqual(ui.presenceLine({ state: 'invisible' }), '');
assert.strictEqual(ui.presenceLine({ state: 'invisible' }), '');
@@ -50,6 +54,17 @@ t('the state line and the member-list order', () => {
@@ -50,6 +54,17 @@ t('the state line and the member-list order', () => {
);
);
});
});
+t('the client draws the same section-free live channel link', () => {
+ const chat = fs.readFileSync(path.join(__dirname, '..', 'public', 'web', 'chat.js'), 'utf8');
+ const fn = /function channelHref\(href\) \{[\s\S]*?\n \}/.exec(chat);
+ assert.ok(fn, 'channelHref is in public/web/chat.js');
+ const run = new Function('URL', `${fn[0]}; return channelHref;`)(URL);
+ assert.strictEqual(run('https://openvibe.live/@alice#about'), 'https://openvibe.live/@alice');
+ assert.strictEqual(run('https://openvibe.live/@alice?section=videos'), 'https://openvibe.live/@alice');
+ assert.strictEqual(run('/@alice#clips'), '/@alice');
+ assert.strictEqual(run('https://openvibe.live/vod/5#t=10'), 'https://openvibe.live/vod/5#t=10');
+});
+
t('the dot CSS copies Shared\'s .ovp-dot', () => {
t('the dot CSS copies Shared\'s .ovp-dot', () => {
const css = fs.readFileSync(path.join(__dirname, '..', 'public', 'web', 'chat.css'), 'utf8');
const css = fs.readFileSync(path.join(__dirname, '..', 'public', 'web', 'chat.css'), 'utf8');
for (const rule of [
for (const rule of [