@@ -77,6 +77,7 @@ const handleAuth = login;
7777const myUsername = localStorage . getItem ( "username" ) ;
7878let ws = null ;
7979let currentReceiver = "Family Group" ;
80+ let userAvatars = { } ; // username -> avatarUrl
8081
8182// Auto-redirect logic
8283if ( window . location . pathname === "/" || window . location . pathname . includes ( "index.html" ) ) {
@@ -90,8 +91,9 @@ if (window.location.pathname.includes("chat.html")) {
9091 window . location . href = "/index.html" ;
9192 } else {
9293 const profileEl = document . getElementById ( "profile-name" ) ;
93- if ( profileEl ) profileEl . innerText = "👤 " + myUsername ;
94+ if ( profileEl ) profileEl . innerText = myUsername ;
9495 initTheme ( ) ;
96+ initSidebar ( ) ;
9597 loadUsers ( ) ;
9698 connectWS ( ) ;
9799 }
@@ -109,6 +111,23 @@ function toggleTheme() {
109111 localStorage . setItem ( "theme" , next ) ;
110112}
111113
114+ function initSidebar ( ) {
115+ const state = localStorage . getItem ( "sidebarMinimized" ) ;
116+ if ( state === "true" ) {
117+ document . getElementById ( "sidebar" ) . classList . add ( "minimized" ) ;
118+ }
119+ }
120+
121+ function toggleSidebar ( ) {
122+ const sidebar = document . getElementById ( "sidebar" ) ;
123+ sidebar . classList . toggle ( "minimized" ) ;
124+ localStorage . setItem ( "sidebarMinimized" , sidebar . classList . contains ( "minimized" ) ) ;
125+ }
126+
127+ function showSidebar ( ) {
128+ document . body . classList . remove ( "chat-open" ) ;
129+ }
130+
112131function toggleSettings ( ) {
113132 const panel = document . getElementById ( "settings-panel" ) ;
114133 panel . style . display = panel . style . display === "flex" ? "none" : "flex" ;
@@ -207,6 +226,7 @@ async function loadUsers() {
207226 // Current user
208227 if ( user . profile_photo ) {
209228 document . getElementById ( "my-avatar" ) . src = user . profile_photo ;
229+ userAvatars [ myUsername ] = user . profile_photo ;
210230 }
211231 }
212232 } ) ;
@@ -221,10 +241,8 @@ function addUserToList(user) {
221241 div . className = "user-item" ;
222242 div . onclick = ( ) => selectUser ( user . username ) ;
223243
224- let imgUrl = "https://via.placeholder.com/40" ;
225- if ( user . profile_photo ) {
226- imgUrl = user . profile_photo ;
227- }
244+ let imgUrl = user . profile_photo || "https://via.placeholder.com/40" ;
245+ userAvatars [ user . username ] = imgUrl ;
228246
229247 div . innerHTML = `
230248 <img src="${ imgUrl } " class="avatar">
@@ -239,9 +257,21 @@ function addUserToList(user) {
239257 ***********************/
240258async function selectUser ( name ) {
241259 currentReceiver = name ;
242- document . getElementById ( "chat-header " ) . innerText = name ;
260+ document . getElementById ( "chat-title " ) . innerText = name ;
243261 document . getElementById ( "messages" ) . innerHTML = "" ;
244262
263+ // Mobile navigation
264+ document . body . classList . add ( "chat-open" ) ;
265+ document . getElementById ( "back-btn" ) . style . display = window . innerWidth <= 768 ? "block" : "none" ;
266+
267+ // Show call button for private chats
268+ const chatActions = document . getElementById ( "chat-actions" ) ;
269+ if ( name !== "Family Group" ) {
270+ chatActions . style . display = "block" ;
271+ } else {
272+ chatActions . style . display = "none" ;
273+ }
274+
245275 // Highlight active user
246276 const userItems = document . querySelectorAll ( ".user-item" ) ;
247277 userItems . forEach ( item => {
@@ -339,6 +369,46 @@ function connectWS() {
339369 }
340370 break ;
341371
372+ case "call_request" :
373+ handleCallRequest ( msg ) ;
374+ break ;
375+
376+ case "call_accept" :
377+ document . getElementById ( "call-status" ) . innerText = "Connecting..." ;
378+ setupWebRTC ( ) ;
379+ break ;
380+
381+ case "call_reject" :
382+ case "call_rejected" :
383+ alert ( msg . sender + " rejected the call" ) ;
384+ stopWebRTC ( ) ;
385+ hideCallOverlay ( ) ;
386+ break ;
387+
388+ case "call_end" :
389+ case "call_ended" :
390+ stopWebRTC ( ) ;
391+ hideCallOverlay ( ) ;
392+ break ;
393+
394+ case "webrtc_offer" :
395+ if ( document . getElementById ( "call-overlay" ) . style . display === "flex" &&
396+ document . getElementById ( "accept-call" ) . style . display === "block" ) {
397+ // If we are showing the incoming call screen, wait for user to accept
398+ pendingOffer = msg ;
399+ } else {
400+ handleOffer ( msg ) ;
401+ }
402+ break ;
403+
404+ case "webrtc_answer" :
405+ handleAnswer ( msg ) ;
406+ break ;
407+
408+ case "ice_candidate" :
409+ handleIceCandidate ( msg ) ;
410+ break ;
411+
342412 case "status" :
343413 // online / offline
344414 break ;
@@ -456,13 +526,18 @@ function isImage(url) {
456526
457527let typingTimeout ;
458528function showTypingIndicator ( sender ) {
459- const header = document . getElementById ( "chat-header" ) ;
460- const originalText = sender ;
461- header . innerText = sender + " is typing..." ;
529+ if ( sender !== currentReceiver ) return ;
530+ const title = document . getElementById ( "chat-title" ) ;
531+ if ( ! title ) return ;
532+
533+ const originalText = currentReceiver ;
534+ title . innerText = sender + " is typing..." ;
462535
463536 clearTimeout ( typingTimeout ) ;
464537 typingTimeout = setTimeout ( ( ) => {
465- header . innerText = originalText ;
538+ if ( currentReceiver === originalText ) {
539+ title . innerText = originalText ;
540+ }
466541 } , 3000 ) ;
467542}
468543
@@ -489,3 +564,220 @@ function handleEnter(e) {
489564 sendMsg ( ) ;
490565 }
491566}
567+
568+ window . addEventListener ( 'resize' , ( ) => {
569+ if ( window . innerWidth > 768 ) {
570+ document . body . classList . remove ( "chat-open" ) ;
571+ document . getElementById ( "back-btn" ) . style . display = "none" ;
572+ } else if ( currentReceiver && document . body . classList . contains ( "chat-open" ) ) {
573+ document . getElementById ( "back-btn" ) . style . display = "block" ;
574+ }
575+ } ) ;
576+
577+ /***********************
578+ * WEBRTC CALLING
579+ ***********************/
580+ let peerConnection = null ;
581+ let localStream = null ;
582+ let callReceiver = null ;
583+ let isCaller = false ;
584+ let pendingOffer = null ;
585+
586+ const rtcConfig = {
587+ iceServers : [
588+ { urls : 'stun:stun.l.google.com:19302' } ,
589+ { urls : 'stun:stun1.l.google.com:19302' }
590+ ]
591+ } ;
592+
593+ async function startCall ( ) {
594+ if ( ! currentReceiver || currentReceiver === "Family Group" ) return ;
595+
596+ callReceiver = currentReceiver ;
597+ isCaller = true ;
598+
599+ showCallOverlay ( callReceiver , "Calling..." ) ;
600+ document . getElementById ( "accept-call" ) . style . display = "none" ;
601+ document . getElementById ( "reject-call" ) . style . display = "none" ;
602+ document . getElementById ( "end-call" ) . style . display = "block" ;
603+
604+ const myPhoto = userAvatars [ myUsername ] ;
605+
606+ const payload = {
607+ type : "call_request" ,
608+ receiver : callReceiver ,
609+ sender : myUsername ,
610+ profile_photo : myPhoto || null
611+ } ;
612+ ws . send ( JSON . stringify ( payload ) ) ;
613+
614+ // Android expects offer immediately
615+ setupWebRTC ( ) ;
616+ }
617+
618+ function showCallOverlay ( username , status ) {
619+ document . getElementById ( "call-username" ) . innerText = username ;
620+ document . getElementById ( "call-status" ) . innerText = status ;
621+ document . getElementById ( "call-overlay" ) . style . display = "flex" ;
622+
623+ if ( userAvatars [ username ] ) {
624+ document . getElementById ( "call-avatar" ) . src = userAvatars [ username ] ;
625+ } else {
626+ document . getElementById ( "call-avatar" ) . src = "https://via.placeholder.com/100" ;
627+ }
628+ }
629+
630+ function hideCallOverlay ( ) {
631+ document . getElementById ( "call-overlay" ) . style . display = "none" ;
632+ document . getElementById ( "ringtone" ) . pause ( ) ;
633+ document . getElementById ( "ringtone" ) . currentTime = 0 ;
634+ pendingOffer = null ;
635+ }
636+
637+ async function handleCallRequest ( msg ) {
638+ callReceiver = msg . sender ;
639+ if ( msg . profile_photo ) {
640+ userAvatars [ msg . sender ] = msg . profile_photo ;
641+ }
642+ isCaller = false ;
643+
644+ showCallOverlay ( callReceiver , "Incoming Call..." ) ;
645+ document . getElementById ( "accept-call" ) . style . display = "block" ;
646+ document . getElementById ( "reject-call" ) . style . display = "block" ;
647+ document . getElementById ( "end-call" ) . style . display = "none" ;
648+
649+ document . getElementById ( "ringtone" ) . play ( ) . catch ( e => console . log ( "Audio play failed:" , e ) ) ;
650+ }
651+
652+ async function acceptCall ( ) {
653+ document . getElementById ( "ringtone" ) . pause ( ) ;
654+ document . getElementById ( "call-status" ) . innerText = "Connecting..." ;
655+ document . getElementById ( "accept-call" ) . style . display = "none" ;
656+ document . getElementById ( "reject-call" ) . style . display = "none" ;
657+ document . getElementById ( "end-call" ) . style . display = "block" ;
658+
659+ const payload = {
660+ type : "call_accept" ,
661+ receiver : callReceiver
662+ } ;
663+ ws . send ( JSON . stringify ( payload ) ) ;
664+
665+ if ( pendingOffer ) {
666+ await handleOffer ( pendingOffer ) ;
667+ pendingOffer = null ;
668+ } else {
669+ await setupWebRTC ( ) ;
670+ }
671+ }
672+
673+ function rejectCall ( ) {
674+ const payload = {
675+ type : "call_rejected" ,
676+ receiver : callReceiver
677+ } ;
678+ ws . send ( JSON . stringify ( payload ) ) ;
679+ hideCallOverlay ( ) ;
680+ }
681+
682+ function endCall ( ) {
683+ const payload = {
684+ type : "call_ended" ,
685+ receiver : callReceiver
686+ } ;
687+ ws . send ( JSON . stringify ( payload ) ) ;
688+ stopWebRTC ( ) ;
689+ hideCallOverlay ( ) ;
690+ }
691+
692+ async function setupWebRTC ( ) {
693+ if ( peerConnection ) return ;
694+ try {
695+ localStream = await navigator . mediaDevices . getUserMedia ( { audio : true , video : false } ) ;
696+
697+ peerConnection = new RTCPeerConnection ( rtcConfig ) ;
698+
699+ localStream . getTracks ( ) . forEach ( track => {
700+ peerConnection . addTrack ( track , localStream ) ;
701+ } ) ;
702+
703+ peerConnection . ontrack = ( event ) => {
704+ const remoteAudio = document . getElementById ( "remote-audio" ) || document . createElement ( "audio" ) ;
705+ remoteAudio . id = "remote-audio" ;
706+ remoteAudio . autoplay = true ;
707+ remoteAudio . srcObject = event . streams [ 0 ] ;
708+ if ( ! remoteAudio . parentElement ) document . body . appendChild ( remoteAudio ) ;
709+ } ;
710+
711+ peerConnection . onicecandidate = ( event ) => {
712+ if ( event . candidate ) {
713+ ws . send ( JSON . stringify ( {
714+ type : "ice_candidate" ,
715+ receiver : callReceiver ,
716+ candidate : event . candidate
717+ } ) ) ;
718+ }
719+ } ;
720+
721+ if ( isCaller ) {
722+ const offer = await peerConnection . createOffer ( ) ;
723+ await peerConnection . setLocalDescription ( offer ) ;
724+ ws . send ( JSON . stringify ( {
725+ type : "webrtc_offer" ,
726+ receiver : callReceiver ,
727+ sdp : offer . sdp
728+ } ) ) ;
729+ }
730+ } catch ( e ) {
731+ console . error ( "WebRTC Setup Error:" , e ) ;
732+ if ( location . protocol !== 'https:' && location . hostname !== 'localhost' ) {
733+ alert ( "Microphone access failed. Web calls require HTTPS to work on LAN." ) ;
734+ } else {
735+ alert ( "Could not access microphone. Please check permissions." ) ;
736+ }
737+ endCall ( ) ;
738+ }
739+ }
740+
741+ async function handleOffer ( msg ) {
742+ if ( ! peerConnection ) await setupWebRTC ( ) ;
743+ await peerConnection . setRemoteDescription ( new RTCSessionDescription ( { type : 'offer' , sdp : msg . sdp } ) ) ;
744+ const answer = await peerConnection . createAnswer ( ) ;
745+ await peerConnection . setLocalDescription ( answer ) ;
746+ ws . send ( JSON . stringify ( {
747+ type : "webrtc_answer" ,
748+ receiver : callReceiver ,
749+ sdp : answer . sdp
750+ } ) ) ;
751+ }
752+
753+ async function handleAnswer ( msg ) {
754+ if ( ! peerConnection ) return ;
755+ try {
756+ await peerConnection . setRemoteDescription ( new RTCSessionDescription ( { type : 'answer' , sdp : msg . sdp } ) ) ;
757+ document . getElementById ( "call-status" ) . innerText = "Connected" ;
758+ } catch ( e ) {
759+ console . error ( "Handle Answer Error:" , e ) ;
760+ }
761+ }
762+
763+ async function handleIceCandidate ( msg ) {
764+ if ( peerConnection ) {
765+ await peerConnection . addIceCandidate ( new RTCIceCandidate ( msg . candidate ) ) ;
766+ }
767+ }
768+
769+ function stopWebRTC ( ) {
770+ if ( localStream ) {
771+ localStream . getTracks ( ) . forEach ( track => track . stop ( ) ) ;
772+ localStream = null ;
773+ }
774+ if ( peerConnection ) {
775+ peerConnection . close ( ) ;
776+ peerConnection = null ;
777+ }
778+ const remoteAudio = document . getElementById ( "remote-audio" ) ;
779+ if ( remoteAudio ) remoteAudio . remove ( ) ;
780+
781+ isCaller = false ;
782+ pendingOffer = null ;
783+ }
0 commit comments