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)

Files changed (4) hide show
  1. src/main.py +2 -0
  2. src/static/app.js +57 -8
  3. src/static/index.html +9 -1
  4. 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
- document.getElementById("call-video-btn").textContent = t.enabled ? "πŸ“Ή Video" : "🚫 Video Off";
 
 
 
 
 
 
 
 
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
- <div class="flex items-center gap-3 px-2 py-2 rounded-lg">
 
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
- ${m.user.id !== me.id ? `<button onclick="removeFromGroup(${m.user.id})" class="text-red-400 hover:text-red-300 text-xs ml-2">Remove</button>` : ""}
1122
- </div>
1123
- `).join("");
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
- await showGroupSettings();
 
 
 
 
 
 
 
 
 
 
 
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
- <video id="remote-video" autoplay playsinline class="max-w-full max-h-full rounded-2xl object-contain bg-black/40"></video>
 
 
 
 
 
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: