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:
+46
-13
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user