Spaces:
Sleeping
Sleeping
ausername-12345 commited on
Commit ·
eeb3363
1
Parent(s): 76078c0
remove group calling, fix video toggle visibility, fix empty Google setup container
Browse files- Removed all group call code (backend handlers, frontend logic,
WebSocket dispatch, HTML) — only 1-to-1 calling remains
- Video toggle button now properly shown/hidden in setupCallMedia
(visible during video calls, hidden during audio calls)
- Empty tab container during Google account setup is now hidden
(auth-tabs div hidden in showGoogleSetup, restored in showTab)
- src/main.py +0 -8
- src/static/app.js +11 -292
- src/static/index.html +1 -10
- src/ws_manager.py +1 -90
src/main.py
CHANGED
|
@@ -65,14 +65,6 @@ async def websocket_endpoint(websocket: WebSocket, token: str, db: Session = Dep
|
|
| 65 |
await manager.handle_ice_candidate(msg, user)
|
| 66 |
elif msg_type == "call_end":
|
| 67 |
await manager.handle_call_end(msg, user)
|
| 68 |
-
elif msg_type == "group_call_start":
|
| 69 |
-
await manager.handle_group_call_start(msg, user, db)
|
| 70 |
-
elif msg_type == "group_call_accept":
|
| 71 |
-
await manager.handle_group_call_accept(msg, user, db)
|
| 72 |
-
elif msg_type == "group_call_leave":
|
| 73 |
-
await manager.handle_group_call_leave(msg, user)
|
| 74 |
-
elif msg_type == "group_call_end":
|
| 75 |
-
await manager.handle_group_call_end(msg, user)
|
| 76 |
|
| 77 |
except WebSocketDisconnect:
|
| 78 |
manager.disconnect(user.id)
|
|
|
|
| 65 |
await manager.handle_ice_candidate(msg, user)
|
| 66 |
elif msg_type == "call_end":
|
| 67 |
await manager.handle_call_end(msg, user)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
|
| 69 |
except WebSocketDisconnect:
|
| 70 |
manager.disconnect(user.id)
|
src/static/app.js
CHANGED
|
@@ -34,12 +34,7 @@ const ICE_SERVERS = {
|
|
| 34 |
]
|
| 35 |
};
|
| 36 |
|
| 37 |
-
|
| 38 |
-
let activeGroupCallId = null;
|
| 39 |
-
let activeGroupCallType = null;
|
| 40 |
-
let groupCallParticipants = {}; // userId -> { display_name, avatar_color, pc, stream }
|
| 41 |
-
let groupCallLocalStream = null;
|
| 42 |
-
let groupCallRinging = false;
|
| 43 |
|
| 44 |
// ── Bootstrap ───────────────────────────────────────────────────────────────
|
| 45 |
|
|
@@ -133,6 +128,7 @@ async function apiFetch(path, tok, method = "GET", body = null) {
|
|
| 133 |
// ── Auth ─────────────────────────────────────────────────────────────────────
|
| 134 |
|
| 135 |
function showTab(tab) {
|
|
|
|
| 136 |
document.getElementById("form-login").classList.toggle("hidden", tab !== "login");
|
| 137 |
document.getElementById("form-register").classList.toggle("hidden", tab !== "register");
|
| 138 |
document.getElementById("form-google-setup").classList.add("hidden");
|
|
@@ -213,8 +209,7 @@ function googleLogin() {
|
|
| 213 |
}
|
| 214 |
|
| 215 |
function showGoogleSetup() {
|
| 216 |
-
document.getElementById("
|
| 217 |
-
document.getElementById("tab-register").classList.add("hidden");
|
| 218 |
document.getElementById("form-login").classList.add("hidden");
|
| 219 |
document.getElementById("form-register").classList.add("hidden");
|
| 220 |
document.getElementById("form-google-setup").classList.remove("hidden");
|
|
@@ -351,33 +346,13 @@ async function handleWsMessage(evt) {
|
|
| 351 |
} else if (msg.type === "read_receipt") {
|
| 352 |
handleReadReceipt(msg);
|
| 353 |
} else if (msg.type === "call_offer") {
|
| 354 |
-
|
| 355 |
-
handleGroupCallOffer(msg.from, msg.sdp);
|
| 356 |
-
} else {
|
| 357 |
-
handleCallOffer(msg);
|
| 358 |
-
}
|
| 359 |
} else if (msg.type === "call_answer") {
|
| 360 |
-
|
| 361 |
-
handleGroupCallAnswer(msg.from, msg.sdp);
|
| 362 |
-
} else {
|
| 363 |
-
handleCallAnswer(msg);
|
| 364 |
-
}
|
| 365 |
} else if (msg.type === "ice_candidate") {
|
| 366 |
handleIceCandidate(msg);
|
| 367 |
} else if (msg.type === "call_end") {
|
| 368 |
handleCallEnd(msg);
|
| 369 |
-
} else if (msg.type === "group_call_started") {
|
| 370 |
-
handleGroupCallStarted(msg);
|
| 371 |
-
} else if (msg.type === "group_call_ringing") {
|
| 372 |
-
handleGroupCallRinging(msg);
|
| 373 |
-
} else if (msg.type === "group_call_joined") {
|
| 374 |
-
handleGroupCallJoined(msg);
|
| 375 |
-
} else if (msg.type === "group_participant_joined") {
|
| 376 |
-
handleGroupParticipantJoined(msg);
|
| 377 |
-
} else if (msg.type === "group_participant_left") {
|
| 378 |
-
handleGroupParticipantLeft(msg);
|
| 379 |
-
} else if (msg.type === "group_call_ended") {
|
| 380 |
-
handleGroupCallEnded(msg);
|
| 381 |
}
|
| 382 |
}
|
| 383 |
|
|
@@ -523,13 +498,8 @@ function hideIncomingCall() {
|
|
| 523 |
}
|
| 524 |
|
| 525 |
async function startCall(type) {
|
|
|
|
| 526 |
document.getElementById("call-dropdown").classList.add("hidden");
|
| 527 |
-
if (!currentChat) return;
|
| 528 |
-
if (currentChat.type === "group") {
|
| 529 |
-
await startGroupCall(type);
|
| 530 |
-
return;
|
| 531 |
-
}
|
| 532 |
-
if (!currentChat.other_user) return;
|
| 533 |
callType = type;
|
| 534 |
callTarget = currentChat.other_user;
|
| 535 |
callState = "calling";
|
|
@@ -568,8 +538,7 @@ async function startCall(type) {
|
|
| 568 |
}
|
| 569 |
|
| 570 |
function handleCallOffer(msg) {
|
| 571 |
-
if (
|
| 572 |
-
if (callState || activeGroupCallId) {
|
| 573 |
ws.send(JSON.stringify({ type: "call_end", target_id: msg.from }));
|
| 574 |
return;
|
| 575 |
}
|
|
@@ -589,10 +558,6 @@ function handleCallOffer(msg) {
|
|
| 589 |
|
| 590 |
async function acceptCall() {
|
| 591 |
hideIncomingCall();
|
| 592 |
-
if (groupCallRinging) {
|
| 593 |
-
await acceptGroupCall();
|
| 594 |
-
return;
|
| 595 |
-
}
|
| 596 |
callState = "connected";
|
| 597 |
try {
|
| 598 |
const constraints = { audio: true, video: callType === "video" };
|
|
@@ -632,10 +597,6 @@ async function acceptCall() {
|
|
| 632 |
|
| 633 |
function rejectCall() {
|
| 634 |
hideIncomingCall();
|
| 635 |
-
if (groupCallRinging) {
|
| 636 |
-
endGroupCall();
|
| 637 |
-
return;
|
| 638 |
-
}
|
| 639 |
if (ws && ws.readyState === WebSocket.OPEN && callTarget) {
|
| 640 |
ws.send(JSON.stringify({ type: "call_end", target_id: callTarget.id }));
|
| 641 |
}
|
|
@@ -666,16 +627,9 @@ function handleCallEnd(msg) {
|
|
| 666 |
}
|
| 667 |
cleanupCall();
|
| 668 |
}
|
| 669 |
-
if (activeGroupCallId) {
|
| 670 |
-
endGroupCall();
|
| 671 |
-
}
|
| 672 |
}
|
| 673 |
|
| 674 |
function endCall() {
|
| 675 |
-
if (activeGroupCallId) {
|
| 676 |
-
endGroupCall();
|
| 677 |
-
return;
|
| 678 |
-
}
|
| 679 |
if (ws && ws.readyState === WebSocket.OPEN && callTarget) {
|
| 680 |
ws.send(JSON.stringify({ type: "call_end", target_id: callTarget.id }));
|
| 681 |
}
|
|
@@ -703,12 +657,15 @@ function setupCallMedia() {
|
|
| 703 |
if (lv) lv.srcObject = localStream;
|
| 704 |
const videoContent = document.getElementById("call-video-content");
|
| 705 |
const audioContent = document.getElementById("call-audio-content");
|
|
|
|
| 706 |
if (callType === "video") {
|
| 707 |
if (videoContent) videoContent.classList.remove("hidden");
|
| 708 |
if (audioContent) audioContent.classList.add("hidden");
|
|
|
|
| 709 |
} else {
|
| 710 |
if (videoContent) videoContent.classList.add("hidden");
|
| 711 |
if (audioContent) audioContent.classList.remove("hidden");
|
|
|
|
| 712 |
const av = document.getElementById("call-audio-avatar");
|
| 713 |
if (av) {
|
| 714 |
av.textContent = callTarget.display_name[0].toUpperCase();
|
|
@@ -737,244 +694,6 @@ function toggleVideo() {
|
|
| 737 |
}
|
| 738 |
}
|
| 739 |
|
| 740 |
-
// ── Group Call Functions ──────────────────────────────────────────────────────
|
| 741 |
-
|
| 742 |
-
async function startGroupCall(type) {
|
| 743 |
-
document.getElementById("call-dropdown").classList.add("hidden");
|
| 744 |
-
if (activeGroupCallId) return;
|
| 745 |
-
activeGroupCallId = currentChat.id;
|
| 746 |
-
activeGroupCallType = type;
|
| 747 |
-
try {
|
| 748 |
-
groupCallLocalStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: type === "video" });
|
| 749 |
-
ws.send(JSON.stringify({ type: "group_call_start", group_id: activeGroupCallId, call_type: type }));
|
| 750 |
-
showCallModal();
|
| 751 |
-
document.getElementById("call-status").textContent = "Calling everyone…";
|
| 752 |
-
document.getElementById("call-peer-name").textContent = currentChat.name || "Group Call";
|
| 753 |
-
document.getElementById("group-video-content").classList.remove("hidden");
|
| 754 |
-
document.getElementById("call-video-content").classList.add("hidden");
|
| 755 |
-
document.getElementById("call-audio-content").classList.add("hidden");
|
| 756 |
-
document.getElementById("call-video-btn").classList.remove("hidden");
|
| 757 |
-
const lv = document.getElementById("group-local-video");
|
| 758 |
-
if (lv) lv.srcObject = groupCallLocalStream;
|
| 759 |
-
showGroupParticipants();
|
| 760 |
-
} catch (e) {
|
| 761 |
-
console.error("startGroupCall error:", e);
|
| 762 |
-
endGroupCall();
|
| 763 |
-
}
|
| 764 |
-
}
|
| 765 |
-
|
| 766 |
-
function handleGroupCallStarted(msg) {
|
| 767 |
-
// We're the initiator — participant list tells us who's in the group
|
| 768 |
-
activeGroupCallId = msg.group_id;
|
| 769 |
-
showGroupCallUI();
|
| 770 |
-
}
|
| 771 |
-
|
| 772 |
-
function handleGroupCallRinging(msg) {
|
| 773 |
-
// Someone else started a call — show incoming notification
|
| 774 |
-
if (activeGroupCallId) { ws.send(JSON.stringify({ type: "group_call_end", group_id: msg.group_id })); return; }
|
| 775 |
-
activeGroupCallId = msg.group_id;
|
| 776 |
-
activeGroupCallType = msg.call_type;
|
| 777 |
-
groupCallRinging = true;
|
| 778 |
-
document.getElementById("incoming-caller-name").textContent = `${msg.initiator_name} started a group call`;
|
| 779 |
-
document.getElementById("incoming-call-type").textContent = msg.call_type === "video" ? "Video Call" : "Audio Call";
|
| 780 |
-
document.getElementById("incoming-call-type-icon").textContent = msg.call_type === "video" ? "📹" : "📞";
|
| 781 |
-
const av = document.getElementById("incoming-avatar");
|
| 782 |
-
av.textContent = msg.initiator_name[0].toUpperCase();
|
| 783 |
-
av.style.background = msg.initiator_avatar || "#6366f1";
|
| 784 |
-
showIncomingCall();
|
| 785 |
-
}
|
| 786 |
-
|
| 787 |
-
async function acceptGroupCall() {
|
| 788 |
-
hideIncomingCall();
|
| 789 |
-
if (!activeGroupCallId) return;
|
| 790 |
-
groupCallRinging = false;
|
| 791 |
-
try {
|
| 792 |
-
groupCallLocalStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: activeGroupCallType === "video" });
|
| 793 |
-
ws.send(JSON.stringify({ type: "group_call_accept", group_id: activeGroupCallId }));
|
| 794 |
-
showCallModal();
|
| 795 |
-
document.getElementById("call-status").textContent = "Connecting…";
|
| 796 |
-
document.getElementById("call-peer-name").textContent = "Group Call";
|
| 797 |
-
setupGroupCallModal();
|
| 798 |
-
} catch (e) {
|
| 799 |
-
console.error("acceptGroupCall error:", e);
|
| 800 |
-
endGroupCall();
|
| 801 |
-
}
|
| 802 |
-
}
|
| 803 |
-
|
| 804 |
-
function handleGroupCallJoined(msg) {
|
| 805 |
-
// We joined successfully — create peer connections to existing participants
|
| 806 |
-
document.getElementById("call-status").textContent = "Connected";
|
| 807 |
-
for (const p of msg.participants) {
|
| 808 |
-
if (p.id !== me.id) {
|
| 809 |
-
createGroupPeerConnection(p.id, p.display_name, p.avatar_color, true);
|
| 810 |
-
}
|
| 811 |
-
}
|
| 812 |
-
showGroupParticipants();
|
| 813 |
-
}
|
| 814 |
-
|
| 815 |
-
function handleGroupParticipantJoined(msg) {
|
| 816 |
-
const uid = msg.user_id;
|
| 817 |
-
if (uid === me.id) return;
|
| 818 |
-
// Check if we already have a connection
|
| 819 |
-
if (groupCallParticipants[uid]) return;
|
| 820 |
-
// If we have a lower ID, send offer; otherwise wait for their offer
|
| 821 |
-
if (me.id < uid) {
|
| 822 |
-
createGroupPeerConnection(uid, msg.display_name, msg.avatar_color, true);
|
| 823 |
-
}
|
| 824 |
-
showGroupParticipants();
|
| 825 |
-
}
|
| 826 |
-
|
| 827 |
-
function handleGroupParticipantLeft(msg) {
|
| 828 |
-
closeGroupPeerConnection(msg.user_id);
|
| 829 |
-
showGroupParticipants();
|
| 830 |
-
}
|
| 831 |
-
|
| 832 |
-
function handleGroupCallEnded(msg) {
|
| 833 |
-
endGroupCall();
|
| 834 |
-
document.getElementById("call-status").textContent = "Call ended";
|
| 835 |
-
setTimeout(hideCallModal, 1500);
|
| 836 |
-
}
|
| 837 |
-
|
| 838 |
-
async function createGroupPeerConnection(uid, displayName, avatarColor, sendOffer) {
|
| 839 |
-
if (groupCallParticipants[uid]) return;
|
| 840 |
-
const pc = new RTCPeerConnection(ICE_SERVERS);
|
| 841 |
-
if (groupCallLocalStream) {
|
| 842 |
-
groupCallLocalStream.getTracks().forEach(t => pc.addTrack(t, groupCallLocalStream));
|
| 843 |
-
}
|
| 844 |
-
const stream = new MediaStream();
|
| 845 |
-
pc.ontrack = (event) => {
|
| 846 |
-
event.streams[0].getTracks().forEach(t => stream.addTrack(t));
|
| 847 |
-
const container = document.getElementById("group-remote-videos");
|
| 848 |
-
if (container) {
|
| 849 |
-
let vid = container.querySelector(`[data-uid="${uid}"]`);
|
| 850 |
-
if (!vid) {
|
| 851 |
-
showGroupParticipants();
|
| 852 |
-
} else {
|
| 853 |
-
vid.srcObject = stream;
|
| 854 |
-
}
|
| 855 |
-
}
|
| 856 |
-
};
|
| 857 |
-
pc.onicecandidate = (event) => {
|
| 858 |
-
if (event.candidate && ws && ws.readyState === WebSocket.OPEN) {
|
| 859 |
-
ws.send(JSON.stringify({ type: "ice_candidate", target_id: uid, candidate: event.candidate, group_id: activeGroupCallId }));
|
| 860 |
-
}
|
| 861 |
-
};
|
| 862 |
-
pc.oniceconnectionstatechange = () => {
|
| 863 |
-
if (pc.connectionState === "disconnected" || pc.connectionState === "failed") {
|
| 864 |
-
closeGroupPeerConnection(uid);
|
| 865 |
-
}
|
| 866 |
-
};
|
| 867 |
-
groupCallParticipants[uid] = { display_name: displayName, avatar_color: avatarColor, pc, stream };
|
| 868 |
-
|
| 869 |
-
if (sendOffer) {
|
| 870 |
-
try {
|
| 871 |
-
const offer = await pc.createOffer();
|
| 872 |
-
await pc.setLocalDescription(offer);
|
| 873 |
-
ws.send(JSON.stringify({ type: "call_offer", target_id: uid, sdp: pc.localDescription, group_id: activeGroupCallId }));
|
| 874 |
-
} catch (e) {
|
| 875 |
-
console.error("createOffer error:", e);
|
| 876 |
-
}
|
| 877 |
-
}
|
| 878 |
-
return pc;
|
| 879 |
-
}
|
| 880 |
-
|
| 881 |
-
function handleGroupCallOffer(uid, sdp) {
|
| 882 |
-
// Only create PC if we don't have one and we have a higher ID (we answer)
|
| 883 |
-
if (groupCallParticipants[uid]) {
|
| 884 |
-
// Existing PC, might be a renegotiation
|
| 885 |
-
const pc = groupCallParticipants[uid].pc;
|
| 886 |
-
if (pc.signalingState !== "stable") return;
|
| 887 |
-
pc.setRemoteDescription(new RTCSessionDescription(sdp)).catch(() => {});
|
| 888 |
-
return;
|
| 889 |
-
}
|
| 890 |
-
if (me.id < uid) return; // We should be sending offer, not receiving
|
| 891 |
-
createGroupPeerConnection(uid, "", "", false).then(async (pc) => {
|
| 892 |
-
if (!pc) return;
|
| 893 |
-
await pc.setRemoteDescription(new RTCSessionDescription(sdp));
|
| 894 |
-
const answer = await pc.createAnswer();
|
| 895 |
-
await pc.setLocalDescription(answer);
|
| 896 |
-
ws.send(JSON.stringify({ type: "call_answer", target_id: uid, sdp: pc.localDescription, group_id: activeGroupCallId }));
|
| 897 |
-
});
|
| 898 |
-
}
|
| 899 |
-
|
| 900 |
-
function handleGroupCallAnswer(uid, sdp) {
|
| 901 |
-
const p = groupCallParticipants[uid];
|
| 902 |
-
if (p && p.pc && p.pc.signalingState !== "closed") {
|
| 903 |
-
p.pc.setRemoteDescription(new RTCSessionDescription(sdp)).catch(() => {});
|
| 904 |
-
}
|
| 905 |
-
}
|
| 906 |
-
|
| 907 |
-
function closeGroupPeerConnection(uid) {
|
| 908 |
-
const p = groupCallParticipants[uid];
|
| 909 |
-
if (p) {
|
| 910 |
-
if (p.pc) p.pc.close();
|
| 911 |
-
if (p.stream) p.stream.getTracks().forEach(t => t.stop());
|
| 912 |
-
delete groupCallParticipants[uid];
|
| 913 |
-
}
|
| 914 |
-
}
|
| 915 |
-
|
| 916 |
-
function showGroupParticipants() {
|
| 917 |
-
const container = document.getElementById("group-remote-videos");
|
| 918 |
-
if (!container) return;
|
| 919 |
-
let html = "";
|
| 920 |
-
for (const [uid, p] of Object.entries(groupCallParticipants)) {
|
| 921 |
-
const name = p.display_name || "User";
|
| 922 |
-
html += `<div class="relative glass rounded-2xl overflow-hidden aspect-video flex items-center justify-center">
|
| 923 |
-
<video data-uid="${uid}" autoplay playsinline class="absolute inset-0 w-full h-full object-cover"></video>
|
| 924 |
-
<div class="absolute bottom-2 left-2 text-xs text-white/80 bg-black/40 px-2 py-0.5 rounded-lg">${escHtml(name)}</div>
|
| 925 |
-
</div>`;
|
| 926 |
-
}
|
| 927 |
-
if (!html) html = '<div class="col-span-full flex items-center justify-center text-gray-500 text-sm">Waiting for participants…</div>';
|
| 928 |
-
container.innerHTML = html;
|
| 929 |
-
// Attach streams to video elements
|
| 930 |
-
for (const [uid, p] of Object.entries(groupCallParticipants)) {
|
| 931 |
-
const vid = container.querySelector(`[data-uid="${uid}"]`);
|
| 932 |
-
if (vid && p.stream) vid.srcObject = p.stream;
|
| 933 |
-
}
|
| 934 |
-
}
|
| 935 |
-
|
| 936 |
-
function setupGroupCallModal() {
|
| 937 |
-
const lv = document.getElementById("group-local-video");
|
| 938 |
-
if (lv) lv.srcObject = groupCallLocalStream;
|
| 939 |
-
document.getElementById("group-video-content").classList.remove("hidden");
|
| 940 |
-
document.getElementById("call-video-content").classList.add("hidden");
|
| 941 |
-
document.getElementById("call-audio-content").classList.add("hidden");
|
| 942 |
-
document.getElementById("call-video-btn").classList.remove("hidden");
|
| 943 |
-
document.getElementById("call-status").textContent = "Connected";
|
| 944 |
-
}
|
| 945 |
-
|
| 946 |
-
function showGroupCallUI() {
|
| 947 |
-
showCallModal();
|
| 948 |
-
document.getElementById("call-status").textContent = "Connected";
|
| 949 |
-
document.getElementById("call-peer-name").textContent = currentChat?.name || "Group Call";
|
| 950 |
-
const lv = document.getElementById("group-local-video");
|
| 951 |
-
if (lv) lv.srcObject = groupCallLocalStream;
|
| 952 |
-
document.getElementById("group-video-content").classList.remove("hidden");
|
| 953 |
-
document.getElementById("call-video-content").classList.add("hidden");
|
| 954 |
-
document.getElementById("call-audio-content").classList.add("hidden");
|
| 955 |
-
document.getElementById("call-video-btn").classList.remove("hidden");
|
| 956 |
-
showGroupParticipants();
|
| 957 |
-
}
|
| 958 |
-
|
| 959 |
-
function endGroupCall() {
|
| 960 |
-
for (const uid of Object.keys(groupCallParticipants)) {
|
| 961 |
-
closeGroupPeerConnection(parseInt(uid));
|
| 962 |
-
}
|
| 963 |
-
groupCallParticipants = {};
|
| 964 |
-
if (groupCallLocalStream) {
|
| 965 |
-
groupCallLocalStream.getTracks().forEach(t => t.stop());
|
| 966 |
-
groupCallLocalStream = null;
|
| 967 |
-
}
|
| 968 |
-
if (activeGroupCallId && ws && ws.readyState === WebSocket.OPEN) {
|
| 969 |
-
ws.send(JSON.stringify({ type: groupCallRinging ? "group_call_end" : "group_call_leave", group_id: activeGroupCallId }));
|
| 970 |
-
}
|
| 971 |
-
activeGroupCallId = null;
|
| 972 |
-
activeGroupCallType = null;
|
| 973 |
-
groupCallRinging = false;
|
| 974 |
-
hideCallModal();
|
| 975 |
-
hideIncomingCall();
|
| 976 |
-
}
|
| 977 |
-
|
| 978 |
async function loadChats() {
|
| 979 |
[conversations, groups] = await Promise.all([
|
| 980 |
apiFetch("/chat/conversations"),
|
|
@@ -1080,7 +799,7 @@ async function openGroupChat(group) {
|
|
| 1080 |
document.getElementById("chat-avatar").style.background = full.avatar_color;
|
| 1081 |
document.getElementById("chat-name").textContent = full.name;
|
| 1082 |
document.getElementById("chat-sub").textContent = `${full.members.length} members`;
|
| 1083 |
-
document.getElementById("call-btn").classList.
|
| 1084 |
document.getElementById("call-dropdown").classList.add("hidden");
|
| 1085 |
document.getElementById("group-settings-btn").classList.remove("hidden");
|
| 1086 |
|
|
|
|
| 34 |
]
|
| 35 |
};
|
| 36 |
|
| 37 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
// ── Bootstrap ───────────────────────────────────────────────────────────────
|
| 40 |
|
|
|
|
| 128 |
// ── Auth ─────────────────────────────────────────────────────────────────────
|
| 129 |
|
| 130 |
function showTab(tab) {
|
| 131 |
+
document.getElementById("auth-tabs").classList.remove("hidden");
|
| 132 |
document.getElementById("form-login").classList.toggle("hidden", tab !== "login");
|
| 133 |
document.getElementById("form-register").classList.toggle("hidden", tab !== "register");
|
| 134 |
document.getElementById("form-google-setup").classList.add("hidden");
|
|
|
|
| 209 |
}
|
| 210 |
|
| 211 |
function showGoogleSetup() {
|
| 212 |
+
document.getElementById("auth-tabs").classList.add("hidden");
|
|
|
|
| 213 |
document.getElementById("form-login").classList.add("hidden");
|
| 214 |
document.getElementById("form-register").classList.add("hidden");
|
| 215 |
document.getElementById("form-google-setup").classList.remove("hidden");
|
|
|
|
| 346 |
} else if (msg.type === "read_receipt") {
|
| 347 |
handleReadReceipt(msg);
|
| 348 |
} else if (msg.type === "call_offer") {
|
| 349 |
+
handleCallOffer(msg);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 350 |
} else if (msg.type === "call_answer") {
|
| 351 |
+
handleCallAnswer(msg);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 352 |
} else if (msg.type === "ice_candidate") {
|
| 353 |
handleIceCandidate(msg);
|
| 354 |
} else if (msg.type === "call_end") {
|
| 355 |
handleCallEnd(msg);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 356 |
}
|
| 357 |
}
|
| 358 |
|
|
|
|
| 498 |
}
|
| 499 |
|
| 500 |
async function startCall(type) {
|
| 501 |
+
if (!currentChat || currentChat.type !== "dm" || !currentChat.other_user) return;
|
| 502 |
document.getElementById("call-dropdown").classList.add("hidden");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 503 |
callType = type;
|
| 504 |
callTarget = currentChat.other_user;
|
| 505 |
callState = "calling";
|
|
|
|
| 538 |
}
|
| 539 |
|
| 540 |
function handleCallOffer(msg) {
|
| 541 |
+
if (callState) {
|
|
|
|
| 542 |
ws.send(JSON.stringify({ type: "call_end", target_id: msg.from }));
|
| 543 |
return;
|
| 544 |
}
|
|
|
|
| 558 |
|
| 559 |
async function acceptCall() {
|
| 560 |
hideIncomingCall();
|
|
|
|
|
|
|
|
|
|
|
|
|
| 561 |
callState = "connected";
|
| 562 |
try {
|
| 563 |
const constraints = { audio: true, video: callType === "video" };
|
|
|
|
| 597 |
|
| 598 |
function rejectCall() {
|
| 599 |
hideIncomingCall();
|
|
|
|
|
|
|
|
|
|
|
|
|
| 600 |
if (ws && ws.readyState === WebSocket.OPEN && callTarget) {
|
| 601 |
ws.send(JSON.stringify({ type: "call_end", target_id: callTarget.id }));
|
| 602 |
}
|
|
|
|
| 627 |
}
|
| 628 |
cleanupCall();
|
| 629 |
}
|
|
|
|
|
|
|
|
|
|
| 630 |
}
|
| 631 |
|
| 632 |
function endCall() {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 633 |
if (ws && ws.readyState === WebSocket.OPEN && callTarget) {
|
| 634 |
ws.send(JSON.stringify({ type: "call_end", target_id: callTarget.id }));
|
| 635 |
}
|
|
|
|
| 657 |
if (lv) lv.srcObject = localStream;
|
| 658 |
const videoContent = document.getElementById("call-video-content");
|
| 659 |
const audioContent = document.getElementById("call-audio-content");
|
| 660 |
+
const videoBtn = document.getElementById("call-video-btn");
|
| 661 |
if (callType === "video") {
|
| 662 |
if (videoContent) videoContent.classList.remove("hidden");
|
| 663 |
if (audioContent) audioContent.classList.add("hidden");
|
| 664 |
+
if (videoBtn) videoBtn.classList.remove("hidden");
|
| 665 |
} else {
|
| 666 |
if (videoContent) videoContent.classList.add("hidden");
|
| 667 |
if (audioContent) audioContent.classList.remove("hidden");
|
| 668 |
+
if (videoBtn) videoBtn.classList.add("hidden");
|
| 669 |
const av = document.getElementById("call-audio-avatar");
|
| 670 |
if (av) {
|
| 671 |
av.textContent = callTarget.display_name[0].toUpperCase();
|
|
|
|
| 694 |
}
|
| 695 |
}
|
| 696 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 697 |
async function loadChats() {
|
| 698 |
[conversations, groups] = await Promise.all([
|
| 699 |
apiFetch("/chat/conversations"),
|
|
|
|
| 799 |
document.getElementById("chat-avatar").style.background = full.avatar_color;
|
| 800 |
document.getElementById("chat-name").textContent = full.name;
|
| 801 |
document.getElementById("chat-sub").textContent = `${full.members.length} members`;
|
| 802 |
+
document.getElementById("call-btn").classList.add("hidden");
|
| 803 |
document.getElementById("call-dropdown").classList.add("hidden");
|
| 804 |
document.getElementById("group-settings-btn").classList.remove("hidden");
|
| 805 |
|
src/static/index.html
CHANGED
|
@@ -48,7 +48,7 @@
|
|
| 48 |
|
| 49 |
<div class="glass rounded-2xl p-8 shadow-2xl">
|
| 50 |
<!-- Tabs -->
|
| 51 |
-
<div class="flex gap-1 mb-6 bg-white/5 p-1 rounded-xl">
|
| 52 |
<button onclick="showTab('login')" id="tab-login" class="flex-1 py-2 rounded-lg text-sm font-medium transition-all glass-btn-active text-white">Sign In</button>
|
| 53 |
<button onclick="showTab('register')" id="tab-register" class="flex-1 py-2 rounded-lg text-sm font-medium transition-all text-gray-400">Create Account</button>
|
| 54 |
</div>
|
|
@@ -360,15 +360,6 @@
|
|
| 360 |
<video id="local-video" autoplay playsinline muted class="w-full h-full object-cover"></video>
|
| 361 |
</div>
|
| 362 |
</div>
|
| 363 |
-
<!-- Group video content (multiple participants) -->
|
| 364 |
-
<div id="group-video-content" class="hidden flex-1 flex flex-col p-4">
|
| 365 |
-
<div id="group-remote-videos" class="flex-1 grid grid-cols-2 gap-3 auto-rows-[40%]"></div>
|
| 366 |
-
<div class="flex justify-center mt-3">
|
| 367 |
-
<div class="w-32 h-24 rounded-xl overflow-hidden border border-white/10">
|
| 368 |
-
<video id="group-local-video" autoplay playsinline muted class="w-full h-full object-cover"></video>
|
| 369 |
-
</div>
|
| 370 |
-
</div>
|
| 371 |
-
</div>
|
| 372 |
<!-- Audio content -->
|
| 373 |
<div id="call-audio-content" class="hidden flex-1 flex flex-col items-center justify-center gap-4">
|
| 374 |
<div id="call-audio-avatar" class="w-28 h-28 rounded-full flex items-center justify-center text-white text-5xl font-semibold"></div>
|
|
|
|
| 48 |
|
| 49 |
<div class="glass rounded-2xl p-8 shadow-2xl">
|
| 50 |
<!-- Tabs -->
|
| 51 |
+
<div id="auth-tabs" class="flex gap-1 mb-6 bg-white/5 p-1 rounded-xl">
|
| 52 |
<button onclick="showTab('login')" id="tab-login" class="flex-1 py-2 rounded-lg text-sm font-medium transition-all glass-btn-active text-white">Sign In</button>
|
| 53 |
<button onclick="showTab('register')" id="tab-register" class="flex-1 py-2 rounded-lg text-sm font-medium transition-all text-gray-400">Create Account</button>
|
| 54 |
</div>
|
|
|
|
| 360 |
<video id="local-video" autoplay playsinline muted class="w-full h-full object-cover"></video>
|
| 361 |
</div>
|
| 362 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
<!-- Audio content -->
|
| 364 |
<div id="call-audio-content" class="hidden flex-1 flex flex-col items-center justify-center gap-4">
|
| 365 |
<div id="call-audio-avatar" class="w-28 h-28 rounded-full flex items-center justify-center text-white text-5xl font-semibold"></div>
|
src/ws_manager.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
from fastapi import WebSocket
|
| 2 |
from sqlalchemy.orm import Session
|
| 3 |
-
from typing import Dict, Optional
|
| 4 |
import json
|
| 5 |
from datetime import datetime
|
| 6 |
|
|
@@ -198,93 +198,4 @@ class ConnectionManager:
|
|
| 198 |
"read_by": sender.id
|
| 199 |
})
|
| 200 |
|
| 201 |
-
# ── Group Calls ────────────────────────────────────────────────────────────
|
| 202 |
-
|
| 203 |
-
active_group_calls: Dict[int, dict] = {}
|
| 204 |
-
|
| 205 |
-
async def handle_group_call_start(self, msg: dict, sender: User, db: Session):
|
| 206 |
-
group_id = msg.get("group_id")
|
| 207 |
-
call_type = msg.get("call_type", "audio")
|
| 208 |
-
member = db.query(GroupMember).filter(
|
| 209 |
-
GroupMember.group_id == group_id, GroupMember.user_id == sender.id
|
| 210 |
-
).first()
|
| 211 |
-
if not member or group_id in self.active_group_calls:
|
| 212 |
-
return
|
| 213 |
-
|
| 214 |
-
members = db.query(GroupMember).filter(GroupMember.group_id == group_id).all()
|
| 215 |
-
member_users = []
|
| 216 |
-
for m in members:
|
| 217 |
-
u = db.query(User).filter(User.id == m.user_id).first()
|
| 218 |
-
if u:
|
| 219 |
-
member_users.append({"id": u.id, "display_name": u.display_name, "avatar_color": u.avatar_color})
|
| 220 |
-
|
| 221 |
-
self.active_group_calls[group_id] = {
|
| 222 |
-
"call_type": call_type,
|
| 223 |
-
"participants": [sender.id],
|
| 224 |
-
"member_ids": [m.user_id for m in members]
|
| 225 |
-
}
|
| 226 |
-
|
| 227 |
-
await self.send_to(sender.id, {
|
| 228 |
-
"type": "group_call_started", "group_id": group_id,
|
| 229 |
-
"call_type": call_type, "participants": member_users
|
| 230 |
-
})
|
| 231 |
-
|
| 232 |
-
for m in members:
|
| 233 |
-
if m.user_id != sender.id and m.user_id in self.active:
|
| 234 |
-
await self.send_to(m.user_id, {
|
| 235 |
-
"type": "group_call_ringing", "group_id": group_id,
|
| 236 |
-
"call_type": call_type, "initiator_id": sender.id,
|
| 237 |
-
"initiator_name": sender.display_name,
|
| 238 |
-
"initiator_avatar": sender.avatar_color
|
| 239 |
-
})
|
| 240 |
-
|
| 241 |
-
async def handle_group_call_accept(self, msg: dict, sender: User, db: Session):
|
| 242 |
-
group_id = msg.get("group_id")
|
| 243 |
-
call = self.active_group_calls.get(group_id)
|
| 244 |
-
if not call or sender.id in call["participants"]:
|
| 245 |
-
return
|
| 246 |
-
call["participants"].append(sender.id)
|
| 247 |
-
|
| 248 |
-
for pid in call["participants"]:
|
| 249 |
-
if pid != sender.id:
|
| 250 |
-
await self.send_to(pid, {
|
| 251 |
-
"type": "group_participant_joined", "group_id": group_id,
|
| 252 |
-
"user_id": sender.id, "display_name": sender.display_name,
|
| 253 |
-
"avatar_color": sender.avatar_color
|
| 254 |
-
})
|
| 255 |
-
|
| 256 |
-
participant_list = []
|
| 257 |
-
for pid in call["participants"]:
|
| 258 |
-
if pid != sender.id:
|
| 259 |
-
u = db.query(User).filter(User.id == pid).first()
|
| 260 |
-
if u:
|
| 261 |
-
participant_list.append({"id": u.id, "display_name": u.display_name, "avatar_color": u.avatar_color})
|
| 262 |
-
|
| 263 |
-
await self.send_to(sender.id, {
|
| 264 |
-
"type": "group_call_joined", "group_id": group_id, "participants": participant_list
|
| 265 |
-
})
|
| 266 |
-
|
| 267 |
-
async def handle_group_call_leave(self, msg: dict, sender: User):
|
| 268 |
-
group_id = msg.get("group_id")
|
| 269 |
-
call = self.active_group_calls.get(group_id)
|
| 270 |
-
if not call:
|
| 271 |
-
return
|
| 272 |
-
call["participants"] = [p for p in call["participants"] if p != sender.id]
|
| 273 |
-
for pid in call["participants"]:
|
| 274 |
-
await self.send_to(pid, {
|
| 275 |
-
"type": "group_participant_left", "group_id": group_id, "user_id": sender.id
|
| 276 |
-
})
|
| 277 |
-
if not call["participants"]:
|
| 278 |
-
del self.active_group_calls[group_id]
|
| 279 |
-
|
| 280 |
-
async def handle_group_call_end(self, msg: dict, sender: User):
|
| 281 |
-
group_id = msg.get("group_id")
|
| 282 |
-
call = self.active_group_calls.pop(group_id, None)
|
| 283 |
-
if not call:
|
| 284 |
-
return
|
| 285 |
-
for pid in call["participants"]:
|
| 286 |
-
if pid != sender.id:
|
| 287 |
-
await self.send_to(pid, {"type": "group_call_ended", "group_id": group_id})
|
| 288 |
-
|
| 289 |
-
|
| 290 |
manager = ConnectionManager()
|
|
|
|
| 1 |
from fastapi import WebSocket
|
| 2 |
from sqlalchemy.orm import Session
|
| 3 |
+
from typing import Dict, Optional
|
| 4 |
import json
|
| 5 |
from datetime import datetime
|
| 6 |
|
|
|
|
| 198 |
"read_by": sender.id
|
| 199 |
})
|
| 200 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
manager = ConnectionManager()
|