openvibers/openvibe.shared Commits
4.130.0: Messages and balances on every site (#318)
Changes
@@@ -1,5 +1,17 @@# Changelog++## 4.130.0 — 2026-11-07++++- **Your things on every site** (the owner, 11-07: "make sure DMs are accessible across all sites … same with open coins++ and vibes … visible everywhere too"). For a signed-in person, every site's bar now shows:++ - **Messages**, beside the bell, with OpenVibe.Chat's unread count (`GET /api/dm/unread`, on load and when the tab++ comes back, at most once a minute). A click opens your conversations in place, in the Dock's Chats tab, read and++ answered there; with the Dock off it is a link to openvibe.chat/messages.++ - **The OpenCoins and Vibes balances** (the `wallet` chips of 4.24), folded into one pill on a phone as before.++++ Both are on unless a site says `wallet: false` / `messages: false`, or brings its own (Live's chips, Chat's++ messages keep theirs, with their own counts). A signed-out page shows and asks nothing.++## 4.129.0 — 2026-11-07- **One menu** (the owner, 11-07, on the right-hand drawer: "sloppy … maybe combining this menu and the other menu and just@@@ -2,7 +2,7 @@> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.--**Status:** alpha, v4.129.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed++**Status:** alpha, v4.130.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployedconsumer installs a tagged release and none keeps a vendored copy([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` namesthe tag it pins.@@@ -4,7 +4,7 @@"code": true,"wave": "W2","package": "openvibe-shared",-- "version": "4.129.0",++ "version": "4.130.0","plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)","deployed": false,"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",@@@ -21,6 +21,7 @@"share cards for every site and for its big pages (openvibe-shared/social)","the shared video player (player.js, player.css): one look for every <video>","the player's AD control: a descriptions track is remembered per person, toggled with D and spoken through speech synthesis (4.126.0)",++ "your things on every site: Messages (opens your DMs in place, in the Dock) with Chat's unread count, and the OpenCoins and Vibes balances, on unless a site says no (4.130.0)","one menu: the burger opens the OpenVibe menu, this site's pages first and Display at its foot; the old drawer only for sites that ask (4.129.0)","the menu rethought around real use: who is live, your places dock, this site's pages, next from here, two places new to you, you and your agents, tools, every site behind a count, jump back in; a click beacon when Network asks (4.128.0)","items.js: what people wear from OpenVibe.Inventory (name effects, hats, particles, community badges), drawn the same on every site",@@@ -951,10 +951,22 @@const a = nav.querySelector('#openvibe-messages-btn'); if (!a || typeof a.addEventListener !== 'function') return;a.addEventListener('click', (e) => {const m = _config.messages || {};-- if (typeof m.onOpen !== 'function' || e.metaKey || e.ctrlKey || e.shiftKey) return;++ if (e.metaKey || e.ctrlKey || e.shiftKey) return;++ // The default (4.130.0): the Dock's Chats tab; without the Dock, the link.++ if (m.__default && _config.dock !== false && _config.token) { e.preventDefault(); openDock('chats'); return; }++ if (typeof m.onOpen !== 'function') return;e.preventDefault(); try { m.onOpen(e); } catch { /* the link still works */ }});}++ // The default Messages button's count: Chat's GET /api/dm/unread, at most once a minute (README, "The navbar").++ let _unreadAt = 0;++ function loadUnread() {++ const m = _config.messages, u = _config.user;++ if (!m || !m.__default || !u || u.is_anon || !_config.token || typeof fetch !== 'function' || Date.now() - _unreadAt < 60000) return;++ _unreadAt = Date.now();++ fetch(`${String(_config.dockChatBase || 'https://openvibe.chat').replace(/\/+$/, '')}/api/dm/unread`, { headers: { Authorization: `Bearer ${_config.token}` }, credentials: 'omit' })++ .then((r) => (r.ok ? r.json() : null)).then((b) => { if (b && _config.messages.__default) OpenVibeNavbar.setMessages(Number(b.unread) || 0); }).catch(() => {});++ }function chipsAllHTML(chips) {const list = (Array.isArray(chips) ? chips : []).filter((c) => c && !c.hidden);@@@ -1805,7 +1817,7 @@_walletFor = key; _walletAt = Date.now();if (!_walletWatch && typeof document !== 'undefined' && document.addEventListener) {_walletWatch = true;-- document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') loadWalletChips(); });++ document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { loadWalletChips(); loadUnread(); } });}const auth = { Authorization: `Bearer ${_config.token}` };const saved = savedWallet(u);@@@ -2033,6 +2045,7 @@attachAvatarFallbacks(nav);hookTop();loadWalletChips();++ loadUnread();renderAppBar();return nav;}@@@ -2260,6 +2273,9 @@init(opts = {}) {Object.assign(_config, opts);++ // Balances and Messages on every site unless it says false or brings its own (4.130.0).++ if (opts.wallet === undefined) _config.wallet = true;++ if (opts.messages === undefined) _config.messages = { __default: true };injectStyles();hookBoost();hookMoves();@@@ -1,6 +1,6 @@{"name": "openvibe-shared",-- "version": "4.129.0",++ "version": "4.130.0","description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site","main": "index.js","bin": {@@@ -13,7 +13,7 @@ const { brotli } = require('../scripts/size-report');const ROOT = path.join(__dirname, '..');const NAVBAR_PATH = path.join(ROOT, 'navbar.js');const ICONS = require('../nav-icons');--const NAVBAR_BROTLI_BUDGET = 42.5 * 1024; // 42.1 KB with the featured link's phone button (4.119.0, 2026-11-06); 41.4 KB with the page-move code (4.70.0: busy(), the mark on plain links, prefetch rules on unboosted sites); was 41 KB with the Dock's loader (4.51.0)++const NAVBAR_BROTLI_BUDGET = 42.75 * 1024; // 42.6 KB with Messages and the balances on every site (4.130.0, 2026-11-07); 42.1 KB with the featured link's phone button (4.119.0, 2026-11-06); 41.4 KB with the page-move code (4.70.0: busy(), the mark on plain links, prefetch rules on unboosted sites); was 41 KB with the Dock's loader (4.51.0)// ── size budget and generated block ──────────────────────────{@@@ -159,7 +159,8 @@ const USER = { id: 7, username: 'vibe', display_name: 'Vibe', avatar_url: null,assert.strictEqual(user.username, 'vibe');assert.ok(navbar.getElement().innerHTML.includes('openvibe-navbar-avatar'), 'renders the avatar chip');assert.ok(!navbar.getElement().innerHTML.includes('openvibe-navbar-login'), 'no Sign In button');-- assert.strictEqual(calls.length, 1, 'exactly one auth call');++ // The balances and the unread count (4.130.0, on for every signed-in page) are not auth calls.++ assert.strictEqual(calls.filter((c) => /\/api\/auth\//.test(c.url)).length, 1, 'exactly one auth call');}// ── 5b. An openvibe.media object avatar is asked for in the chip's size, as WebP (Media's /o/:id?w=&fmt=) ─@@@ -370,7 +371,7 @@ const USER = { id: 7, username: 'vibe', display_name: 'Vibe', avatar_url: null,});navbar.init({ service: 'network', token: 'whatever', user: USER });await new Promise((r) => setImmediate(r));-- assert.strictEqual(calls.length, 0, 'pages that pass a user keep full control');++ assert.strictEqual(calls.filter((c) => /\/api\/auth\//.test(c.url)).length, 0, 'pages that pass a user keep full control (no auth traffic; the balances and unread count are not auth)');assert.ok(navbar.getElement().innerHTML.includes('openvibe-navbar-avatar'));}nnew file mode 100644@@@ -0,0 +1,101 @@++'use strict';++// Your things on every site (4.130.0; the owner, 2026-11-07: "make sure DMs are accessible across all sites … same with++// open coins and vibes … visible everywhere"). With nothing said about them:++// - a signed-in page shows the balances and a Messages button, and asks Chat for the unread count;++// - wallet: false / messages: false turn them off; a signed-out page shows and asks nothing;++// - a site with its own messages keeps its own count (no request to Chat).++const assert = require('assert');++const path = require('path');++const NAVBAR_PATH = path.join(__dirname, '..', 'navbar.js');++++function stubElement() {++ return {++ style: {}, textContent: '', className: '', id: '', innerHTML: '', dataset: {}, attrs: {}, children: [],++ classList: { add() {}, remove() {}, toggle() {}, contains() { return false; } },++ setAttribute(k, v) { this.attrs[k] = String(v); }, getAttribute(k) { return this.attrs[k] ?? null; },++ appendChild(c) { this.children.push(c); }, prepend(c) { this.children.unshift(c); }, remove() {}, addEventListener() {},++ querySelector() { return stubElement(); }, querySelectorAll() { return []; },++ };++}++++function load(answers) {++ global.location = { hostname: 'openvibe.games', href: 'https://openvibe.games/', pathname: '/', protocol: 'https:' };++ global.window = { location: global.location };++ try { Object.defineProperty(global, 'navigator', { value: { userAgent: 'test' }, configurable: true }); } catch { /* */ }++ const navs = [];++ const badge = Object.assign(stubElement(), { parentElement: stubElement() });++ global.document = {++ cookie: '', head: stubElement(), body: stubElement(), title: 'T', visibilityState: 'visible',++ getElementById: () => null, querySelectorAll: () => [],++ querySelector: (sel) => (sel === '#openvibe-messages-btn b' ? badge : null),++ createElement: (tag) => { const e = stubElement(); e.tag = tag; if (tag === 'nav') navs.push(e); return e; },++ addEventListener() {}, removeEventListener() {}, dispatchEvent() {},++ };++ global.CustomEvent = class { constructor(t, i) { this.type = t; this.detail = i && i.detail; } };++ const store = {};++ global.localStorage = { getItem: (k) => (k in store ? store[k] : null), setItem: (k, v) => { store[k] = String(v); }, removeItem: (k) => { delete store[k]; } };++ global.sessionStorage = { getItem: () => null, setItem() {}, removeItem() {} };++ const calls = [];++ global.fetch = async (url) => {++ calls.push(url);++ const a = /\/api\/dm\/unread$/.test(url) ? answers.unread++ : /\/api\/coins\/me(\?|$)/.test(url) ? answers.coins++ : { status: 404, body: {} };++ return { ok: a.status >= 200 && a.status < 300, status: a.status, json: async () => a.body };++ };++ delete require.cache[require.resolve(NAVBAR_PATH)];++ return { navbar: require(NAVBAR_PATH), navs, calls, badge };++}++const settle = () => new Promise((r) => setTimeout(r, 20));++const user = { id: 7, subject: 'usr_7', username: 'maya' };++const base = { service: 'games', auth: 'external', apiBase: 'https://openvibe.network', user, token: 'tok' };++const answers = { coins: { status: 200, body: { balance: 1200, vibes: 300 } }, unread: { status: 200, body: { unread: 3 } } };++const asked = (calls, re) => calls.filter((u) => re.test(u));++++(async () => {++ // Nothing said: the balances and Messages, and one question to Chat for the unread count.++ let { navbar, navs, calls, badge } = load(answers);++ navbar.init(base);++ let bar = navs[navs.length - 1].innerHTML;++ assert.match(bar, /id="openvibe-messages-btn"/, 'Messages is in the bar');++ assert.match(bar, /href="https:\/\/openvibe\.chat\/messages"/, 'and is a real link (a site without the Dock follows it)');++ assert.match(bar, /data-chip-id="coins"/, 'the OpenCoins chip');++ assert.match(bar, /data-chip-id="vibes"/, 'the Vibes chip');++ await settle();++ assert.deepStrictEqual(asked(calls, /\/api\/dm\/unread$/), ['https://openvibe.chat/api/dm/unread'], 'Chat is asked for the count');++ assert.strictEqual(asked(calls, /\/api\/coins\/me/).length, 1, 'and the Network for the balances');++ assert.strictEqual(badge.textContent, '3', 'the count shows on the button');++ navbar.init(base);++ await settle();++ assert.strictEqual(asked(calls, /\/api\/dm\/unread$/).length, 1, 'at most once a minute');++++ // A site that says no.++ ({ navbar, navs, calls } = load(answers));++ navbar.init({ ...base, wallet: false, messages: false });++ await settle();++ bar = navs[navs.length - 1].innerHTML;++ assert.doesNotMatch(bar, /openvibe-messages-btn|data-chip-id="coins"/);++ assert.strictEqual(asked(calls, /\/api\/(dm\/unread|coins\/me)/).length, 0);++++ // Signed out: nothing shown, nothing asked.++ ({ navbar, navs, calls } = load(answers));++ navbar.init({ ...base, user: null, token: null });++ await settle();++ assert.doesNotMatch(navs.map((n) => n.innerHTML).join(''), /openvibe-messages-btn|data-chip-id="coins"/);++ assert.strictEqual(asked(calls, /\/api\/(dm\/unread|coins\/me)/).length, 0);++++ // A site with its own Messages (Live, Chat) keeps its own count: Chat is not asked by the navbar.++ ({ navbar, navs, calls } = load(answers));++ navbar.init({ ...base, messages: { href: '/messages' } });++ await settle();++ assert.match(navs[navs.length - 1].innerHTML, /href="\/messages"/);++ assert.strictEqual(asked(calls, /\/api\/dm\/unread$/).length, 0);++++ // Chat away: the button stays, without a number.++ ({ navbar, navs, calls, badge } = load({ ...answers, unread: { status: 503, body: {} } }));++ navbar.init(base);++ await settle();++ assert.match(navs[navs.length - 1].innerHTML, /id="openvibe-messages-btn"/);++ assert.strictEqual(badge.textContent, '');++ console.log('navbar-yours-everywhere: all checks passed');++})().catch((e) => { console.error(e); process.exit(1); });
@@ -1,5 +1,17 @@
@@ -1,5 +1,17 @@
# Changelog
# Changelog
+## 4.130.0 — 2026-11-07
+
+- **Your things on every site** (the owner, 11-07: "make sure DMs are accessible across all sites … same with open coins
+ and vibes … visible everywhere too"). For a signed-in person, every site's bar now shows:
+ - **Messages**, beside the bell, with OpenVibe.Chat's unread count (`GET /api/dm/unread`, on load and when the tab
+ comes back, at most once a minute). A click opens your conversations in place, in the Dock's Chats tab, read and
+ answered there; with the Dock off it is a link to openvibe.chat/messages.
+ - **The OpenCoins and Vibes balances** (the `wallet` chips of 4.24), folded into one pill on a phone as before.
+
+ Both are on unless a site says `wallet: false` / `messages: false`, or brings its own (Live's chips, Chat's
+ messages keep theirs, with their own counts). A signed-out page shows and asks nothing.
+
## 4.129.0 — 2026-11-07
## 4.129.0 — 2026-11-07
- **One menu** (the owner, 11-07, on the right-hand drawer: "sloppy … maybe combining this menu and the other menu and just
- **One menu** (the owner, 11-07, on the right-hand drawer: "sloppy … maybe combining this menu and the other menu and just
@@ -2,7 +2,7 @@
@@ -2,7 +2,7 @@
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
-**Status:** alpha, v4.129.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
+**Status:** alpha, v4.130.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
consumer installs a tagged release and none keeps a vendored copy
consumer installs a tagged release and none keeps a vendored copy
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
the tag it pins.
the tag it pins.
@@ -4,7 +4,7 @@
@@ -4,7 +4,7 @@
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
"package": "openvibe-shared",
"package": "openvibe-shared",
- "version": "4.129.0",
+ "version": "4.130.0",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"deployed": false,
"deployed": false,
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
@@ -21,6 +21,7 @@
@@ -21,6 +21,7 @@
"share cards for every site and for its big pages (openvibe-shared/social)",
"share cards for every site and for its big pages (openvibe-shared/social)",
"the shared video player (player.js, player.css): one look for every <video>",
"the shared video player (player.js, player.css): one look for every <video>",
"the player's AD control: a descriptions track is remembered per person, toggled with D and spoken through speech synthesis (4.126.0)",
"the player's AD control: a descriptions track is remembered per person, toggled with D and spoken through speech synthesis (4.126.0)",
+ "your things on every site: Messages (opens your DMs in place, in the Dock) with Chat's unread count, and the OpenCoins and Vibes balances, on unless a site says no (4.130.0)",
"one menu: the burger opens the OpenVibe menu, this site's pages first and Display at its foot; the old drawer only for sites that ask (4.129.0)",
"one menu: the burger opens the OpenVibe menu, this site's pages first and Display at its foot; the old drawer only for sites that ask (4.129.0)",
"the menu rethought around real use: who is live, your places dock, this site's pages, next from here, two places new to you, you and your agents, tools, every site behind a count, jump back in; a click beacon when Network asks (4.128.0)",
"the menu rethought around real use: who is live, your places dock, this site's pages, next from here, two places new to you, you and your agents, tools, every site behind a count, jump back in; a click beacon when Network asks (4.128.0)",
"items.js: what people wear from OpenVibe.Inventory (name effects, hats, particles, community badges), drawn the same on every site",
"items.js: what people wear from OpenVibe.Inventory (name effects, hats, particles, community badges), drawn the same on every site",
@@ -951,10 +951,22 @@
@@ -951,10 +951,22 @@
const a = nav.querySelector('#openvibe-messages-btn'); if (!a || typeof a.addEventListener !== 'function') return;
const a = nav.querySelector('#openvibe-messages-btn'); if (!a || typeof a.addEventListener !== 'function') return;
a.addEventListener('click', (e) => {
a.addEventListener('click', (e) => {
const m = _config.messages || {};
const m = _config.messages || {};
- if (typeof m.onOpen !== 'function' || e.metaKey || e.ctrlKey || e.shiftKey) return;
+ if (e.metaKey || e.ctrlKey || e.shiftKey) return;
+ // The default (4.130.0): the Dock's Chats tab; without the Dock, the link.
+ if (m.__default && _config.dock !== false && _config.token) { e.preventDefault(); openDock('chats'); return; }
+ if (typeof m.onOpen !== 'function') return;
e.preventDefault(); try { m.onOpen(e); } catch { /* the link still works */ }
e.preventDefault(); try { m.onOpen(e); } catch { /* the link still works */ }
});
});
}
}
+ // The default Messages button's count: Chat's GET /api/dm/unread, at most once a minute (README, "The navbar").
+ let _unreadAt = 0;
+ function loadUnread() {
+ const m = _config.messages, u = _config.user;
+ if (!m || !m.__default || !u || u.is_anon || !_config.token || typeof fetch !== 'function' || Date.now() - _unreadAt < 60000) return;
+ _unreadAt = Date.now();
+ fetch(`${String(_config.dockChatBase || 'https://openvibe.chat').replace(/\/+$/, '')}/api/dm/unread`, { headers: { Authorization: `Bearer ${_config.token}` }, credentials: 'omit' })
+ .then((r) => (r.ok ? r.json() : null)).then((b) => { if (b && _config.messages.__default) OpenVibeNavbar.setMessages(Number(b.unread) || 0); }).catch(() => {});
+ }
function chipsAllHTML(chips) {
function chipsAllHTML(chips) {
const list = (Array.isArray(chips) ? chips : []).filter((c) => c && !c.hidden);
const list = (Array.isArray(chips) ? chips : []).filter((c) => c && !c.hidden);
@@ -1805,7 +1817,7 @@
@@ -1805,7 +1817,7 @@
_walletFor = key; _walletAt = Date.now();
_walletFor = key; _walletAt = Date.now();
if (!_walletWatch && typeof document !== 'undefined' && document.addEventListener) {
if (!_walletWatch && typeof document !== 'undefined' && document.addEventListener) {
_walletWatch = true;
_walletWatch = true;
- document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') loadWalletChips(); });
+ document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { loadWalletChips(); loadUnread(); } });
}
}
const auth = { Authorization: `Bearer ${_config.token}` };
const auth = { Authorization: `Bearer ${_config.token}` };
const saved = savedWallet(u);
const saved = savedWallet(u);
@@ -2033,6 +2045,7 @@
@@ -2033,6 +2045,7 @@
attachAvatarFallbacks(nav);
attachAvatarFallbacks(nav);
hookTop();
hookTop();
loadWalletChips();
loadWalletChips();
+ loadUnread();
renderAppBar();
renderAppBar();
return nav;
return nav;
}
}
@@ -2260,6 +2273,9 @@
@@ -2260,6 +2273,9 @@
init(opts = {}) {
init(opts = {}) {
Object.assign(_config, opts);
Object.assign(_config, opts);
+ // Balances and Messages on every site unless it says false or brings its own (4.130.0).
+ if (opts.wallet === undefined) _config.wallet = true;
+ if (opts.messages === undefined) _config.messages = { __default: true };
injectStyles();
injectStyles();
hookBoost();
hookBoost();
hookMoves();
hookMoves();
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.129.0",
+ "version": "4.130.0",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"main": "index.js",
"bin": {
"bin": {
@@ -13,7 +13,7 @@ const { brotli } = require('../scripts/size-report');
@@ -13,7 +13,7 @@ const { brotli } = require('../scripts/size-report');
const ROOT = path.join(__dirname, '..');
const ROOT = path.join(__dirname, '..');
const NAVBAR_PATH = path.join(ROOT, 'navbar.js');
const NAVBAR_PATH = path.join(ROOT, 'navbar.js');
const ICONS = require('../nav-icons');
const ICONS = require('../nav-icons');
-const NAVBAR_BROTLI_BUDGET = 42.5 * 1024; // 42.1 KB with the featured link's phone button (4.119.0, 2026-11-06); 41.4 KB with the page-move code (4.70.0: busy(), the mark on plain links, prefetch rules on unboosted sites); was 41 KB with the Dock's loader (4.51.0)
+const NAVBAR_BROTLI_BUDGET = 42.75 * 1024; // 42.6 KB with Messages and the balances on every site (4.130.0, 2026-11-07); 42.1 KB with the featured link's phone button (4.119.0, 2026-11-06); 41.4 KB with the page-move code (4.70.0: busy(), the mark on plain links, prefetch rules on unboosted sites); was 41 KB with the Dock's loader (4.51.0)
// ── size budget and generated block ──────────────────────────
// ── size budget and generated block ──────────────────────────
{
{
@@ -159,7 +159,8 @@ const USER = { id: 7, username: 'vibe', display_name: 'Vibe', avatar_url: null,
@@ -159,7 +159,8 @@ const USER = { id: 7, username: 'vibe', display_name: 'Vibe', avatar_url: null,
assert.strictEqual(user.username, 'vibe');
assert.strictEqual(user.username, 'vibe');
assert.ok(navbar.getElement().innerHTML.includes('openvibe-navbar-avatar'), 'renders the avatar chip');
assert.ok(navbar.getElement().innerHTML.includes('openvibe-navbar-avatar'), 'renders the avatar chip');
assert.ok(!navbar.getElement().innerHTML.includes('openvibe-navbar-login'), 'no Sign In button');
assert.ok(!navbar.getElement().innerHTML.includes('openvibe-navbar-login'), 'no Sign In button');
- assert.strictEqual(calls.length, 1, 'exactly one auth call');
+ // The balances and the unread count (4.130.0, on for every signed-in page) are not auth calls.
+ assert.strictEqual(calls.filter((c) => /\/api\/auth\//.test(c.url)).length, 1, 'exactly one auth call');
}
}
// ── 5b. An openvibe.media object avatar is asked for in the chip's size, as WebP (Media's /o/:id?w=&fmt=) ─
// ── 5b. An openvibe.media object avatar is asked for in the chip's size, as WebP (Media's /o/:id?w=&fmt=) ─
@@ -370,7 +371,7 @@ const USER = { id: 7, username: 'vibe', display_name: 'Vibe', avatar_url: null,
@@ -370,7 +371,7 @@ const USER = { id: 7, username: 'vibe', display_name: 'Vibe', avatar_url: null,
});
});
navbar.init({ service: 'network', token: 'whatever', user: USER });
navbar.init({ service: 'network', token: 'whatever', user: USER });
await new Promise((r) => setImmediate(r));
await new Promise((r) => setImmediate(r));
- assert.strictEqual(calls.length, 0, 'pages that pass a user keep full control');
+ assert.strictEqual(calls.filter((c) => /\/api\/auth\//.test(c.url)).length, 0, 'pages that pass a user keep full control (no auth traffic; the balances and unread count are not auth)');
assert.ok(navbar.getElement().innerHTML.includes('openvibe-navbar-avatar'));
assert.ok(navbar.getElement().innerHTML.includes('openvibe-navbar-avatar'));
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1,101 @@
@@ -0,0 +1,101 @@
+'use strict';
+// Your things on every site (4.130.0; the owner, 2026-11-07: "make sure DMs are accessible across all sites … same with
+// open coins and vibes … visible everywhere"). With nothing said about them:
+// - a signed-in page shows the balances and a Messages button, and asks Chat for the unread count;
+// - wallet: false / messages: false turn them off; a signed-out page shows and asks nothing;
+// - a site with its own messages keeps its own count (no request to Chat).
+const assert = require('assert');
+const path = require('path');
+const NAVBAR_PATH = path.join(__dirname, '..', 'navbar.js');
+
+function stubElement() {
+ return {
+ style: {}, textContent: '', className: '', id: '', innerHTML: '', dataset: {}, attrs: {}, children: [],
+ classList: { add() {}, remove() {}, toggle() {}, contains() { return false; } },
+ setAttribute(k, v) { this.attrs[k] = String(v); }, getAttribute(k) { return this.attrs[k] ?? null; },
+ appendChild(c) { this.children.push(c); }, prepend(c) { this.children.unshift(c); }, remove() {}, addEventListener() {},
+ querySelector() { return stubElement(); }, querySelectorAll() { return []; },
+ };
+}
+
+function load(answers) {
+ global.location = { hostname: 'openvibe.games', href: 'https://openvibe.games/', pathname: '/', protocol: 'https:' };
+ global.window = { location: global.location };
+ try { Object.defineProperty(global, 'navigator', { value: { userAgent: 'test' }, configurable: true }); } catch { /* */ }
+ const navs = [];
+ const badge = Object.assign(stubElement(), { parentElement: stubElement() });
+ global.document = {
+ cookie: '', head: stubElement(), body: stubElement(), title: 'T', visibilityState: 'visible',
+ getElementById: () => null, querySelectorAll: () => [],
+ querySelector: (sel) => (sel === '#openvibe-messages-btn b' ? badge : null),
+ createElement: (tag) => { const e = stubElement(); e.tag = tag; if (tag === 'nav') navs.push(e); return e; },
+ addEventListener() {}, removeEventListener() {}, dispatchEvent() {},
+ };
+ global.CustomEvent = class { constructor(t, i) { this.type = t; this.detail = i && i.detail; } };
+ const store = {};
+ global.localStorage = { getItem: (k) => (k in store ? store[k] : null), setItem: (k, v) => { store[k] = String(v); }, removeItem: (k) => { delete store[k]; } };
+ global.sessionStorage = { getItem: () => null, setItem() {}, removeItem() {} };
+ const calls = [];
+ global.fetch = async (url) => {
+ calls.push(url);
+ const a = /\/api\/dm\/unread$/.test(url) ? answers.unread
+ : /\/api\/coins\/me(\?|$)/.test(url) ? answers.coins
+ : { status: 404, body: {} };
+ return { ok: a.status >= 200 && a.status < 300, status: a.status, json: async () => a.body };
+ };
+ delete require.cache[require.resolve(NAVBAR_PATH)];
+ return { navbar: require(NAVBAR_PATH), navs, calls, badge };
+}
+const settle = () => new Promise((r) => setTimeout(r, 20));
+const user = { id: 7, subject: 'usr_7', username: 'maya' };
+const base = { service: 'games', auth: 'external', apiBase: 'https://openvibe.network', user, token: 'tok' };
+const answers = { coins: { status: 200, body: { balance: 1200, vibes: 300 } }, unread: { status: 200, body: { unread: 3 } } };
+const asked = (calls, re) => calls.filter((u) => re.test(u));
+
+(async () => {
+ // Nothing said: the balances and Messages, and one question to Chat for the unread count.
+ let { navbar, navs, calls, badge } = load(answers);
+ navbar.init(base);
+ let bar = navs[navs.length - 1].innerHTML;
+ assert.match(bar, /id="openvibe-messages-btn"/, 'Messages is in the bar');
+ assert.match(bar, /href="https:\/\/openvibe\.chat\/messages"/, 'and is a real link (a site without the Dock follows it)');
+ assert.match(bar, /data-chip-id="coins"/, 'the OpenCoins chip');
+ assert.match(bar, /data-chip-id="vibes"/, 'the Vibes chip');
+ await settle();
+ assert.deepStrictEqual(asked(calls, /\/api\/dm\/unread$/), ['https://openvibe.chat/api/dm/unread'], 'Chat is asked for the count');
+ assert.strictEqual(asked(calls, /\/api\/coins\/me/).length, 1, 'and the Network for the balances');
+ assert.strictEqual(badge.textContent, '3', 'the count shows on the button');
+ navbar.init(base);
+ await settle();
+ assert.strictEqual(asked(calls, /\/api\/dm\/unread$/).length, 1, 'at most once a minute');
+
+ // A site that says no.
+ ({ navbar, navs, calls } = load(answers));
+ navbar.init({ ...base, wallet: false, messages: false });
+ await settle();
+ bar = navs[navs.length - 1].innerHTML;
+ assert.doesNotMatch(bar, /openvibe-messages-btn|data-chip-id="coins"/);
+ assert.strictEqual(asked(calls, /\/api\/(dm\/unread|coins\/me)/).length, 0);
+
+ // Signed out: nothing shown, nothing asked.
+ ({ navbar, navs, calls } = load(answers));
+ navbar.init({ ...base, user: null, token: null });
+ await settle();
+ assert.doesNotMatch(navs.map((n) => n.innerHTML).join(''), /openvibe-messages-btn|data-chip-id="coins"/);
+ assert.strictEqual(asked(calls, /\/api\/(dm\/unread|coins\/me)/).length, 0);
+
+ // A site with its own Messages (Live, Chat) keeps its own count: Chat is not asked by the navbar.
+ ({ navbar, navs, calls } = load(answers));
+ navbar.init({ ...base, messages: { href: '/messages' } });
+ await settle();
+ assert.match(navs[navs.length - 1].innerHTML, /href="\/messages"/);
+ assert.strictEqual(asked(calls, /\/api\/dm\/unread$/).length, 0);
+
+ // Chat away: the button stays, without a number.
+ ({ navbar, navs, calls, badge } = load({ ...answers, unread: { status: 503, body: {} } }));
+ navbar.init(base);
+ await settle();
+ assert.match(navs[navs.length - 1].innerHTML, /id="openvibe-messages-btn"/);
+ assert.strictEqual(badge.textContent, '');
+ console.log('navbar-yours-everywhere: all checks passed');
+})().catch((e) => { console.error(e); process.exit(1); });