- #9: sharing your whole screen no longer creates a 'hall of mirrors' — your own screen is NOT shown back to you (you see your camera/avatar + a 'You're sharing' badge); other participants still see your screen. Your own tile is never the local stage. - #1: opening a DM now syncs the sidebar tick to the thread's true delivered/read state, so it can't show a single tick while the conversation shows double. - #3: nativePlatform() now also detects older desktop builds that expose bizConnectNative but not __NATIVE__. Those were wrongly subscribing to Web Push, so the service worker's openWindow RELOADED the page on notification click (losing the header DP). Desktop now uses only the native toast → opens in-place. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+26
-8
@@ -349,6 +349,7 @@
|
|||||||
.meet{display:flex;flex-direction:column;width:100%;height:100%;}
|
.meet{display:flex;flex-direction:column;width:100%;height:100%;}
|
||||||
.meet-grid{flex:1;min-height:0;display:grid;gap:.6rem;padding:.8rem;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));align-content:center;overflow:hidden;background:#0b1220;} /* #16: center tiles + no stray scrollbar on 1:1/small calls */
|
.meet-grid{flex:1;min-height:0;display:grid;gap:.6rem;padding:.8rem;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));align-content:center;overflow:hidden;background:#0b1220;} /* #16: center tiles + no stray scrollbar on 1:1/small calls */
|
||||||
.meet-tile{position:relative;background:#0b1220;border-radius:12px;overflow:hidden;aspect-ratio:4/3;border:1px solid #1e293b;transition:box-shadow .12s,border-color .12s;}
|
.meet-tile{position:relative;background:#0b1220;border-radius:12px;overflow:hidden;aspect-ratio:4/3;border:1px solid #1e293b;transition:box-shadow .12s,border-color .12s;}
|
||||||
|
.meet-tile .meet-sharing-self{position:absolute;left:0;right:0;bottom:0;display:flex;align-items:center;justify-content:center;gap:.3rem;font-size:.72rem;color:#fff;background:rgba(37,99,235,.9);padding:.25rem;}
|
||||||
.meet-tile.waiting{border-color:#334155;}
|
.meet-tile.waiting{border-color:#334155;}
|
||||||
.meet-tile.waiting .ringing{position:absolute;left:0;right:0;bottom:0;display:flex;align-items:center;justify-content:center;gap:.3rem;font-size:.8rem;color:#cbd5e1;background:rgba(0,0,0,.35);padding:.35rem;animation:ringPulse 1.4s ease-in-out infinite;}
|
.meet-tile.waiting .ringing{position:absolute;left:0;right:0;bottom:0;display:flex;align-items:center;justify-content:center;gap:.3rem;font-size:.8rem;color:#cbd5e1;background:rgba(0,0,0,.35);padding:.35rem;animation:ringPulse 1.4s ease-in-out infinite;}
|
||||||
@keyframes ringPulse{0%,100%{opacity:.6;}50%{opacity:1;}}
|
@keyframes ringPulse{0%,100%{opacity:.6;}50%{opacity:1;}}
|
||||||
@@ -826,7 +827,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<script src="/icons.js?v=5"></script>
|
<script src="/icons.js?v=5"></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-07-batch54';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
|
<script>window.__BUILD='2026-07-08-batch55';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>
|
||||||
@@ -1692,7 +1693,14 @@ function onChatDeleted(d){ if(!d||!d.id) return; markMsgDeleted(d.id); try{ load
|
|||||||
function onChatDelivered(d){ if(!d||!d.id) return;
|
function onChatDelivered(d){ if(!d||!d.id) return;
|
||||||
const m=THREAD.find(x=>x.id===d.id); if(m && !m.delivered_at){ m.delivered_at=Date.now(); updateBubble(m); }
|
const m=THREAD.find(x=>x.id===d.id); if(m && !m.delivered_at){ m.delivered_at=Date.now(); updateBubble(m); }
|
||||||
// #6: mirror the delivered state onto the chat-list tick (double grey), unless it's already read.
|
// #6: mirror the delivered state onto the chat-list tick (double grey), unless it's already read.
|
||||||
const it=d.with?rowFor('dm', d.with):null; if(it && it.last_from_me && it.last_status==='sent'){ it.last_status='delivered'; renderChats(searchVal()); }
|
const it=d.with?rowFor('dm', d.with):null; if(it && it.last_from_me && it.last_status!=='read' && it.last_status!=='delivered'){ it.last_status='delivered'; renderChats(searchVal()); }
|
||||||
|
}
|
||||||
|
// #1: opening a DM loads the true delivered/read state into the thread; make the sidebar tick match
|
||||||
|
// (it can lag when a delivered/read event arrived before the row reflected my latest message).
|
||||||
|
function syncDmRowTick(id){
|
||||||
|
const it=rowFor('dm', id); if(!it || !THREAD.length) return;
|
||||||
|
const last=[...THREAD].reverse().find(m=>!m.system && !m.deleted); if(!last) return;
|
||||||
|
if(last.from===ME.id){ const st=last.read_at?'read':(last.delivered_at?'delivered':'sent'); if(it.last_status!==st || it.last_msg_id!==last.id){ it.last_status=st; it.last_from_me=true; it.last_msg_id=last.id; renderChats(searchVal()); } }
|
||||||
}
|
}
|
||||||
// Group read: a member opened the group → add them to "Seen by" on my messages up to that time.
|
// Group read: a member opened the group → add them to "Seen by" on my messages up to that time.
|
||||||
function onGroupRead(d){ if(!d||!d.group) return; if(selected && selected.kind==='group' && selected.id===d.group){ THREAD.forEach(m=>{ if(m.from===ME.id && m.created_at<=d.at){ m.seenBy=m.seenBy||[]; if(d.byName && !m.seenBy.includes(d.byName)){ m.seenBy.push(d.byName); updateBubble(m); } } }); refreshGroupRowTick(d.group); } }
|
function onGroupRead(d){ if(!d||!d.group) return; if(selected && selected.kind==='group' && selected.id===d.group){ THREAD.forEach(m=>{ if(m.from===ME.id && m.created_at<=d.at){ m.seenBy=m.seenBy||[]; if(d.byName && !m.seenBy.includes(d.byName)){ m.seenBy.push(d.byName); updateBubble(m); } } }); refreshGroupRowTick(d.group); } }
|
||||||
@@ -1982,6 +1990,7 @@ async function openConvo(kind,id){
|
|||||||
THREAD_CACHE.set(ckey, THREAD.slice());
|
THREAD_CACHE.set(ckey, THREAD.slice());
|
||||||
_hasMoreOlder = msgs.length >= PAGE; _loadingOlder=false; // a full page back means there may be older history
|
_hasMoreOlder = msgs.length >= PAGE; _loadingOlder=false; // a full page back means there may be older history
|
||||||
renderThread();
|
renderThread();
|
||||||
|
if(kind==='dm') syncDmRowTick(id); // #1: keep the sidebar tick consistent with the loaded thread
|
||||||
// #3: if there were unread messages, drop a "New messages" divider, scroll to the first unread,
|
// #3: if there were unread messages, drop a "New messages" divider, scroll to the first unread,
|
||||||
// and show the "jump to newest" arrow so the user can return to the bottom.
|
// and show the "jump to newest" arrow so the user can return to the bottom.
|
||||||
if(_unreadN>0 && THREAD.length>=_unreadN){
|
if(_unreadN>0 && THREAD.length>=_unreadN){
|
||||||
@@ -2221,7 +2230,7 @@ function urlB64ToUint8(base64){ const pad='='.repeat((4-base64.length%4)%4); con
|
|||||||
// plugin. We register the OS device token with /api/v1/devices so the server can deliver via
|
// plugin. We register the OS device token with /api/v1/devices so the server can deliver via
|
||||||
// FCM/APNs (see server/push.js). No bundler needed — plugins live on Capacitor.Plugins.
|
// FCM/APNs (see server/push.js). No bundler needed — plugins live on Capacitor.Plugins.
|
||||||
function capPlugin(name){ const C=window.Capacitor; return (C && C.isNativePlatform && C.isNativePlatform() && C.Plugins && C.Plugins[name]) ? C.Plugins[name] : null; }
|
function capPlugin(name){ const C=window.Capacitor; return (C && C.isNativePlatform && C.isNativePlatform() && C.Plugins && C.Plugins[name]) ? C.Plugins[name] : null; }
|
||||||
function nativePlatform(){ const C=window.Capacitor; if(C && C.getPlatform){ const p=C.getPlatform(); if(p==='ios'||p==='android') return p; } if(window.__NATIVE__==='desktop') return 'desktop'; return ''; }
|
function nativePlatform(){ const C=window.Capacitor; if(C && C.getPlatform){ const p=C.getPlatform(); if(p==='ios'||p==='android') return p; } if(window.__NATIVE__==='desktop' || (window.bizConnectNative && window.bizConnectNative.platform==='desktop')) return 'desktop'; return ''; } // also detect older desktop builds that expose bizConnectNative but not __NATIVE__ (#3: those wrongly subscribed Web Push → SW openWindow reloaded on notification click)
|
||||||
// Record this install against the signed-in user (desktop shell exposes a stable installId).
|
// Record this install against the signed-in user (desktop shell exposes a stable installId).
|
||||||
// Fires post-login; the request carries the session so admins can see who installed the app.
|
// Fires post-login; the request carries the session so admins can see who installed the app.
|
||||||
function reportInstall(){
|
function reportInstall(){
|
||||||
@@ -3046,7 +3055,9 @@ async function toggleScreen(){
|
|||||||
const st=pub&&pub.track&&pub.track.mediaStreamTrack; if(!st){ return; }
|
const st=pub&&pub.track&&pub.track.mediaStreamTrack; if(!st){ return; }
|
||||||
meetScreen=true; meetScreenStream=new MediaStream([st]);
|
meetScreen=true; meetScreenStream=new MediaStream([st]);
|
||||||
try{ st.addEventListener('ended', ()=>stopScreen()); }catch(_){} // browser's native "Stop sharing" bar
|
try{ st.addEventListener('ended', ()=>stopScreen()); }catch(_){} // browser's native "Stop sharing" bar
|
||||||
addTile('__local', meetScreenStream, ((ME&&ME.name)?ME.name:'You')+' (screen)', true); setTileScreen('__local', true);
|
// #9: keep MY tile on camera/avatar — do NOT show my own screen back to me. Sharing the whole
|
||||||
|
// screen would then capture the tile showing it → infinite "hall of mirrors". Others still see it.
|
||||||
|
addTile('__local', meetLocalStream, (ME&&ME.name)?ME.name:'You', true); setLocalSharing(true);
|
||||||
updateScreenBtn(); meetSend({ type:'meeting-screen', on:true }); return;
|
updateScreenBtn(); meetSend({ type:'meeting-screen', on:true }); return;
|
||||||
}
|
}
|
||||||
let ds; try{ ds=await navigator.mediaDevices.getDisplayMedia({ video:true, audio:false }); }
|
let ds; try{ ds=await navigator.mediaDevices.getDisplayMedia({ video:true, audio:false }); }
|
||||||
@@ -3058,22 +3069,29 @@ async function toggleScreen(){
|
|||||||
if(p.vsender){ try{ await p.vsender.replaceTrack(track); }catch(_){} }
|
if(p.vsender){ try{ await p.vsender.replaceTrack(track); }catch(_){} }
|
||||||
else { try{ p.vsender=p.pc.addTrack(track, meetLocalStream); const off=await p.pc.createOffer(); await p.pc.setLocalDescription(off); meetSend({type:'meeting-signal',to:pid,data:{sdp:p.pc.localDescription}}); }catch(_){} }
|
else { try{ p.vsender=p.pc.addTrack(track, meetLocalStream); const off=await p.pc.createOffer(); await p.pc.setLocalDescription(off); meetSend({type:'meeting-signal',to:pid,data:{sdp:p.pc.localDescription}}); }catch(_){} }
|
||||||
}
|
}
|
||||||
addTile('__local', ds, ((ME&&ME.name)?ME.name:'You')+' (screen)', true); setTileScreen('__local', true);
|
addTile('__local', meetLocalStream, (ME&&ME.name)?ME.name:'You', true); setLocalSharing(true); // #9: keep camera/avatar locally, not my own screen
|
||||||
updateScreenBtn(); meetSend({ type:'meeting-screen', on:true });
|
updateScreenBtn(); meetSend({ type:'meeting-screen', on:true });
|
||||||
}
|
}
|
||||||
|
// #9: a badge on MY tile that I'm sharing (instead of showing my own screen, which recurses).
|
||||||
|
function setLocalSharing(on){
|
||||||
|
const t=document.getElementById('meet-tile-__local'); if(!t) return;
|
||||||
|
let b=t.querySelector('.meet-sharing-self');
|
||||||
|
if(on){ if(!b){ b=document.createElement('div'); b.className='meet-sharing-self'; b.innerHTML=ic('monitor',13)+' You’re sharing your screen'; t.appendChild(b); } }
|
||||||
|
else if(b){ b.remove(); }
|
||||||
|
}
|
||||||
function stopScreen(){
|
function stopScreen(){
|
||||||
if(!meetScreen) return; meetScreen=false;
|
if(!meetScreen) return; meetScreen=false;
|
||||||
if(SFU.on){ try{ SFU.room.localParticipant.setScreenShareEnabled(false); }catch(_){} meetScreenStream=null; addTile('__local', meetLocalStream, (ME&&ME.name)?ME.name:'You', true); setTileScreen('__local', false); updateScreenBtn(); meetSend({ type:'meeting-screen', on:false }); return; }
|
if(SFU.on){ try{ SFU.room.localParticipant.setScreenShareEnabled(false); }catch(_){} meetScreenStream=null; addTile('__local', meetLocalStream, (ME&&ME.name)?ME.name:'You', true); setLocalSharing(false); updateScreenBtn(); meetSend({ type:'meeting-screen', on:false }); return; }
|
||||||
const cam=meetLocalStream && meetLocalStream.getVideoTracks()[0];
|
const cam=meetLocalStream && meetLocalStream.getVideoTracks()[0];
|
||||||
for(const [,p] of meetPeers){ if(p.vsender){ try{ p.vsender.replaceTrack(cam||null); }catch(_){} } }
|
for(const [,p] of meetPeers){ if(p.vsender){ try{ p.vsender.replaceTrack(cam||null); }catch(_){} } }
|
||||||
if(meetScreenStream){ try{ meetScreenStream.getTracks().forEach(t=>t.stop()); }catch(_){} meetScreenStream=null; }
|
if(meetScreenStream){ try{ meetScreenStream.getTracks().forEach(t=>t.stop()); }catch(_){} meetScreenStream=null; }
|
||||||
addTile('__local', meetLocalStream, (ME&&ME.name)?ME.name:'You', true); setTileScreen('__local', false);
|
addTile('__local', meetLocalStream, (ME&&ME.name)?ME.name:'You', true); setLocalSharing(false);
|
||||||
updateScreenBtn(); meetSend({ type:'meeting-screen', on:false });
|
updateScreenBtn(); meetSend({ type:'meeting-screen', on:false });
|
||||||
}
|
}
|
||||||
function updateScreenBtn(){ const b=document.getElementById('meetScreenBtn'); if(!b) return; b.classList.toggle('on', meetScreen); b.title=meetScreen?'Stop sharing':'Share screen'; b.innerHTML=ic('monitor',20); }
|
function updateScreenBtn(){ const b=document.getElementById('meetScreenBtn'); if(!b) return; b.classList.toggle('on', meetScreen); b.title=meetScreen?'Stop sharing':'Share screen'; b.innerHTML=ic('monitor',20); }
|
||||||
function setTileScreen(id, on){ const t=document.getElementById('meet-tile-'+id); if(!t) return; t.classList.toggle('sharing', !!on); let b=t.querySelector('.meet-screen'); if(on){ if(!b){ b=document.createElement('div'); b.className='meet-screen'; b.innerHTML=ic('monitor',12)+' Screen'; t.appendChild(b); } } else if(b){ b.remove(); } updateShareMode(); }
|
function setTileScreen(id, on){ const t=document.getElementById('meet-tile-'+id); if(!t) return; t.classList.toggle('sharing', !!on); let b=t.querySelector('.meet-screen'); if(on){ if(!b){ b=document.createElement('div'); b.className='meet-screen'; b.innerHTML=ic('monitor',12)+' Screen'; t.appendChild(b); } } else if(b){ b.remove(); } updateShareMode(); }
|
||||||
// Stage mode: one chosen shared screen fills the area; other screens + people are small on the right.
|
// Stage mode: one chosen shared screen fills the area; other screens + people are small on the right.
|
||||||
function getSharerIds(){ const a=[]; if(meetScreen) a.push('__local'); meetSharers.forEach(id=>a.push(id)); return a; }
|
function getSharerIds(){ const a=[]; meetSharers.forEach(id=>a.push(id)); return a; } // #9: '__local' excluded — I don't display my own screen, so it's never the local stage
|
||||||
function updateShareMode(){ const g=document.getElementById('meetGrid'); if(!g) return;
|
function updateShareMode(){ const g=document.getElementById('meetGrid'); if(!g) return;
|
||||||
const sharers=getSharerIds(); const on=sharers.length>0;
|
const sharers=getSharerIds(); const on=sharers.length>0;
|
||||||
g.classList.toggle('sharing-mode', on);
|
g.classList.toggle('sharing-mode', on);
|
||||||
|
|||||||
Reference in New Issue
Block a user