diff --git a/desktop/main.js b/desktop/main.js index 394c635..8193673 100644 --- a/desktop/main.js +++ b/desktop/main.js @@ -273,10 +273,12 @@ function createWindow() { // no-login "Share my screen" option + sign-in. It redirects logged-in users straight to /home. 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 }) => { 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: diff --git a/desktop/package.json b/desktop/package.json index 96093b3..441ee8a 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,6 +1,6 @@ { "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", "author": { "name": "BizGaze", diff --git a/server/public/connect.html b/server/public/connect.html index 98ee416..23b38f6 100644 --- a/server/public/connect.html +++ b/server/public/connect.html @@ -185,7 +185,9 @@ function connectWS(){ case 'session-ready': if(statusEl)statusEl.innerHTML='Allowed — connecting…'; break; case 'offer': await pc.setRemoteDescription(new RTCSessionDescription(m.sdp)); // 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); 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; @@ -329,7 +331,7 @@ function buildBar(){ // 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)'; 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 chat=_btn('chatBtn',I('chat'),'Chat','#334155'); const rec=_btn('recBtn','','Record','#334155'); @@ -340,13 +342,22 @@ function buildBar(){ // 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'; }); 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. if(!document.getElementById('ctrlHint')){ const h=document.createElement('div'); h.id='ctrlHint'; h.textContent='Click the screen to take control · Esc to release'; 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=''+I(t.enabled?'mic':'micOff')+'';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; 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(_){} }; @@ -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 // 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'); } +// 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=''+(window.ic?window.ic(on?'mic':'micOff',16):'')+''; + b.style.background=on?'#2563eb':'#6b7280'; +} let rcEngaged=false; function setEngaged(on){ const next=!!on; if(next===rcEngaged) return; diff --git a/server/public/home.html b/server/public/home.html index 9a8ae05..ac4a049 100644 --- a/server/public/home.html +++ b/server/public/home.html @@ -19,8 +19,11 @@