feat(ui): move users to channel list with hover volume popup, dedicate main stage to screenshares, add ws ping
This commit is contained in:
1 parent
d3fd800ea7
commit
a2be7cf6a5
4 files changed
+186
-48
No files matched your search
+91
-27
@@ -27,12 +27,13 @@ const S = {
|
||||
iceServers: null, // resolved on join
|
||||
sharing: false,
|
||||
connecting: false,
|
||||
pingTimer: null,
|
||||
};
|
||||
window.__dbg = S; // POC debug handle
|
||||
|
||||
const $ = (sel) => document.querySelector(sel);
|
||||
const els = {
|
||||
stage: $('#stage'), screens: $('#screens'), tiles: $('#tiles'),
|
||||
stage: $('#stage'), screens: $('#screens'),
|
||||
spatial: $('#spatial'), status: $('#voice-status'), nick: $('#nick'),
|
||||
meAvatar: $('#me-avatar'), mic: $('#btn-mic'), deafen: $('#btn-deafen'),
|
||||
};
|
||||
@@ -118,6 +119,7 @@ function leaveVoice(reason) {
|
||||
if (S.sharing) stopScreenShare(true);
|
||||
for (const peer of S.peers.values()) destroyPeer(peer);
|
||||
S.peers.clear();
|
||||
if (S.pingTimer) { clearInterval(S.pingTimer); S.pingTimer = null; }
|
||||
if (S.ws) { try { S.ws.close(); } catch {} S.ws = null; }
|
||||
if (S.micStream) { S.micStream.getTracks().forEach((t) => t.stop()); S.micStream = null; }
|
||||
els.mic.disabled = false;
|
||||
@@ -151,12 +153,19 @@ function openSignaling(room) {
|
||||
try { handleSignal(msg); } catch (err) { console.error('signal handler error', msg.type, err); }
|
||||
};
|
||||
ws.onclose = () => {
|
||||
if (S.pingTimer) { clearInterval(S.pingTimer); S.pingTimer = null; }
|
||||
if (S.ws === ws && S.room) {
|
||||
toast('Signaling connection lost', 'error');
|
||||
leaveVoice('disconnected');
|
||||
}
|
||||
};
|
||||
ws.onopen = () => resolve();
|
||||
ws.onopen = () => {
|
||||
if (S.pingTimer) clearInterval(S.pingTimer);
|
||||
S.pingTimer = setInterval(() => {
|
||||
send({ type: 'ping' });
|
||||
}, 10000);
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -524,12 +533,11 @@ function renderAll() {
|
||||
}
|
||||
|
||||
function renderStageEmpty() {
|
||||
const anyone = S.peers.size > 0 || S.sharing || S.room;
|
||||
els.stage.classList.toggle('empty', !anyone);
|
||||
const hasScreens = activeShares().length > 0;
|
||||
els.stage.classList.toggle('empty', !hasScreens);
|
||||
}
|
||||
|
||||
function renderChannelCounts() {
|
||||
// POC: server does not expose cross-room rosters; count is local-room only
|
||||
const counts = {};
|
||||
for (const p of S.peers.values()) counts[S.room] = (counts[S.room] || 0) + 1;
|
||||
document.querySelectorAll('[data-room-count]').forEach((el) => {
|
||||
@@ -539,35 +547,91 @@ function renderChannelCounts() {
|
||||
}
|
||||
|
||||
function renderTiles() {
|
||||
els.tiles.innerHTML = '';
|
||||
// Clear any existing user rosters in voice channels
|
||||
document.querySelectorAll('.channel-users').forEach((el) => { el.innerHTML = ''; });
|
||||
|
||||
if (!S.room) {
|
||||
renderStageEmpty();
|
||||
return;
|
||||
}
|
||||
|
||||
const activeChannelEl = document.querySelector(`.channel.voice[data-room="${S.room}"]`);
|
||||
if (!activeChannelEl) {
|
||||
renderStageEmpty();
|
||||
return;
|
||||
}
|
||||
|
||||
let roster = activeChannelEl.nextElementSibling;
|
||||
if (!roster || !roster.classList.contains('channel-users')) {
|
||||
roster = document.createElement('div');
|
||||
roster.className = 'channel-users';
|
||||
activeChannelEl.after(roster);
|
||||
}
|
||||
|
||||
// Render "Me" first
|
||||
const meItem = document.createElement('div');
|
||||
meItem.className = 'channel-user-item' + (els.meAvatar.classList.contains('speaking') ? ' speaking' : '');
|
||||
const meAv = document.createElement('div');
|
||||
meAv.className = 'avatar user-line-avatar';
|
||||
setAvatar(meAv, S.name, S.myId || 'me');
|
||||
const meName = document.createElement('span');
|
||||
meName.className = 'user-line-name';
|
||||
meName.textContent = `${S.name} (you)`;
|
||||
|
||||
const meBadges = document.createElement('span');
|
||||
meBadges.className = 'user-line-badges';
|
||||
if (S.muted) meBadges.innerHTML += '<span class="badge-mute" title="Muted">🎤️‍❌</span>';
|
||||
if (S.deafened) meBadges.innerHTML += '<span class="badge-deafen" title="Deafened">👂❌</span>';
|
||||
if (S.sharing) meBadges.innerHTML += '<span class="badge-share" title="Sharing screen">🖥️</span>';
|
||||
|
||||
meItem.append(meAv, meName, meBadges);
|
||||
roster.appendChild(meItem);
|
||||
|
||||
// Render Peers
|
||||
for (const peer of S.peers.values()) {
|
||||
const tile = document.createElement('div');
|
||||
tile.className = 'tile' + (peer.speaking ? ' speaking' : '');
|
||||
const item = document.createElement('div');
|
||||
item.className = 'channel-user-item' + (peer.speaking ? ' speaking' : '');
|
||||
|
||||
const av = document.createElement('div');
|
||||
av.className = 'avatar';
|
||||
av.className = 'avatar user-line-avatar';
|
||||
setAvatar(av, peer.name, peer.id);
|
||||
const name = document.createElement('div');
|
||||
name.className = 'name';
|
||||
|
||||
const name = document.createElement('span');
|
||||
name.className = 'user-line-name';
|
||||
name.textContent = peer.name;
|
||||
const sub = document.createElement('div');
|
||||
sub.className = 'sub';
|
||||
const bits = [];
|
||||
if (!peer.pc) bits.push('<span class="connecting">signaling…</span>');
|
||||
else if (peer.pc.connectionState !== 'connected') bits.push(`<span class="connecting">${peer.pc.connectionState}…</span>`);
|
||||
else if (!peer.micStream && !peer.state.muted) bits.push('<span class="connecting">no audio yet</span>');
|
||||
if (peer.state.muted) bits.push('<span class="mute-tag">muted</span>');
|
||||
if (peer.state.sharing) bits.push('<span class="share-tag">sharing</span>');
|
||||
sub.innerHTML = bits.join(' ') || 'connected';
|
||||
const vol = document.createElement('input');
|
||||
vol.type = 'range'; vol.min = 0; vol.max = 2; vol.step = 0.05; vol.value = peer.vol;
|
||||
vol.title = 'Volume';
|
||||
vol.oninput = () => {
|
||||
peer.vol = Number(vol.value);
|
||||
|
||||
const badges = document.createElement('span');
|
||||
badges.className = 'user-line-badges';
|
||||
if (!peer.pc || peer.pc.connectionState !== 'connected') {
|
||||
badges.innerHTML += '<span class="badge-conn" title="Connecting...">…</span>';
|
||||
}
|
||||
if (peer.state.muted) badges.innerHTML += '<span class="badge-mute" title="Muted">🎤️‍❌</span>';
|
||||
if (peer.state.sharing) badges.innerHTML += '<span class="badge-share" title="Sharing screen">🖥️</span>';
|
||||
|
||||
// Volume hover popup
|
||||
const volPop = document.createElement('div');
|
||||
volPop.className = 'volume-popup';
|
||||
const volLabel = document.createElement('div');
|
||||
volLabel.className = 'vol-title';
|
||||
volLabel.textContent = `User Volume: ${Math.round(peer.vol * 100)}%`;
|
||||
const volInput = document.createElement('input');
|
||||
volInput.type = 'range';
|
||||
volInput.min = 0;
|
||||
volInput.max = 2;
|
||||
volInput.step = 0.05;
|
||||
volInput.value = peer.vol;
|
||||
volInput.oninput = (e) => {
|
||||
e.stopPropagation();
|
||||
peer.vol = Number(volInput.value);
|
||||
volLabel.textContent = `User Volume: ${Math.round(peer.vol * 100)}%`;
|
||||
if (peer.gain && !S.deafened) peer.gain.gain.value = peer.vol;
|
||||
};
|
||||
tile.append(av, name, sub, vol);
|
||||
els.tiles.appendChild(tile);
|
||||
volPop.append(volLabel, volInput);
|
||||
|
||||
item.append(av, name, badges, volPop);
|
||||
roster.appendChild(item);
|
||||
}
|
||||
|
||||
renderStageEmpty();
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user