openvibers/openvibe.live Commits

Live: the wallet pill catches up a channel's points on its page (#840)

OP OpenVibers committed 1 days ago · e0d3997885 · 6 files, +58 −10

Live's channel pages know their streamer only after the channel data loads, so the shared purse never asked for that channel's points. app-channel.js passes ch.user_id (the numeric user id the channel-points ledger keys a streamer by) through LiveNav.setStreamer once the page loads; every other page clears it. Shared 4.212.0's purse catch-up then shows the channel's points and their changes in the pill.

Bumps openvibe-shared to 4.212.0. shared-wallet.test.js covers the config, the numeric id, the one refresh and the clear.

Changes

ddiff --git a/package-lock.json b/package-lock.json
iindex a11bf0fc..7820665d 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -26,7 +26,7 @@
"onnxruntime-node": "^1.20.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.408.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.150.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.211.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.212.0",
"pg": "^8.23.1",
"sanitize-html": "^2.17.7",
"sdp-transform": "^3.0.0",
@@@ -3417,9 +3417,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "4.211.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.211.0",
-- "integrity": "sha512-3qMNLpByLNQPoCb9HGirVM5UTIPVYMLNshyhB185w1djaF3MLKnKahgwDpSlO3L8MAvhr+ZDzhyaedQdqTWcBQ==",
++ "version": "4.212.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.212.0",
++ "integrity": "sha512-IwF+K4ZXJ25bNJkiwmC1niAv2QvzV9yzOQmI8lBmFT96Vr9N/RacWFCLmE/8myy0M+kXE/6HEdIW2+aVpK2Bgg==",
"license": "MIT",
"dependencies": {
"clean-css": "^5.3.3",
ddiff --git a/package.json b/package.json
iindex e368c474..0f025c9d 100644
---- a/package.json
++++ b/package.json
@@@ -46,7 +46,7 @@
"onnxruntime-node": "^1.20.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.408.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.150.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.211.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.212.0",
"pg": "^8.23.1",
"sanitize-html": "^2.17.7",
"sdp-transform": "^3.0.0",
ddiff --git a/public/js/app-channel.js b/public/js/app-channel.js
iindex 54587c89..4c730251 100644
---- a/public/js/app-channel.js
++++ b/public/js/app-channel.js
@@@ -964,6 +964,9 @@ async function loadChannelPage(username, managedStreamRef = null, legacySessionI
if (isNewChannel) _popularAhead = { username: String(username).toLowerCase(), at: Date.now(), p: api(`/streams/channel/${encodeURIComponent(username)}/popular`).catch(() => null) };
const data = await api(`/streams/channel/${username}?vodLimit=${CHANNEL_VODS_PAGE_SIZE}&vodOffset=${channelVodOffset}&clipLimit=${CHANNEL_CLIPS_PAGE_SIZE}&clipOffset=${channelClipOffset}${initialVodExtra}`);
const ch = data.channel;
++ // The wallet pill shows this channel's points once they are known (Shared 4.212.0, `purse.streamer`): the
++ // channel-points ledger keys a streamer by the owner's numeric user id, ch.user_id, and only now is it here.
++ if (ch && currentChannelUsername === username && window.LiveNav) window.LiveNav.setStreamer(ch.user_id);
{ const page = document.getElementById('page-channel'); if (page) page.classList.add('ch-data'); if (page && !page.classList.contains('ch-ready')) _channelReadyTimer = setTimeout(_channelReady, 2500); }
// The tab says whose channel this is, as the server-rendered <title> did (a live stream
// replaces it with the stream's title in activateChannelStream).
ddiff --git a/public/js/app.js b/public/js/app.js
iindex b2a489dc..5637d276 100644
---- a/public/js/app.js
++++ b/public/js/app.js
@@@ -1392,6 +1392,8 @@ function showPage(page) {
if (link) link.classList.add('active');
}
if (window.LiveNav) window.LiveNav.setActive(navPage || null);
++ // The wallet pill's channel-points context belongs to a channel page (Shared 4.212.0): no channel, no streamer.
++ if (window.LiveNav && page !== 'channel') window.LiveNav.setStreamer(null);
updateNavHeroTransparency();
}
ddiff --git a/public/js/ov-navbar-live.js b/public/js/ov-navbar-live.js
iindex 8b219bf8..de5a20a3 100644
---- a/public/js/ov-navbar-live.js
++++ b/public/js/ov-navbar-live.js
@@@ -8,12 +8,19 @@
* LiveNav.sync(user) after sign-in, sign-out or a profile change
* LiveNav.setActive(page) the router calls this on every page change
* LiveNav.setLive(isLive) the red dot on "Go Live"
++ * LiveNav.setStreamer(id) a channel page's owner id, so the wallet pill catches up its channel points
*/
(function () {
'use strict';
const call = (name, ...a) => (typeof window[name] === 'function' ? window[name](...a) : undefined);
const nav = () => window.OpenVibeNavbar;
let booted = false;
++ let current = null;
++ // The purse's page context (Shared 4.212.0): a channel page passes its streamer's numeric user id, which is what the
++ // channel-points ledger calls `streamer` (Live's channel_points.streamer_id, the owner of ch.user_id); every other
++ // page passes nothing. The object is the same one on every render, so a channel that loads after the bar still
++ // reaches the wallet's next catch-up without re-rendering the bar.
++ const purse = {};
// The bar's places, most used first (the owner, 2026-10-28: "move search after the Rundown and the drop-down… reanalyze
// the nav bars… redundant options"). Search is the navbar's magnifier now (Shared 4.21: Live's results first, then all
@@@ -77,6 +84,8 @@
// The magnifier (Shared 4.21): Live's streams, recordings and clips first, Live's own search page, then OpenVibe.
search: { owner: 'live', label: 'OpenVibe.Live', page: '/search' },
wallet: true,
++ // The wallet pill's page context: a channel page's streamer, so its points and their changes are caught up.
++ purse,
// Same-origin links are routes of this single-page app.
onNavigate: (href, e) => (typeof handleLinkClick === 'function' ? handleLinkClick(e, href) : true),
// No speculation rules: the router answers same-site links, so a prefetched page was never used, and the
@@@ -92,8 +101,9 @@
let last = { live: false, page: null };
function sync(user) {
++ current = user || null;
const N = nav(); if (!N) return;
-- booted = true; N.init(config(user)); // init() merges the new config and re-renders
++ booted = true; N.init(config(current)); // init() merges the new config and re-renders
if (last.page) N.setActive(last.page);
if (last.live) N.updateLink('broadcast', { dot: true });
}
@@@ -102,6 +112,17 @@
sync,
setActive(page) { last.page = page; const N = nav(); if (N && booted) N.setActive(page); },
setLive(isLive) { last.live = !!isLive; const N = nav(); if (N && booted) N.updateLink('broadcast', { dot: !!isLive, label: isLive ? 'LIVE' : 'Go Live' }); },
++ // A channel page's data carries its owner's numeric user id only after the page has loaded; from then on the
++ // wallet's next catch-up asks for that channel's points too (Shared 4.212.0, /wallet/changes?streamer=). With
++ // null — any other page — the purse asks for the person's own balances alone.
++ setStreamer(id) {
++ const n = Number(id);
++ const next = Number.isSafeInteger(n) && n > 0 ? n : null;
++ if (next === (purse.streamer || null)) return;
++ if (next) purse.streamer = next; else delete purse.streamer;
++ const N = nav();
++ if (next && current && booted && N && typeof N.refreshWallet === 'function') N.refreshWallet();
++ },
};
// First paint: the bar appears signed out immediately; app.js calls sync() again when it knows who is here.
ddiff --git a/test/shared-wallet.test.js b/test/shared-wallet.test.js
iindex a156ec9c..6a2e1ba4 100644
---- a/test/shared-wallet.test.js
++++ b/test/shared-wallet.test.js
@@@ -3,9 +3,9 @@ const assert = require('assert');
const fs = require('fs');
const path = require('path');
const vm = require('vm');
--let config, opens = 0, refreshes = 0;
++let config, opens = 0, refreshes = 0, inits = 0;
const context = { window: { OpenVibeNavbar: {
-- init(c) { config = c; }, setActive() {}, updateLink() {},
++ init(c) { config = c; inits++; }, setActive() {}, updateLink() {},
openWallet() { opens++; }, refreshWallet() { refreshes++; },
} }, document: { readyState: 'loading', addEventListener() {} }, currentUser: null };
vm.createContext(context);
@@@ -13,6 +13,29 @@ vm.runInContext(fs.readFileSync(path.join(__dirname, '../public/js/ov-navbar-liv
context.window.LiveNav.sync({ id: 1, username: 'alice' });
assert.strictEqual(config.wallet, true);
assert.strictEqual(config.chips, undefined, 'Live does not override the shared balances');
++// The purse (Shared 4.212.0, ask 273): on a channel page the wallet pill catches up that channel's points too. The
++// streamer is the channel owner's numeric user id (app-channel.js reads ch.user_id after the channel data loads);
++// elsewhere the bar passes nothing, so /wallet/changes carries no streamer.
++assert.strictEqual(config.purse.streamer, undefined, 'no channel page: the purse gets no streamer');
++context.window.LiveNav.setStreamer('alice');
++assert.strictEqual(config.purse.streamer, undefined, 'a non-numeric id is not a streamer');
++context.window.LiveNav.setStreamer(0);
++assert.strictEqual(config.purse.streamer, undefined, 'zero is not a streamer');
++context.window.LiveNav.setStreamer('42');
++assert.strictEqual(config.purse.streamer, 42, 'the channel\'s numeric owner id reaches the purse');
++assert.strictEqual(refreshes, 1, 'the wallet catches up at once instead of waiting for the 60s chip cache');
++assert.strictEqual(inits, 1, 'the bar is not re-rendered for it');
++context.window.LiveNav.setStreamer('42');
++assert.strictEqual(refreshes, 1, 'the same streamer does not fetch twice');
++context.window.LiveNav.setStreamer(null);
++assert.strictEqual(config.purse.streamer, undefined, 'leaving the channel clears it');
++assert.strictEqual(refreshes, 1, 'leaving a channel does not fetch again');
++assert.strictEqual(config.purse.streamer, undefined, 'the purse is the same object, so the navbar sees the clear');
++// The page wires it: app-channel.js after the channel data, app.js for every page that is not a channel.
++const page = fs.readFileSync(path.join(__dirname, '../public/js/app-channel.js'), 'utf8');
++assert.match(page, /const ch = data\.channel;\n[\s\S]{0,400}LiveNav\.setStreamer\(ch\.user_id\)/, 'a channel page passes ch.user_id once it loads');
++const appSrc = fs.readFileSync(path.join(__dirname, '../public/js/app.js'), 'utf8');
++assert.match(appSrc, /page !== 'channel'\) window\.LiveNav\.setStreamer\(null\)/, 'every other page passes no streamer');
const wallet = config.menu.sections.flatMap((s) => s.items).find((i) => i.id === 'wallet');
wallet.onClick(); assert.strictEqual(opens, 1);
const root = path.join(__dirname, '..');
@@@ -29,7 +52,6 @@ assert(!security.includes('"https://billing.openvibe.network"') && !security.inc
// "refreshWallet is not a function" in onAuthChange for every signed-in person (2026-11-09, Live #717 on 4.168.0).
const pin = /refs\/tags\/v(\d+)\.(\d+)\.(\d+)/.exec(JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).dependencies['openvibe-shared']);
assert(pin && (Number(pin[1]) > 4 || (Number(pin[1]) === 4 && Number(pin[2]) >= 170)), 'openvibe-shared is pinned at 4.170.0 or later');
--const app = fs.readFileSync(path.join(root, 'public/js/app.js'), 'utf8');
--assert(/typeof window\.OpenVibeNavbar\.refreshWallet === 'function'\) window\.OpenVibeNavbar\.refreshWallet\(\)/.test(app),
++assert(/typeof window\.OpenVibeNavbar\.refreshWallet === 'function'\) window\.OpenVibeNavbar\.refreshWallet\(\)/.test(appSrc),
'sign-in never stops on a navbar without the wallet');
console.log('Live delegates wallet and store to Shared, retaining cosmetic renderers and channel actions');