aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/app.js
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-26 15:09:20 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-26 15:09:20 +0200
commitdb7f81fd08742847f3ebea061c75530b7b31b934 (patch)
tree14b308686860eea0a9c12c47c39f8a68a1bba376 /packages/meshbay-hub/src/meshbay_hub/static/app.js
parent7126fd3c265ba75d77b449bfd0f83f5f3e584b74 (diff)
parent59d9f50bf41b2b38b7c95f8da52b698dff923c2d (diff)
downloadmeshbay-db7f81fd08742847f3ebea061c75530b7b31b934.tar.gz
Merge branch 'debug/webrtc-lock-resume'
WebRTC transport dies silently after an extended mobile screen lock (confirmed live via client-side trace + node logs): ICE goes disconnected -> failed within ~10s of each other on both ends, but the DataChannel's readyState stays "open" throughout, so nothing failed fast — every request just sat out its own timeout, matching the reported symptom (poster spinners, blocked chat, dead new streams, stuck music). - Automatic reconnect on WebRTC "failed": capped exponential backoff, redoes the full signaling handshake, wakes immediately on visibilitychange instead of waiting out a throttled backoff timer. - Fixed two real bugs the reconnect work exposed: the signaling POST to the hub kept using the token captured at construction, never the fresh one fetched per reconnect attempt (401 loop, no possible recovery); and connect() re-armed a diagnostic listener/interval on every attempt without disposing of the previous one. - pipelinedDownload retries a lost chunk instead of aborting the whole transfer — covers Files downloads, video poster/thumbnail fetches, and music-player.js's blob-based track download. - music-player.js: don't throw "Transport not connected" while a reconnect is already landing (waitForReconnect); prefetch depth now adapts to network type (5 tracks ahead on Wi-Fi, 3 on cellular or unrecognized — Firefox/Safari included, where the detection API is simply absent). - video-player.js: onReconnected reissues the existing seek-to-current-time path, so a mid-stream reconnect looks like an ordinary seek rather than a dead player; holds a Screen Wake Lock unconditionally while open. - New opt-in (off by default) user preference: keep the screen on during audio playback, for whoever wants to trade battery for sidestepping the screen-lock gap entirely — off by default because the ordinary expectation (matching Spotify/Deezer) is that the phone locks on its own while listening. - hub: /app and / now serve Cache-Control: no-store — the SPA shell had no cache header at all, so a browser that cached it heuristically could keep re-serving an old build (old ASSET_V, old JS) through any number of reloads or pull-to-refreshes. Verified against real production use across many rounds (demo groups, actual mobile screen-lock testing) rather than synthetic reproduction alone. 430 hub tests + 650 node tests passing throughout.
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/app.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/app.js33
1 files changed, 33 insertions, 0 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js
index 5ec1ac8..446360c 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js
@@ -1593,6 +1593,12 @@ function SettingsPage({ user, theme, onThemeChange, groups, onPrefsChange }) {
() => Object.fromEntries((groups || []).map(g => [g.id, !!g.muted])));
const [globalMute, setGlobalMute] = useState(false);
const [defaultTab, setDefaultTab] = useState('chat');
+ // Off by default (musicbay.md §2.2): the ordinary expectation, matching
+ // Spotify/Deezer, is that the phone locks on its own idle timer while
+ // listening. This is for whoever would rather trade battery for it —
+ // e.g. to ride out the WebRTC screen-lock reconnect gap without waiting
+ // on the automatic recovery at all.
+ const [keepScreenOnAudio, setKeepScreenOnAudio] = useState(false);
const onLocaleChange = useCallback((e) => {
const code = e.target.value;
@@ -1610,10 +1616,29 @@ function SettingsPage({ user, theme, onThemeChange, groups, onPrefsChange }) {
.then(prefs => {
if (prefs.notifications_disabled === 'true') setGlobalMute(true);
if (prefs.default_tab) setDefaultTab(prefs.default_tab);
+ if (prefs.music_keep_screen_on === 'true') setKeepScreenOnAudio(true);
})
.catch(() => {});
}, [user.token]);
+ const toggleKeepScreenOnAudio = useCallback(async () => {
+ const next = !keepScreenOnAudio;
+ setKeepScreenOnAudio(next);
+ try {
+ await hubFetch('/v1/users/me/preferences/music_keep_screen_on', {
+ method: 'PUT', token: user.token,
+ body: { value: next ? 'true' : 'false' },
+ });
+ // A string, matching what a fresh page load reads from the hub
+ // (prefs.music_keep_screen_on === 'true' above) — music-player.js
+ // compares against that same string, and userPrefs is one shared bag
+ // fed from both this immediate update and that load.
+ if (onPrefsChange) onPrefsChange({ music_keep_screen_on: next ? 'true' : 'false' });
+ } catch (err) {
+ setKeepScreenOnAudio(!next);
+ }
+ }, [keepScreenOnAudio, user.token, onPrefsChange]);
+
const toggleGlobalMute = useCallback(async () => {
const next = !globalMute;
setGlobalMute(next);
@@ -1802,6 +1827,14 @@ function SettingsPage({ user, theme, onThemeChange, groups, onPrefsChange }) {
</select>
</div>
<p class="settings-hint">${t('settings.default_tab_hint')}</p>
+ <div class="settings-row">
+ <span class="settings-label">${t('settings.music_keep_screen_on')}</span>
+ <label class="settings-value" style="cursor:pointer">
+ <input type="checkbox" checked=${keepScreenOnAudio}
+ onChange=${toggleKeepScreenOnAudio} />
+ </label>
+ </div>
+ <p class="settings-hint">${t('settings.music_keep_screen_on_hint')}</p>
</div>
${platform.isNative && html`