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:
@@ -198,6 +198,11 @@ function createWindow() {
|
|||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
// Persist cookies/localStorage so the technician stays logged in between launches.
|
// Persist cookies/localStorage so the technician stays logged in between launches.
|
||||||
partition: 'persist:bizconnect',
|
partition: 'persist:bizconnect',
|
||||||
|
// Keep the renderer at full speed when the window is in the BACKGROUND/minimized. Electron
|
||||||
|
// throttles hidden windows by default, which stalled the chat WebSocket's onmessage + timers —
|
||||||
|
// so incoming messages and their notifications only landed when you refocused the app (the
|
||||||
|
// "notifications slow / messages don't update live" report). Off = real-time even in the tray.
|
||||||
|
backgroundThrottling: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "biz-connect-desktop",
|
"name": "biz-connect-desktop",
|
||||||
"version": "0.1.9",
|
"version": "0.1.10",
|
||||||
"description": "Biz Connect technician desktop client — loads the Connect web UI with native screen capture",
|
"description": "Biz Connect technician desktop client — loads the Connect web UI with native screen capture",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "BizGaze",
|
"name": "BizGaze",
|
||||||
|
|||||||
+46
-13
@@ -553,10 +553,16 @@
|
|||||||
.modal input#grpName:focus,.modal input#giName:focus{outline:none;border-color:var(--brand);}
|
.modal input#grpName:focus,.modal input#giName:focus{outline:none;border-color:var(--brand);}
|
||||||
/* Branded confirm dialog (replaces the OS/Electron window.confirm). */
|
/* 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;}
|
.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-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:.35rem .3rem;}
|
.seen-modal .seen-row{display:flex;align-items:center;gap:.6rem;padding:.32rem .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;}
|
/* dark lettering on the pastel avatar (white was invisible on the light background), unless a real DP covers it */
|
||||||
.seen-modal .seen-row .sn{font-size:.92rem;}
|
.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{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-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);}
|
.bzc-cancel{background:var(--blue-soft);color:var(--blue);}
|
||||||
@@ -840,7 +846,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-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
|
// 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>
|
||||||
@@ -1372,11 +1378,18 @@ function setDesktopBadge(count){
|
|||||||
if(!(window.bizConnectNative && window.bizConnectNative.setUnread)) return;
|
if(!(window.bizConnectNative && window.bizConnectNative.setUnread)) return;
|
||||||
let dataUrl=null;
|
let dataUrl=null;
|
||||||
if(count>0){
|
if(count>0){
|
||||||
const s=32, c=document.createElement('canvas'); c.width=s; c.height=s; const g=c.getContext('2d');
|
// Draw at 2× for a crisp taskbar overlay. A vertical red→pink gradient pill with a white ring
|
||||||
g.fillStyle='#ef4444'; g.beginPath(); g.arc(s/2,s/2,s/2,0,2*Math.PI); g.fill();
|
// 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';
|
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.font='800 '+(label.length>2?26:(label.length>1?32:38))+'px system-ui,Segoe UI,sans-serif';
|
||||||
g.fillText(label, s/2, s/2+1);
|
g.fillText(label, cx, cy+2);
|
||||||
dataUrl=c.toDataURL('image/png');
|
dataUrl=c.toDataURL('image/png');
|
||||||
}
|
}
|
||||||
window.bizConnectNative.setUnread(count, dataUrl);
|
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;
|
// 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).
|
// 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).
|
// #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;
|
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 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: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>';
|
||||||
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>';
|
|
||||||
document.body.appendChild(ov);
|
document.body.appendChild(ov);
|
||||||
ov.addEventListener('mousedown',e=>{ if(e.target===ov) ov.remove(); });
|
ov.addEventListener('mousedown',e=>{ if(e.target===ov) ov.remove(); });
|
||||||
ov.querySelector('#seenClose').onclick=()=>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 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 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 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
|
// 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
|
// icons; the action only fires on a SECOND tap once they're shown (icons are pointer-events:none
|
||||||
// until revealed). Tapping elsewhere hides them.
|
// 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(); } }
|
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){
|
async function enterMeeting(code, audioOnly){
|
||||||
if(meetState==='call'){ switchTab('meeting'); return; } // already in a call — ignore double-join
|
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;
|
meetAudioOnly=!!audioOnly;
|
||||||
// Start with NO media — mic & cam OFF by default (no permission prompt until the user
|
// 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.
|
// turns one on). Tracks are acquired on demand by toggleMic / toggleCam.
|
||||||
|
|||||||
Reference in New Issue
Block a user