fix: DP missing in 1:1 for un-messaged contacts; make web updates fully silent (batch80)

DP bug — real cause found (client-side, not the DB):
loadSidebar builds a DM row for every contact you haven't messaged yet, but it copied only
{name, online} from the contact and DROPPED `avatar` (and status/email). So an un-messaged
contact always rendered initials in the 1:1, while the SAME person showed their photo in a
group (which reads /api/groups/members). Carry the whole contact through.
Kept a server-side safety net: avatarsFor() now indexes known photos under person-id, email
AND name, so a duplicate row missing a photo can match on any of them (the previous single
composite key missed twins with different emails).

Web updates are now completely silent: no banner, no toast. A web build is an implementation
detail — surfacing it makes users reason about "web build vs app version", which is exactly
the confusion to avoid. New code simply applies itself as soon as it's safe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 16:06:28 +05:30
parent 2aeeb0a096
commit c1f01e796a
2 changed files with 26 additions and 10 deletions
+8 -5
View File
@@ -1002,7 +1002,7 @@
<body>
<script src="/icons.js?v=6"></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-14-batch79';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
<script>window.__BUILD='2026-07-14-batch80';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>
@@ -1649,7 +1649,10 @@ async function loadSidebar(){
CONTACTS=(Array.isArray(contacts)?contacts:[]).filter(c=>c.id!==(ME&&ME.id)); // self isn't a normal contact (#4)
const items=Array.isArray(convos)?convos.slice():[];
const dmIds=new Set(items.filter(i=>i.kind==='dm').map(i=>i.id));
for(const c of CONTACTS){ if(!dmIds.has(c.id)) items.push({ kind:'dm', id:c.id, name:c.name, online:!!c.online, last_body:'', last_at:0, last_from_me:false, unread:0 }); }
// A contact you haven't messaged YET still needs their profile photo/status: this row was being built
// with only name+online, silently dropping `avatar` — so the same person showed their DP in a group
// (which reads /api/groups/members) but fell back to initials in the 1:1. Carry the whole contact.
for(const c of CONTACTS){ if(!dmIds.has(c.id)) items.push({ kind:'dm', id:c.id, name:c.name, email:c.email||'', avatar:c.avatar||null, online:!!c.online, status:c.status||'active', last_body:'', last_at:0, last_from_me:false, unread:0 }); }
const onlineById={}; CONTACTS.forEach(c=>onlineById[c.id]=!!c.online);
items.forEach(it=>{ if(it.kind==='dm') it.online=!!onlineById[it.id]; });
// Always-available "You" chat (note to self), pinned to the top (#4).
@@ -2814,8 +2817,7 @@ function scheduleAutoRefresh(){
if(!_newBuild) return;
if(!safeToRefresh()) return; // busy → try again shortly
clearInterval(_autoRefreshTimer); _autoRefreshTimer=null;
try{ sessionStorage.setItem('bzc_autoref','1'); }catch(_){} // so we can confirm after reload
hardReloadApp();
hardReloadApp(); // silent: no banner, no toast
};
_autoRefreshTimer=setInterval(tryNow, 15000);
setTimeout(tryNow, 1200); // usually applies right away
@@ -2832,7 +2834,8 @@ setInterval(checkWebBuild, 5*60*1000); // every 5 min
window.addEventListener('focus', checkWebBuild); // and whenever the user comes back to the app
document.addEventListener('visibilitychange', ()=>{ if(!document.hidden) checkWebBuild(); });
setTimeout(checkWebBuild, 3000); // shortly after boot
try{ if(sessionStorage.getItem('bzc_autoref')){ sessionStorage.removeItem('bzc_autoref'); setTimeout(()=>toast('Updated to the latest version'),1200); } }catch(_){}
// Deliberately NO banner and NO toast: a web build is an implementation detail. Surfacing it would make
// users think about "web build vs app version". It just updates.
// 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());
+18 -5
View File
@@ -22,12 +22,25 @@ function namesFor(teamId){ const o = {}; for (const x of R.users.listByTenant(te
// person id, else email, else name) and let a photo-less row borrow its twin's photo.
function avatarsFor(teamId) {
const users = R.users.listByTenant(teamId);
const personKey = (x) => (x.bizgaze_user_id ? 'bz:' + x.bizgaze_user_id
: (x.email ? 'em:' + String(x.email).toLowerCase() : 'nm:' + String(x.name || '').trim().toLowerCase()));
const byPerson = {};
for (const x of users) { if (x.avatar_url) { const k = personKey(x); if (!byPerson[k]) byPerson[k] = x.avatar_url; } }
const em = (x) => (x.email ? String(x.email).toLowerCase() : '');
const nm = (x) => String(x.name || '').trim().toLowerCase();
// Index every KNOWN photo under all three identities, then let a photo-less row match on ANY of them —
// a single composite key missed the common case where the twin rows have different emails.
const byBz = {}, byEmail = {}, byName = {};
for (const x of users) {
if (!x.avatar_url) continue;
if (x.bizgaze_user_id && !byBz[x.bizgaze_user_id]) byBz[x.bizgaze_user_id] = x.avatar_url;
if (em(x) && !byEmail[em(x)]) byEmail[em(x)] = x.avatar_url;
if (nm(x) && !byName[nm(x)]) byName[nm(x)] = x.avatar_url;
}
const out = {};
for (const x of users) out[x.id] = x.avatar_url || byPerson[personKey(x)] || null;
for (const x of users) {
out[x.id] = x.avatar_url
|| (x.bizgaze_user_id && byBz[x.bizgaze_user_id])
|| (em(x) && byEmail[em(x)])
|| (nm(x) && byName[nm(x)])
|| null;
}
return out;
}
// Next future occurrence (same time-of-day) of a weekly-recurring meeting; searches 14 days ahead.