From c672f7b65f51a297cd4cdf0e0be4530825c3494b Mon Sep 17 00:00:00 2001 From: sravan Date: Tue, 7 Jul 2026 15:54:28 +0530 Subject: [PATCH] fix: notification-open in-place (#2/#7/#19) + polish (#9 scrollbar, #16 call scrollbar, #17 ringback) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - #2/#7/#19: openFromNotif opened chats via a full-page reload (location.assign), which was slow AND dropped live state — the sidebar hadn't reloaded so the header showed 'Conversation' with no DP, and an incoming call's Join/invite popup was lost. Now it opens the chat IN-PLACE (selectChat) since the app is already running; full-page nav only as a fallback when the app isn't initialised. - #9: slimmer 6px scrollbar; explicitly hide all webkit stepper-arrow variants. - #16: 1:1/small call no longer shows a stray scrollbar — meet-grid centers tiles and clips overflow (call UI fits the viewport). - #17: caller now hears a gentle ringback while waiting; stops on answer, on any call exit, and auto-stops after 45s so it never rings forever. Co-Authored-By: Claude Opus 4.8 --- server/public/home.html | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/server/public/home.html b/server/public/home.html index b147b33..b819f42 100644 --- a/server/public/home.html +++ b/server/public/home.html @@ -139,10 +139,10 @@ /* Modern thin scrollbars for every scroll area (chat, modals, lists, …). Without this, Electron on Windows falls back to the classic scrollbar with up/down stepper arrows. */ - ::-webkit-scrollbar{width:9px;height:9px;} - ::-webkit-scrollbar-button{display:none!important;width:0;height:0;} + ::-webkit-scrollbar{width:6px;height:6px;} + ::-webkit-scrollbar-button,::-webkit-scrollbar-button:start:decrement,::-webkit-scrollbar-button:end:increment,::-webkit-scrollbar-button:vertical:start,::-webkit-scrollbar-button:vertical:end,::-webkit-scrollbar-button:horizontal:start,::-webkit-scrollbar-button:horizontal:end{display:none!important;width:0!important;height:0!important;} ::-webkit-scrollbar-track{background:transparent;} - ::-webkit-scrollbar-thumb{background:#c7d0dd;border-radius:9px;border:2px solid transparent;background-clip:content-box;} + ::-webkit-scrollbar-thumb{background:#c7d0dd;border-radius:9px;border:1px solid transparent;background-clip:content-box;} ::-webkit-scrollbar-thumb:hover{background:#aab6c8;} ::-webkit-scrollbar-corner{background:transparent;} *{scrollbar-width:thin;scrollbar-color:#c7d0dd transparent;} @@ -342,7 +342,7 @@ .ap-x:hover{color:var(--red);background:#fee2e2;} /* meetings */ .meet{display:flex;flex-direction:column;width:100%;height:100%;} - .meet-grid{flex:1;display:grid;gap:.6rem;padding:.8rem;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));align-content:start;overflow:auto;background:#0b1220;} + .meet-grid{flex:1;min-height:0;display:grid;gap:.6rem;padding:.8rem;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));align-content:center;overflow:hidden;background:#0b1220;} /* #16: center tiles + no stray scrollbar on 1:1/small calls */ .meet-tile{position:relative;background:#0b1220;border-radius:12px;overflow:hidden;aspect-ratio:4/3;border:1px solid #1e293b;transition:box-shadow .12s,border-color .12s;} .meet-tile.speaking{border-color:#22c55e;box-shadow:0 0 0 2px #22c55e, 0 0 14px rgba(34,197,94,.5);} .meet-tile .meet-screen{position:absolute;right:.5rem;top:.5rem;display:inline-flex;align-items:center;gap:.25rem;background:rgba(37,99,235,.92);color:#fff;font-size:.66rem;font-weight:700;padding:.14rem .42rem;border-radius:6px;} @@ -790,7 +790,7 @@ - @@ -1604,7 +1604,7 @@ function onGroupCall(d){ function dismissCallInvite(room){ if(!room) return; const el=document.getElementById('ci-'+room); if(el){ try{ el.remove(); }catch(_){} stopRing(); } } // 1:1 call: start/join from the DM header; live state updates the button + shows an incoming invite. async function startOrJoinDmCall(otherId){ - try{ const r=await postJSON('/api/calls/dm/start',{ to:otherId }); if(r&&r.room){ meetReturn={kind:'dm',id:otherId}; switchTab('meeting'); enterMeeting(r.room); } } + try{ const r=await postJSON('/api/calls/dm/start',{ to:otherId }); if(r&&r.room){ meetReturn={kind:'dm',id:otherId}; switchTab('meeting'); enterMeeting(r.room); startRingback(); /* #17: ring until they answer */ } } catch(e){ toast(e.message||'Could not start the call'); } } // Live presence: a contact came online/offline or entered/left a call — update their dot + the @@ -2153,7 +2153,19 @@ async function unsubscribePush(){ // Open the chat from an in-page notification. Navigation reliably repaints across browsers (a // notification click is not an in-page gesture, so an in-place open won't paint until you // tap). The reload is made fast by HTTP caching + a boot fast-path that opens the chat first. -function openFromNotif(kind,id){ try{ if(window.bizConnectNative&&window.bizConnectNative.focusApp) window.bizConnectNative.focusApp(); }catch(_){} try{ window.focus(); }catch(_){} location.assign('/home?openKind='+encodeURIComponent(kind||'')+'&openId='+encodeURIComponent(id||'')); } +function openFromNotif(kind,id){ + try{ if(window.bizConnectNative&&window.bizConnectNative.focusApp) window.bizConnectNative.focusApp(); }catch(_){} + try{ window.focus(); }catch(_){} + // The app is already running (the notification came from it) → open the chat IN-PLACE. A full-page + // reload was slow (#19) and dropped live state: the sidebar hadn't reloaded so the header showed + // "Conversation" with no DP (#7), and an incoming call's Join/invite popup was lost (#2). + if(window.ME && ME.id && typeof selectChat==='function'){ + if(id){ try{ switchTab('chat'); }catch(_){} try{ selectChat(kind||'dm', id); }catch(_){} } + return; + } + // Fallback: app not initialised yet → navigate (boot reads openKind/openId and opens it). + location.assign(id ? ('/home?openKind='+encodeURIComponent(kind||'')+'&openId='+encodeURIComponent(id||'')) : '/home'); +} // Draw the conversation's avatar (DM = sender pic, group = group DP; else colored initials) to // a round PNG data URL for the desktop toast image. function notifAvatarDataUrl(kind,id,fallbackName){ @@ -2233,6 +2245,12 @@ function ringOnce(){ } function startRing(){ _ringRefs++; if(_ringTimer) return; ringOnce(); _ringTimer=setInterval(ringOnce, 3500); } function stopRing(force){ _ringRefs = force ? 0 : Math.max(0, _ringRefs-1); if(_ringRefs>0) return; if(_ringTimer){ clearInterval(_ringTimer); _ringTimer=null; } } +// #17 Outgoing ringback — the CALLER hears a gentle periodic ring while waiting for the other side to +// pick up. Stops as soon as a peer joins, or the call ends. +let _ringbackTimer=null, _ringbackStop=null; +function ringbackOnce(){ try{ _audioCtx=_audioCtx||new (window.AudioContext||window.webkitAudioContext)(); if(_audioCtx.state==='suspended') _audioCtx.resume(); const t=_audioCtx.currentTime; ringTone(t, 440, 1.1, 0.11); ringTone(t+0.16, 480, 1.1, 0.11); }catch(_){} } +function startRingback(){ if(_ringbackTimer) return; ringbackOnce(); _ringbackTimer=setInterval(ringbackOnce, 3000); _ringbackStop=setTimeout(stopRingback, 45000); /* never ring forever if unanswered/declined */ } +function stopRingback(){ if(_ringbackTimer){ clearInterval(_ringbackTimer); _ringbackTimer=null; } if(_ringbackStop){ clearTimeout(_ringbackStop); _ringbackStop=null; } } function onChatMessage(m){ const isGroupMsg=!!m.conversation_id; const kind=isGroupMsg?'group':'dm'; @@ -3012,6 +3030,7 @@ async function onMeetMsg(e){ } if(m.type==='meeting-ended'){ toast('Call ended'); leaveMeeting(true); return; } // 1:1 hangup, or host ended if(m.type==='meeting-peer-joined'){ + stopRingback(); // #17: they answered → stop the outgoing ring 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); } const pc=meetMakePeer(m.peerId,m.name); // I'm an existing peer → I OFFER to the newcomer (carries my screen) if(pc){ try{ const offer=await pc.createOffer(); await pc.setLocalDescription(offer); meetSend({type:'meeting-signal',to:m.peerId,data:{sdp:pc.localDescription}}); }catch(_){} } // (SFU: LiveKit handles media, no offer) @@ -3086,6 +3105,7 @@ let meetLeaving=false; // there wrongly announced "Host handed to " (bug). function leaveMeeting(forced){ if(meetLeaving) return; meetLeaving=true; + stopRingback(); // #17: any call exit stops the outgoing ring const isDm=!!(meetReturn && meetReturn.kind==='dm'); if(!forced && !isDm && meetIsHost && meetPeers.size>0){ const next=meetPeers.keys().next().value; // first remaining participant