openvibers/openvibe.live Commits

Open Live messages in the shared Dock (#716)

OP OpenVibers committed 4 days ago · 48db2a81c9 · 17 files, +63 −100

* Use shared Dock chats instead of Live messenger

* Cover removal of Live messenger entry points

* Merge remote-tracking branch 'origin/main' into auto/messages-dock-live-1109

* Keep N-1 messenger assets available for open tabs

* Expect N-1 messenger assets in Dock test

Changes

ddiff --git a/public/css/features/broadcast.css b/public/css/features/broadcast.css
iindex 64375658..43416dc7 100644
---- a/public/css/features/broadcast.css
++++ b/public/css/features/broadcast.css
@@@ -2556,7 +2556,6 @@ body.bc-studio-on .bc-video-container .bc-video-placeholder { display: none !imp
body.bc-studio-on #bc-stats-overlay.bc-stats-overlay {
position: fixed; top: calc(52px + env(safe-area-inset-top, 0px)); left: auto; right: max(12px, env(safe-area-inset-right, 0px)); z-index: 1235;
}
--body.bc-studio-on .messenger-toggle,
body.bc-studio-on .bc-mobile-chat-fab,
body.bc-studio-on #navbar-mount,
body.bc-studio-on .navbar,
@@@ -2854,7 +2853,6 @@ body.bc-studio-on #bc-chat-messages .chat-conn-status { display: block !importan
.bcs-golive-btn:disabled { opacity: 0.6; cursor: default; }
.bcs-golive-btn:focus-visible { outline: 3px solid #93c5fd; outline-offset: 2px; }
.bcs-golive-where { color: var(--text-secondary, #a1a1aa); font-size: 0.76rem; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
--body.bc-golive-dock-on .messenger-toggle { display: none !important; }
/* The Dock's pill (openvibe-shared dock.js, in a bottom corner) rides above the Go live dock: it sat on the button's
right half, so a tap there opened the Agent panel instead of going live (11-09). Top corners stay where they are. */
body.bc-golive-dock-on .ovdk-pill:not([data-corner^="top"]) {
ddiff --git a/public/css/features/channel.css b/public/css/features/channel.css
iindex 0ad69c89..29ae91b9 100644
---- a/public/css/features/channel.css
++++ b/public/css/features/channel.css
@@@ -1628,10 +1628,8 @@ button.viewer-badge:focus-visible { outline: 2px solid #93c5fd; outline-offset:
#page-channel #video-element,
#page-channel #video-canvas { max-height: none; }
body:has(#page-channel.active) .chat-sidebar { top: 0; height: 100vh; height: 100dvh; }
-- /* The player's controls run along the bottom edge: the chat button moves to the top right corner, smaller, and the
-- messages button waits until the phone is upright again (it covered play and volume). */
++ /* The player's controls run along the bottom edge: the chat button moves to the top right corner. */
#page-channel .mobile-chat-toggle { top: 10px; bottom: auto; right: 10px; width: 42px; height: 42px; font-size: 0.95rem; }
-- body:has(#page-channel.active) #messenger-toggle { display: none !important; }
}
/* The next scheduled stream, from OpenVibe.Today (offline header). */
@@@ -1655,7 +1653,7 @@ button.viewer-badge:focus-visible { outline: 2px solid #93c5fd; outline-offset:
@media (max-width: 768px) and (orientation: portrait) {
html:has(#page-channel.active.ch-is-live) { --ovnav-appbar-h: 0px; }
html:has(#page-channel.active.ch-is-live) .ovnav-appbar,
-- body:has(#page-channel.active.ch-is-live) :is(.mobile-chat-toggle, #messenger-toggle, #floating-chat-fab) { display: none !important; }
++ body:has(#page-channel.active.ch-is-live) :is(.mobile-chat-toggle, #floating-chat-fab) { display: none !important; }
#page-channel.ch-is-live .stream-layout {
display: flex; flex-direction: column; overflow: hidden;
ddiff --git a/public/css/style.css b/public/css/style.css
iindex da47f51f..9f956c00 100644
---- a/public/css/style.css
++++ b/public/css/style.css
@@@ -7305,9 +7305,9 @@ textarea.paste-textarea {
}
/* Phones, scrolling down: the floating buttons step aside (draggable-fab.js) and come back on the way up. */
--body .messenger-toggle, body .floating-chat-fab, body .mobile-chat-toggle { transition: transform var(--ov-dur-2, 0.2s), opacity var(--ov-dur-2, 0.2s), background var(--ov-dur-2, 0.15s); }
--body.fabs-tucked .messenger-toggle, body.fabs-tucked .floating-chat-fab, body.fabs-tucked .mobile-chat-toggle { opacity: 0; transform: scale(0.6); pointer-events: none; }
--@media (prefers-reduced-motion: reduce) { body.fabs-tucked .messenger-toggle, body.fabs-tucked .floating-chat-fab, body.fabs-tucked .mobile-chat-toggle { transform: none; } }
++body .floating-chat-fab, body .mobile-chat-toggle { transition: transform var(--ov-dur-2, 0.2s), opacity var(--ov-dur-2, 0.2s), background var(--ov-dur-2, 0.15s); }
++body.fabs-tucked .floating-chat-fab, body.fabs-tucked .mobile-chat-toggle { opacity: 0; transform: scale(0.6); pointer-events: none; }
++@media (prefers-reduced-motion: reduce) { body.fabs-tucked .floating-chat-fab, body.fabs-tucked .mobile-chat-toggle { transform: none; } }
/* ── Floating Chat Widget ─────────────────────────────────────── */
.floating-chat-fab {
@@@ -7334,7 +7334,7 @@ body.fabs-tucked .messenger-toggle, body.fabs-tucked .floating-chat-fab, body.fa
background: var(--accent-light);
}
--/* ── Draggable floating buttons (messenger toggle + chat FAB) ── */
++/* ── Draggable floating chat button ── */
.fab-dragging {
cursor: grabbing !important;
opacity: 0.92;
@@@ -7784,7 +7784,6 @@ body.fabs-tucked .messenger-toggle, body.fabs-tucked .floating-chat-fab, body.fa
transform: translateY(0);
}
body.mobile-chat-visible .mobile-chat-toggle { display: none !important; }
-- body.mobile-chat-visible #messenger-toggle { display: none !important; }
.video-container { max-height: 50vh; }
.mobile-chat-toggle { display: flex; }
/* The chat is an overlay at ≤1180px — give its header a working close button. */
@@@ -7805,7 +7804,6 @@ body.fabs-tucked .messenger-toggle, body.fabs-tucked .floating-chat-fab, body.fa
.chat-sidebar.mobile-chat-open {
transform: translateX(0);
}
-- body.mobile-chat-visible #messenger-toggle { display: none !important; }
}
@media (max-width: 1180px) and (orientation: portrait) {
.chat-sidebar {
@@@ -9995,11 +9993,9 @@ body.ov-panel-open #floating-chat-fab { opacity: 0; pointer-events: none; transf
/* The site switcher stays beside the logo on a phone: the two balance chips fold into one there (Shared 4.16.0). */
@media (max-width: 560px) { .openvibe-navbar.navbar .ovnav-chip { padding: 0 8px; } }
--/* Phones with the app bar (Shared 4.20, js/ov-navbar-live.js): Chat is a tab, so the global chat button steps aside; the
-- messages button sits above the bar and the page keeps room for it (it covered a channel's followers and buttons). */
++/* Phones with the app bar: the global chat button steps aside for its Chat tab. */
@media (max-width: 760px) {
html.ov-has-appbar #floating-chat-fab { display: none !important; }
-- html.ov-has-appbar .messenger-toggle { bottom: calc(var(--ovnav-appbar-h, 58px) + 14px); }
html.ov-has-appbar body { padding-bottom: calc(var(--ovnav-appbar-h, 58px) + 64px); }
}
ddiff --git a/public/features.json b/public/features.json
iindex 49ec0f69..5907a243 100644
---- a/public/features.json
++++ b/public/features.json
@@@ -279,17 +279,6 @@
"saveDashPointsConfig"
]
},
-- "messenger": {
-- "css": [
-- "/css/messenger.css"
-- ],
-- "js": [
-- "/js/messenger.js"
-- ],
-- "stubs": [
-- "openMessengerDm"
-- ]
-- },
"dashboard": {
"fragment": "dashboard",
"css": [
ddiff --git a/public/index.html b/public/index.html
iindex 9546f000..8203ced5 100644
---- a/public/index.html
++++ b/public/index.html
@@@ -99,15 +99,13 @@
.ov-route-error>div{flex:1;min-width:180px;display:grid;gap:2px}
.ov-route-error span{color:var(--text-muted,#8b93ad);font-size:.9rem}
@media (prefers-reduced-motion:reduce){.ov-route-skel span{animation:none}}
-- /* Widgets their scripts build before cosmetics.css / messenger.css land (neither blocks): closed, they
-- stay out of the page instead of painting unstyled under the footer, and the toggle sits where it will. */
-- .cosmetics-modal-overlay:not(.active),.messenger-panel:not(.open){display:none}
-- .messenger-toggle{position:fixed;bottom:20px;left:20px;width:52px;height:52px;border:0;border-radius:50%;background:var(--accent-strong,#3472d8);color:var(--on-accent-strong,#fff)}
++ /* Keep the cosmetics modal out of the page before its stylesheet lands. */
++ .cosmetics-modal-overlay:not(.active){display:none}
</style>
<!-- Stylesheets.
Blocking set = what the home page paints with. Everything else belongs to another route
-- (the messenger, the cosmetics modal, after-show reports, the guide
++ (the cosmetics modal, after-show reports, the guide
overlay) and is fetched without blocking the first paint: media="print" makes the browser
treat it as non-critical, and the onload flips it back to all once it has arrived. -->
<link rel="stylesheet" href="/css/style.css">
@@@ -492,7 +490,7 @@
</div>
</div>
--<!-- vibes.js, opencoins.js, messenger.js: features.json → money, messenger (on demand). -->
++<!-- vibes.js, opencoins.js: features.json → money (on demand). -->
<script src="/js/draggable-fab.js" defer></script>
<script src="/shared/footer.js" defer></script>
<script src="/js/footer-live.js" defer></script>
ddiff --git a/public/js/app.js b/public/js/app.js
iindex 204f3a2a..c95ffd90 100644
---- a/public/js/app.js
++++ b/public/js/app.js
@@@ -784,8 +784,13 @@ function onAuthChange() {
// The navbar is the network's shared component; tell it who is here (js/ov-navbar-live.js).
if (window.LiveNav) window.LiveNav.sync(currentUser || null);
if (currentUser) loadBalance();
-- // The DM panel is for a signed-in person only (features.json → messenger).
-- if (currentUser && window.ov && ov.load) ov.load('messenger').catch(() => { /* in ov.diagnostics() */ });
++ // Older notification links point at a Live DM route. Keep the reader here in the Dock.
++ if (currentUser && new URLSearchParams(location.search).has('dm')) {
++ const params = new URLSearchParams(location.search);
++ params.delete('dm');
++ history.replaceState(history.state, '', location.pathname + (params.size ? `?${params}` : '') + location.hash);
++ window.OpenVibeNavbar?.openDock('chats');
++ }
try {
window.dispatchEvent(new CustomEvent('openvibe-auth-changed', {
ddiff --git a/public/js/chat.js b/public/js/chat.js
iindex d0b43f1b..e6cce9b2 100644
---- a/public/js/chat.js
++++ b/public/js/chat.js
@@@ -2167,13 +2167,6 @@ function handleChatMessage(msg) {
if (rContainer && typeof renderRedemption === 'function') renderRedemption(msg, rContainer);
break;
}
-- case 'dm':
-- case 'dm-participant-added':
-- // Route DM events to the messenger widget
-- if (typeof window._messengerHandleDm === 'function') {
-- window._messengerHandleDm(msg);
-- }
-- break;
case 'vc-call-invite': {
_showIncomingVcCallOverlay(msg);
break;
@@@ -4050,14 +4043,16 @@ function ctxReply() {
dismissContextMenu();
}
--function ctxWhisper(username) {
++async function ctxWhisper(username) {
dismissContextMenu();
-- // Route to the DM messenger widget instead of the old whisper command
-- if (typeof window.openMessengerDm === 'function') {
-- window.openMessengerDm(username);
-- } else {
-- toast('Messenger not available', 'error');
-- }
++ if (!currentUser) { toast('Sign in to send messages', 'error'); return; }
++ try {
++ const users = (await api(`/dm/users/search?q=${encodeURIComponent(username)}`)).users || [];
++ const target = users.find((u) => u.username.toLowerCase() === username.toLowerCase());
++ if (!target) { toast(`User "${username}" not found`, 'error'); return; }
++ await api('/dm/conversations', { method: 'POST', body: { user_ids: [target.id] } });
++ window.OpenVibeNavbar?.openDock('chats');
++ } catch { toast('Failed to open messages', 'error'); }
}
async function ctxCallUser(username, userId) {
ddiff --git a/public/js/draggable-fab.js b/public/js/draggable-fab.js
iindex b9a5abfa..51bba9b1 100644
---- a/public/js/draggable-fab.js
++++ b/public/js/draggable-fab.js
@@@ -1,5 +1,5 @@
/**
-- * draggable-fab.js — make the floating buttons (DM messenger toggle + global-chat FAB)
++ * draggable-fab.js — make the floating buttons (global chat button)
* drag-and-droppable, robustly, on desktop and touch.
*
* Key behaviours:
@@@ -157,41 +157,12 @@
}
window.makeFabDraggable = makeFabDraggable;
-- // Auto-wire the two known buttons. The FAB is static; the messenger toggle is injected
-- // later by messenger.js — watch for it. (messenger.js also calls this directly.)
-- function tryInit() {
++ // The channel chat button is the only floating button.
++ function boot() {
var fab = document.getElementById('floating-chat-fab');
if (fab) makeFabDraggable(fab, 'fabpos:floating-chat-fab');
-- var toggle = document.getElementById('messenger-toggle');
-- if (toggle) makeFabDraggable(toggle, 'fabpos:messenger-toggle');
-- return !!(fab && toggle);
-- }
--
-- function boot() {
-- if (tryInit()) return;
-- var obs = new MutationObserver(function () { if (tryInit()) { /* both wired */ } });
-- obs.observe(document.body, { childList: true, subtree: false });
-- setTimeout(function () { try { obs.disconnect(); } catch (_) {} }, 60000);
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot);
else boot();
})();
--
--/* Phones: the floating buttons (messages, global chat, a channel's chat) tuck away while you scroll down to read (they
-- covered cards, titles and the clip feed's sound buttons) and come back when you scroll up, near the top, or at the
-- end of the page. */
--(function () {
-- 'use strict';
-- var phone = window.matchMedia('(max-width: 768px) and (pointer: coarse)');
-- var last = window.scrollY || 0, ticking = false;
-- function update() {
-- ticking = false;
-- var y = window.scrollY || 0;
-- var atEnd = y + window.innerHeight >= document.documentElement.scrollHeight - 80;
-- if (!phone.matches || y <= 120 || atEnd || y < last - 4) document.body.classList.remove('fabs-tucked');
-- else if (y > last + 4) document.body.classList.add('fabs-tucked');
-- last = y;
-- }
-- window.addEventListener('scroll', function () { if (!ticking) { ticking = true; requestAnimationFrame(update); } }, { passive: true });
--})();
ddiff --git a/public/js/ov-navbar-live.js b/public/js/ov-navbar-live.js
iindex acf70b3b..826301f9 100644
---- a/public/js/ov-navbar-live.js
++++ b/public/js/ov-navbar-live.js
@@@ -82,8 +82,6 @@
{ id: 'coins', icon: 'fa-coins', tone: 'gold', value: '0', valueId: 'nav-coins-amount', label: 'OpenCoins', title: 'OpenCoins: click to open your inventory', onClick: () => call('openCosmeticsModal') },
{ id: 'vibes', icon: 'fa-wallet', tone: 'green', value: '0', valueId: 'nav-balance-amount', label: 'Vibes', title: 'Vibes balance', onClick: () => call('showModal', 'buy-funds') },
],
-- // DMs beside the bell (Shared 4.25): the button opens the messenger panel; without it, Chat's inbox.
-- messages: { onOpen: () => { if (window.messengerWidget && window.messengerWidget.toggle) window.messengerWidget.toggle(); else if (window.ov && ov.load) ov.load('messenger').then(() => { if (window.messengerWidget && window.messengerWidget.toggle) window.messengerWidget.toggle(); else location.href = 'https://openvibe.chat/messages'; }).catch(() => { location.href = 'https://openvibe.chat/messages'; }); else location.href = 'https://openvibe.chat/messages'; }, href: 'https://openvibe.chat/messages' },
// 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
ddiff --git a/server/index.js b/server/index.js
iindex 73b1fcde..b012cfa9 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -1144,7 +1144,7 @@ app.get('/media/:username', (req, res) => {
});
// DM notifications sent before 2026-09-23 linked to /dm/<conversation id>, a page Live never had;
--// the messenger opens the thread from /?dm=<id> now.
++// open the shared Dock on Live for these older links.
app.get(/^\/dm\/(\d{1,12})\/?$/, (req, res) => res.redirect(302, `/?dm=${req.params[0]}`));
// The Arena became The Rundown (2026-10-19): a fighter's page is the streamer's Rundown page; every other Arena page
// (drama stories, beefs, the ears) is gone, so it lands on the Rundown.
ddiff --git a/test/avatar-widths.test.js b/test/avatar-widths.test.js
iindex 9499e1de..7f5e837e 100644
---- a/test/avatar-widths.test.js
++++ b/test/avatar-widths.test.js
@@@ -30,14 +30,11 @@ const uses = [
['chat.js', /ovImgW\(clip\.creator_avatar, 64\)/],
['chat.js', /ovImgW\(u\.avatar_url, 64\)/],
['chat-shared.js', /typeof ovImgW === 'function' \? ovImgW\(p\.avatar_url, 64\) : p\.avatar_url/],
-- ['messenger.js', /ovImgW\(user\.avatar_url, 96\)/],
];
uses.forEach(([f, re]) => assert.match(js(f), re, `${f}: ${re}`));
--// Read by name too, so the test runner maps the kiosk-safe chat file and the messenger to this test.
++// Read by name too, so the test runner maps the kiosk-safe chat file to this test.
const chatShared = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'chat-shared.js'), 'utf8');
assert.match(chatShared, /data-full="\$\{esc\(p\.avatar_url\)\}"/);
--const messenger = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'messenger.js'), 'utf8');
--assert.match(messenger, /data-full="\$\{esc\(user\.avatar_url\)\}"/);
// The content feed (Content, AI Moments) builds its avatars as nodes.
const feed = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'content-feed.js'), 'utf8');
assert.match(feed, /typeof ovImgW === 'function' \? ovImgW\(full, 64\) : full/);
ddiff --git a/test/channel-phone-watch.test.js b/test/channel-phone-watch.test.js
iindex fdf2ff5b..1a0d70b4 100644
---- a/test/channel-phone-watch.test.js
++++ b/test/channel-phone-watch.test.js
@@@ -17,7 +17,7 @@ assert.ok(phone.startsWith('@media (max-width: 768px) and (orientation: portrait
// The app bar and every floating button step aside while a stream is live on the open channel page.
assert.match(phone, /html:has\(#page-channel\.active\.ch-is-live\) \{ --ovnav-appbar-h: 0px; \}/, 'the page keeps no room for the bar');
assert.match(phone, /html:has\(#page-channel\.active\.ch-is-live\) \.ovnav-appbar,/, 'the bar is hidden');
--assert.match(phone, /:is\(\.mobile-chat-toggle, #messenger-toggle, #floating-chat-fab\) \{ display: none !important; \}/, 'no floating buttons');
++assert.match(phone, /:is\(\.mobile-chat-toggle, #floating-chat-fab\) \{ display: none !important; \}/, 'no floating buttons');
// The chat is in the page under the player, taking what is left: not a sheet behind a button.
assert.match(phone, /#page-channel\.ch-is-live\.page \.stream-layout > \.chat-sidebar \{[^}]*position: relative;[^}]*flex: 1 1 0;[^}]*transform: none;/, 'the chat is docked');
ddiff --git a/test/first-paint-stability.test.js b/test/first-paint-stability.test.js
iindex a4cc99c0..8642c10b 100644
---- a/test/first-paint-stability.test.js
++++ b/test/first-paint-stability.test.js
@@@ -57,7 +57,7 @@ assert.ok(home.includes('.hero-latest:empty { visibility: hidden; min-height: 33
assert.ok(/\.hero-stats \{ margin: 16px 0 10px; gap: 10px; min-height: 120px; \}/.test(home) && home.includes('.hero-stats { min-height: 204px; }') && home.includes('.hero-stats { min-height: 256px; margin-top: 14px; }'),
'the stats board reserves its loaded height, "Show all" button included');
const inline = /<style id="ovsk-css">([\s\S]*?)<\/style>/.exec(index)[1];
--assert.ok(inline.includes('.cosmetics-modal-overlay:not(.active),.messenger-panel:not(.open){display:none}') && /\.messenger-toggle\{position:fixed;/.test(inline), 'closed widgets stay out of the page until their stylesheets land');
++assert.ok(inline.includes('.cosmetics-modal-overlay:not(.active){display:none}'), 'closed widgets stay out of the page until their stylesheets land');
ok('the home hero reserves its late lines and the early widgets stay out of the flow');
// ── Without JavaScript ──
ddiff --git a/test/messages-dock.test.js b/test/messages-dock.test.js
nnew file mode 100644
iindex 00000000..ad2de4b9
---- /dev/null
++++ b/test/messages-dock.test.js
@@@ -0,0 +1,21 @@
++'use strict';
++
++const assert = require('assert');
++const fs = require('fs');
++const path = require('path');
++
++const root = path.join(__dirname, '..');
++const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
++
++for (const file of ['public/js/messenger.js', 'public/css/messenger.css']) {
++ assert.ok(fs.existsSync(path.join(root, file)), `${file} remains available to N-1 pages`);
++}
++
++const features = JSON.stringify(JSON.parse(read('public/features.json')));
++assert.ok(!/messenger/i.test(features), 'the lazy feature registry no longer loads the local messenger');
++
++for (const file of ['public/index.html', 'public/js/app.js', 'public/js/ov-navbar-live.js']) {
++ assert.ok(!/messenger/i.test(read(file)), `${file} does not load or open the local messenger`);
++}
++
++console.log('messages Dock: current pages use the Dock; N-1 messenger assets remain available');
ddiff --git a/test/money-on-demand.test.js b/test/money-on-demand.test.js
iindex 372c1f22..bef65743 100644
---- a/test/money-on-demand.test.js
++++ b/test/money-on-demand.test.js
@@@ -1,10 +1,9 @@
'use strict';
/**
-- * Vibes, tips, subscriptions and channel points (vibes.js, opencoins.js) and the DM panel (messenger.js) are not on
++ * Vibes, tips, subscriptions and channel points (vibes.js, opencoins.js) are not on
* every page (11-07, the owner: "make things faster and more optimized"; 31 KB that most visits never ran):
* - features.json → money comes with the pages that sell, tip or reward (channel, broadcast, dashboard), its
* inline handlers are stubs, and showModal() fetches it for a money modal on any other page;
-- * - features.json → messenger loads once someone is signed in, and the navbar's Messages waits for it.
* node test/money-on-demand.test.js
*/
const assert = require('assert');
@@@ -18,14 +17,13 @@ const app = pub('js/app.js');
const nav = pub('js/ov-navbar-live.js');
// Not on every page.
--for (const f of ['vibes.js', 'opencoins.js', 'messenger.js', 'messenger.css']) {
++for (const f of ['vibes.js', 'opencoins.js']) {
assert.ok(!new RegExp(`<(script|link)[^>]+/(js|css)/${f.replace('.', '\\.')}`).test(html), `${f} is not on every page`);
}
// The two features, and who depends on money.
assert.deepStrictEqual(reg.money.js, ['/js/vibes.js', '/js/opencoins.js']);
--assert.deepStrictEqual(reg.messenger.js, ['/js/messenger.js']);
--assert.deepStrictEqual(reg.messenger.css, ['/css/messenger.css']);
++assert.ok(!reg.messenger, 'the retired messenger is not a feature');
for (const name of ['channel', 'broadcast', 'dashboard']) assert.ok((reg[name].deps || []).includes('money'), `${name} brings money`);
// Every money function a fragment calls from an inline handler is a stub (or its page brings money).
for (const fn of ['openSubscribeForCurrentStream', 'toggleRewardsPanel', 'saveDashPointsConfig']) {
@@@ -38,8 +36,8 @@ assert.ok(/donate: \(\) => openvibeBucksDonateModal\(\),/.test(app) && /'buy-fun
assert.ok(/if \(MONEY_MODALS\.has\(id\) && typeof openvibeBucksDonateModal !== 'function' && window\.ov && ov\.load\) \{\n\s*ov\.load\('money'\)\.then\(\(\) => showModal\(id\)\)/.test(app), 'a money modal fetches money first');
assert.ok(/function openChannelRewards\(\) \{[\s\S]{0,200}ov\.load\('money'\)/.test(app), 'the rewards button fetches money');
--// The DM panel: on sign-in, and from the navbar's Messages.
--assert.ok(/if \(currentUser && window\.ov && ov\.load\) ov\.load\('messenger'\)/.test(app), 'messenger loads on sign-in');
--assert.ok(/messages: \{ onOpen: \(\) => \{[^\n]*ov\.load\('messenger'\)/.test(nav), 'the navbar\'s Messages loads it if needed');
++// The navbar uses Shared's default Messages action, which opens the Dock's Chats tab.
++assert.ok(!/messages:\s*\{/.test(nav), 'Live does not override Shared messages');
++assert.ok(!/ov\.load\('messenger'\)/.test(app), 'Live does not load the retired messenger');
--console.log('money on demand: vibes, opencoins and messenger load only where they are used');
++console.log('money on demand: money loads only where it is used; messages use the shared Dock');
ddiff --git a/test/n-1/service.js b/test/n-1/service.js
iindex 01504a82..ff7daa77 100644
---- a/test/n-1/service.js
++++ b/test/n-1/service.js
@@@ -20,7 +20,7 @@ const PRELOAD = path.join(__dirname, 'preload.js');
const ISSUER = 'https://openvibe.network';
// Served by OpenVibe.Chat on openvibe.live (Chat's deploy/nginx/openvibe.live-chat.locations.conf):
--// Live's chat widget and messenger are Chat's N-1 clients, checked there. /api/emotes is Chat's too
++// Live's chat widget is Chat's N-1 client, checked there. /api/emotes is Chat's too
// (plan T3; nginx sends it there), and the channel-moderation router Live used to answer at
// /api/channels/* moved to Chat's /api/chat/channels/* — so an N-1 tab's calls to those are
// Chat's to answer, and this harness neither records nor replays them against Live. Calls are Chat's too since Live's
ddiff --git a/test/security-redaction.test.js b/test/security-redaction.test.js
iindex b271ce50..fe8903cf 100644
---- a/test/security-redaction.test.js
++++ b/test/security-redaction.test.js
@@@ -255,7 +255,6 @@ const ok = (name) => { pass++; console.log(' ok -', name); };
// in value="…". Each helper is evaluated here against a breakout payload.
const cases = [
['public/js/app.js', /function escapeHtml\(str\) \{[\s\S]*?\n\}/, 'escapeHtml'],
-- ['public/js/messenger.js', /function esc\(s\) \{[\s\S]*?\n \}/, 'esc'],
['public/js/vibes.js', /function escHb\(s\) \{[^\n]*\}/, 'escHb'],
['public/js/call.js', /function _esc\(str\) \{[\s\S]*?\n\}/, '_esc'],
];