openvibers/openvibe.live Commits

An offline channel offers an alert when it goes live, through OpenVibe.Watch (#585)

OP OpenVibers committed 10 days ago · 8fb3f1410c · 4 files, +58 −0

Live's follow notifies people on Live. Watch could already alert anyone when a channel goes live (the
live.stream.started event: an email, a webhook, or an agent woken up), but nothing on Live led there. Beside the
follower count of an offline channel, 'Get an alert' opens Watch's form filled in for that channel. Not on one's
own channel.

Changes

ddiff --git a/public/css/features/channel.css b/public/css/features/channel.css
iindex 8c2090ea..2a500558 100644
---- a/public/css/features/channel.css
++++ b/public/css/features/channel.css
@@@ -1111,6 +1111,11 @@ button.viewer-badge:focus-visible { outline: 2px solid #93c5fd; outline-offset:
.ch-meta { display: flex; flex-wrap: wrap; gap: 6px 12px; align-items: center; font-size: 0.85rem; }
/* Each item stays on one line; on a phone the row wraps instead ("22 / followers" and "IRL · / inferred" did). */
.ch-follower-count { color: var(--text-muted); white-space: nowrap; }
++/* "Get an alert" (OpenVibe.Watch) sits in the same quiet row: a link, not a second button beside Follow. */
++.ch-watch-alert { color: var(--text-muted); text-decoration: none; white-space: nowrap; }
++.ch-watch-alert:hover, .ch-watch-alert:focus-visible { color: var(--accent); }
++.ch-watch-alert[hidden] { display: none; }
++@media (pointer: coarse) { .ch-watch-alert { position: relative; } .ch-watch-alert::after { content: ''; position: absolute; left: -4px; right: -4px; top: -12px; bottom: -12px; } }
.ch-meta .ch-category-badge { white-space: nowrap; }
.ch-actions { flex-shrink: 0; }
ddiff --git a/public/fragments/channel.html b/public/fragments/channel.html
iindex 4fc09799..25231c41 100644
---- a/public/fragments/channel.html
++++ b/public/fragments/channel.html
@@@ -252,6 +252,7 @@
<div class="ch-next" id="ch-next-offline" hidden></div>
<div class="ch-meta">
<span class="ch-follower-count" id="ch-follower-count-offline">0 followers</span>
++ <a class="ch-watch-alert" id="ch-watch-offline" href="https://openvibe.watch/" target="_blank" rel="noopener" hidden><i class="fa-solid fa-bell" aria-hidden="true"></i> Get an alert</a>
<a class="ch-game-badge" id="ch-game-badge-offline" href="https://openvibe.games/" target="_blank" rel="noopener" hidden></a>
<span class="ch-category-badge badge" id="ch-category-badge-offline">Offline</span>
</div>
ddiff --git a/public/js/app-channel.js b/public/js/app-channel.js
iindex 260c0adf..a81de18c 100644
---- a/public/js/app-channel.js
++++ b/public/js/app-channel.js
@@@ -947,6 +947,7 @@ async function loadChannelPage(username, managedStreamRef = null, legacySessionI
if (window.ChannelSocials) ChannelSocials.pills(document.getElementById('ch-socials-offline'), ch.social_links || []);
_loadNextStream(ch.username);
document.getElementById('ch-follower-count-offline').textContent = `${ch.follower_count || 0} followers`;
++ _setWatchAlert(document.getElementById('ch-watch-offline'), ch.username);
_loadGameBadge(ch.username);
_setCategoryBadge(document.getElementById('ch-category-badge-offline'), ch.ai_category || ch.category || 'Offline', !!ch.ai_category);
setupFollowBtn(document.getElementById('ch-btn-follow-offline'));
@@@ -3253,6 +3254,21 @@ function _wireAboutDrag() {
});
}
++/**
++ * "Get an alert" beside the follower count while a channel is offline: OpenVibe.Watch's form, filled in for this
++ * channel going live (the live.stream.started event, nothing polled). It reaches people where Live's own follow cannot:
++ * an email, a webhook, or one of their agents woken up. Not on one's own channel.
++ */
++function _setWatchAlert(a, username) {
++ if (!a) return;
++ const own = !!(typeof currentUser !== 'undefined' && currentUser && currentUser.username === username);
++ if (own || !username) { a.hidden = true; return; }
++ const q = new URLSearchParams({ template: 'event', event: 'live.stream.started', event_filter: username, name: `@${username} goes live` });
++ a.href = `https://openvibe.watch/watches/new?${q}`;
++ a.title = `An alert when @${username} goes live: by email, a webhook or your agent (OpenVibe.Watch)`;
++ a.hidden = false;
++}
++
// Render a streamer's customizable offline screen into #ch-offline-screen.
// image/video → served asset; html → sandboxed iframe (no same-origin, so the
// streamer's markup can't touch viewers' session). Falls back to a tasteful default.
ddiff --git a/test/channel-watch-alert.test.js b/test/channel-watch-alert.test.js
nnew file mode 100644
iindex 00000000..3d838819
---- /dev/null
++++ b/test/channel-watch-alert.test.js
@@@ -0,0 +1,36 @@
++'use strict';
++// "Get an alert" on an offline channel (plan §4, Agents and devices: the gap was an entry point on Live's channel
++// page): OpenVibe.Watch's form, filled in for this channel going live (live.stream.started, nothing polled), so an
++// alert can reach an email, a webhook or an agent. Hidden on one's own channel and until the channel is known.
++const assert = require('assert');
++const fs = require('fs');
++const path = require('path');
++const vm = require('vm');
++
++const ROOT = path.join(__dirname, '..');
++const fragment = fs.readFileSync(path.join(ROOT, 'public/fragments/channel.html'), 'utf8');
++assert.match(fragment, /<a class="ch-watch-alert" id="ch-watch-offline" href="https:\/\/openvibe\.watch\/" target="_blank" rel="noopener" hidden>/, 'the link is in the offline header, hidden until filled');
++
++const src = fs.readFileSync(path.join(ROOT, 'public/js/app-channel.js'), 'utf8');
++const fn = /function _setWatchAlert\(a, username\) \{[\s\S]*?\n\}/.exec(src);
++assert.ok(fn, '_setWatchAlert exists');
++assert.match(src, /_setWatchAlert\(document\.getElementById\('ch-watch-offline'\), ch\.username\);/, 'the offline header fills it');
++
++const run = (viewer, username) => {
++ const a = { hidden: true, href: 'https://openvibe.watch/', title: '' };
++ const ctx = { currentUser: viewer, URLSearchParams };
++ vm.createContext(ctx);
++ vm.runInContext(`${fn[0]}\n_setWatchAlert(a, ${JSON.stringify(username)});`, Object.assign(ctx, { a }));
++ return a;
++};
++
++const a = run({ username: 'someone' }, 'goosely');
++assert.strictEqual(a.hidden, false);
++const u = new URL(a.href);
++assert.strictEqual(u.origin + u.pathname, 'https://openvibe.watch/watches/new');
++assert.deepStrictEqual(Object.fromEntries(u.searchParams), { template: 'event', event: 'live.stream.started', event_filter: 'goosely', name: '@goosely goes live' });
++assert.match(a.title, /@goosely goes live: by email, a webhook or your agent/);
++assert.strictEqual(run(null, 'goosely').hidden, false, 'signed out too: Watch asks them to sign in');
++assert.strictEqual(run({ username: 'goosely' }, 'goosely').hidden, true, 'not on one\'s own channel');
++assert.strictEqual(run(null, '').hidden, true, 'not before the channel is known');
++console.log('channel watch alert: all checks passed');