fix: chat-open scroll regression, SFU screen reliability, unanswered calls, selected-chat border

- REGRESSION (More#1): removed overflow-anchor:none (it stopped the view from staying
  at the bottom as images settle) + renderThread re-asserts scroll-to-bottom after late
  content, unless a 'New messages' divider is shown → chats open at the latest message.
- #9: dynacast off — a screen-share layer was being paused unless a camera track was
  also flowing (blank / only-with-camera / slow). Now every published track flows.
- More#7/#9: 1:1 calls track 'answered'; unanswered calls auto-end after ~40s (caller
  no longer stuck ringing) and post 'Missed call' instead of a duration; answered calls
  show duration from the answer time. meeting-ended reason 'unanswered' → 'No answer'.
- More#6: selected chat now has a thin yellow (brand) boundary.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-08 13:20:19 +05:30
parent f6962a0b4a
commit 096683385a
3 changed files with 35 additions and 14 deletions
+14 -9
View File
@@ -171,8 +171,7 @@
.perm-state.default,.perm-state.unsupported{background:#f1f5f9;color:#475569;}
.chat-row{display:flex;gap:.7rem;align-items:center;padding:.6rem .65rem;border-radius:12px;cursor:pointer;position:relative;}
.chat-row:hover{background:#f3f6fb;}
.chat-row.active{background:var(--blue-soft);box-shadow:inset 3px 0 0 var(--brand);}
.chat-row.active::before{content:"";position:absolute;left:0;top:.7rem;bottom:.7rem;width:3px;border-radius:3px;background:var(--blue);}
.chat-row.active{background:var(--blue-soft);box-shadow:inset 0 0 0 1.5px var(--brand);} /* #6: thin yellow boundary around the selected chat */
.avatar{width:42px;height:42px;flex:0 0 42px;border-radius:50%;display:grid;place-items:center;color:#334155;font-weight:700;font-size:.92rem;position:relative;}
.avatar .av-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;border-radius:inherit;}
.avatar .dot{position:absolute;right:-1px;bottom:-1px;width:11px;height:11px;border-radius:50%;border:2px solid #fff;background:#cbd2dd;z-index:1;}
@@ -291,7 +290,7 @@
.convo-body{flex:1;display:grid;place-items:center;text-align:center;color:var(--muted);padding:2rem;}
.convo-body .big{font-size:2.4rem;margin-bottom:.4rem;}
/* message thread */
.convo-msgs{flex:1;overflow-y:auto;overflow-anchor:none;padding:1rem 1.2rem;display:flex;flex-direction:column;gap:.35rem;background:var(--bg);} /* overflow-anchor:none so prepending older history doesn't fight our manual scroll anchor */
.convo-msgs{flex:1;overflow-y:auto;padding:1rem 1.2rem;display:flex;flex-direction:column;gap:.35rem;background:var(--bg);}
.bubble{max-width:72%;padding:.5rem .75rem;border-radius:14px;font-size:.9rem;line-height:1.4;white-space:pre-wrap;word-break:break-word;box-shadow:0 1px 2px rgba(20,30,60,.06);}
.bubble.them{align-self:flex-start;background:#fff;border:1px solid var(--line);color:var(--ink);border-bottom-left-radius:4px;}
.bubble.mine{align-self:flex-end;background:var(--blue);color:#fff;border-bottom-right-radius:4px;}
@@ -827,7 +826,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-batch55';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
<script>window.__BUILD='2026-07-08-batch56';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>
@@ -1884,7 +1883,11 @@ function renderThread(keepScroll){
if(!THREAD.length){ box.innerHTML='<div class="empty-thread">No messages yet — say hello 👋</div>'; return; }
let html='';
for(const m of THREAD){ const dk=dayKey(m.created_at); if(dk!==_lastDay){ html+='<div class="day-sep"><span>'+pEsc(dayLabel(m.created_at))+'</span></div>'; _lastDay=dk; } rendered.add(m.id); html+=bubbleHTML(m); }
box.innerHTML=html; twemojify(box); if(!keepScroll) box.scrollTop=box.scrollHeight; // keepScroll: prepending older history — don't jump to bottom
box.innerHTML=html; twemojify(box);
if(!keepScroll){ box.scrollTop=box.scrollHeight; // open/refresh → newest at the bottom; re-assert after late content (images/twemoji) settles, unless we jumped to a "New messages" divider
const pin=()=>{ const b=document.getElementById('msgs'); if(b && !b.querySelector('.new-sep')) b.scrollTop=b.scrollHeight; };
requestAnimationFrame(pin); setTimeout(pin,150); setTimeout(pin,400);
}
if(_selMode) box.querySelectorAll('.bubble').forEach(b=>{ if(_selIds.has(b.dataset.id)) b.classList.add('selected'); }); // #1: keep selection across re-render
}
function appendBubble(m){
@@ -2717,9 +2720,11 @@ function peerIdForUid(uid){ for(const [pid,u] of meetPeerUids){ if(u===uid) retu
async function sfuConnect(){
const LK=await sfuLoadLib(); SFU.lib=LK;
const tk=await postJSON('/api/meetings/token',{ room:meetRoom }); // per-user, per-room join credential
// adaptiveStream off: we attach media via manual srcObject (not track.attach), so LiveKit can't
// see tile visibility to pause/resume — keep all subscribed tracks flowing. dynacast stays on.
const room=new LK.Room({ adaptiveStream:false, dynacast:true }); SFU.room=room;
// adaptiveStream + dynacast BOTH off: we attach media via manual srcObject (not track.attach), so
// LiveKit can't observe tile visibility. With dynacast on, a screen-share layer was paused unless a
// camera track was also flowing → the screen showed blank / only when the camera was on, and was
// slow to appear (#9). Off = every published track always flows at full quality.
const room=new LK.Room({ adaptiveStream:false, dynacast:false }); SFU.room=room;
room.on(LK.RoomEvent.TrackSubscribed, (track,pub,participant)=>sfuAttach(pub,track,participant));
room.on(LK.RoomEvent.TrackUnsubscribed, (track,pub,participant)=>sfuDetach(pub,track,participant));
await room.connect(SFU.url, tk.token);
@@ -3210,7 +3215,7 @@ async function onMeetMsg(e){
refreshMeetPanel(); updateHostControls();
return;
}
if(m.type==='meeting-ended'){ toast('Call ended'); leaveMeeting(true); return; } // 1:1 hangup, or host ended
if(m.type==='meeting-ended'){ toast(m.reason==='unanswered'?'No answer':'Call ended'); leaveMeeting(true); return; } // 1:1 hangup / host ended / unanswered
if(m.type==='meeting-peer-joined'){
stopRingback(); removeWaitingTile(); _dmCallWaiting=null; // #17/#7: they answered → stop ring + drop the ringing tile
meetNames.set(m.peerId,m.name); if(m.avatar) meetAvatars.set(m.peerId,m.avatar); if(m.uid){ meetPeerUids.set(m.peerId,m.uid); meetInviteJoined(m.uid); }