fix: off-state colors, mobile audio route, iOS rail/PTR, in-place join, draggable bars (0.1.16/batch76)
Regressions I introduced, now fixed:
#8d Mic/Camera OFF went DIM — my new `.meet-ic.off` rule overrode the original RED.
Scoped the lite style to the speaker button only; mic/cam OFF are red again.
#7 Guest "left the meeting" was unstyled (dark text on blue, no card): the card CSS was
scoped to .guest-prejoin only. Shared with .guest-left. Rejoin no longer dead-ends —
if the room is gone we say "This meeting has ended" and hide the button.
Reported:
#8d Speaker button no longer shows on desktop (devices already live under the mic ▾).
#8c/#8e Mobile speaker button now CYCLES Speaker → Earpiece → Bluetooth (icon follows the
route); dropped the redundant "Audio devices" entry from the ⋮ menu.
#8a iOS gap under the bottom rail: the page rubber-band-bounced, exposing background beneath
the fixed bar. overscroll-behavior:none + fixed body pins it.
#8b Pull-to-refresh now fires on iOS too (overscroll-behavior:contain on the lists so Safari's
rubber band stops swallowing the gesture; scrollTop>2 tolerance for momentum).
#5 A meeting link no longer opens a whole new window: same-origin urls navigate the main
window in the shell, and a link clicked in chat joins the meeting IN PLACE.
New observations:
1. The viewer's mic now starts MUTED on a screen session (and can actually be un/muted).
2. iOS lightbox close/download buttons moved below the Dynamic Island (safe-area insets).
3. Meeting chat: recipient picker moved to the BOTTOM next to the input; a private message
auto-targets your reply back to that person; private vs everyone are visibly different
(brand amber vs blue/neutral) and the compose area tints in private mode.
4. Meeting bar + screen-session bars are DRAGGABLE (position remembered) so they stop
covering the shared screen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+4
-2
@@ -273,10 +273,12 @@ function createWindow() {
|
|||||||
// no-login "Share my screen" option + sign-in. It redirects logged-in users straight to /home.
|
// no-login "Share my screen" option + sign-in. It redirects logged-in users straight to /home.
|
||||||
win.loadURL(SERVER_URL + '/');
|
win.loadURL(SERVER_URL + '/');
|
||||||
|
|
||||||
// Open target=_blank / external links in the system browser instead of a new Electron window.
|
// Links: EXTERNAL ones go to the system browser. OUR OWN urls (e.g. a meeting invite link clicked in
|
||||||
|
// chat) must NOT spawn a second app window (#5) — navigate the main window instead.
|
||||||
win.webContents.setWindowOpenHandler(({ url }) => {
|
win.webContents.setWindowOpenHandler(({ url }) => {
|
||||||
if (!url.startsWith(SERVER_URL)) { shell.openExternal(url); return { action: 'deny' }; }
|
if (!url.startsWith(SERVER_URL)) { shell.openExternal(url); return { action: 'deny' }; }
|
||||||
return { action: 'allow' };
|
try { if (win && !win.isDestroyed()) { if (win.isMinimized()) win.restore(); win.show(); win.focus(); win.loadURL(url); } } catch (_) {}
|
||||||
|
return { action: 'deny' };
|
||||||
});
|
});
|
||||||
|
|
||||||
// Spell-check menu. Two ways in:
|
// Spell-check menu. Two ways in:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "biz-connect-desktop",
|
"name": "biz-connect-desktop",
|
||||||
"version": "0.1.15",
|
"version": "0.1.16",
|
||||||
"description": "Biz Connect technician desktop client — loads the Connect web UI with native screen capture",
|
"description": "Biz Connect technician desktop client — loads the Connect web UI with native screen capture",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "BizGaze",
|
"name": "BizGaze",
|
||||||
|
|||||||
@@ -185,7 +185,9 @@ function connectWS(){
|
|||||||
case 'session-ready': if(statusEl)statusEl.innerHTML='<img src="/loaders/loader-orbit.svg" width="20" height="20" style="vertical-align:-5px;margin-right:7px" alt="">Allowed — connecting…'; break;
|
case 'session-ready': if(statusEl)statusEl.innerHTML='<img src="/loaders/loader-orbit.svg" width="20" height="20" style="vertical-align:-5px;margin-right:7px" alt="">Allowed — connecting…'; break;
|
||||||
case 'offer': await pc.setRemoteDescription(new RTCSessionDescription(m.sdp));
|
case 'offer': await pc.setRemoteDescription(new RTCSessionDescription(m.sdp));
|
||||||
// Acquire the agent mic once; on renegotiation (e.g. customer unmutes) just answer.
|
// Acquire the agent mic once; on renegotiation (e.g. customer unmutes) just answer.
|
||||||
if(!window.__mic){ try{ const mic=await navigator.mediaDevices.getUserMedia({audio:true}); window.__mic=mic; mic.getAudioTracks().forEach(t=>pc.addTrack(t,mic)); }catch(e){} }
|
// Acquire the agent mic MUTED by default — joining a screen session shouldn't open a hot mic on the
|
||||||
|
// customer without the agent choosing to speak (new #1). The Mic button unmutes it.
|
||||||
|
if(!window.__mic){ try{ const mic=await navigator.mediaDevices.getUserMedia({audio:true}); window.__mic=mic; mic.getAudioTracks().forEach(t=>{ t.enabled=false; pc.addTrack(t,mic); }); try{ setMicBtn(false); }catch(_){} }catch(e){} }
|
||||||
const ans=await pc.createAnswer(); await pc.setLocalDescription(ans);
|
const ans=await pc.createAnswer(); await pc.setLocalDescription(ans);
|
||||||
ws.send(JSON.stringify({type:'answer',sessionId,sdp:pc.localDescription})); break;
|
ws.send(JSON.stringify({type:'answer',sessionId,sdp:pc.localDescription})); break;
|
||||||
case 'ice-candidate': if(m.candidate&&pc) await pc.addIceCandidate(new RTCIceCandidate(m.candidate)); break;
|
case 'ice-candidate': if(m.candidate&&pc) await pc.addIceCandidate(new RTCIceCandidate(m.candidate)); break;
|
||||||
@@ -329,7 +331,7 @@ function buildBar(){
|
|||||||
// Floats over the bottom-right corner with tiny icons, so the shared screen fills the whole viewport.
|
// Floats over the bottom-right corner with tiny icons, so the shared screen fills the whole viewport.
|
||||||
bar.style.cssText='position:fixed;right:16px;bottom:16px;z-index:2147483000;display:flex;flex-direction:row;gap:8px;align-items:center;background:rgba(15,23,42,.72);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);padding:7px 9px;border-radius:14px;box-shadow:0 8px 22px rgba(0,0,0,.35)';
|
bar.style.cssText='position:fixed;right:16px;bottom:16px;z-index:2147483000;display:flex;flex-direction:row;gap:8px;align-items:center;background:rgba(15,23,42,.72);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);padding:7px 9px;border-radius:14px;box-shadow:0 8px 22px rgba(0,0,0,.35)';
|
||||||
const I=(n)=>(window.ic?window.ic(n,16):'');
|
const I=(n)=>(window.ic?window.ic(n,16):'');
|
||||||
const mic=_btn('micBtn',I('mic'),'Mic','#2563eb');
|
const mic=_btn('micBtn',I('micOff'),'Unmute','#6b7280'); // starts MUTED (new #1)
|
||||||
const ctrl=_btn('ctrlBtn',I('monitor'),'Control OFF — click their screen to take control','#6b7280');
|
const ctrl=_btn('ctrlBtn',I('monitor'),'Control OFF — click their screen to take control','#6b7280');
|
||||||
const chat=_btn('chatBtn',I('chat'),'Chat','#334155');
|
const chat=_btn('chatBtn',I('chat'),'Chat','#334155');
|
||||||
const rec=_btn('recBtn','<svg viewBox="0 0 24 24" width="15" height="15"><circle cx="12" cy="12" r="7" fill="#ef4444"/></svg>','Record','#334155');
|
const rec=_btn('recBtn','<svg viewBox="0 0 24 24" width="15" height="15"><circle cx="12" cy="12" r="7" fill="#ef4444"/></svg>','Record','#334155');
|
||||||
@@ -340,13 +342,22 @@ function buildBar(){
|
|||||||
// Shrink from the default 48px round to a compact 38px so they read as "tiny icons".
|
// Shrink from the default 48px round to a compact 38px so they read as "tiny icons".
|
||||||
[mic,ctrl,chat,rec,end].forEach(b=>{ b.style.width='38px'; b.style.height='38px'; b.style.boxShadow='none'; });
|
[mic,ctrl,chat,rec,end].forEach(b=>{ b.style.width='38px'; b.style.height='38px'; b.style.boxShadow='none'; });
|
||||||
ctrl.onclick=()=>setEngaged(!rcEngaged);
|
ctrl.onclick=()=>setEngaged(!rcEngaged);
|
||||||
|
makeBarDraggable(bar,'bzc_connectbar_pos'); // new #4: the bar hides part of the shared screen → move it
|
||||||
// A hint over the screen until they take control, so it's obvious how to start driving.
|
// A hint over the screen until they take control, so it's obvious how to start driving.
|
||||||
if(!document.getElementById('ctrlHint')){
|
if(!document.getElementById('ctrlHint')){
|
||||||
const h=document.createElement('div'); h.id='ctrlHint';
|
const h=document.createElement('div'); h.id='ctrlHint';
|
||||||
h.textContent='Click the screen to take control · Esc to release';
|
h.textContent='Click the screen to take control · Esc to release';
|
||||||
document.body.appendChild(h);
|
document.body.appendChild(h);
|
||||||
}
|
}
|
||||||
mic.onclick=()=>{const m=window.__mic;if(!m)return;const t=m.getAudioTracks()[0];if(!t)return;t.enabled=!t.enabled;mic.title=t.enabled?'Mute':'Unmute';mic.innerHTML='<span style="display:inline-flex">'+I(t.enabled?'mic':'micOff')+'</span>';mic.style.background=t.enabled?'#2563eb':'#6b7280';};
|
mic.onclick=async()=>{
|
||||||
|
// If the mic wasn't acquired yet (e.g. the customer never renegotiated), get it now, then toggle.
|
||||||
|
if(!window.__mic){
|
||||||
|
try{ const s=await navigator.mediaDevices.getUserMedia({audio:true}); window.__mic=s; s.getAudioTracks().forEach(t=>{ t.enabled=false; if(pc) pc.addTrack(t,s); }); }
|
||||||
|
catch(e){ toast('Microphone permission was blocked.'); return; }
|
||||||
|
}
|
||||||
|
const t=window.__mic.getAudioTracks()[0]; if(!t) return;
|
||||||
|
t.enabled=!t.enabled; setMicBtn(t.enabled);
|
||||||
|
};
|
||||||
chat.onclick=toggleChat;
|
chat.onclick=toggleChat;
|
||||||
rec.onclick=()=>{ if(mediaRecorder&&mediaRecorder.state==='recording') stopRecording(); else startRecording(); };
|
rec.onclick=()=>{ if(mediaRecorder&&mediaRecorder.state==='recording') stopRecording(); else startRecording(); };
|
||||||
end.onclick=()=>{ try{ws.send(JSON.stringify({type:'end-session',sessionId,reason:'agent-ended'}));}catch(_){} };
|
end.onclick=()=>{ try{ws.send(JSON.stringify({type:'end-session',sessionId,reason:'agent-ended'}));}catch(_){} };
|
||||||
@@ -420,6 +431,39 @@ video.addEventListener('contextmenu',e=>e.preventDefault());
|
|||||||
// in chat, never leaks keystrokes to the remote desktop). Click the screen (or the Control button) to
|
// in chat, never leaks keystrokes to the remote desktop). Click the screen (or the Control button) to
|
||||||
// take control; click away, press Esc, or leave the window to release it.
|
// take control; click away, press Esc, or leave the window to release it.
|
||||||
function rcTyping(){ const a=document.activeElement; return a && (a.tagName==='INPUT'||a.tagName==='TEXTAREA'); }
|
function rcTyping(){ const a=document.activeElement; return a && (a.tagName==='INPUT'||a.tagName==='TEXTAREA'); }
|
||||||
|
// new #4: drag the floating control bar anywhere — it otherwise sits over part of the shared screen.
|
||||||
|
// Drag from the bar background (not the buttons); position is remembered.
|
||||||
|
function makeBarDraggable(el,key){
|
||||||
|
if(!el||el._drag) return; el._drag=true;
|
||||||
|
let sx=0,sy=0,ox=0,oy=0,dragging=false;
|
||||||
|
const pt=(e)=>e.touches?e.touches[0]:e;
|
||||||
|
const clamp=()=>{ const w=el.offsetWidth,h=el.offsetHeight;
|
||||||
|
let x=parseFloat(el.style.left||'0'), y=parseFloat(el.style.top||'0');
|
||||||
|
x=Math.max(4,Math.min(x,window.innerWidth-w-4)); y=Math.max(4,Math.min(y,window.innerHeight-h-4));
|
||||||
|
el.style.left=x+'px'; el.style.top=y+'px'; };
|
||||||
|
const pin=(r)=>{ el.style.left=r.left+'px'; el.style.top=r.top+'px'; el.style.right='auto'; el.style.bottom='auto'; };
|
||||||
|
try{ const s=JSON.parse(localStorage.getItem(key)||'null'); if(s&&typeof s.x==='number'){ pin({left:s.x,top:s.y}); clamp(); } }catch(_){}
|
||||||
|
const move=(e)=>{ if(!dragging) return; const p=pt(e); el.style.left=(ox+(p.clientX-sx))+'px'; el.style.top=(oy+(p.clientY-sy))+'px'; clamp(); if(e.cancelable) e.preventDefault(); };
|
||||||
|
const up=()=>{ if(!dragging) return; dragging=false; el.style.opacity='';
|
||||||
|
document.removeEventListener('mousemove',move); document.removeEventListener('mouseup',up);
|
||||||
|
document.removeEventListener('touchmove',move); document.removeEventListener('touchend',up);
|
||||||
|
try{ localStorage.setItem(key, JSON.stringify({x:parseFloat(el.style.left), y:parseFloat(el.style.top)})); }catch(_){} };
|
||||||
|
const down=(e)=>{ if(e.target.closest('button,select,input,a')) return;
|
||||||
|
const r=el.getBoundingClientRect(); pin(r);
|
||||||
|
const p=pt(e); sx=p.clientX; sy=p.clientY; ox=r.left; oy=r.top; dragging=true; el.style.opacity='.92';
|
||||||
|
document.addEventListener('mousemove',move); document.addEventListener('mouseup',up);
|
||||||
|
document.addEventListener('touchmove',move,{passive:false}); document.addEventListener('touchend',up);
|
||||||
|
if(e.cancelable) e.preventDefault(); };
|
||||||
|
el.addEventListener('mousedown',down); el.addEventListener('touchstart',down,{passive:false});
|
||||||
|
el.style.cursor='move'; el.title='Drag to move';
|
||||||
|
}
|
||||||
|
// Mic button state (kept global so the offer handler can set it once the mic is acquired, muted).
|
||||||
|
function setMicBtn(on){
|
||||||
|
const b=document.getElementById('micBtn'); if(!b) return;
|
||||||
|
b.title=on?'Mute':'Unmute';
|
||||||
|
b.innerHTML='<span style="display:inline-flex">'+(window.ic?window.ic(on?'mic':'micOff',16):'')+'</span>';
|
||||||
|
b.style.background=on?'#2563eb':'#6b7280';
|
||||||
|
}
|
||||||
let rcEngaged=false;
|
let rcEngaged=false;
|
||||||
function setEngaged(on){
|
function setEngaged(on){
|
||||||
const next=!!on; if(next===rcEngaged) return;
|
const next=!!on; if(next===rcEngaged) return;
|
||||||
|
|||||||
+125
-44
@@ -19,8 +19,11 @@
|
|||||||
<style>
|
<style>
|
||||||
:root{ --brand:#FFC708; --brand-d:#E0AC00; --blue:#1F3B73; --blue-d:#16294f; --blue-soft:#EAF0FB; --ink:#1f2430; --muted:#6b7280; --bg:#f6f8fb; --card:#fff; --line:#e6e9ef; --green:#16a34a; --red:#b91c1c; }
|
:root{ --brand:#FFC708; --brand-d:#E0AC00; --blue:#1F3B73; --blue-d:#16294f; --blue-soft:#EAF0FB; --ink:#1f2430; --muted:#6b7280; --bg:#f6f8fb; --card:#fff; --line:#e6e9ef; --green:#16a34a; --red:#b91c1c; }
|
||||||
*{box-sizing:border-box;}
|
*{box-sizing:border-box;}
|
||||||
html,body{height:100%;}
|
/* #8a: iOS rubber-band scrolling bounced the whole page, exposing the background BELOW the fixed
|
||||||
body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--ink);margin:0;display:flex;flex-direction:column;height:100vh;height:100dvh;overflow:hidden;}
|
bottom rail (the "gap"). overscroll-behavior:none pins the page; the rail's own height already
|
||||||
|
includes the home-indicator inset. */
|
||||||
|
html,body{height:100%;overscroll-behavior:none;}
|
||||||
|
body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--ink);margin:0;display:flex;flex-direction:column;height:100vh;height:100dvh;overflow:hidden;position:fixed;inset:0;width:100%;}
|
||||||
|
|
||||||
/* ---- Top bar ---- */
|
/* ---- Top bar ---- */
|
||||||
/* #4: the top blue bar is removed everywhere — bell+profile live in the chat-list header. */
|
/* #4: the top blue bar is removed everywhere — bell+profile live in the chat-list header. */
|
||||||
@@ -153,7 +156,10 @@
|
|||||||
::-webkit-scrollbar-corner{background:transparent;}
|
::-webkit-scrollbar-corner{background:transparent;}
|
||||||
*{scrollbar-width:thin;scrollbar-color:#c7d0dd transparent;}
|
*{scrollbar-width:thin;scrollbar-color:#c7d0dd transparent;}
|
||||||
|
|
||||||
.chatlist{overflow-y:auto;flex:1 1 auto;padding:.4rem;scrollbar-width:thin;scrollbar-color:transparent transparent;}
|
/* #8b: overscroll-behavior:contain stops Safari/Chrome's own rubber-band + page pull-to-refresh from
|
||||||
|
swallowing the gesture, so OUR pull-to-refresh fires on iOS as well as Android. */
|
||||||
|
.chatlist{overflow-y:auto;flex:1 1 auto;padding:.4rem;scrollbar-width:thin;scrollbar-color:transparent transparent;overscroll-behavior-y:contain;-webkit-overflow-scrolling:touch;}
|
||||||
|
.convo-msgs{overscroll-behavior-y:contain;-webkit-overflow-scrolling:touch;}
|
||||||
.chatlist:hover{scrollbar-color:#c7d0dd transparent;}
|
.chatlist:hover{scrollbar-color:#c7d0dd transparent;}
|
||||||
.chatlist::-webkit-scrollbar{width:6px;}
|
.chatlist::-webkit-scrollbar{width:6px;}
|
||||||
.chatlist::-webkit-scrollbar-button,.chatlist::-webkit-scrollbar-button:start:decrement,.chatlist::-webkit-scrollbar-button:end:increment{display:none!important;height:0!important;width:0!important;}
|
.chatlist::-webkit-scrollbar-button,.chatlist::-webkit-scrollbar-button:start:decrement,.chatlist::-webkit-scrollbar-button:end:increment{display:none!important;height:0!important;width:0!important;}
|
||||||
@@ -328,9 +334,11 @@
|
|||||||
.bubble.mine code{background:rgba(255,255,255,.2);}
|
.bubble.mine code{background:rgba(255,255,255,.2);}
|
||||||
.lightbox{position:fixed;inset:0;z-index:9900;background:rgba(8,12,22,.88);display:flex;align-items:center;justify-content:center;} /* above modals (9800) so the media→image preview opens on top */
|
.lightbox{position:fixed;inset:0;z-index:9900;background:rgba(8,12,22,.88);display:flex;align-items:center;justify-content:center;} /* above modals (9800) so the media→image preview opens on top */
|
||||||
.lightbox img{max-width:92vw;max-height:88vh;border-radius:10px;box-shadow:0 16px 50px rgba(0,0,0,.5);}
|
.lightbox img{max-width:92vw;max-height:88vh;border-radius:10px;box-shadow:0 16px 50px rgba(0,0,0,.5);}
|
||||||
.lightbox .lb-close,.lightbox .lb-dl{position:absolute;top:18px;border:none;background:rgba(255,255,255,.14);color:#fff;width:44px;height:44px;border-radius:50%;display:grid;place-items:center;cursor:pointer;text-decoration:none;}
|
/* new #2: on iOS these sat UNDER the Dynamic Island / notch and couldn't be tapped. Push them below
|
||||||
.lightbox .lb-close{right:18px;}
|
the safe area (and clear of the right-side inset in landscape). */
|
||||||
.lightbox .lb-dl{right:74px;}
|
.lightbox .lb-close,.lightbox .lb-dl{position:absolute;top:calc(18px + env(safe-area-inset-top,0px));border:none;background:rgba(255,255,255,.14);color:#fff;width:44px;height:44px;border-radius:50%;display:grid;place-items:center;cursor:pointer;text-decoration:none;z-index:2;}
|
||||||
|
.lightbox .lb-close{right:calc(18px + env(safe-area-inset-right,0px));}
|
||||||
|
.lightbox .lb-dl{right:calc(74px + env(safe-area-inset-right,0px));}
|
||||||
.lightbox .lb-close:hover,.lightbox .lb-dl:hover{background:rgba(255,255,255,.28);}
|
.lightbox .lb-close:hover,.lightbox .lb-dl:hover{background:rgba(255,255,255,.28);}
|
||||||
.lightbox img{max-width:82vw;}
|
.lightbox img{max-width:82vw;}
|
||||||
.lightbox .lb-nav{position:absolute;top:50%;transform:translateY(-50%);border:none;background:rgba(255,255,255,.16);color:#fff;width:32px;height:32px;border-radius:50%;display:grid;place-items:center;cursor:pointer;}
|
.lightbox .lb-nav{position:absolute;top:50%;transform:translateY(-50%);border:none;background:rgba(255,255,255,.16);color:#fff;width:32px;height:32px;border-radius:50%;display:grid;place-items:center;cursor:pointer;}
|
||||||
@@ -414,23 +422,28 @@
|
|||||||
.meet-panel .mp-copylink:hover{filter:brightness(1.05);}
|
.meet-panel .mp-copylink:hover{filter:brightness(1.05);}
|
||||||
/* In-meeting chat panel (brand-styled) */
|
/* In-meeting chat panel (brand-styled) */
|
||||||
.meet-chat{width:320px;}
|
.meet-chat{width:320px;}
|
||||||
.meet-chat .mc-to{display:flex;align-items:center;gap:.5rem;padding:.5rem .8rem;border-bottom:1px solid var(--line);font-size:.8rem;color:var(--muted);}
|
/* Compose block at the BOTTOM: recipient picker sits right above the input (new #3). */
|
||||||
.meet-chat .mc-to select{flex:1;border:1px solid var(--line);border-radius:8px;padding:.35rem .5rem;font:inherit;font-size:.82rem;background:#fbfcfe;color:var(--ink);}
|
.meet-chat .mc-compose{border-top:1px solid var(--line);background:var(--card);}
|
||||||
|
.meet-chat .mc-compose.private{background:#fffbeb;} /* private mode is visibly different */
|
||||||
|
.meet-chat .mc-to{display:flex;align-items:center;gap:.5rem;padding:.45rem .7rem .1rem;font-size:.75rem;color:var(--muted);}
|
||||||
|
.meet-chat .mc-to select{flex:1;border:1px solid var(--line);border-radius:8px;padding:.3rem .45rem;font:inherit;font-size:.8rem;background:#fbfcfe;color:var(--ink);}
|
||||||
|
.meet-chat .mc-compose.private .mc-to select{border-color:#f5c518;background:#fffdf5;color:#7a5c00;font-weight:600;}
|
||||||
.meet-chat .mc-log{flex:1;overflow-y:auto;padding:.6rem .7rem;display:flex;flex-direction:column;gap:.4rem;}
|
.meet-chat .mc-log{flex:1;overflow-y:auto;padding:.6rem .7rem;display:flex;flex-direction:column;gap:.4rem;}
|
||||||
.meet-chat .mc-empty{color:var(--muted);font-size:.82rem;text-align:center;margin:auto;padding:1rem;}
|
.meet-chat .mc-empty{color:var(--muted);font-size:.82rem;text-align:center;margin:auto;padding:1rem;}
|
||||||
.meet-chat .mc-row{display:flex;}
|
.meet-chat .mc-row{display:flex;}
|
||||||
.meet-chat .mc-row.mine{justify-content:flex-end;}
|
.meet-chat .mc-row.mine{justify-content:flex-end;}
|
||||||
.meet-chat .mc-bub{max-width:82%;background:#f1f5f9;border-radius:12px;padding:.4rem .6rem;}
|
/* Everyone = neutral/blue. Private = brand amber, clearly a different conversation (new #3). */
|
||||||
|
.meet-chat .mc-bub{max-width:82%;background:#f1f5f9;border:1px solid transparent;border-radius:12px;padding:.4rem .6rem;}
|
||||||
.meet-chat .mc-row.mine .mc-bub{background:var(--blue);color:#fff;}
|
.meet-chat .mc-row.mine .mc-bub{background:var(--blue);color:#fff;}
|
||||||
.meet-chat .mc-row.priv .mc-bub{outline:1.5px dashed rgba(31,59,115,.4);}
|
.meet-chat .mc-row.priv .mc-bub{background:#fef3c7;border-color:#fcd34d;color:#1f2430;}
|
||||||
.meet-chat .mc-row.mine.priv .mc-bub{outline-color:rgba(255,255,255,.55);}
|
.meet-chat .mc-row.mine.priv .mc-bub{background:#7a5c00;border-color:var(--brand,#FFC708);color:#fff;}
|
||||||
.meet-chat .mc-nm{font-size:.7rem;font-weight:700;color:var(--blue);margin-bottom:.1rem;display:flex;gap:.35rem;align-items:center;}
|
.meet-chat .mc-nm{font-size:.7rem;font-weight:700;color:var(--blue);margin-bottom:.1rem;display:flex;gap:.35rem;align-items:center;}
|
||||||
.meet-chat .mc-row.mine .mc-nm{color:rgba(255,255,255,.9);}
|
.meet-chat .mc-row.mine .mc-nm{color:rgba(255,255,255,.9);}
|
||||||
.meet-chat .mc-priv{font-weight:600;font-size:.62rem;text-transform:uppercase;letter-spacing:.03em;background:rgba(31,59,115,.1);color:var(--blue);border-radius:99px;padding:.02rem .35rem;}
|
.meet-chat .mc-priv{font-weight:600;font-size:.62rem;text-transform:uppercase;letter-spacing:.03em;background:rgba(31,59,115,.1);color:var(--blue);border-radius:99px;padding:.02rem .35rem;}
|
||||||
.meet-chat .mc-row.mine .mc-priv{background:rgba(255,255,255,.2);color:#fff;}
|
.meet-chat .mc-row.mine .mc-priv{background:rgba(255,255,255,.2);color:#fff;}
|
||||||
.meet-chat .mc-tx{font-size:.88rem;line-height:1.35;word-wrap:break-word;overflow-wrap:anywhere;}
|
.meet-chat .mc-tx{font-size:.88rem;line-height:1.35;word-wrap:break-word;overflow-wrap:anywhere;}
|
||||||
.meet-chat .mc-at{font-size:.62rem;opacity:.6;margin-top:.15rem;text-align:right;}
|
.meet-chat .mc-at{font-size:.62rem;opacity:.6;margin-top:.15rem;text-align:right;}
|
||||||
.meet-chat .mc-foot{display:flex;gap:.4rem;padding:.5rem .6rem;border-top:1px solid var(--line);}
|
.meet-chat .mc-foot{display:flex;gap:.4rem;padding:.4rem .6rem .55rem;}
|
||||||
.meet-chat .mc-foot input{flex:1;border:1px solid var(--line);border-radius:9px;padding:.5rem .6rem;font:inherit;font-size:.86rem;background:#fbfcfe;color:var(--ink);outline:none;}
|
.meet-chat .mc-foot input{flex:1;border:1px solid var(--line);border-radius:9px;padding:.5rem .6rem;font:inherit;font-size:.86rem;background:#fbfcfe;color:var(--ink);outline:none;}
|
||||||
.meet-chat .mc-foot input:focus{border-color:var(--blue);}
|
.meet-chat .mc-foot input:focus{border-color:var(--blue);}
|
||||||
.meet-chat .mc-send{flex:0 0 auto;width:38px;border:none;border-radius:9px;background:var(--brand,#FFC708);color:#1f2430;display:grid;place-items:center;cursor:pointer;}
|
.meet-chat .mc-send{flex:0 0 auto;width:38px;border:none;border-radius:9px;background:var(--brand,#FFC708);color:#1f2430;display:grid;place-items:center;cursor:pointer;}
|
||||||
@@ -589,10 +602,11 @@
|
|||||||
/* Guest pre-join: brand backdrop, camera preview, mic/cam toggles, name. */
|
/* Guest pre-join: brand backdrop, camera preview, mic/cam toggles, name. */
|
||||||
.guest-prejoin{position:fixed;inset:0;z-index:9900;display:flex;align-items:center;justify-content:center;padding:1rem;overflow:auto;
|
.guest-prejoin{position:fixed;inset:0;z-index:9900;display:flex;align-items:center;justify-content:center;padding:1rem;overflow:auto;
|
||||||
background:radial-gradient(1200px 600px at 50% -10%, #2b4f96 0%, #1F3B73 55%, #16294f 100%);}
|
background:radial-gradient(1200px 600px at 50% -10%, #2b4f96 0%, #1F3B73 55%, #16294f 100%);}
|
||||||
.guest-prejoin .gp-card{background:#fff;border-radius:20px;padding:1.5rem 1.5rem 1.3rem;width:100%;max-width:420px;box-shadow:0 24px 60px rgba(0,0,0,.35);text-align:center;}
|
.guest-prejoin .gp-card, .guest-left .gp-card{background:#fff;border-radius:20px;padding:1.5rem 1.5rem 1.3rem;width:100%;max-width:420px;box-shadow:0 24px 60px rgba(0,0,0,.35);text-align:center;}
|
||||||
.guest-prejoin .gp-brand{display:flex;align-items:center;justify-content:center;gap:.5rem;margin-bottom:1rem;}
|
.guest-prejoin .gp-brand, .guest-left .gp-brand{display:flex;align-items:center;justify-content:center;gap:.5rem;margin-bottom:1rem;}
|
||||||
.guest-prejoin .gp-brand img{height:34px;width:auto;object-fit:contain;}
|
.guest-prejoin .gp-brand img, .guest-left .gp-brand img{height:34px;width:auto;object-fit:contain;}
|
||||||
.guest-prejoin .gp-brand span{font-size:1rem;font-weight:600;color:var(--blue);} .guest-prejoin .gp-brand b{color:var(--blue);}
|
.guest-prejoin .gp-brand span, .guest-left .gp-brand span{font-size:1rem;font-weight:600;color:var(--blue);} .guest-prejoin .gp-brand b, .guest-left .gp-brand b{color:var(--blue);}
|
||||||
|
.guest-left .gp-card h2{margin:.2rem 0 .3rem;font-size:1.2rem;color:var(--ink);}
|
||||||
.guest-prejoin .gp-preview{position:relative;width:100%;aspect-ratio:16/10;border-radius:14px;overflow:hidden;background:#0f172a;display:grid;place-items:center;margin-bottom:.8rem;}
|
.guest-prejoin .gp-preview{position:relative;width:100%;aspect-ratio:16/10;border-radius:14px;overflow:hidden;background:#0f172a;display:grid;place-items:center;margin-bottom:.8rem;}
|
||||||
.guest-prejoin .gp-preview video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;transform:scaleX(-1);}
|
.guest-prejoin .gp-preview video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;transform:scaleX(-1);}
|
||||||
.guest-prejoin .gp-avatar{width:76px;height:76px;border-radius:50%;display:grid;place-items:center;font-weight:700;font-size:1.5rem;color:#334155;z-index:1;}
|
.guest-prejoin .gp-avatar{width:76px;height:76px;border-radius:50%;display:grid;place-items:center;font-weight:700;font-size:1.5rem;color:#334155;z-index:1;}
|
||||||
@@ -601,13 +615,13 @@
|
|||||||
.guest-prejoin .gp-tg{display:inline-flex;align-items:center;gap:.4rem;border:1px solid var(--line);background:var(--blue-soft);color:var(--blue);border-radius:99px;padding:.45rem .85rem;font:inherit;font-size:.82rem;font-weight:600;cursor:pointer;}
|
.guest-prejoin .gp-tg{display:inline-flex;align-items:center;gap:.4rem;border:1px solid var(--line);background:var(--blue-soft);color:var(--blue);border-radius:99px;padding:.45rem .85rem;font:inherit;font-size:.82rem;font-weight:600;cursor:pointer;}
|
||||||
.guest-prejoin .gp-tg.off{background:#fee2e2;border-color:#fecaca;color:#b91c1c;}
|
.guest-prejoin .gp-tg.off{background:#fee2e2;border-color:#fecaca;color:#b91c1c;}
|
||||||
.guest-prejoin h2{margin:0 0 .2rem;font-size:1.2rem;color:var(--ink);}
|
.guest-prejoin h2{margin:0 0 .2rem;font-size:1.2rem;color:var(--ink);}
|
||||||
.guest-prejoin .gp-sub{margin:0 0 1rem;font-size:.86rem;color:var(--muted);}
|
.guest-prejoin .gp-sub, .guest-left .gp-sub{margin:0 0 1rem;font-size:.86rem;color:var(--muted);}
|
||||||
.guest-prejoin input{width:100%;box-sizing:border-box;border:1.5px solid var(--line);border-radius:11px;padding:.7rem .8rem;font-size:.95rem;font-family:inherit;background:#fbfcfe;color:var(--ink);outline:none;}
|
.guest-prejoin input{width:100%;box-sizing:border-box;border:1.5px solid var(--line);border-radius:11px;padding:.7rem .8rem;font-size:.95rem;font-family:inherit;background:#fbfcfe;color:var(--ink);outline:none;}
|
||||||
.guest-prejoin input:focus{border-color:var(--blue);}
|
.guest-prejoin input:focus{border-color:var(--blue);}
|
||||||
.guest-prejoin input.err{border-color:#dc2626;background:#fef2f2;}
|
.guest-prejoin input.err{border-color:#dc2626;background:#fef2f2;}
|
||||||
.guest-prejoin .gp-join{width:100%;margin-top:.75rem;border:none;border-radius:11px;background:var(--blue);color:#fff;font:inherit;font-size:.95rem;font-weight:600;padding:.75rem;cursor:pointer;}
|
.guest-prejoin .gp-join, .guest-left .gp-join{width:100%;margin-top:.75rem;border:none;border-radius:11px;background:var(--blue);color:#fff;font:inherit;font-size:.95rem;font-weight:600;padding:.75rem;cursor:pointer;}
|
||||||
.guest-prejoin .gp-join:hover{filter:brightness(1.08);}
|
.guest-prejoin .gp-join:hover{filter:brightness(1.08);}
|
||||||
.guest-prejoin .gp-note{display:flex;align-items:center;justify-content:center;gap:.3rem;margin-top:.7rem;font-size:.74rem;color:var(--muted);}
|
.guest-prejoin .gp-note, .guest-left .gp-note{display:flex;align-items:center;justify-content:center;gap:.3rem;margin-top:.7rem;font-size:.74rem;color:var(--muted);}
|
||||||
/* Guest "left the meeting" — same brand language as the pre-join card. */
|
/* Guest "left the meeting" — same brand language as the pre-join card. */
|
||||||
.guest-left{position:fixed;inset:0;z-index:9900;display:flex;align-items:center;justify-content:center;padding:1rem;
|
.guest-left{position:fixed;inset:0;z-index:9900;display:flex;align-items:center;justify-content:center;padding:1rem;
|
||||||
background:radial-gradient(1200px 600px at 50% -10%, #2b4f96 0%, #1F3B73 55%, #16294f 100%);}
|
background:radial-gradient(1200px 600px at 50% -10%, #2b4f96 0%, #1F3B73 55%, #16294f 100%);}
|
||||||
@@ -691,7 +705,14 @@
|
|||||||
.meet-bar .mic-caret{position:absolute;right:-2px;bottom:-2px;width:20px;height:20px;border-radius:50%;border:2px solid var(--card,#fff);background:var(--blue);color:#fff;display:grid;place-items:center;cursor:pointer;padding:0;z-index:2;}
|
.meet-bar .mic-caret{position:absolute;right:-2px;bottom:-2px;width:20px;height:20px;border-radius:50%;border:2px solid var(--card,#fff);background:var(--blue);color:#fff;display:grid;place-items:center;cursor:pointer;padding:0;z-index:2;}
|
||||||
.meet-bar .mic-caret:hover{filter:brightness(1.12);}
|
.meet-bar .mic-caret:hover{filter:brightness(1.12);}
|
||||||
.meet-bar .more-btn{display:none;} /* desktop: everything is on the bar */
|
.meet-bar .more-btn{display:none;} /* desktop: everything is on the bar */
|
||||||
.meet-bar .meet-ic.off{background:#e2e8f0;color:#94a3b8;} /* speaker off → lite colour (#8d) */
|
/* new #4: the bar can be dragged clear of the shared screen */
|
||||||
|
.draggable{cursor:move;}
|
||||||
|
.draggable.dragging{opacity:.92;box-shadow:0 18px 40px rgba(0,0,0,.35);z-index:9500;}
|
||||||
|
.meet-bar.draggable{border-radius:14px;}
|
||||||
|
/* Desktop already has speaker+mic devices behind the mic ▾ — no separate speaker button (#8d). */
|
||||||
|
.meet-bar .spk-btn{display:none;}
|
||||||
|
/* Speaker OFF is a "lite" state; mic/cam OFF stay RED (see .meet-ic.off above — do NOT override it). */
|
||||||
|
.meet-bar .spk-btn.off{background:#e2e8f0;color:#94a3b8;}
|
||||||
.gi-list .mrow.dm-able{cursor:pointer;border-radius:9px;}
|
.gi-list .mrow.dm-able{cursor:pointer;border-radius:9px;}
|
||||||
.gi-list .mrow.dm-able:hover{background:var(--blue-soft);}
|
.gi-list .mrow.dm-able:hover{background:var(--blue-soft);}
|
||||||
.spk-menu .mm-opt{display:flex;align-items:center;gap:.55rem;}
|
.spk-menu .mm-opt{display:flex;align-items:center;gap:.55rem;}
|
||||||
@@ -701,6 +722,7 @@
|
|||||||
.meet-bar .mic-caret{display:none;} /* #8c: no device dropdown on mobile — plain on/off */
|
.meet-bar .mic-caret{display:none;} /* #8c: no device dropdown on mobile — plain on/off */
|
||||||
.meet-bar .sec-btn{display:none;} /* screen, record, transcript, chat, participants → ⋮ */
|
.meet-bar .sec-btn{display:none;} /* screen, record, transcript, chat, participants → ⋮ */
|
||||||
.meet-bar .more-btn{display:inline-flex;}
|
.meet-bar .more-btn{display:inline-flex;}
|
||||||
|
.meet-bar .spk-btn{display:inline-flex;} /* speaker/earpiece/bluetooth toggle is MOBILE-only */
|
||||||
}
|
}
|
||||||
.call-invite .ci-ico{width:38px;height:38px;border-radius:50%;background:#dcfce7;color:#15803d;display:grid;place-items:center;flex:0 0 auto;}
|
.call-invite .ci-ico{width:38px;height:38px;border-radius:50%;background:#dcfce7;color:#15803d;display:grid;place-items:center;flex:0 0 auto;}
|
||||||
.call-invite .ci-txt{font-size:.88rem;color:var(--ink);line-height:1.25;}
|
.call-invite .ci-txt{font-size:.88rem;color:var(--ink);line-height:1.25;}
|
||||||
@@ -967,7 +989,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<script src="/icons.js?v=6"></script>
|
<script src="/icons.js?v=6"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@twemoji/api@15.1.0/dist/twemoji.min.js" crossorigin="anonymous"></script>
|
<script src="https://cdn.jsdelivr.net/npm/@twemoji/api@15.1.0/dist/twemoji.min.js" crossorigin="anonymous"></script>
|
||||||
<script>window.__BUILD='2026-07-13-batch75';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
|
<script>window.__BUILD='2026-07-13-batch76';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
|
||||||
// Render modern (Twemoji) emojis in place of the OS's flat ones. No-op if the CDN didn't load
|
// Render modern (Twemoji) emojis in place of the OS's flat ones. No-op if the CDN didn't load
|
||||||
// (emojis stay as plain Unicode). (#5)
|
// (emojis stay as plain Unicode). (#5)
|
||||||
function twemojify(el){ try{ if(el && window.twemoji) window.twemoji.parse(el, { folder:'svg', ext:'.svg' }); }catch(_){} }</script>
|
function twemojify(el){ try{ if(el && window.twemoji) window.twemoji.parse(el, { folder:'svg', ext:'.svg' }); }catch(_){} }</script>
|
||||||
@@ -1231,7 +1253,7 @@ function enablePullRefresh(el, onRefresh){
|
|||||||
if(getComputedStyle(el).position==='static') el.style.position='relative';
|
if(getComputedStyle(el).position==='static') el.style.position='relative';
|
||||||
const ind=document.createElement('div'); ind.className='ptr-ind'; ind.innerHTML='<span class="ptr-g">↻</span>'; el.appendChild(ind);
|
const ind=document.createElement('div'); ind.className='ptr-ind'; ind.innerHTML='<span class="ptr-g">↻</span>'; el.appendChild(ind);
|
||||||
let startY=0, pulling=false, dist=0, busy=false; const TRIGGER=70;
|
let startY=0, pulling=false, dist=0, busy=false; const TRIGGER=70;
|
||||||
el.addEventListener('touchstart',(e)=>{ if(busy||el.scrollTop>0||e.touches.length!==1) return; startY=e.touches[0].clientY; pulling=true; dist=0; },{passive:true});
|
el.addEventListener('touchstart',(e)=>{ if(busy||el.scrollTop>2||e.touches.length!==1) return; startY=e.touches[0].clientY; pulling=true; dist=0; },{passive:true}); // >2 (not >0): iOS momentum leaves a sub-pixel scrollTop
|
||||||
el.addEventListener('touchmove',(e)=>{ if(!pulling||busy) return; dist=e.touches[0].clientY-startY; if(dist<=0||el.scrollTop>0){ pulling=false; ind.style.opacity='0'; ind.style.transform='translateY(-48px)'; return; } const d=Math.min(dist*0.5,84); ind.style.opacity=String(Math.min(d/56,1)); ind.style.transform='translateY('+(d-48)+'px)'; if(dist>10) e.preventDefault(); },{passive:false});
|
el.addEventListener('touchmove',(e)=>{ if(!pulling||busy) return; dist=e.touches[0].clientY-startY; if(dist<=0||el.scrollTop>0){ pulling=false; ind.style.opacity='0'; ind.style.transform='translateY(-48px)'; return; } const d=Math.min(dist*0.5,84); ind.style.opacity=String(Math.min(d/56,1)); ind.style.transform='translateY('+(d-48)+'px)'; if(dist>10) e.preventDefault(); },{passive:false});
|
||||||
el.addEventListener('touchend',async()=>{ if(!pulling) return; pulling=false; if(dist>=TRIGGER){ busy=true; ind.classList.add('spin'); ind.style.opacity='1'; ind.style.transform='translateY(14px)'; try{ await onRefresh(); }catch(_){} await new Promise(r=>setTimeout(r,250)); ind.classList.remove('spin'); busy=false; } ind.style.opacity='0'; ind.style.transform='translateY(-48px)'; dist=0; });
|
el.addEventListener('touchend',async()=>{ if(!pulling) return; pulling=false; if(dist>=TRIGGER){ busy=true; ind.classList.add('spin'); ind.style.opacity='1'; ind.style.transform='translateY(14px)'; try{ await onRefresh(); }catch(_){} await new Promise(r=>setTimeout(r,250)); ind.classList.remove('spin'); busy=false; } ind.style.opacity='0'; ind.style.transform='translateY(-48px)'; dist=0; });
|
||||||
}
|
}
|
||||||
@@ -2058,12 +2080,16 @@ function renderMeetChat(){
|
|||||||
const recips=meetChatRecipients();
|
const recips=meetChatRecipients();
|
||||||
if(meetChatTo && !recips.some(r=>r.id===meetChatTo)) meetChatTo=''; // recipient left → fall back to Everyone
|
if(meetChatTo && !recips.some(r=>r.id===meetChatTo)) meetChatTo=''; // recipient left → fall back to Everyone
|
||||||
const head='<div class="mp-head"><b>'+ic('chat',15)+' Meeting chat</b><span style="flex:1"></span><button class="mp-x" title="Close">'+ic('x',16)+'</button></div>';
|
const head='<div class="mp-head"><b>'+ic('chat',15)+' Meeting chat</b><span style="flex:1"></span><button class="mp-x" title="Close">'+ic('x',16)+'</button></div>';
|
||||||
const toSel='<div class="mc-to"><span>To</span><select id="mcTo"><option value=""'+(!meetChatTo?' selected':'')+'>Everyone</option>'+recips.map(r=>'<option value="'+pEsc(r.id)+'"'+(meetChatTo===r.id?' selected':'')+'>'+pEsc(r.name)+' (private)</option>').join('')+'</select></div>';
|
|
||||||
const body='<div class="mc-log" id="mcLog">'+(meetChatLog.length?meetChatLog.map(mcRowHTML).join(''):'<div class="mc-empty">No messages yet — say hello 👋</div>')+'</div>';
|
const body='<div class="mc-log" id="mcLog">'+(meetChatLog.length?meetChatLog.map(mcRowHTML).join(''):'<div class="mc-empty">No messages yet — say hello 👋</div>')+'</div>';
|
||||||
const foot='<form class="mc-foot" id="mcForm"><input id="mcInput" placeholder="Type a message…" autocomplete="off" maxlength="2000"><button type="submit" class="mc-send" title="Send">'+ic('send',16)+'</button></form>';
|
// The recipient picker lives at the BOTTOM, right where you type (new #3).
|
||||||
p.innerHTML=head+toSel+body+foot;
|
const foot='<div class="mc-compose'+(meetChatTo?' private':'')+'">'
|
||||||
|
+'<div class="mc-to"><span>To</span><select id="mcTo"><option value=""'+(!meetChatTo?' selected':'')+'>Everyone</option>'
|
||||||
|
+recips.map(r=>'<option value="'+pEsc(r.id)+'"'+(meetChatTo===r.id?' selected':'')+'>'+pEsc(r.name)+' (private)</option>').join('')+'</select></div>'
|
||||||
|
+'<form class="mc-foot" id="mcForm"><input id="mcInput" placeholder="'+(meetChatTo?'Private message…':'Message everyone…')+'" autocomplete="off" maxlength="2000"><button type="submit" class="mc-send" title="Send">'+ic('send',16)+'</button></form>'
|
||||||
|
+'</div>';
|
||||||
|
p.innerHTML=head+body+foot;
|
||||||
p.querySelector('.mp-x').onclick=()=>p.remove();
|
p.querySelector('.mp-x').onclick=()=>p.remove();
|
||||||
const sel=p.querySelector('#mcTo'); if(sel) sel.onchange=()=>{ meetChatTo=sel.value; };
|
const sel=p.querySelector('#mcTo'); if(sel) sel.onchange=()=>{ meetChatTo=sel.value; renderMeetChat(); };
|
||||||
const form=p.querySelector('#mcForm'); if(form) form.onsubmit=(e)=>{ e.preventDefault(); meetChatSend(); };
|
const form=p.querySelector('#mcForm'); if(form) form.onsubmit=(e)=>{ e.preventDefault(); meetChatSend(); };
|
||||||
const log=p.querySelector('#mcLog'); if(log) log.scrollTop=log.scrollHeight;
|
const log=p.querySelector('#mcLog'); if(log) log.scrollTop=log.scrollHeight;
|
||||||
const inp=p.querySelector('#mcInput'); if(inp) setTimeout(()=>inp.focus(),0);
|
const inp=p.querySelector('#mcInput'); if(inp) setTimeout(()=>inp.focus(),0);
|
||||||
@@ -2083,8 +2109,16 @@ function meetChatSend(){
|
|||||||
}
|
}
|
||||||
function meetChatAppend(m){
|
function meetChatAppend(m){
|
||||||
meetChatLog.push(m); if(meetChatLog.length>500) meetChatLog.shift();
|
meetChatLog.push(m); if(meetChatLog.length>500) meetChatLog.shift();
|
||||||
|
// A PRIVATE message to me → my reply defaults back to that person (new #3), so you don't accidentally
|
||||||
|
// answer a private note in front of everyone.
|
||||||
|
const privFrom=(m.direct && m.from!=='__self' && meetPeers.has(m.from)) ? m.from : null;
|
||||||
|
if(privFrom) meetChatTo=privFrom;
|
||||||
const panel=document.getElementById('meetChat');
|
const panel=document.getElementById('meetChat');
|
||||||
if(panel){ const log=document.getElementById('mcLog'); if(log){ const empty=log.querySelector('.mc-empty'); if(empty) empty.remove(); log.insertAdjacentHTML('beforeend', mcRowHTML(m)); log.scrollTop=log.scrollHeight; } }
|
if(panel){
|
||||||
|
if(privFrom){ renderMeetChat(); return; } // re-render so the "To" picker + compose tint follow
|
||||||
|
const log=document.getElementById('mcLog');
|
||||||
|
if(log){ const empty=log.querySelector('.mc-empty'); if(empty) empty.remove(); log.insertAdjacentHTML('beforeend', mcRowHTML(m)); log.scrollTop=log.scrollHeight; }
|
||||||
|
}
|
||||||
else if(m.from!=='__self'){ meetChatUnread++; updateMeetChatBadge(); try{ playPing(); }catch(_){} }
|
else if(m.from!=='__self'){ meetChatUnread++; updateMeetChatBadge(); try{ playPing(); }catch(_){} }
|
||||||
}
|
}
|
||||||
function refreshMeetPanel(){ if(document.getElementById('meetPanel')) renderMeetPanel(); }
|
function refreshMeetPanel(){ if(document.getElementById('meetPanel')) renderMeetPanel(); }
|
||||||
@@ -2284,6 +2318,10 @@ async function openConvo(kind,id){
|
|||||||
if(_selMode){ const bb=e.target.closest('.bubble'); if(bb) toggleSel(bb); return; } // #1: selection mode — tap toggles
|
if(_selMode){ const bb=e.target.closest('.bubble'); if(bb) toggleSel(bb); return; } // #1: selection mode — tap toggles
|
||||||
const fw=e.target.closest('.fwd-btn'); if(fw){ enterSelect(fw.dataset.fwd); return; } // #1: enter forward-selection
|
const fw=e.target.closest('.fwd-btn'); if(fw){ enterSelect(fw.dataset.fwd); return; } // #1: enter forward-selection
|
||||||
const qz=e.target.closest('.quote'); if(qz && qz.dataset.jid){ jumpToMessage(qz.dataset.jid, +qz.dataset.jat||0); return; } // #8: tap a reply → go to the original
|
const qz=e.target.closest('.quote'); if(qz && qz.dataset.jid){ jumpToMessage(qz.dataset.jid, +qz.dataset.jat||0); return; } // #8: tap a reply → go to the original
|
||||||
|
// #5: a Biz Connect meeting link posted in chat should JOIN IN PLACE — not open a new tab/window.
|
||||||
|
const ml=e.target.closest('a.msg-link');
|
||||||
|
if(ml && ml.href){ try{ const u=new URL(ml.href, location.origin); const code=u.searchParams.get('meet');
|
||||||
|
if(u.origin===location.origin && code && /^\d{6}$/.test(code)){ e.preventDefault(); switchTab('meeting'); enterMeeting(code); return; } }catch(_){} }
|
||||||
const im=e.target.closest('.att-img'); if(im && im.dataset.img){ openLightbox(im.dataset.img); return; }
|
const im=e.target.closest('.att-img'); if(im && im.dataset.img){ openLightbox(im.dataset.img); return; }
|
||||||
const po=e.target.closest('.poll-opt'); if(po){ if(!po.disabled) votePoll(po.dataset.poll, +po.dataset.idx); return; }
|
const po=e.target.closest('.poll-opt'); if(po){ if(!po.disabled) votePoll(po.dataset.poll, +po.dataset.idx); return; }
|
||||||
const pcl=e.target.closest('.poll-close'); if(pcl){ closePoll(pcl.dataset.poll); return; }
|
const pcl=e.target.closest('.poll-close'); if(pcl){ closePoll(pcl.dataset.poll); return; }
|
||||||
@@ -3445,11 +3483,43 @@ function renderCall(){
|
|||||||
addTile('__local', meetLocalStream, (ME&&ME.name)?ME.name:'You', true);
|
addTile('__local', meetLocalStream, (ME&&ME.name)?ME.name:'You', true);
|
||||||
setTileMute('__local', !meetMic);
|
setTileMute('__local', !meetMic);
|
||||||
detectBtOutput(); // #8d: pick the right speaker/bluetooth glyph for the current route
|
detectBtOutput(); // #8d: pick the right speaker/bluetooth glyph for the current route
|
||||||
|
makeDraggable(document.querySelector('.meet-bar'), 'bzc_meetbar_pos'); // new #4: bar covers the shared screen → let them move it
|
||||||
}
|
}
|
||||||
// ---- Audio devices (#3): one menu behind the MIC's ▾ caret, listing Speaker + Microphone (like Teams),
|
// ---- Audio devices (#3): one menu behind the MIC's ▾ caret, listing Speaker + Microphone (like Teams),
|
||||||
// instead of a separate headphones button. Choosing a speaker applies setSinkId to every meeting media
|
// instead of a separate headphones button. Choosing a speaker applies setSinkId to every meeting media
|
||||||
// element; choosing a mic switches the live input device.
|
// element; choosing a mic switches the live input device.
|
||||||
function isMobileUA(){ return /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent||''); }
|
function isMobileUA(){ return /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent||''); }
|
||||||
|
// new #4: let the user drag a floating bar out of the way — it otherwise covers part of the shared
|
||||||
|
// screen. Drag from anywhere on the bar except a control. Position is remembered per bar.
|
||||||
|
function makeDraggable(el, key){
|
||||||
|
if(!el || el._drag) return; el._drag=true;
|
||||||
|
let sx=0, sy=0, ox=0, oy=0, dragging=false;
|
||||||
|
const clamp=()=>{ const w=el.offsetWidth, h=el.offsetHeight;
|
||||||
|
let x=parseFloat(el.style.left||'0'), y=parseFloat(el.style.top||'0');
|
||||||
|
x=Math.max(4, Math.min(x, window.innerWidth-w-4)); y=Math.max(4, Math.min(y, window.innerHeight-h-4));
|
||||||
|
el.style.left=x+'px'; el.style.top=y+'px'; };
|
||||||
|
const pin=(r)=>{ el.style.position='fixed'; el.style.left=r.left+'px'; el.style.top=r.top+'px'; el.style.right='auto'; el.style.bottom='auto'; el.style.margin='0'; el.style.width=r.width+'px'; };
|
||||||
|
try{ const s=JSON.parse(localStorage.getItem(key)||'null'); if(s&&typeof s.x==='number'){ const r=el.getBoundingClientRect(); pin({left:s.x, top:s.y, width:r.width}); clamp(); } }catch(_){}
|
||||||
|
const pt=(e)=>e.touches?e.touches[0]:e;
|
||||||
|
const move=(e)=>{ if(!dragging) return; const p=pt(e);
|
||||||
|
el.style.left=(ox+(p.clientX-sx))+'px'; el.style.top=(oy+(p.clientY-sy))+'px'; clamp();
|
||||||
|
if(e.cancelable) e.preventDefault(); };
|
||||||
|
const up=()=>{ if(!dragging) return; dragging=false; el.classList.remove('dragging');
|
||||||
|
document.removeEventListener('mousemove',move); document.removeEventListener('mouseup',up);
|
||||||
|
document.removeEventListener('touchmove',move); document.removeEventListener('touchend',up);
|
||||||
|
try{ localStorage.setItem(key, JSON.stringify({x:parseFloat(el.style.left), y:parseFloat(el.style.top)})); }catch(_){} };
|
||||||
|
const down=(e)=>{
|
||||||
|
if(e.target.closest('button,select,input,a')) return; // don't hijack the controls
|
||||||
|
const r=el.getBoundingClientRect(); pin(r);
|
||||||
|
const p=pt(e); sx=p.clientX; sy=p.clientY; ox=r.left; oy=r.top; dragging=true; el.classList.add('dragging');
|
||||||
|
document.addEventListener('mousemove',move); document.addEventListener('mouseup',up);
|
||||||
|
document.addEventListener('touchmove',move,{passive:false}); document.addEventListener('touchend',up);
|
||||||
|
if(e.cancelable) e.preventDefault();
|
||||||
|
};
|
||||||
|
el.addEventListener('mousedown',down);
|
||||||
|
el.addEventListener('touchstart',down,{passive:false});
|
||||||
|
el.classList.add('draggable');
|
||||||
|
}
|
||||||
let meetSinkId=(()=>{ try{ return localStorage.getItem('bzc_sink')||''; }catch(_){ return ''; } })();
|
let meetSinkId=(()=>{ try{ return localStorage.getItem('bzc_sink')||''; }catch(_){ return ''; } })();
|
||||||
let meetMicId=(()=>{ try{ return localStorage.getItem('bzc_micdev')||''; }catch(_){ return ''; } })();
|
let meetMicId=(()=>{ try{ return localStorage.getItem('bzc_micdev')||''; }catch(_){ return ''; } })();
|
||||||
let meetSpeakerOn=(()=>{ try{ return localStorage.getItem('bzc_spk')!=='0'; }catch(_){ return true; } })(); // mobile speakerphone
|
let meetSpeakerOn=(()=>{ try{ return localStorage.getItem('bzc_spk')!=='0'; }catch(_){ return true; } })(); // mobile speakerphone
|
||||||
@@ -3457,19 +3527,24 @@ function applySink(el){ try{ if(el && meetSinkId && typeof el.setSinkId==='funct
|
|||||||
function applySinkAll(){ document.querySelectorAll('#meetGrid video').forEach(applySink); }
|
function applySinkAll(){ document.querySelectorAll('#meetGrid video').forEach(applySink); }
|
||||||
// Bluetooth/headset devices report themselves in the label — used to pick a sensible icon on mobile.
|
// Bluetooth/headset devices report themselves in the label — used to pick a sensible icon on mobile.
|
||||||
function isBtLabel(l){ return /bluetooth|airpod|buds|headset|headphone|wh-|wf-/i.test(l||''); }
|
function isBtLabel(l){ return /bluetooth|airpod|buds|headset|headphone|wh-|wf-/i.test(l||''); }
|
||||||
// #8d: the speaker button's icon reflects the ACTUAL route — bluetooth/headset when one is in use,
|
// #8c/#8e MOBILE audio route: ONE button that cycles Speaker → Earpiece → Bluetooth (only when a headset
|
||||||
// otherwise speaker-on / speaker-off (dimmed when off).
|
// is connected) and back. The icon always shows the route in use, so no separate "Audio devices" entry is
|
||||||
|
// needed on mobile. meetRoute is the source of truth; meetSpeakerOn stays in sync for the lite/off styling.
|
||||||
let _btConnected=false;
|
let _btConnected=false;
|
||||||
function spkIcon(){ if(_btConnected && !meetSpeakerOn) return 'bluetooth'; return meetSpeakerOn?'speaker':'speakerOff'; }
|
let meetRoute=(()=>{ try{ return localStorage.getItem('bzc_route')||'speaker'; }catch(_){ return 'speaker'; } })(); // 'speaker'|'earpiece'|'bt'
|
||||||
|
function spkIcon(){ return meetRoute==='bt' ? 'bluetooth' : (meetRoute==='speaker' ? 'speaker' : 'speakerOff'); }
|
||||||
|
function routeLabel(){ return meetRoute==='bt' ? 'Bluetooth / headset' : (meetRoute==='speaker' ? 'Speaker' : 'Earpiece'); }
|
||||||
function refreshSpkBtn(){
|
function refreshSpkBtn(){
|
||||||
const b=document.getElementById('meetSpkBtn'); if(!b) return;
|
const b=document.getElementById('meetSpkBtn'); if(!b) return;
|
||||||
b.classList.toggle('off', !meetSpeakerOn);
|
meetSpeakerOn=(meetRoute==='speaker');
|
||||||
|
b.classList.toggle('off', meetRoute!=='speaker');
|
||||||
b.innerHTML=ic(spkIcon(),20);
|
b.innerHTML=ic(spkIcon(),20);
|
||||||
b.title=(_btConnected&&!meetSpeakerOn)?'Using headset / Bluetooth':(meetSpeakerOn?'Speaker on':'Speaker off');
|
b.title=routeLabel()+' — tap to switch';
|
||||||
}
|
}
|
||||||
// Detect a connected BT/headset output so the icon can switch to the bluetooth glyph.
|
// Detect a connected BT/headset output so the cycle can include it (and the icon can show it).
|
||||||
async function detectBtOutput(){
|
async function detectBtOutput(){
|
||||||
try{ const d=await navigator.mediaDevices.enumerateDevices(); _btConnected=d.some(x=>x.kind==='audiooutput' && isBtLabel(x.label)); }catch(_){ _btConnected=false; }
|
try{ const d=await navigator.mediaDevices.enumerateDevices(); _btConnected=d.some(x=>x.kind==='audiooutput' && isBtLabel(x.label)); }catch(_){ _btConnected=false; }
|
||||||
|
if(meetRoute==='bt' && !_btConnected) meetRoute='speaker'; // headset unplugged → fall back
|
||||||
refreshSpkBtn();
|
refreshSpkBtn();
|
||||||
}
|
}
|
||||||
try{ if(navigator.mediaDevices && navigator.mediaDevices.addEventListener) navigator.mediaDevices.addEventListener('devicechange', ()=>{ detectBtOutput(); }); }catch(_){}
|
try{ if(navigator.mediaDevices && navigator.mediaDevices.addEventListener) navigator.mediaDevices.addEventListener('devicechange', ()=>{ detectBtOutput(); }); }catch(_){}
|
||||||
@@ -3482,7 +3557,7 @@ function openMeetMore(anchor){
|
|||||||
if(!(ME&&ME.guest)) items.push({ic:'fileText', label:(meetTranscribe?'Stop transcript':'Live transcript'), fn:toggleTranscribe});
|
if(!(ME&&ME.guest)) items.push({ic:'fileText', label:(meetTranscribe?'Stop transcript':'Live transcript'), fn:toggleTranscribe});
|
||||||
items.push({ic:'chat', label:'Chat'+(meetChatUnread?(' ('+meetChatUnread+')'):''), fn:toggleMeetChat});
|
items.push({ic:'chat', label:'Chat'+(meetChatUnread?(' ('+meetChatUnread+')'):''), fn:toggleMeetChat});
|
||||||
items.push({ic:'users', label:'Participants', fn:toggleMeetPanel});
|
items.push({ic:'users', label:'Participants', fn:toggleMeetPanel});
|
||||||
items.push({ic:'headphones', label:'Audio devices', fn:()=>openAudioMenu(anchor)});
|
// No "Audio devices" here (#8e): on mobile the speaker button itself cycles Speaker/Earpiece/Bluetooth.
|
||||||
const menu=document.createElement('div'); menu.className='spk-menu';
|
const menu=document.createElement('div'); menu.className='spk-menu';
|
||||||
menu.innerHTML=items.map((it,i)=>'<button class="spk-opt mm-opt" data-i="'+i+'">'+ic(it.ic,15)+'<span>'+pEsc(it.label)+'</span></button>').join('');
|
menu.innerHTML=items.map((it,i)=>'<button class="spk-opt mm-opt" data-i="'+i+'">'+ic(it.ic,15)+'<span>'+pEsc(it.label)+'</span></button>').join('');
|
||||||
document.body.appendChild(menu);
|
document.body.appendChild(menu);
|
||||||
@@ -3557,18 +3632,21 @@ async function openAudioMenu(anchor){
|
|||||||
// headset icon; otherwise we toggle between the loudspeaker and the default (earpiece) route. NOTE: true
|
// headset icon; otherwise we toggle between the loudspeaker and the default (earpiece) route. NOTE: true
|
||||||
// earpiece routing is an OS capability — on mobile WEB the browser owns it, so this picks the best
|
// earpiece routing is an OS capability — on mobile WEB the browser owns it, so this picks the best
|
||||||
// available output device. In the native mobile build this maps to the OS audio route.
|
// available output device. In the native mobile build this maps to the OS audio route.
|
||||||
|
// Tap = advance to the next available route. Bluetooth is only in the cycle while a headset is connected.
|
||||||
async function toggleSpeakerphone(){
|
async function toggleSpeakerphone(){
|
||||||
const btn=document.getElementById('meetSpkBtn'); if(!btn) return;
|
|
||||||
meetSpeakerOn=!meetSpeakerOn; try{ localStorage.setItem('bzc_spk', meetSpeakerOn?'1':'0'); }catch(_){}
|
|
||||||
let outs=[]; try{ outs=(await navigator.mediaDevices.enumerateDevices()).filter(x=>x.kind==='audiooutput'); }catch(_){}
|
let outs=[]; try{ outs=(await navigator.mediaDevices.enumerateDevices()).filter(x=>x.kind==='audiooutput'); }catch(_){}
|
||||||
const bt=outs.find(d=>isBtLabel(d.label)); _btConnected=!!bt;
|
const bt=outs.find(d=>isBtLabel(d.label)); _btConnected=!!bt;
|
||||||
if(meetSpeakerOn){
|
const routes=['speaker','earpiece'].concat(bt?['bt']:[]);
|
||||||
const spk=outs.find(d=>/speaker/i.test(d.label)) || outs.find(d=>d.deviceId==='default') || outs[0];
|
const i=routes.indexOf(meetRoute);
|
||||||
if(spk) await setSpeakerDevice(spk.deviceId);
|
meetRoute=routes[(i<0?0:(i+1)%routes.length)];
|
||||||
} else if(bt){ await setSpeakerDevice(bt.deviceId); } // speaker off + headset present → route to the headset
|
try{ localStorage.setItem('bzc_route', meetRoute); }catch(_){}
|
||||||
else { await setSpeakerDevice(''); } // no headset → back to the system default (earpiece on mobile)
|
if(meetRoute==='bt' && bt) await setSpeakerDevice(bt.deviceId);
|
||||||
|
else if(meetRoute==='speaker'){
|
||||||
|
const spk=outs.find(d=>/speaker/i.test(d.label)) || outs.find(d=>d.deviceId==='default') || (outs[0]&&outs[0].deviceId);
|
||||||
|
await setSpeakerDevice(typeof spk==='string'?spk:(spk&&spk.deviceId)||'');
|
||||||
|
} else { await setSpeakerDevice(''); } // earpiece → system default route
|
||||||
refreshSpkBtn();
|
refreshSpkBtn();
|
||||||
toast(meetSpeakerOn?'Speaker on':(bt?'Using headset':'Speaker off'));
|
toast(routeLabel());
|
||||||
}
|
}
|
||||||
function addTile(id, stream, label, muted){
|
function addTile(id, stream, label, muted){
|
||||||
const grid=document.getElementById('meetGrid'); if(!grid) return;
|
const grid=document.getElementById('meetGrid'); if(!grid) return;
|
||||||
@@ -3814,6 +3892,9 @@ async function onMeetMsg(e){
|
|||||||
refreshMeetPanel(); updateHostControls();
|
refreshMeetPanel(); updateHostControls();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// The room is gone (host ended it / code expired). Say so plainly instead of hanging on "Connecting…"
|
||||||
|
// — and for a guest, don't offer a Rejoin that can never work (#7).
|
||||||
|
if(m.type==='error'){ const gone=/not found/i.test(m.message||''); if(ME&&ME.guest){ _guestRoom=gone?null:_guestRoom; toast(gone?'This meeting has ended.':(m.message||'Could not join')); leaveMeeting(true); return; } toast(m.message||'Could not join the meeting'); leaveMeeting(true); return; }
|
||||||
if(m.type==='meeting-lobby-wait'){ renderLobbyWait(); return; } // #4: guest waits for host to admit
|
if(m.type==='meeting-lobby-wait'){ renderLobbyWait(); return; } // #4: guest waits for host to admit
|
||||||
if(m.type==='meeting-rejected'){ toast('The host didn’t let you in.'); leaveMeeting(true); return; }
|
if(m.type==='meeting-rejected'){ toast('The host didn’t let you in.'); leaveMeeting(true); return; }
|
||||||
if(m.type==='meeting-lobby-request'){ showLobbyRequest(m.peerId, m.name); return; } // host: someone wants in
|
if(m.type==='meeting-lobby-request'){ showLobbyRequest(m.peerId, m.name); return; } // host: someone wants in
|
||||||
@@ -3928,8 +4009,8 @@ function renderGuestLeft(){
|
|||||||
el.innerHTML='<div class="gp-card">'
|
el.innerHTML='<div class="gp-card">'
|
||||||
+'<div class="gp-brand"><img src="/mark-light.png" alt="" onerror="this.remove()"><span>Biz <b>Connect</b></span></div>'
|
+'<div class="gp-brand"><img src="/mark-light.png" alt="" onerror="this.remove()"><span>Biz <b>Connect</b></span></div>'
|
||||||
+'<div class="gl-tick">'+ic('check',30)+'</div>'
|
+'<div class="gl-tick">'+ic('check',30)+'</div>'
|
||||||
+'<h2>You’ve left the meeting</h2>'
|
+'<h2>'+(_guestRoom?'You’ve left the meeting':'This meeting has ended')+'</h2>'
|
||||||
+'<p class="gp-sub">Thanks for joining'+((ME&&ME.name)?(', '+pEsc(firstName(ME.name))):'')+'.</p>'
|
+'<p class="gp-sub">'+(_guestRoom?('Thanks for joining'+((ME&&ME.name)?(', '+pEsc(firstName(ME.name))):'')+'.'):'Thanks for joining.')+'</p>'
|
||||||
+(_guestRoom?'<button class="gp-join" id="glRejoin">Rejoin meeting</button>':'')
|
+(_guestRoom?'<button class="gp-join" id="glRejoin">Rejoin meeting</button>':'')
|
||||||
+'<div class="gp-note">'+ic('info',13)+' You can close this tab safely.</div>'
|
+'<div class="gp-note">'+ic('info',13)+' You can close this tab safely.</div>'
|
||||||
+'</div>';
|
+'</div>';
|
||||||
|
|||||||
@@ -264,6 +264,31 @@ function rcGrantControl(){
|
|||||||
rcAllowed=true; rcPost({type:'rc-arm', on:true}); showControlBanner(); updateRcBtn();
|
rcAllowed=true; rcPost({type:'rc-arm', on:true}); showControlBanner(); updateRcBtn();
|
||||||
}
|
}
|
||||||
function rcStopControl(){ rcAllowed=false; rcPrompted=false; rcPost({type:'rc-arm', on:false}); const b=document.getElementById('rcBanner'); if(b) b.remove(); updateRcBtn(); }
|
function rcStopControl(){ rcAllowed=false; rcPrompted=false; rcPost({type:'rc-arm', on:false}); const b=document.getElementById('rcBanner'); if(b) b.remove(); updateRcBtn(); }
|
||||||
|
// new #4: drag the floating bar off whatever it's covering. Position is remembered.
|
||||||
|
function makeBarDraggable(el,key){
|
||||||
|
if(!el||el._drag) return; el._drag=true;
|
||||||
|
let sx=0,sy=0,ox=0,oy=0,dragging=false;
|
||||||
|
const pt=(e)=>e.touches?e.touches[0]:e;
|
||||||
|
const clamp=()=>{ const w=el.offsetWidth,h=el.offsetHeight;
|
||||||
|
let x=parseFloat(el.style.left||'0'), y=parseFloat(el.style.top||'0');
|
||||||
|
x=Math.max(4,Math.min(x,window.innerWidth-w-4)); y=Math.max(4,Math.min(y,window.innerHeight-h-4));
|
||||||
|
el.style.left=x+'px'; el.style.top=y+'px'; };
|
||||||
|
const pin=(r)=>{ el.style.left=r.left+'px'; el.style.top=r.top+'px'; el.style.right='auto'; el.style.bottom='auto'; };
|
||||||
|
try{ const s=JSON.parse(localStorage.getItem(key)||'null'); if(s&&typeof s.x==='number'){ pin({left:s.x,top:s.y}); clamp(); } }catch(_){}
|
||||||
|
const move=(e)=>{ if(!dragging) return; const p=pt(e); el.style.left=(ox+(p.clientX-sx))+'px'; el.style.top=(oy+(p.clientY-sy))+'px'; clamp(); if(e.cancelable) e.preventDefault(); };
|
||||||
|
const up=()=>{ if(!dragging) return; dragging=false; el.style.opacity='';
|
||||||
|
document.removeEventListener('mousemove',move); document.removeEventListener('mouseup',up);
|
||||||
|
document.removeEventListener('touchmove',move); document.removeEventListener('touchend',up);
|
||||||
|
try{ localStorage.setItem(key, JSON.stringify({x:parseFloat(el.style.left), y:parseFloat(el.style.top)})); }catch(_){} };
|
||||||
|
const down=(e)=>{ if(e.target.closest('button,select,input,a')) return;
|
||||||
|
const r=el.getBoundingClientRect(); pin(r);
|
||||||
|
const p=pt(e); sx=p.clientX; sy=p.clientY; ox=r.left; oy=r.top; dragging=true; el.style.opacity='.92';
|
||||||
|
document.addEventListener('mousemove',move); document.addEventListener('mouseup',up);
|
||||||
|
document.addEventListener('touchmove',move,{passive:false}); document.addEventListener('touchend',up);
|
||||||
|
if(e.cancelable) e.preventDefault(); };
|
||||||
|
el.addEventListener('mousedown',down); el.addEventListener('touchstart',down,{passive:false});
|
||||||
|
el.style.cursor='move'; el.title='Drag to move';
|
||||||
|
}
|
||||||
function updateRcBtn(){
|
function updateRcBtn(){
|
||||||
const b=document.getElementById('rcBtn'); if(!b) return;
|
const b=document.getElementById('rcBtn'); if(!b) return;
|
||||||
b.style.background=rcAllowed?'#16a34a':'#6b7280';
|
b.style.background=rcAllowed?'#16a34a':'#6b7280';
|
||||||
@@ -328,6 +353,7 @@ function buildBar(){
|
|||||||
document.body.appendChild(bar);
|
document.body.appendChild(bar);
|
||||||
rcb.onclick=()=>{ if(rcAllowed) rcStopControl(); else rcGrantControl(); };
|
rcb.onclick=()=>{ if(rcAllowed) rcStopControl(); else rcGrantControl(); };
|
||||||
updateRcBtn();
|
updateRcBtn();
|
||||||
|
makeBarDraggable(bar,'bzc_sharebar_pos'); // new #4: let the customer move the bar off their content
|
||||||
const setMic=(on)=>{mic.title=on?'Mute':'Unmute';mic.innerHTML='<span style="display:inline-flex">'+I(on?'mic':'micOff')+'</span>';mic.style.background=on?'#2563eb':'#6b7280';};
|
const setMic=(on)=>{mic.title=on?'Mute':'Unmute';mic.innerHTML='<span style="display:inline-flex">'+I(on?'mic':'micOff')+'</span>';mic.style.background=on?'#2563eb':'#6b7280';};
|
||||||
mic.onclick=async()=>{
|
mic.onclick=async()=>{
|
||||||
if(!window.__mic){
|
if(!window.__mic){
|
||||||
|
|||||||
Reference in New Issue
Block a user