':'')
@@ -2172,6 +2219,7 @@ function renderMeetPanel(){
const mm=p.querySelector('#mpMulti'); if(mm) mm.onchange=()=>{ meetMultiShare=mm.checked; meetSend({type:'meeting-sharemode', multi:meetMultiShare}); };
p.querySelectorAll('.mp-makehost').forEach(b=>b.onclick=()=>{ meetSend({type:'meeting-host', to:b.dataset.id}); meetHostId=b.dataset.id; meetIsHost=false; renderMeetPanel(); });
const cpl=p.querySelector('#mpCopyLink'); if(cpl) cpl.onclick=()=>copyMeetingLink();
+ { const ms=p.querySelector('#mpSearch'); if(ms) ms.addEventListener('input',()=>{ const q=ms.value.trim().toLowerCase(); p.querySelectorAll('.mp-list .chk').forEach(l=>{ l.style.display=(!q||(l.dataset.nm||'').includes(q))?'':'none'; }); }); } // #5
const inv=p.querySelector('#mpInvite'); if(inv) inv.onclick=async()=>{ const ids=[...p.querySelectorAll('input:checked')].map(i=>i.value); if(!ids.length){ toast('Pick people to invite'); return; } try{ await postJSON('/api/calls/invite',{ room:meetRoom, userIds:ids }); meetAddInvited(ids); meetPanelTab='people'; toast(ids.length===1?'Waiting for them to join…':'Waiting for '+ids.length+' people to join…'); renderMeetPanel(); }catch(e){ toast(e.message||'Could not invite'); } };
}
// Add people to the current call (from the in-call bar).
@@ -3462,7 +3510,7 @@ function renderCall(){
+ ''
+ ((ME&&ME.guest)?'':'') // #12: transcript is a signed-in feature (guests can't download it)
+ ''
- + ''
+ + (canRouteAudio()?(''):'') // #1: only where output switching actually works
+ ''
+ ''
+ '';
@@ -3489,6 +3537,11 @@ function renderCall(){
// instead of a separate headphones button. Choosing a speaker applies setSinkId to every meeting media
// element; choosing a mic switches the live input device.
function isMobileUA(){ return /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent||''); }
+// #1: choosing the audio OUTPUT from the web needs setSinkId, which Android Chrome and iOS Safari do NOT
+// implement — the OS owns the route there (and iOS forces loudspeaker while a mic track is live). Rather
+// than ship a button that silently does nothing, we only show the speaker control where it really works.
+// Real speaker/earpiece/Bluetooth switching on phones needs the native (Capacitor) build's audio plugin.
+function canRouteAudio(){ try{ return typeof HTMLMediaElement!=='undefined' && typeof HTMLMediaElement.prototype.setSinkId==='function'; }catch(_){ return false; } }
// 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){
@@ -3550,7 +3603,11 @@ async function detectBtOutput(){
try{ if(navigator.mediaDevices && navigator.mediaDevices.addEventListener) navigator.mediaDevices.addEventListener('devicechange', ()=>{ detectBtOutput(); }); }catch(_){}
// #8e: on mobile the bar keeps only Mic/Camera/Speaker/End — the rest live behind this ⋮ menu.
function openMeetMore(anchor){
+ // #4: tapping ⋮ again must CLOSE it. Previously we removed the menu then rebuilt it immediately, so it
+ // looked like nothing happened.
+ const open=document.querySelector('.spk-menu.mm-menu');
document.querySelectorAll('.spk-menu').forEach(x=>x.remove());
+ if(open) return;
const items=[];
items.push({ic:'monitor', label:'Share screen', fn:toggleScreen});
if(meetIsHost) items.push({ic:'record', label:(meetRec?'Stop recording':'Record meeting'), fn:toggleRecord});
@@ -3558,7 +3615,7 @@ function openMeetMore(anchor){
items.push({ic:'chat', label:'Chat'+(meetChatUnread?(' ('+meetChatUnread+')'):''), fn:toggleMeetChat});
items.push({ic:'users', label:'Participants', fn:toggleMeetPanel});
// 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 mm-menu';
menu.innerHTML=items.map((it,i)=>'').join('');
document.body.appendChild(menu);
const r=anchor.getBoundingClientRect();
@@ -3667,7 +3724,7 @@ let _dmCallWaiting=null;
function addWaitingTile(name, avatar){
const grid=document.getElementById('meetGrid'); if(!grid || document.getElementById('meet-tile-__waiting')) return;
const tile=document.createElement('div'); tile.className='meet-tile novid waiting'; tile.id='meet-tile-__waiting';
- tile.innerHTML='
'+pEsc(initials(name||'?'))+(avatar?'':'')+'
'+pEsc(name||'')+'
'+ic('phone',13)+' Ringing…
';
+ tile.innerHTML='
'+pEsc(initials(name||'?'))+(avatar?'':'')+'
'+pEsc(name||'')+'
'+ic('phone',22)+'Ringing…
';
grid.appendChild(tile);
}
function removeWaitingTile(){ const t=document.getElementById('meet-tile-__waiting'); if(t) t.remove(); }