feat(chat): rich shared-media view, status selector, drag-drop upload + fixes

Chat / shared media:
- Media/Docs/Links: clean underline tabs (green active), audio & video now
  classified as Media and rendered as tiles (download + headphone/play +
  duration) instead of broken-image glyphs; image thumbnails -> lightbox
- Drag-and-drop a file/video/image onto a conversation to send it
- Fix: removed #chatPanel{position:relative} override that collapsed the
  conversation pane (messages spilled into a clipped right-edge strip)
- "Media, links & docs" row cleaned up (no folder/placeholder icon); media
  popup keeps the back arrow, drops the redundant close button

Presence / status:
- Single current-status row with an arrow that expands Available/Away/On leave
- On leave = circle with minus, In a call = solid red indicators
- Fix: selected-status tick now follows the chosen option

Icons: added headphones + play; bumped icons.js cache-bust to v4

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 17:01:15 +05:30
parent e9e5c7f406
commit 06f0b08a18
12 changed files with 589 additions and 116 deletions
+4 -3
View File
@@ -65,12 +65,13 @@ function handle(ws, m, req) {
if (hostUserId === undefined) { try { const s = R.scheduledMeetings.byCode(room); if (s) { hostUserId = s.created_by; roomHost.set(room, hostUserId); } } catch (_) {} }
const ju = currentUser(req);
ws._meetingUserId = ju ? ju.id : null; // for per-user transcript ownership
const avatar = (ju && ju.avatar_url) ? ju.avatar_url : null; // for participant-tile profile pics
const isHost = !!(ju && hostUserId && ju.id === hostUserId);
// Tell the newcomer who's already here (they initiate offers to existing peers)…
ws.send(JSON.stringify({ type: 'meeting-joined', room, peerId, isHost, peers: [...peers.entries()].map(([id, p]) => ({ peerId: id, name: p.name })) }));
ws.send(JSON.stringify({ type: 'meeting-joined', room, peerId, isHost, peers: [...peers.entries()].map(([id, p]) => ({ peerId: id, name: p.name, avatar: p.avatar || null, uid: p.uid || null })) }));
// …and tell existing peers a newcomer arrived.
for (const [, p] of peers) { if (p.ws.readyState === 1) p.ws.send(JSON.stringify({ type: 'meeting-peer-joined', peerId, name })); }
peers.set(peerId, { ws, name });
for (const [, p] of peers) { if (p.ws.readyState === 1) p.ws.send(JSON.stringify({ type: 'meeting-peer-joined', peerId, name, avatar, uid: ws._meetingUserId || null })); }
peers.set(peerId, { ws, name, avatar, uid: ws._meetingUserId || null });
const tsubs = transcriptSubs.get(room); if (tsubs && tsubs.size > 0) ws.send(JSON.stringify({ type: 'meeting-transcribe-state', active: true })); // catch up: already transcribing
break;
}