Spaces:
Sleeping
Sleeping
ausername-12345 commited on
Commit Β·
fb8eda3
1
Parent(s): eeb3363
video-off shows avatar, show 'You' in GC, allow self-removal
Browse files- When camera is turned off during video call, local video shows
avatar overlay; signal sent to remote peer who also shows avatar
overlay instead of frozen/black frame
- Group settings shows '(You)' next to your own name
- 'Remove' button on others, 'Leave' button on yourself
- Leaving the group navigates back to empty state
- Added video_toggle WebSocket message (backend handler + dispatch)
- src/main.py +2 -0
- src/static/app.js +57 -8
- src/static/index.html +9 -1
- src/ws_manager.py +10 -0
src/main.py
CHANGED
|
@@ -65,6 +65,8 @@ 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 |
|
| 69 |
except WebSocketDisconnect:
|
| 70 |
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 |
+
elif msg_type == "video_toggle":
|
| 69 |
+
await manager.handle_video_toggle(msg, user)
|
| 70 |
|
| 71 |
except WebSocketDisconnect:
|
| 72 |
manager.disconnect(user.id)
|
src/static/app.js
CHANGED
|
@@ -353,6 +353,8 @@ async function handleWsMessage(evt) {
|
|
| 353 |
handleIceCandidate(msg);
|
| 354 |
} else if (msg.type === "call_end") {
|
| 355 |
handleCallEnd(msg);
|
|
|
|
|
|
|
| 356 |
}
|
| 357 |
}
|
| 358 |
|
|
@@ -650,11 +652,25 @@ function cleanupCall() {
|
|
| 650 |
const rv = document.getElementById("remote-video");
|
| 651 |
if (lv) lv.srcObject = null;
|
| 652 |
if (rv) rv.srcObject = null;
|
|
|
|
|
|
|
| 653 |
}
|
| 654 |
|
| 655 |
function setupCallMedia() {
|
| 656 |
const lv = document.getElementById("local-video");
|
| 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");
|
|
@@ -689,11 +705,32 @@ function toggleVideo() {
|
|
| 689 |
const t = localStream.getVideoTracks()[0];
|
| 690 |
if (t) {
|
| 691 |
t.enabled = !t.enabled;
|
| 692 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 693 |
}
|
| 694 |
}
|
| 695 |
}
|
| 696 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 697 |
async function loadChats() {
|
| 698 |
[conversations, groups] = await Promise.all([
|
| 699 |
apiFetch("/chat/conversations"),
|
|
@@ -1111,16 +1148,17 @@ async function showGroupSettings() {
|
|
| 1111 |
currentGroupData = full;
|
| 1112 |
document.getElementById("gs-name").textContent = full.name;
|
| 1113 |
|
| 1114 |
-
document.getElementById("gs-members").innerHTML = full.members.map(m =>
|
| 1115 |
-
|
|
|
|
| 1116 |
<div class="w-8 h-8 rounded-full flex items-center justify-center text-white text-sm font-semibold" style="background:${m.user.avatar_color}">${m.user.display_name[0].toUpperCase()}</div>
|
| 1117 |
<div class="flex-1 min-w-0">
|
| 1118 |
-
<p class="text-sm font-medium">${escHtml(m.user.display_name)}</p>
|
| 1119 |
<p class="text-xs text-gray-500">@${m.user.username}</p>
|
| 1120 |
</div>
|
| 1121 |
-
|
| 1122 |
-
</div>
|
| 1123 |
-
|
| 1124 |
|
| 1125 |
document.getElementById("gs-add-section").style.display = "block";
|
| 1126 |
document.getElementById("gs-search-input").value = "";
|
|
@@ -1149,7 +1187,18 @@ async function addToGroup(userId) {
|
|
| 1149 |
|
| 1150 |
async function removeFromGroup(userId) {
|
| 1151 |
await apiFetch(`/chat/groups/${currentGroupData.id}/members/${userId}`, token, "DELETE");
|
| 1152 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1153 |
}
|
| 1154 |
|
| 1155 |
// ββ Modals βββββββββββββββββββββββββββββββββββββββββββοΏ½οΏ½ββββββββββββββββββββββββ
|
|
|
|
| 353 |
handleIceCandidate(msg);
|
| 354 |
} else if (msg.type === "call_end") {
|
| 355 |
handleCallEnd(msg);
|
| 356 |
+
} else if (msg.type === "video_toggle") {
|
| 357 |
+
handleVideoToggle(msg);
|
| 358 |
}
|
| 359 |
}
|
| 360 |
|
|
|
|
| 652 |
const rv = document.getElementById("remote-video");
|
| 653 |
if (lv) lv.srcObject = null;
|
| 654 |
if (rv) rv.srcObject = null;
|
| 655 |
+
document.getElementById("local-video-avatar").classList.add("hidden");
|
| 656 |
+
document.getElementById("remote-video-avatar").classList.add("hidden");
|
| 657 |
}
|
| 658 |
|
| 659 |
function setupCallMedia() {
|
| 660 |
const lv = document.getElementById("local-video");
|
| 661 |
if (lv) lv.srcObject = localStream;
|
| 662 |
+
// Init local avatar
|
| 663 |
+
const localInner = document.getElementById("local-video-avatar-inner");
|
| 664 |
+
if (localInner && callTarget) {
|
| 665 |
+
localInner.textContent = callTarget.display_name[0].toUpperCase();
|
| 666 |
+
localInner.style.background = callTarget.avatar_color || "#6366f1";
|
| 667 |
+
}
|
| 668 |
+
// Init remote avatar
|
| 669 |
+
const remoteInner = document.getElementById("remote-video-avatar-inner");
|
| 670 |
+
if (remoteInner && callTarget) {
|
| 671 |
+
remoteInner.textContent = callTarget.display_name[0].toUpperCase();
|
| 672 |
+
remoteInner.style.background = callTarget.avatar_color || "#6366f1";
|
| 673 |
+
}
|
| 674 |
const videoContent = document.getElementById("call-video-content");
|
| 675 |
const audioContent = document.getElementById("call-audio-content");
|
| 676 |
const videoBtn = document.getElementById("call-video-btn");
|
|
|
|
| 705 |
const t = localStream.getVideoTracks()[0];
|
| 706 |
if (t) {
|
| 707 |
t.enabled = !t.enabled;
|
| 708 |
+
const isOn = t.enabled;
|
| 709 |
+
document.getElementById("call-video-btn").textContent = isOn ? "πΉ Video" : "π« Video Off";
|
| 710 |
+
const overlay = document.getElementById("local-video-avatar");
|
| 711 |
+
if (overlay) {
|
| 712 |
+
overlay.classList.toggle("hidden", isOn);
|
| 713 |
+
}
|
| 714 |
+
if (ws && ws.readyState === WebSocket.OPEN && callTarget) {
|
| 715 |
+
ws.send(JSON.stringify({ type: "video_toggle", target_id: callTarget.id, enabled: isOn }));
|
| 716 |
+
}
|
| 717 |
}
|
| 718 |
}
|
| 719 |
}
|
| 720 |
|
| 721 |
+
function handleVideoToggle(msg) {
|
| 722 |
+
const overlay = document.getElementById("remote-video-avatar");
|
| 723 |
+
const inner = document.getElementById("remote-video-avatar-inner");
|
| 724 |
+
if (!overlay || !inner) return;
|
| 725 |
+
if (msg.enabled) {
|
| 726 |
+
overlay.classList.add("hidden");
|
| 727 |
+
} else {
|
| 728 |
+
inner.textContent = (msg.display_name || "?")[0].toUpperCase();
|
| 729 |
+
inner.style.background = msg.avatar_color || "#6366f1";
|
| 730 |
+
overlay.classList.remove("hidden");
|
| 731 |
+
}
|
| 732 |
+
}
|
| 733 |
+
|
| 734 |
async function loadChats() {
|
| 735 |
[conversations, groups] = await Promise.all([
|
| 736 |
apiFetch("/chat/conversations"),
|
|
|
|
| 1148 |
currentGroupData = full;
|
| 1149 |
document.getElementById("gs-name").textContent = full.name;
|
| 1150 |
|
| 1151 |
+
document.getElementById("gs-members").innerHTML = full.members.map(m => {
|
| 1152 |
+
const isMe = m.user.id === me.id;
|
| 1153 |
+
return `<div class="flex items-center gap-3 px-2 py-2 rounded-lg">
|
| 1154 |
<div class="w-8 h-8 rounded-full flex items-center justify-center text-white text-sm font-semibold" style="background:${m.user.avatar_color}">${m.user.display_name[0].toUpperCase()}</div>
|
| 1155 |
<div class="flex-1 min-w-0">
|
| 1156 |
+
<p class="text-sm font-medium">${escHtml(m.user.display_name)}${isMe ? ' <span class="text-xs text-gray-500">(You)</span>' : ''}</p>
|
| 1157 |
<p class="text-xs text-gray-500">@${m.user.username}</p>
|
| 1158 |
</div>
|
| 1159 |
+
<button onclick="removeFromGroup(${m.user.id})" class="text-red-400 hover:text-red-300 text-xs ml-2">${isMe ? 'Leave' : 'Remove'}</button>
|
| 1160 |
+
</div>`;
|
| 1161 |
+
}).join("");
|
| 1162 |
|
| 1163 |
document.getElementById("gs-add-section").style.display = "block";
|
| 1164 |
document.getElementById("gs-search-input").value = "";
|
|
|
|
| 1187 |
|
| 1188 |
async function removeFromGroup(userId) {
|
| 1189 |
await apiFetch(`/chat/groups/${currentGroupData.id}/members/${userId}`, token, "DELETE");
|
| 1190 |
+
if (userId === me.id) {
|
| 1191 |
+
closeModal("modal-group-settings");
|
| 1192 |
+
currentChat = null;
|
| 1193 |
+
currentGroupData = null;
|
| 1194 |
+
document.getElementById("chat-window").classList.add("hidden");
|
| 1195 |
+
document.getElementById("chat-window").classList.remove("flex");
|
| 1196 |
+
document.getElementById("empty-state").classList.remove("hidden");
|
| 1197 |
+
document.getElementById("call-btn").classList.add("hidden");
|
| 1198 |
+
await loadChats();
|
| 1199 |
+
} else {
|
| 1200 |
+
await showGroupSettings();
|
| 1201 |
+
}
|
| 1202 |
}
|
| 1203 |
|
| 1204 |
// ββ Modals βββββββββββββββββββββββββββββββββββββββββββοΏ½οΏ½ββββββββββββββββββββββββ
|
src/static/index.html
CHANGED
|
@@ -355,9 +355,17 @@
|
|
| 355 |
</div>
|
| 356 |
<!-- Video content (1-to-1) -->
|
| 357 |
<div id="call-video-content" class="hidden flex-1 relative flex items-center justify-center p-4">
|
| 358 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 359 |
<div class="absolute top-6 right-6 w-48 h-36 rounded-xl overflow-hidden shadow-2xl border-2 border-white/20">
|
| 360 |
<video id="local-video" autoplay playsinline muted class="w-full h-full object-cover"></video>
|
|
|
|
|
|
|
|
|
|
| 361 |
</div>
|
| 362 |
</div>
|
| 363 |
<!-- Audio content -->
|
|
|
|
| 355 |
</div>
|
| 356 |
<!-- Video content (1-to-1) -->
|
| 357 |
<div id="call-video-content" class="hidden flex-1 relative flex items-center justify-center p-4">
|
| 358 |
+
<div class="relative max-w-full max-h-full">
|
| 359 |
+
<video id="remote-video" autoplay playsinline class="max-w-full max-h-full rounded-2xl object-contain bg-black/40" style="max-height:70vh"></video>
|
| 360 |
+
<div id="remote-video-avatar" class="hidden absolute inset-0 flex items-center justify-center bg-black/60 rounded-2xl">
|
| 361 |
+
<div id="remote-video-avatar-inner" class="w-24 h-24 rounded-full flex items-center justify-center text-white text-4xl font-semibold"></div>
|
| 362 |
+
</div>
|
| 363 |
+
</div>
|
| 364 |
<div class="absolute top-6 right-6 w-48 h-36 rounded-xl overflow-hidden shadow-2xl border-2 border-white/20">
|
| 365 |
<video id="local-video" autoplay playsinline muted class="w-full h-full object-cover"></video>
|
| 366 |
+
<div id="local-video-avatar" class="hidden absolute inset-0 flex items-center justify-center bg-black/70">
|
| 367 |
+
<div id="local-video-avatar-inner" class="w-12 h-12 rounded-full flex items-center justify-center text-white text-xl font-semibold"></div>
|
| 368 |
+
</div>
|
| 369 |
</div>
|
| 370 |
</div>
|
| 371 |
<!-- Audio content -->
|
src/ws_manager.py
CHANGED
|
@@ -178,6 +178,16 @@ class ConnectionManager:
|
|
| 178 |
"from": sender.id
|
| 179 |
})
|
| 180 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
async def handle_read(self, msg: dict, sender: User, db: Session):
|
| 182 |
conversation_id = msg.get("conversation_id")
|
| 183 |
if conversation_id:
|
|
|
|
| 178 |
"from": sender.id
|
| 179 |
})
|
| 180 |
|
| 181 |
+
async def handle_video_toggle(self, msg: dict, sender: User):
|
| 182 |
+
target_id = msg.get("target_id")
|
| 183 |
+
await self.send_to(target_id, {
|
| 184 |
+
"type": "video_toggle",
|
| 185 |
+
"from": sender.id,
|
| 186 |
+
"enabled": msg.get("enabled", True),
|
| 187 |
+
"display_name": sender.display_name,
|
| 188 |
+
"avatar_color": sender.avatar_color
|
| 189 |
+
})
|
| 190 |
+
|
| 191 |
async def handle_read(self, msg: dict, sender: User, db: Session):
|
| 192 |
conversation_id = msg.get("conversation_id")
|
| 193 |
if conversation_id:
|