openvibers/openvibe.live Commits
Open Live messages in the shared Dock (#716)
Changes
@@@ -2556,7 +2556,6 @@ body.bc-studio-on .bc-video-container .bc-video-placeholder { display: none !impbody.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'sright 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"]) {@@@ -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;@@@ -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.fabackground: 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.fatransform: 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); }}@@@ -279,17 +279,6 @@"saveDashPointsConfig"]},-- "messenger": {-- "css": [-- "/css/messenger.css"-- ],-- "js": [-- "/js/messenger.js"-- ],-- "stubs": [-- "openMessengerDm"-- ]-- },"dashboard": {"fragment": "dashboard","css": [@@@ -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 guideoverlay) and is fetched without blocking the first paint: media="print" makes the browsertreat 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>@@@ -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', {@@@ -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) {@@@ -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 });--})();@@@ -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@@@ -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.@@@ -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/);@@@ -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');@@@ -57,7 +57,7 @@ assert.ok(home.includes('.hero-latest:empty { visibility: hidden; min-height: 33assert.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 ──nnew file mode 100644@@@ -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');@@@ -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-funassert.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');@@@ -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@@@ -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'],];
@@ -2556,7 +2556,6 @@ body.bc-studio-on .bc-video-container .bc-video-placeholder { display: none !imp
@@ -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 {
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;
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 .bc-mobile-chat-fab,
body.bc-studio-on #navbar-mount,
body.bc-studio-on #navbar-mount,
body.bc-studio-on .navbar,
body.bc-studio-on .navbar,
@@ -2854,7 +2853,6 @@ body.bc-studio-on #bc-chat-messages .chat-conn-status { display: block !importan
@@ -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:disabled { opacity: 0.6; cursor: default; }
.bcs-golive-btn:focus-visible { outline: 3px solid #93c5fd; outline-offset: 2px; }
.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; }
.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
/* 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. */
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"]) {
body.bc-golive-dock-on .ovdk-pill:not([data-corner^="top"]) {
@@ -1628,10 +1628,8 @@ button.viewer-badge:focus-visible { outline: 2px solid #93c5fd; outline-offset:
@@ -1628,10 +1628,8 @@ button.viewer-badge:focus-visible { outline: 2px solid #93c5fd; outline-offset:
#page-channel #video-element,
#page-channel #video-element,
#page-channel #video-canvas { max-height: none; }
#page-channel #video-canvas { max-height: none; }
body:has(#page-channel.active) .chat-sidebar { top: 0; height: 100vh; height: 100dvh; }
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; }
#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). */
/* The next scheduled stream, from OpenVibe.Today (offline header). */
@@ -1655,7 +1653,7 @@ button.viewer-badge:focus-visible { outline: 2px solid #93c5fd; outline-offset:
@@ -1655,7 +1653,7 @@ button.viewer-badge:focus-visible { outline: 2px solid #93c5fd; outline-offset:
@media (max-width: 768px) and (orientation: portrait) {
@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-h: 0px; }
html:has(#page-channel.active.ch-is-live) .ovnav-appbar,
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 {
#page-channel.ch-is-live .stream-layout {
display: flex; flex-direction: column; overflow: hidden;
display: flex; flex-direction: column; overflow: hidden;
@@ -7305,9 +7305,9 @@ textarea.paste-textarea {
@@ -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. */
/* 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 Widget ─────────────────────────────────────── */
.floating-chat-fab {
.floating-chat-fab {
@@ -7334,7 +7334,7 @@ body.fabs-tucked .messenger-toggle, body.fabs-tucked .floating-chat-fab, body.fa
@@ -7334,7 +7334,7 @@ body.fabs-tucked .messenger-toggle, body.fabs-tucked .floating-chat-fab, body.fa
background: var(--accent-light);
background: var(--accent-light);
}
}
-/* ── Draggable floating buttons (messenger toggle + chat FAB) ── */
+/* ── Draggable floating chat button ── */
.fab-dragging {
.fab-dragging {
cursor: grabbing !important;
cursor: grabbing !important;
opacity: 0.92;
opacity: 0.92;
@@ -7784,7 +7784,6 @@ body.fabs-tucked .messenger-toggle, body.fabs-tucked .floating-chat-fab, body.fa
@@ -7784,7 +7784,6 @@ body.fabs-tucked .messenger-toggle, body.fabs-tucked .floating-chat-fab, body.fa
transform: translateY(0);
transform: translateY(0);
}
}
body.mobile-chat-visible .mobile-chat-toggle { display: none !important; }
body.mobile-chat-visible .mobile-chat-toggle { display: none !important; }
- body.mobile-chat-visible #messenger-toggle { display: none !important; }
.video-container { max-height: 50vh; }
.video-container { max-height: 50vh; }
.mobile-chat-toggle { display: flex; }
.mobile-chat-toggle { display: flex; }
/* The chat is an overlay at ≤1180px — give its header a working close button. */
/* 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
@@ -7805,7 +7804,6 @@ body.fabs-tucked .messenger-toggle, body.fabs-tucked .floating-chat-fab, body.fa
.chat-sidebar.mobile-chat-open {
.chat-sidebar.mobile-chat-open {
transform: translateX(0);
transform: translateX(0);
}
}
- body.mobile-chat-visible #messenger-toggle { display: none !important; }
}
}
@media (max-width: 1180px) and (orientation: portrait) {
@media (max-width: 1180px) and (orientation: portrait) {
.chat-sidebar {
.chat-sidebar {
@@ -9995,11 +9993,9 @@ body.ov-panel-open #floating-chat-fab { opacity: 0; pointer-events: none; transf
@@ -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). */
/* 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; } }
@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) {
@media (max-width: 760px) {
html.ov-has-appbar #floating-chat-fab { display: none !important; }
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); }
html.ov-has-appbar body { padding-bottom: calc(var(--ovnav-appbar-h, 58px) + 64px); }
}
}
@@ -279,17 +279,6 @@
@@ -279,17 +279,6 @@
"saveDashPointsConfig"
"saveDashPointsConfig"
]
]
},
},
- "messenger": {
- "css": [
- "/css/messenger.css"
- ],
- "js": [
- "/js/messenger.js"
- ],
- "stubs": [
- "openMessengerDm"
- ]
- },
"dashboard": {
"dashboard": {
"fragment": "dashboard",
"fragment": "dashboard",
"css": [
"css": [
@@ -99,15 +99,13 @@
@@ -99,15 +99,13 @@
.ov-route-error>div{flex:1;min-width:180px;display:grid;gap:2px}
.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}
.ov-route-error span{color:var(--text-muted,#8b93ad);font-size:.9rem}
@media (prefers-reduced-motion:reduce){.ov-route-skel span{animation:none}}
@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>
</style>
<!-- Stylesheets.
<!-- Stylesheets.
Blocking set = what the home page paints with. Everything else belongs to another route
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
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. -->
treat it as non-critical, and the onload flips it back to all once it has arrived. -->
<link rel="stylesheet" href="/css/style.css">
<link rel="stylesheet" href="/css/style.css">
@@ -492,7 +490,7 @@
@@ -492,7 +490,7 @@
</div>
</div>
</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="/js/draggable-fab.js" defer></script>
<script src="/shared/footer.js" defer></script>
<script src="/shared/footer.js" defer></script>
<script src="/js/footer-live.js" defer></script>
<script src="/js/footer-live.js" defer></script>
@@ -784,8 +784,13 @@ function onAuthChange() {
@@ -784,8 +784,13 @@ function onAuthChange() {
// The navbar is the network's shared component; tell it who is here (js/ov-navbar-live.js).
// 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 (window.LiveNav) window.LiveNav.sync(currentUser || null);
if (currentUser) loadBalance();
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 {
try {
window.dispatchEvent(new CustomEvent('openvibe-auth-changed', {
window.dispatchEvent(new CustomEvent('openvibe-auth-changed', {
@@ -2167,13 +2167,6 @@ function handleChatMessage(msg) {
@@ -2167,13 +2167,6 @@ function handleChatMessage(msg) {
if (rContainer && typeof renderRedemption === 'function') renderRedemption(msg, rContainer);
if (rContainer && typeof renderRedemption === 'function') renderRedemption(msg, rContainer);
break;
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': {
case 'vc-call-invite': {
_showIncomingVcCallOverlay(msg);
_showIncomingVcCallOverlay(msg);
break;
break;
@@ -4050,14 +4043,16 @@ function ctxReply() {
@@ -4050,14 +4043,16 @@ function ctxReply() {
dismissContextMenu();
dismissContextMenu();
}
}
-function ctxWhisper(username) {
+async function ctxWhisper(username) {
dismissContextMenu();
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) {
async function ctxCallUser(username, userId) {
@@ -1,5 +1,5 @@
@@ -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.
* drag-and-droppable, robustly, on desktop and touch.
*
*
* Key behaviours:
* Key behaviours:
@@ -157,41 +157,12 @@
@@ -157,41 +157,12 @@
}
}
window.makeFabDraggable = makeFabDraggable;
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');
var fab = document.getElementById('floating-chat-fab');
if (fab) makeFabDraggable(fab, 'fabpos: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);
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot);
else 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 });
-})();
@@ -82,8 +82,6 @@
@@ -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: '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') },
{ 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.
// Same-origin links are routes of this single-page app.
onNavigate: (href, e) => (typeof handleLinkClick === 'function' ? handleLinkClick(e, href) : true),
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
// No speculation rules: the router answers same-site links, so a prefetched page was never used, and the
@@ -1144,7 +1144,7 @@ app.get('/media/:username', (req, res) => {
@@ -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;
// 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]}`));
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
// 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.
// (drama stories, beefs, the ears) is gone, so it lands on the Rundown.
@@ -30,14 +30,11 @@ const uses = [
@@ -30,14 +30,11 @@ const uses = [
['chat.js', /ovImgW\(clip\.creator_avatar, 64\)/],
['chat.js', /ovImgW\(clip\.creator_avatar, 64\)/],
['chat.js', /ovImgW\(u\.avatar_url, 64\)/],
['chat.js', /ovImgW\(u\.avatar_url, 64\)/],
['chat-shared.js', /typeof ovImgW === 'function' \? ovImgW\(p\.avatar_url, 64\) : p\.avatar_url/],
['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}`));
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');
const chatShared = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'chat-shared.js'), 'utf8');
assert.match(chatShared, /data-full="\$\{esc\(p\.avatar_url\)\}"/);
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.
// The content feed (Content, AI Moments) builds its avatars as nodes.
const feed = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'content-feed.js'), 'utf8');
const feed = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'content-feed.js'), 'utf8');
assert.match(feed, /typeof ovImgW === 'function' \? ovImgW\(full, 64\) : full/);
assert.match(feed, /typeof ovImgW === 'function' \? ovImgW\(full, 64\) : full/);
@@ -17,7 +17,7 @@ assert.ok(phone.startsWith('@media (max-width: 768px) and (orientation: portrait
@@ -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.
// 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-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, /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.
// 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');
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');
@@ -57,7 +57,7 @@ assert.ok(home.includes('.hero-latest:empty { visibility: hidden; min-height: 33
@@ -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; }'),
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');
'the stats board reserves its loaded height, "Show all" button included');
const inline = /<style id="ovsk-css">([\s\S]*?)<\/style>/.exec(index)[1];
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');
ok('the home hero reserves its late lines and the early widgets stay out of the flow');
// ── Without JavaScript ──
// ── Without JavaScript ──
new file mode 100644
new file mode 100644
@@ -0,0 +1,21 @@
@@ -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');
@@ -1,10 +1,9 @@
@@ -1,10 +1,9 @@
'use strict';
'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):
* 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
* - 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;
* 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
* node test/money-on-demand.test.js
*/
*/
const assert = require('assert');
const assert = require('assert');
@@ -18,14 +17,13 @@ const app = pub('js/app.js');
@@ -18,14 +17,13 @@ const app = pub('js/app.js');
const nav = pub('js/ov-navbar-live.js');
const nav = pub('js/ov-navbar-live.js');
// Not on every page.
// 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`);
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.
// The two features, and who depends on money.
assert.deepStrictEqual(reg.money.js, ['/js/vibes.js', '/js/opencoins.js']);
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`);
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).
// 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']) {
for (const fn of ['openSubscribeForCurrentStream', 'toggleRewardsPanel', 'saveDashPointsConfig']) {
@@ -38,8 +36,8 @@ assert.ok(/donate: \(\) => openvibeBucksDonateModal\(\),/.test(app) && /'buy-fun
@@ -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(/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');
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');
@@ -20,7 +20,7 @@ const PRELOAD = path.join(__dirname, 'preload.js');
@@ -20,7 +20,7 @@ const PRELOAD = path.join(__dirname, 'preload.js');
const ISSUER = 'https://openvibe.network';
const ISSUER = 'https://openvibe.network';
// Served by OpenVibe.Chat on openvibe.live (Chat's deploy/nginx/openvibe.live-chat.locations.conf):
// 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
// (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
// /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
// Chat's to answer, and this harness neither records nor replays them against Live. Calls are Chat's too since Live's
@@ -255,7 +255,6 @@ const ok = (name) => { pass++; console.log(' ok -', name); };
@@ -255,7 +255,6 @@ const ok = (name) => { pass++; console.log(' ok -', name); };
// in value="…". Each helper is evaluated here against a breakout payload.
// in value="…". Each helper is evaluated here against a breakout payload.
const cases = [
const cases = [
['public/js/app.js', /function escapeHtml\(str\) \{[\s\S]*?\n\}/, 'escapeHtml'],
['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/vibes.js', /function escHb\(s\) \{[^\n]*\}/, 'escHb'],
['public/js/call.js', /function _esc\(str\) \{[\s\S]*?\n\}/, '_esc'],
['public/js/call.js', /function _esc\(str\) \{[\s\S]*?\n\}/, '_esc'],
];
];