fix: seen-by popup (#2), info-view DP preview (#5), notification DP cache + no Close btn, branded update flow (0.1.7)
- #2: 'Seen by' opens an on-screen popup listing readers (was a flash toast). - #5: the DM contact-info view now shows the DP; clicking it previews full-size. - Desktop notifications: cache DPs per-sender (fast AND with photo after the first); removed timeoutType:'never' which added an unwanted 'Close' button. - Update flow: dropped the unbranded native restart dialog — the branded web banner handles Restart. Banner text clearer ('Downloading update…'), plus an update indicator that cascades profile 'i' badge → Settings → version line, with the Settings button becoming 'Restart now' when the update is downloaded. desktop 0.1.7. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+40
-7
@@ -449,6 +449,9 @@
|
||||
.upd-banner{position:fixed;left:50%;bottom:16px;transform:translateX(-50%) translateY(70px);z-index:9500;display:none;align-items:center;gap:.5rem;background:var(--blue);color:#fff;border-radius:999px;padding:.5rem .5rem .5rem .9rem;box-shadow:0 10px 30px rgba(20,30,60,.32);font-size:.85rem;font-weight:600;transition:transform .25s ease;}
|
||||
.upd-banner.show{display:flex;transform:translateX(-50%) translateY(0);}
|
||||
.upd-banner button{border:none;background:var(--brand);color:var(--blue-d);border-radius:999px;padding:.35rem .8rem;font-weight:700;cursor:pointer;}
|
||||
.upd-dot{position:absolute;top:-3px;right:-3px;width:15px;height:15px;border-radius:50%;background:var(--brand);color:var(--blue-d);font-size:.66rem;font-weight:800;font-style:italic;display:grid;place-items:center;border:2px solid var(--blue);pointer-events:none;}
|
||||
#setUpd.has-update{background:var(--brand);color:var(--blue-d);}
|
||||
.set-ver-i{display:inline-grid;place-items:center;width:15px;height:15px;border-radius:50%;background:var(--brand);color:var(--blue-d);font-size:.66rem;font-weight:800;font-style:italic;margin-left:.35rem;vertical-align:middle;}
|
||||
.bubble .msg-link{color:inherit;text-decoration:underline;text-underline-offset:2px;word-break:break-word;}
|
||||
.bubble.them .msg-link{color:var(--blue);} .bubble.mine .msg-link{color:#dbe9ff;}
|
||||
.bubble .fwd-label{display:flex;align-items:center;gap:.2rem;font-size:.72rem;font-style:italic;opacity:.7;margin-bottom:.2rem;}
|
||||
@@ -550,6 +553,10 @@
|
||||
.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;}
|
||||
.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);}
|
||||
@@ -833,7 +840,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-batch59';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
|
||||
<script>window.__BUILD='2026-07-08-batch60';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>
|
||||
@@ -1010,13 +1017,15 @@ function openSettings(){
|
||||
+sw('setGroup','Group message notifications', notifOn('group'))
|
||||
+sw('setDm','Direct message notifications', notifOn('dm'))
|
||||
+'<label class="gi-setting"><span>Notifications <span class="perm-state '+perm+'">'+permLabel+'</span><div style="font-size:.72rem;color:var(--muted);font-weight:400;margin-top:.15rem">Pop up messages & calls even when this tab isn’t open</div></span>'+(granted?'':'<button class="btn sm" id="setPerm">'+(perm==='denied'?'Allow':'Enable')+'</button>')+'</label>'
|
||||
+(window.bizConnectNative?('<label class="gi-setting"><span>Biz Connect for Desktop<div style="font-size:.72rem;color:var(--muted);font-weight:400;margin-top:.15rem" id="setVer">Version '+pEsc(window.bizConnectNative.version||'—')+'</div></span><button class="btn sm" id="setUpd">Check for updates</button></label>'):'')
|
||||
+(window.bizConnectNative?('<label class="gi-setting"><span>Biz Connect for Desktop<div style="font-size:.72rem;color:var(--muted);font-weight:400;margin-top:.15rem" id="setVer">Version '+pEsc(window.bizConnectNative.version||'—')+(_pendingUpdate?('<span class="set-ver-i" title="Update available">i</span> '+(_pendingUpdate.phase==='ready'?'Update ready':'Update '+pEsc(_pendingUpdate.version||'')+' available')):'')+'</div></span><button class="btn sm'+(_pendingUpdate?' has-update':'')+'" id="setUpd">'+(_pendingUpdate?(_pendingUpdate.phase==='ready'?'Restart now':'Downloading…'):'Check for updates')+'</button></label>'):'')
|
||||
+'<div class="hint" style="margin-top:.4rem">These preferences are saved on this device.</div></div>';
|
||||
document.body.appendChild(ov);
|
||||
ov.onclick=e=>{ if(e.target===ov) ov.remove(); };
|
||||
ov.querySelector('#setClose').onclick=()=>ov.remove();
|
||||
const updBtn=ov.querySelector('#setUpd'); // #12: desktop version + manual update check
|
||||
if(updBtn) updBtn.onclick=async()=>{
|
||||
if(_pendingUpdate&&_pendingUpdate.phase==='ready'){ try{ window.bizConnectNative.restartToUpdate&&window.bizConnectNative.restartToUpdate(); }catch(_){} return; } // update already downloaded → restart
|
||||
if(_pendingUpdate){ toast('Update '+(_pendingUpdate.version||'')+' is downloading…'); return; }
|
||||
const ver=ov.querySelector('#setVer'); updBtn.disabled=true; updBtn.textContent='Checking…';
|
||||
let done=false;
|
||||
try{
|
||||
@@ -1244,11 +1253,12 @@ async function openSharedItems(kind,id,name){
|
||||
if(document.getElementById('sharedModal')) return;
|
||||
const r0=rowFor(kind,id); const fav=!!(r0&&r0.favorite); const online=!!(r0&&r0.online);
|
||||
const ov=document.createElement('div'); ov.className='modal-ov'; ov.id='sharedModal';
|
||||
ov.innerHTML='<div class="modal gi"><div class="gi-head" style="margin-bottom:.6rem"><span class="avatar" style="width:46px;height:46px;flex:0 0 46px;background:'+avColor(name)+'">'+pEsc(initials(name||'?'))+'</span><div class="gi-name"><div class="gi-title-row"><span class="gi-title">'+pEsc(name||'')+'</span><button class="fav-star'+(fav?' on':'')+'" id="shFav" title="'+(fav?'Remove from favourites':'Add to favourites')+'">'+ic('star',16)+'</button></div><div class="gi-sub"><span class="st-dot '+statusCls(r0)+'"></span>'+statusLabel(r0)+'</div></div><button class="iconbtn" id="shClose">'+ic('x',18)+'</button></div>'
|
||||
ov.innerHTML='<div class="modal gi"><div class="gi-head" style="margin-bottom:.6rem"><button class="gi-photo" id="shPhoto" title="View photo"><span class="avatar" style="width:46px;height:46px;flex:0 0 46px;background:'+avColor(name)+'">'+pEsc(initials(name||'?'))+((r0&&r0.avatar)?'<img class="av-img" src="'+pEsc(r0.avatar)+'" alt="" onerror="this.remove()">':'')+'</span></button><div class="gi-name"><div class="gi-title-row"><span class="gi-title">'+pEsc(name||'')+'</span><button class="fav-star'+(fav?' on':'')+'" id="shFav" title="'+(fav?'Remove from favourites':'Add to favourites')+'">'+ic('star',16)+'</button></div><div class="gi-sub"><span class="st-dot '+statusCls(r0)+'"></span>'+statusLabel(r0)+'</div></div><button class="iconbtn" id="shClose">'+ic('x',18)+'</button></div>'
|
||||
+mediaRowHTML()+'</div>';
|
||||
document.body.appendChild(ov);
|
||||
ov.onclick=e=>{ if(e.target===ov) ov.remove(); };
|
||||
ov.querySelector('#shClose').onclick=()=>ov.remove();
|
||||
const sp=ov.querySelector('#shPhoto'); if(sp) sp.onclick=()=>{ const r=rowFor(kind,id)||r0; if(r&&r.avatar) openLightbox(r.avatar); else toast('No profile photo'); }; // #5: click the DP in the info view to preview it
|
||||
const fb=ov.querySelector('#shFav'); if(fb) fb.onclick=async()=>{ const r=rowFor(kind,id); const on=!(r&&r.favorite); if(r) r.favorite=on; fb.classList.toggle('on',on); fb.title=on?'Remove from favourites':'Add to favourites'; try{ await postJSON('/api/favorites',{kind,id,on}); }catch(_){} renderChats(searchVal()); };
|
||||
wireMediaEntry(ov, kind, id, name);
|
||||
}
|
||||
@@ -1670,6 +1680,16 @@ function onChatRead(d){ if(!d||!d.by) return;
|
||||
// Branded confirm dialog — replaces window.confirm (which shows the OS/Electron default dialog on
|
||||
// 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){
|
||||
if(document.getElementById('seenModal')) return;
|
||||
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>';
|
||||
document.body.appendChild(ov);
|
||||
ov.addEventListener('mousedown',e=>{ if(e.target===ov) ov.remove(); });
|
||||
ov.querySelector('#seenClose').onclick=()=>ov.remove();
|
||||
}
|
||||
function bzConfirm(message, opts){
|
||||
opts=opts||{};
|
||||
return new Promise((resolve)=>{
|
||||
@@ -1972,7 +1992,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); toast(ns.length?('Seen by: '+ns.join(', ')):'Not seen yet'); return; }
|
||||
const sb=e.target.closest('.seenby,.rcpt.grp'); if(sb){ const ns=(sb.dataset.seen||'').split('|').filter(Boolean); showSeenByModal(ns); 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.
|
||||
@@ -2349,19 +2369,32 @@ function notifAvatarDataUrl(kind,id,fallbackName){
|
||||
}
|
||||
// Reply to a conversation without opening it (used by the notification quick-reply).
|
||||
async function sendReplyTo(kind,id,text){ try{ await postJSON('/api/messages', kind==='group'?{group:id,body:text}:{to:id,body:text}); }catch(_){} }
|
||||
// #3: desktop auto-update progress — show a banner so the user knows an update is downloading/ready.
|
||||
// #3: desktop auto-update — a progress banner (branded restart) + an update indicator that cascades
|
||||
// from the profile → Settings → version, so the user always knows a newer version is available.
|
||||
let _pendingUpdate=null; // {phase, version}
|
||||
function wireUpdateBanner(){
|
||||
const n=window.bizConnectNative; if(!(n&&n.onUpdateEvent)) return;
|
||||
n.onUpdateEvent((d)=>{
|
||||
if(!d||!d.phase) return;
|
||||
if(d.phase==='available'||d.phase==='downloading'||d.phase==='ready') _pendingUpdate={ phase:d.phase, version:d.version||(_pendingUpdate&&_pendingUpdate.version)||'' };
|
||||
else if(d.phase==='current') _pendingUpdate=null;
|
||||
applyUpdateBadge();
|
||||
let el=document.getElementById('updBanner');
|
||||
const ensure=()=>{ if(!el){ el=document.createElement('div'); el.id='updBanner'; el.className='upd-banner'; document.body.appendChild(el); } return el; };
|
||||
if(d.phase==='downloading'){ ensure().innerHTML=ic('download',15)+' <span>Downloading update… '+(d.percent||0)+'%</span>'; el.classList.add('show'); el.classList.remove('ready'); }
|
||||
else if(d.phase==='available'){ ensure().innerHTML=ic('download',15)+' <span>Update '+pEsc(d.version||'')+' found — downloading…</span>'; el.classList.add('show'); }
|
||||
if(d.phase==='available'){ ensure().innerHTML=ic('download',15)+' <span>Downloading update'+(d.version?(' '+pEsc(d.version)):'')+'…</span>'; el.classList.add('show'); el.classList.remove('ready'); }
|
||||
else if(d.phase==='downloading'){ ensure().innerHTML=ic('download',15)+' <span>Downloading update… '+(d.percent||0)+'%</span>'; el.classList.add('show'); el.classList.remove('ready'); }
|
||||
else if(d.phase==='ready'){ ensure().innerHTML=ic('check',15)+' <span>Update ready</span> <button id="updRestart">Restart now</button>'; el.classList.add('show','ready'); const b=el.querySelector('#updRestart'); if(b) b.onclick=()=>{ try{ n.restartToUpdate&&n.restartToUpdate(); }catch(_){} }; }
|
||||
else if(d.phase==='current'||d.phase==='error'){ if(el){ el.classList.remove('show'); setTimeout(()=>{ if(el&&!el.classList.contains('show')) el.remove(); }, 400); } }
|
||||
});
|
||||
}
|
||||
// Small 'i' badge on the profile button when an update is pending; clicking opens Settings.
|
||||
function applyUpdateBadge(){
|
||||
document.querySelectorAll('.upd-dot').forEach(x=>x.remove());
|
||||
if(!_pendingUpdate) return;
|
||||
const host=document.getElementById('pbtn'); if(!host) return;
|
||||
const dot=document.createElement('span'); dot.className='upd-dot'; dot.title='Update available — open Settings'; dot.textContent='i';
|
||||
host.appendChild(dot);
|
||||
}
|
||||
// #13: track shown page-notifications by conversation tag so reading elsewhere can dismiss them.
|
||||
const _shownNotifs={};
|
||||
function onNotifClear(d){
|
||||
|
||||
Reference in New Issue
Block a user