Skip to content

Commit d2422d4

Browse files
Merge pull request #8 from DivyanshuChipa/modernize-web-client-14722832941266930639
Modernize Web Client: Sidebar, Mobile, and Calling
2 parents bfcb41a + 7ced9ba commit d2422d4

4 files changed

Lines changed: 451 additions & 26 deletions

File tree

backend/chat.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
# ✅ List of messages that should NOT be saved to DB
1818
SIGNAL_TYPES = {
19-
"call_request", "call_accept", "call_reject", "call_end", "call_rejected",
19+
"call_request", "call_accept", "call_reject", "call_end", "call_rejected", "call_ended",
2020
"webrtc_offer", "webrtc_answer", "ice_candidate"
2121
}
2222

backend/static/app.js

Lines changed: 302 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ const handleAuth = login;
7777
const myUsername = localStorage.getItem("username");
7878
let ws = null;
7979
let currentReceiver = "Family Group";
80+
let userAvatars = {}; // username -> avatarUrl
8081

8182
// Auto-redirect logic
8283
if (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+
112131
function 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
***********************/
240258
async 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

457527
let typingTimeout;
458528
function 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

Comments
 (0)