zebra-spaces: mute state survives hard refresh

sessionStorage key MUTE_STATE_KEY persists '1' (muted) / '0' (live) so
the page reconnects in the same mute state the user last clicked. No
hot-mic on refresh — if you left muted, you come back muted.

Restored from ensureMicAndUI() after getMic() succeeds; applyMuteState()
syncs the button label, the mic track enable flag, the user's own room
row, and broadcasts mic-state to peers if we came back muted. Cleared
only on explicit leave; survives role demote → re-promote so reclaiming
the mic keeps the user's previously-chosen state.
This commit is contained in:
Russell Ballestrini 2026-06-02 17:49:34 -04:00
parent 8631c106d2
commit 40cc4a5598
No known key found for this signature in database

View file

@ -1236,6 +1236,13 @@ const ACTIVE_CALL_KEY = 'zebra-spaces-active-call-v1';
* 'resume screen' button is the same friction as clicking 'share
* screen' again, so we just don't track screen state across reload. */
const ACTIVE_CAM_KEY = 'zebra-spaces-active-cam-v1';
/* mute state for the current tab — '1' means muted. Survives a hard
* refresh via sessionStorage so the page reconnects in the same state
* the user last set. Cleared on explicit leave. NOT cleared on role
* demotion (mic dropped), because the next promotion should respect
* the user's previously-chosen mute state instead of silently
* un-muting them. */
const MUTE_STATE_KEY = 'zebra-spaces-mute-v1';
/* theme toggle. The class is already applied pre-paint by the head
* script, so this just wires the click + keeps the button label in
@ -3052,6 +3059,11 @@ async function ensureMicAndUI(){
try {
await getMic(); await refreshMicList();
$('btn-mute').disabled = false;
/* restore the user's last mute choice. Hard refresh keeps
* sessionStorage so reconnecting in muted state is preserved
* without anyone hearing a hot-mic moment. */
applyMuteState();
if (muted) sendMicState();
if (myUUID) startMeter(myUUID, micStream);
} catch(e){ logLine('err','mic blocked: '+e.message); }
updateRoleUI();
@ -3503,16 +3515,23 @@ $('btn-decline-mic').addEventListener('click', () => {
* mute applies to all live senders (we may have many).
* ================================================================== */
let muted = false;
try { muted = sessionStorage.getItem(MUTE_STATE_KEY) === '1'; } catch(_){}
function applyMuteState(){
/* keep button UI, mic-track enable, room row, and peer broadcast in
* sync. Used by both the click handler and the post-mic-acquire
* restore path (so a hard refresh comes back muted if that's how the
* user left it). Safe to call without a mic — guards each side. */
if (micStream){ micStream.getAudioTracks().forEach(t=>t.enabled=!muted); }
$('btn-mute').textContent = muted ? 'unmute' : 'mute';
$('btn-mute').className = muted ? 'invert' : '';
const mm = members.get(myUUID);
if (mm){ mm.muted = muted; renderRoom(); }
}
$('btn-mute').addEventListener('click', () => {
if (!micStream) return;
muted = !muted;
micStream.getAudioTracks().forEach(t=>t.enabled=!muted);
$('btn-mute').textContent = muted ? 'unmute' : 'mute';
$('btn-mute').className = muted ? 'invert' : '';
/* flip our own mic icon — mic-state broadcasts inform OTHER peers, but
* without this our own row never updates locally */
const mm = members.get(myUUID);
if (mm){ mm.muted = muted; renderRoom(); }
try { sessionStorage.setItem(MUTE_STATE_KEY, muted ? '1' : '0'); } catch(_){}
applyMuteState();
sendMicState();
});
$('mic-select').addEventListener('change', async (e) => {
@ -3649,6 +3668,7 @@ $('btn-leave').addEventListener('click', async () => {
$('btn-leave').disabled = true;
$('btn-mute').disabled = true;
$('btn-mute').textContent = 'mute'; $('btn-mute').className=''; muted = false;
try { sessionStorage.removeItem(MUTE_STATE_KEY); } catch(_){}
logLine('', 'left the space');
});
@ -3658,8 +3678,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');
<footer style="margin:2.2rem auto 0;font-size:0.65rem;color:#999;line-height:1.7;word-break:break-all;font-family:monospace">
<span id="pi-seal" style="color:#777;cursor:default;user-select:none" title="">page integrity</span> &nbsp;·&nbsp; built <span class="stamp-date">2026-06-02</span><br>
md5 <span class="stamp-md5">b3db2fd79fc51b75927a92257dee6295</span><br>
sha256 <span class="stamp-sha">08421e83202e6042104b1b06b7f5075eab58c844f5b8109d9a00f5d295cd1cde</span><br>
md5 <span class="stamp-md5">60f9f219a6148fe06d689b2b8fa0affe</span><br>
sha256 <span class="stamp-sha">a6e49160225e5cf39525e12417491de5531c4b5388075bef3faed15e3799ad59</span><br>
<span style="color:#bbb">hashes are of this page with these two fields zeroed — to verify, blank them and re-hash</span><br>
<span style="color:#bbb">one self-contained file — <strong>save a copy</strong> and verify against these hashes; point at your own servers with ?signal= and ?turncred=, or <a href="host-your-own.html" style="color:#999">host your own community</a></span>
</footer>