openvibers/openvibe.shared Commits

4.130.0: Messages and balances on every site (#318)

OP OpenVibers committed 6 days ago · 1ecd898ace · 8 files, +139 −8

* 4.130.0: your things on every site — Messages (your DMs in place, with Chat's unread count) and the OpenCoins and Vibes balances, on unless a site says no

* navbar.js inside its brotli budget: shorter comments, one visibility listener; the budget notes 4.130.0 (42.6 KB)

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 9f97d7e..0cbd62f 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -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
ddiff --git a/README.md b/README.md
iindex a66913c..591c499 100644
---- a/README.md
++++ b/README.md
@@@ -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 deployed
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
the tag it pins.
ddiff --git a/STATUS.json b/STATUS.json
iindex 0921ca7..c82e43e 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -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",
ddiff --git a/navbar.js b/navbar.js
iindex 888478e..1f629d6 100644
---- a/navbar.js
++++ b/navbar.js
@@@ -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();
ddiff --git a/package.json b/package.json
iindex e75c0e4..5c96c03 100644
---- a/package.json
++++ b/package.json
@@@ -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": {
ddiff --git a/test/nav-icons.test.js b/test/nav-icons.test.js
iindex f8b0e80..fb35799 100644
---- a/test/nav-icons.test.js
++++ b/test/nav-icons.test.js
@@@ -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 ──────────────────────────
{
ddiff --git a/test/navbar-auth.test.js b/test/navbar-auth.test.js
iindex bbc52bc..f1e54de 100644
---- a/test/navbar-auth.test.js
++++ b/test/navbar-auth.test.js
@@@ -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'));
}
ddiff --git a/test/navbar-yours-everywhere.test.js b/test/navbar-yours-everywhere.test.js
nnew file mode 100644
iindex 0000000..4c9064f
---- /dev/null
++++ b/test/navbar-yours-everywhere.test.js
@@@ -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); });