openvibers/openvibe.live Commits

Popout chat: get off /chat — that's the SPA's global chat page

OP OpenVibers committed 26 Aug 2026 · 33835ee52a · 4 files, +18 −12

The pretty popout URLs shadowed the SPA route serving the global chat page
(voice channels and all): /chat/* returned the popout shell instead of the
app. Popout now lives at /popout/<user>[/<id>] with /popout-chat/… as an
alias; bare /popout (or no username) is global chat, and /chat/* is the
SPA's again. The SPA boot/popstate guards now match the new prefixes so
the popout window never runs the SPA router.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_017AyeSFu7qdLdR64Zv99QNm

Changes

ddiff --git a/public/js/app.js b/public/js/app.js
iindex 3bb8ca36..7d1fbf6b 100644
---- a/public/js/app.js
++++ b/public/js/app.js
@@@ -8335,13 +8335,15 @@ document.addEventListener('DOMContentLoaded', async () => {
// Route from current URL instead of always going home — but not in the popout
// chat window, which drives its own chat UI and has no SPA pages to route to.
-- if (!location.pathname.startsWith('/popout-chat')) {
++ // Covers /popout-chat.html AND the pretty /popout/<user>[/<id>] URLs.
++ if (!location.pathname.startsWith('/popout')) {
routeFromURL();
}
});
// Handle browser back/forward — wait for auth to be resolved first
window.addEventListener('popstate', () => {
++ if (location.pathname.startsWith('/popout')) return; // popout window: no SPA routing
if (_userLoaded) {
routeFromURL();
}
ddiff --git a/public/js/chat.js b/public/js/chat.js
iindex a58fba4c..0e6ac9c5 100644
---- a/public/js/chat.js
++++ b/public/js/chat.js
@@@ -6613,15 +6613,16 @@ function popoutChat(mode = 'global', streamId = null, username = null) {
const left = window.screenX + window.outerWidth - w - 20;
const top = window.screenY + 80;
-- // Pretty URL: /chat/<username>[/<streamId>] (the popout auto-follows the channel's
-- // live state from there). Legacy query form only when no username is known.
++ // Pretty URL: /popout/<username>[/<streamId>] (the popout auto-follows the
++ // channel's live state from there). NOT /chat/… — that's the SPA's global chat
++ // page. Legacy query form only when no username is known.
let url;
if (mode === 'stream' && username) {
-- url = `/chat/${encodeURIComponent(username)}${streamId ? '/' + streamId : ''}`;
++ url = `/popout/${encodeURIComponent(username)}${streamId ? '/' + streamId : ''}`;
} else if (mode === 'stream' && streamId) {
url = `/popout-chat.html?popout=1&mode=stream&stream=${encodeURIComponent(streamId)}`; // popout resolves + upgrades
} else {
-- url = '/chat/global';
++ url = '/popout/global';
}
const popup = window.open(url, `openvibe_chat_${key}`,
ddiff --git a/public/popout-chat.html b/public/popout-chat.html
iindex 860654c8..d98b51fc 100644
---- a/public/popout-chat.html
++++ b/public/popout-chat.html
@@@ -285,8 +285,9 @@
let activeStreamId = null; // stream id the chat is currently joined to (null = channel chat)
(function parseUrl() {
-- const seg = window.location.pathname.split('/').filter(Boolean); // ['chat', user?, id?]
-- if (seg[0] === 'chat') {
++ const seg = window.location.pathname.split('/').filter(Boolean); // ['popout'|'popout-chat', user?, id?]
++ if (seg[0] === 'popout' || seg[0] === 'popout-chat') {
++ // No username (or explicit "global") → global chat.
if (!seg[1] || seg[1].toLowerCase() === 'global') { mode = 'global'; return; }
mode = 'channel';
channelName = decodeURIComponent(seg[1]);
@@@ -302,8 +303,8 @@
})();
function prettyPath() {
-- if (mode !== 'channel' || !channelName) return '/chat/global';
-- return '/chat/' + encodeURIComponent(channelName) + (activeStreamId ? '/' + activeStreamId : '');
++ if (mode !== 'channel' || !channelName) return '/popout/global';
++ return '/popout/' + encodeURIComponent(channelName) + (activeStreamId ? '/' + activeStreamId : '');
}
// Keep the address bar honest without polluting history — swaps replace, they
// don't stack, so Back still leaves the popout instead of replaying old streams.
ddiff --git a/server/index.js b/server/index.js
iindex 6cf44786..a9952c3a 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -663,11 +663,13 @@ app.get(['/kiosk', '/kiosk.html'], (req, res) => {
res.sendFile(path.join(__dirname, '../public/kiosk.html'));
});
--// Popout chat pretty URLs: /chat/global, /chat/:username (channel chat) and
--// /chat/:username/:streamId (pinned stream, auto-follows the channel's live state).
++// Popout chat pretty URLs: /popout/global, /popout/:username (channel chat) and
++// /popout/:username/:streamId (pinned stream, auto-follows the channel's live state).
++// NOT /chat/… — the SPA's global chat page (voice channels et al) lives there.
// The legacy /popout-chat.html?mode=…&stream=… form keeps working (static file) and
// self-upgrades to the pretty URL client-side.
--app.get(['/chat', '/chat/*'], (req, res) => {
++// /popout-chat/… works as an alias; with no username both prefixes mean global chat.
++app.get(['/popout', '/popout/*', '/popout-chat', '/popout-chat/*'], (req, res) => {
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
res.sendFile(path.join(__dirname, '../public/popout-chat.html'));
});