fix: real-time bg messages, join clears invite, richer seen-by, nicer badge (0.1.10/batch63)

1. Notifications/messages lagged when the desktop window was backgrounded —
   Electron throttles hidden renderers by default, stalling the chat WS's
   onmessage + timers. Set backgroundThrottling:false → real-time in the tray.
   (Transport is WebSocket; no Firebase needed.)
2. Joining a call via the header Join button left the Join/Decline invite popup
   on screen — enterMeeting now dismisses the invite (and stops the ring).
3. Seen-by is now "Message info": real contact DPs, a "Not seen yet" list for
   groups, and dark lettering (white-on-pastel initials were invisible).
4. App-icon unread badge: red→pink gradient pill with a white ring + shadow,
   drawn at 2x, instead of a flat red disc.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 12:51:14 +05:30
parent 62a5f750af
commit 70a776fa6c
3 changed files with 52 additions and 14 deletions
+46 -13
View File
@@ -553,10 +553,16 @@
.modal input#grpName:focus,.modal input#giName:focus{outline:none;border-color:var(--brand);}
/* Branded confirm dialog (replaces the OS/Electron window.confirm). */
.bz-confirm .bzc-msg{margin:.2rem 0 1.1rem;color:var(--ink);font-size:.92rem;line-height:1.5;}
.seen-modal .seen-list{max-height:44vh;overflow-y:auto;display:flex;flex-direction:column;gap:.2rem;}
.seen-modal .seen-row{display:flex;align-items:center;gap:.6rem;padding:.35rem .3rem;}
.seen-modal .seen-row .mini-av{width:30px;height:30px;flex:0 0 auto;border-radius:50%;display:grid;place-items:center;color:#fff;font-weight:700;font-size:.72rem;}
.seen-modal .seen-row .sn{font-size:.92rem;}
.seen-modal .seen-list{max-height:38vh;overflow-y:auto;display:flex;flex-direction:column;gap:.15rem;}
.seen-modal .seen-row{display:flex;align-items:center;gap:.6rem;padding:.32rem .3rem;}
/* dark lettering on the pastel avatar (white was invisible on the light background), unless a real DP covers it */
.seen-modal .seen-row .mini-av{width:32px;height:32px;flex:0 0 auto;border-radius:50%;display:grid;place-items:center;color:#334155;font-weight:700;font-size:.74rem;overflow:hidden;position:relative;}
.seen-modal .seen-row .mini-av .av-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;}
.seen-modal .seen-row .sn{font-size:.92rem;color:var(--ink);}
.seen-modal .seen-sec{display:flex;align-items:center;gap:.4rem;font-size:.72rem;font-weight:700;letter-spacing:.02em;text-transform:uppercase;color:var(--blue);margin:.7rem .2rem .25rem;}
.seen-modal .seen-sec.dim{color:var(--muted);}
.seen-modal .seen-sec .dot{width:7px;height:7px;border-radius:50%;background:#cbd5e1;}
.seen-modal .seen-sec .dot.ok{background:#22c55e;}
.bzc-actions{display:flex;justify-content:flex-end;gap:.5rem;}
.bzc-actions button{border:none;border-radius:10px;padding:.55rem 1rem;font-size:.9rem;font-weight:600;cursor:pointer;font-family:inherit;}
.bzc-cancel{background:var(--blue-soft);color:var(--blue);}
@@ -840,7 +846,7 @@
<body>
<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>window.__BUILD='2026-07-08-batch62';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
<script>window.__BUILD='2026-07-10-batch63';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
// (emojis stay as plain Unicode). (#5)
function twemojify(el){ try{ if(el && window.twemoji) window.twemoji.parse(el, { folder:'svg', ext:'.svg' }); }catch(_){} }</script>
@@ -1372,11 +1378,18 @@ function setDesktopBadge(count){
if(!(window.bizConnectNative && window.bizConnectNative.setUnread)) return;
let dataUrl=null;
if(count>0){
const s=32, c=document.createElement('canvas'); c.width=s; c.height=s; const g=c.getContext('2d');
g.fillStyle='#ef4444'; g.beginPath(); g.arc(s/2,s/2,s/2,0,2*Math.PI); g.fill();
// Draw at 2× for a crisp taskbar overlay. A vertical red→pink gradient pill with a white ring
// and a soft drop shadow reads as a modern notification badge (was a flat red disc).
const s=64, c=document.createElement('canvas'); c.width=s; c.height=s; const g=c.getContext('2d');
const label=count>99?'99+':String(count);
const cx=s/2, cy=s/2, r=s/2-4;
g.save(); g.shadowColor='rgba(0,0,0,.35)'; g.shadowBlur=5; g.shadowOffsetY=2;
const grad=g.createLinearGradient(0,cy-r,0,cy+r); grad.addColorStop(0,'#ff5b6e'); grad.addColorStop(1,'#e11d48');
g.fillStyle=grad; g.beginPath(); g.arc(cx,cy,r,0,2*Math.PI); g.fill(); g.restore();
g.lineWidth=3.5; g.strokeStyle='#fff'; g.beginPath(); g.arc(cx,cy,r,0,2*Math.PI); g.stroke(); // white ring pops on any taskbar
g.fillStyle='#fff'; g.textAlign='center'; g.textBaseline='middle';
const label=count>99?'99+':String(count); g.font='bold '+(label.length>2?15:20)+'px system-ui,Segoe UI,sans-serif';
g.fillText(label, s/2, s/2+1);
g.font='800 '+(label.length>2?26:(label.length>1?32:38))+'px system-ui,Segoe UI,sans-serif';
g.fillText(label, cx, cy+2);
dataUrl=c.toDataURL('image/png');
}
window.bizConnectNative.setUnread(count, dataUrl);
@@ -1692,11 +1705,26 @@ function onChatRead(d){ if(!d||!d.by) return;
// desktop). Returns a Promise<boolean>. Reuses .modal-ov so the global Esc handler dismisses it;
// a MutationObserver resolves false whenever the overlay is removed (Esc / backdrop / cancel).
// #2: "Seen by" as an on-screen popup listing the readers (not a flash toast).
function showSeenByModal(names){
function showSeenByModal(msg){
if(document.getElementById('seenModal')) return;
// Accept either a message (rich: DPs + "not seen yet") or a bare names[] (fallback).
const seenNames = Array.isArray(msg) ? msg : (Array.isArray(msg&&msg.seenBy) ? msg.seenBy.slice() : []);
const senderId = (msg && !Array.isArray(msg)) ? msg.from : null;
const members = (convoIsGroup && Array.isArray(convoMembers)) ? convoMembers : [];
// name → real DP (match member by name, since seenBy carries names).
const avByName={}; members.forEach(m=>{ avByName[(m.name||'').trim().toLowerCase()] = m.avatar||null; });
// Who hasn't seen it yet = members minus the sender, minus me (I'm reading it), minus those who have.
const seenSet=new Set(seenNames.map(n=>String(n).trim().toLowerCase()));
const notSeen = members.filter(m=> m.id!==senderId && m.id!==(ME&&ME.id) && !seenSet.has((m.name||'').trim().toLowerCase()));
const chip=(name,avatar)=>'<span class="mini-av" style="background:'+avColor(name)+'">'+(avatar?('<img class="av-img" src="'+pEsc(avatar)+'" onerror="this.remove()">'):'')+pEsc(initials(name))+'</span>';
const rowsFromNames=(arr)=>arr.map(n=>'<div class="seen-row">'+chip(n, avByName[String(n).trim().toLowerCase()]||null)+'<span class="sn">'+pEsc(n)+'</span></div>').join('');
const rowsFromMembers=(arr)=>arr.map(m=>'<div class="seen-row">'+chip(m.name, m.avatar||null)+'<span class="sn">'+pEsc(m.name)+'</span></div>').join('');
let body='';
if(seenNames.length) body+='<div class="seen-sec"><span class="dot ok"></span>Seen · '+seenNames.length+'</div><div class="seen-list">'+rowsFromNames(seenNames)+'</div>';
else body+='<div class="gi-noresult" style="padding:.6rem 0">No one has seen this yet.</div>';
if(convoIsGroup && notSeen.length) body+='<div class="seen-sec dim"><span class="dot"></span>Not seen yet · '+notSeen.length+'</div><div class="seen-list">'+rowsFromMembers(notSeen)+'</div>';
const ov=document.createElement('div'); ov.className='modal-ov'; ov.id='seenModal';
const body=(names&&names.length)?('<div class="seen-list">'+names.map(n=>'<div class="seen-row"><span class="mini-av" style="background:'+avColor(n)+'">'+pEsc(initials(n))+'</span><span class="sn">'+pEsc(n)+'</span></div>').join('')+'</div>'):'<div class="gi-noresult" style="padding:.6rem 0">No one has seen this yet.</div>';
ov.innerHTML='<div class="modal seen-modal" style="max-width:320px"><h3>'+ic('checkCheck',16)+' Seen by'+((names&&names.length)?(' <span class="muted" style="font-weight:400;font-size:.8rem">('+names.length+')</span>'):'')+'</h3>'+body+'<div class="bzc-actions" style="margin-top:.9rem"><button type="button" class="bzc-ok" id="seenClose">Close</button></div></div>';
ov.innerHTML='<div class="modal seen-modal" style="max-width:340px"><h3>'+ic('checkCheck',16)+' Message info</h3>'+body+'<div class="bzc-actions" style="margin-top:.9rem"><button type="button" class="bzc-ok" id="seenClose">Close</button></div></div>';
document.body.appendChild(ov);
ov.addEventListener('mousedown',e=>{ if(e.target===ov) ov.remove(); });
ov.querySelector('#seenClose').onclick=()=>ov.remove();
@@ -2013,7 +2041,7 @@ async function openConvo(kind,id){
const dl=e.target.closest('.del-btn'); if(dl){ deleteMessage(dl.dataset.del); return; }
const ab=e.target.closest('.react-btn'); if(ab){ openEmojiForReact(ab.dataset.id, ab); return; }
const ch=e.target.closest('.react-chip'); if(ch){ reactToMessage(ch.dataset.id, ch.dataset.emoji); return; }
const sb=e.target.closest('.seenby,.rcpt.grp'); if(sb){ const ns=(sb.dataset.seen||'').split('|').filter(Boolean); showSeenByModal(ns); return; }
const sb=e.target.closest('.seenby,.rcpt.grp'); if(sb){ const bub=sb.closest('.bubble'); const mid=bub&&bub.dataset.id; const msg=(mid&&THREAD.find(x=>x.id===mid))||{ seenBy:(sb.dataset.seen||'').split('|').filter(Boolean) }; showSeenByModal(msg); return; }
// Tap-to-reveal (mobile): a tap on the bubble body (not an action) reveals its reply/react/delete
// icons; the action only fires on a SECOND tap once they're shown (icons are pointer-events:none
// until revealed). Tapping elsewhere hides them.
@@ -3270,6 +3298,11 @@ function updateTransBtn(){ const b=document.getElementById('meetTransBtn'); if(!
function transcribeNotice(on){ let el=document.getElementById('txNotice'); if(on){ if(!el){ el=document.createElement('div'); el.id='txNotice'; el.className='tx-notice'; el.innerHTML=ic('fileText',12)+' Transcribing'; document.body.appendChild(el); } } else if(el){ el.remove(); } }
async function enterMeeting(code, audioOnly){
if(meetState==='call'){ switchTab('meeting'); return; } // already in a call — ignore double-join
// Joining this room → clear any lingering incoming-call invite popup for it (and stop its ring).
// Fixes: joining via the header "Join" button left the Join/Decline popup on screen. Belt-and-braces
// we clear ALL open invites, since you can only be in one call at a time.
if(code) dismissCallInvite(code);
document.querySelectorAll('.call-invite').forEach(el=>{ try{ el.remove(); }catch(_){} }); stopRing(true);
meetAudioOnly=!!audioOnly;
// Start with NO media — mic & cam OFF by default (no permission prompt until the user
// turns one on). Tracks are acquired on demand by toggleMic / toggleCam.