diff --git a/public/app.js b/public/app.js
index b1e66f8..bc37dd3 100644
--- a/public/app.js
+++ b/public/app.js
@@ -34,6 +34,7 @@ window.__dbg = S; // POC debug handle
const $ = (sel) => document.querySelector(sel);
const els = {
+ voiceView: $('#voice-view'),
stage: $('#stage'), screens: $('#screens'),
spatial: $('#spatial'), status: $('#voice-status'), statusDot: $('#status-dot'),
statusRow: $('#status-row'), nick: $('#nick'),
@@ -50,6 +51,13 @@ const shareButtons = [...document.querySelectorAll('#btn-share, .mirror-share')]
const statusTextEls = [...document.querySelectorAll('#voice-status, #mobile-status-text')];
const statusDotEls = [...document.querySelectorAll('#status-dot, #mobile-status-dot')];
+function mIcon(name, extraClass = '') {
+ const span = document.createElement('span');
+ span.className = `material-symbols-outlined${extraClass ? ` ${extraClass}` : ''}`;
+ span.textContent = name;
+ return span;
+}
+
// ---------------------------------------------------------------------------
// helpers
// ---------------------------------------------------------------------------
@@ -784,9 +792,27 @@ function renderTiles() {
const meBadges = document.createElement('span');
meBadges.className = 'user-line-badges';
- if (S.muted) meBadges.innerHTML += '🎤️❌';
- if (S.deafened) meBadges.innerHTML += '👂❌';
- if (S.sharing) meBadges.innerHTML += '🖥️';
+ if (S.muted) {
+ const b = document.createElement('span');
+ b.className = 'badge-mute';
+ b.title = 'Muted';
+ b.appendChild(mIcon('mic_off'));
+ meBadges.appendChild(b);
+ }
+ if (S.deafened) {
+ const b = document.createElement('span');
+ b.className = 'badge-deafen';
+ b.title = 'Deafened';
+ b.appendChild(mIcon('headset_off'));
+ meBadges.appendChild(b);
+ }
+ if (S.sharing) {
+ const b = document.createElement('span');
+ b.className = 'badge-share';
+ b.title = 'Sharing screen';
+ b.appendChild(mIcon('present_to_all'));
+ meBadges.appendChild(b);
+ }
meItem.append(meAv, meName, meBadges);
roster.appendChild(meItem);
@@ -808,8 +834,20 @@ function renderTiles() {
if (!peer.pc || peer.pc.connectionState !== 'connected') {
badges.innerHTML += '…';
}
- if (peer.state.muted) badges.innerHTML += '🎤️❌';
- if (peer.state.sharing) badges.innerHTML += '🖥️';
+ if (peer.state.muted) {
+ const b = document.createElement('span');
+ b.className = 'badge-mute';
+ b.title = 'Muted';
+ b.appendChild(mIcon('mic_off'));
+ badges.appendChild(b);
+ }
+ if (peer.state.sharing) {
+ const b = document.createElement('span');
+ b.className = 'badge-share';
+ b.title = 'Sharing screen';
+ b.appendChild(mIcon('present_to_all'));
+ badges.appendChild(b);
+ }
// Volume hover popup
const volPop = document.createElement('div');
@@ -858,8 +896,20 @@ function renderTiles() {
const badges = document.createElement('span');
badges.className = 'user-line-badges';
- if (user.state?.muted) badges.innerHTML += '🎤️❌';
- if (user.state?.sharing) badges.innerHTML += '🖥️';
+ if (user.state?.muted) {
+ const b = document.createElement('span');
+ b.className = 'badge-mute';
+ b.title = 'Muted';
+ b.appendChild(mIcon('mic_off'));
+ badges.appendChild(b);
+ }
+ if (user.state?.sharing) {
+ const b = document.createElement('span');
+ b.className = 'badge-share';
+ b.title = 'Sharing screen';
+ b.appendChild(mIcon('present_to_all'));
+ badges.appendChild(b);
+ }
item.append(av, name, badges);
roster.appendChild(item);
@@ -878,7 +928,9 @@ function activeShares() {
const shares = [];
if (S.sharing && S.screenStream) shares.push({ key: 'me', name: `${S.name} (you)`, stream: S.screenStream });
for (const peer of S.peers.values()) {
- if (peer.screenStream) shares.push({ key: peer.id, name: peer.name, stream: peer.screenStream });
+ if (peer.state.sharing && peer.screenStream) {
+ shares.push({ key: peer.id, name: peer.name, stream: peer.screenStream });
+ }
}
return shares;
}
@@ -893,6 +945,8 @@ function renderScreens() {
}
}
+ if (!els.screens) return;
+
els.screens.innerHTML = '';
els.screens.classList.toggle('multi', activeShares().length > 1);
for (const share of activeShares()) {
@@ -902,7 +956,8 @@ function renderScreens() {
ph.className = 'screen-card popped';
const btn = document.createElement('button');
btn.className = 'pop-btn';
- btn.textContent = '⧉ re-embed';
+ btn.appendChild(mIcon('open_in_new'));
+ btn.appendChild(document.createTextNode(' re-embed'));
btn.onclick = () => closePopout(share.key);
const label = document.createElement('div');
label.className = 'label';
@@ -926,11 +981,12 @@ function renderScreens() {
label.appendChild(document.createTextNode(share.name));
const micBadge = document.createElement('span');
micBadge.className = 'tile-mic-badge';
+ micBadge.appendChild(mIcon('mic_off'));
label.appendChild(micBadge);
const pop = document.createElement('button');
pop.className = 'pop-btn';
pop.title = 'Pop out in new window';
- pop.textContent = '⧉';
+ pop.appendChild(mIcon('open_in_new'));
pop.onclick = () => popOutShare(share);
card.append(video, label, pop);
els.screens.appendChild(card);
@@ -1428,8 +1484,24 @@ function mxRenderTimeline() {
els.mxMessages.scrollTop = els.mxMessages.scrollHeight;
}
-function mxShowPanel() {
+function switchToChatView() {
+ if (els.voiceView) els.voiceView.classList.add('hidden');
if (els.mxPanel) els.mxPanel.classList.remove('hidden');
+ document.querySelectorAll('.channel.text').forEach((c) => c.classList.add('active'));
+ if (els.mxMessages) els.mxMessages.scrollTop = els.mxMessages.scrollHeight;
+ if (els.mxInput) els.mxInput.focus();
+}
+
+function switchToVoiceView() {
+ if (els.mxPanel) els.mxPanel.classList.add('hidden');
+ if (els.voiceView) els.voiceView.classList.remove('hidden');
+ document.querySelectorAll('.channel.text').forEach((c) => c.classList.remove('active'));
+ // Trigger Cytoscape redraw/resize because voice-view was unhidden
+ if (typeof cy !== 'undefined') cy.resize();
+}
+
+function mxShowPanel() {
+ switchToChatView();
}
function mxSend(text) {
@@ -1462,12 +1534,13 @@ mxInit();
document.querySelectorAll('.channel.text').forEach((el) => {
el.onclick = () => {
- if (Mx.roomId) { mxShowPanel(); return; }
+ closeSidebar();
+ if (Mx.roomId) { switchToChatView(); return; }
if (Mx.client) { toast('Connecting to Matrix…'); return; }
mxStartLogin();
};
});
-$('#mx-close')?.addEventListener('click', () => els.mxPanel.classList.add('hidden'));
+$('#mx-close')?.addEventListener('click', () => switchToVoiceView());
$('#mx-send')?.addEventListener('click', () => {
mxSend(els.mxInput.value);
els.mxInput.value = '';
@@ -1486,12 +1559,15 @@ function updateMicButton() {
const off = S.muted || !S.micStream;
for (const btn of micButtons) {
btn.classList.toggle('off', off);
- btn.textContent = off ? '\u{1F507}' : '\u{1F3A4}'; // 🔇 muted/no-mic vs 🎤 live
+ btn.replaceChildren(mIcon(off ? 'mic_off' : 'mic'));
btn.disabled = !S.micStream;
}
}
function updateDeafenButton() {
- for (const btn of deafenButtons) btn.classList.toggle('off', S.deafened);
+ for (const btn of deafenButtons) {
+ btn.classList.toggle('off', S.deafened);
+ btn.replaceChildren(mIcon(S.deafened ? 'headset_off' : 'headphones'));
+ }
}
function sendState() {
send({ type: 'state', state: { muted: S.muted, deafened: S.deafened, sharing: S.sharing } });
@@ -1524,9 +1600,15 @@ $('#btn-settings').onclick = openSettings;
els.nick.value = S.name;
els.nick.addEventListener('change', () => {
- const trimmed = els.nick.value.trim();
- if (!trimmed) { els.nick.value = S.name; return; } // nickname can't be blanked out
- S.name = trimmed;
+ const v = els.nick.value.trim();
+ if (v) S.name = v;
+ els.nick.value = S.name;
+ localStorage.setItem(LS_NAME, S.name);
+ setAvatar(els.meAvatar, S.name, S.myId || 'me');
+ if (S.ws && S.ws.readyState === 1) send({ type: 'rename', name: S.name });
+ renderTiles();
+});
+els.nick.addEventListener('blur', () => {
els.nick.value = S.name;
localStorage.setItem(LS_NAME, S.name);
setAvatar(els.meAvatar, S.name, S.myId || 'me');
@@ -1536,6 +1618,7 @@ setAvatar(els.meAvatar, S.name, 'me');
document.querySelectorAll('.channel.voice').forEach((el) => {
el.onclick = () => {
closeSidebar();
+ switchToVoiceView();
if (S.room === el.dataset.room) return;
cancelReconnect();
document.querySelectorAll('.channel.voice').forEach((c) => c.classList.remove('active'));
diff --git a/public/index.html b/public/index.html
index 4751cfb..3c65256 100644
--- a/public/index.html
+++ b/public/index.html
@@ -5,6 +5,7 @@
gibbz voice — POC
+
@@ -28,7 +29,7 @@
-
+
gibbz voice
@@ -46,12 +47,12 @@
gibbz voice POC
Text channels
- #
+
tag
general matrix
Voice channels
-
🔊 General
-
🔊 Gaming
-
🔊 Lounge
+
volume_up General
+
volume_up Gaming
+
volume_up Lounge
-
-
-
-
-
+
+
+
+
+
-
+
-
-
-
-
🖥️
-
No active screen shares
-
When someone shares their screen, it will appear here. Click the screen button below or press S to share.
+
+
+
+
+
+
present_to_all
+
No active screen shares
+
When someone shares their screen, it will appear here. Click the screen button below or press S to share.
+
+
+
+
+
+ SPATIAL AUDIO
+ drag your avatar to move; drag peers for local positioning • scroll to zoom
+
+
+
-
-
-
SPATIAL AUDIO
-
drag your avatar to move; drag peers for local positioning • scroll to zoom
-
+
+
-
-
-
+
+
+
@@ -138,7 +159,7 @@
Click a voice channel in the sidebar to join it — click another to switch.
Mic, Deafen and Share screen are the buttons bottom-left, or press M / D / S.
On the spatial audio map, drag your dot around — you hear people relative to where they stand.
-
Screen shares show up at the top of the main area; pop one into its own window with ⧦.
+
Screen shares show up at the top of the main area; pop one into its own window with open_in_new.
@@ -148,18 +169,6 @@
-
-
Connection lost
diff --git a/public/style.css b/public/style.css
index 0e5c805..755d08b 100644
--- a/public/style.css
+++ b/public/style.css
@@ -22,6 +22,41 @@ body {
color: var(--text-0);
overflow: hidden;
}
+
+/* Material Symbols font configuration */
+.material-symbols-outlined {
+ font-family: 'Material Symbols Outlined';
+ font-weight: normal;
+ font-style: normal;
+ font-size: 20px;
+ line-height: 1;
+ letter-spacing: normal;
+ text-transform: none;
+ display: inline-block;
+ white-space: nowrap;
+ word-wrap: normal;
+ direction: ltr;
+ -webkit-font-smoothing: antialiased;
+ text-rendering: optimizeLegibility;
+ -moz-osx-font-smoothing: grayscale;
+ font-feature-settings: 'liga';
+ user-select: none;
+ vertical-align: middle;
+}
+.inline-icon {
+ font-size: 16px;
+ vertical-align: -2px;
+}
+.channel-icon {
+ font-size: 18px;
+ color: var(--text-2);
+ flex-shrink: 0;
+}
+.channel:hover .channel-icon,
+.channel.active .channel-icon {
+ color: var(--text-0);
+}
+
#app { display: flex; height: 100vh; }
/* sidebar */
@@ -46,12 +81,14 @@ body {
.channel {
margin: 1px 8px; padding: 7px 8px; border-radius: 6px; color: var(--text-1);
cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
+ display: flex; align-items: center; gap: 6px;
}
.channel:hover { background: var(--bg-3); color: var(--text-0); }
.channel.text { color: var(--text-2); }
+.channel.text.active { background: rgba(88,101,242,.25); color: var(--text-0); }
.channel.text .hint { font-size: 10px; margin-left: 4px; }
.channel.voice.active { background: rgba(88,101,242,.25); color: var(--text-0); }
-.channel .count { float: right; color: var(--text-2); font-size: 12px; }
+.channel .count { margin-left: auto; color: var(--text-2); font-size: 12px; }
#user-panel {
margin-top: auto; background: var(--bg-0); padding: 8px;
@@ -85,17 +122,16 @@ body {
width: 28px; height: 28px; border-radius: 6px; cursor: pointer; position: relative;
display: grid; place-items: center; padding: 0;
}
+.ctl .material-symbols-outlined { font-size: 20px; }
.ctl:hover { background: var(--bg-3); color: var(--text-0); }
.ctl.off { color: var(--red); }
-.ctl.off::after {
- content: ""; position: absolute; left: 5px; top: 13px; width: 18px; height: 2px;
- background: var(--red); transform: rotate(-40deg); border-radius: 1px;
-}
.ctl:disabled { opacity: .35; cursor: not-allowed; }
.ctl.active { color: var(--green); }
/* main */
-#main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
+#main { flex: 1; display: flex; flex-direction: column; min-width: 0; position: relative; height: 100%; }
+#voice-view { flex: 1; display: flex; flex-direction: column; min-width: 0; height: 100%; }
+#voice-view.hidden { display: none; }
#stage {
flex: 1; display: flex; flex-direction: column; padding: 16px; gap: 16px;
overflow: auto; position: relative;
@@ -105,6 +141,7 @@ body {
text-align: center; gap: 8px; color: var(--text-1); padding: 24px;
}
#stage-empty .big-icon { font-size: 56px; }
+#stage-empty .big-icon .material-symbols-outlined { font-size: 64px; opacity: 0.6; }
#stage-empty h2 { font-size: 20px; color: var(--text-0); }
#stage-empty p { max-width: 420px; color: var(--text-2); }
#stage.empty #stage-empty { display: grid; }
@@ -120,8 +157,10 @@ body {
.pop-btn {
position: absolute; right: 8px; top: 8px; z-index: 2;
background: rgba(0,0,0,.65); color: var(--text-0); border: none; cursor: pointer;
- border-radius: 5px; padding: 3px 8px; font-size: 13px; font-weight: 600;
+ border-radius: 5px; padding: 4px 8px; font-size: 12px; font-weight: 600;
+ display: inline-flex; align-items: center; gap: 4px;
}
+.pop-btn .material-symbols-outlined { font-size: 16px; }
.pop-btn:hover { background: var(--brand); }
.screen-card {
position: relative; background: #000; border-radius: var(--radius); overflow: hidden;
@@ -131,12 +170,12 @@ body {
.screen-card .label {
position: absolute; left: 8px; bottom: 8px; background: rgba(0,0,0,.65);
padding: 3px 8px; border-radius: 5px; font-size: 12px; font-weight: 600;
+ display: inline-flex; align-items: center;
}
.screen-card .label .live { color: var(--red); font-weight: 800; margin-right: 5px; }
.screen-card.speaking { box-shadow: 0 0 0 3px var(--green), 0 8px 24px rgba(0,0,0,.4); }
-.tile-mic-badge { margin-left: 6px; }
-.tile-mic-badge.muted::before { content: "\1F507"; } /* 🔇 */
-.tile-mic-badge:not(.muted)::before { content: ""; }
+.tile-mic-badge { margin-left: 6px; display: inline-flex; align-items: center; }
+.tile-mic-badge .material-symbols-outlined { font-size: 15px; color: var(--red); }
/* channel user lines */
.channel-users {
@@ -180,15 +219,27 @@ body {
gap: 4px;
font-size: 12px;
}
+.user-line-badges .material-symbols-outlined {
+ font-size: 15px;
+}
.badge-conn {
color: var(--yellow);
font-weight: bold;
}
.badge-mute {
- font-size: 11px;
+ color: var(--red);
+ display: inline-flex;
+ align-items: center;
+}
+.badge-deafen {
+ color: var(--red);
+ display: inline-flex;
+ align-items: center;
}
.badge-share {
- font-size: 11px;
+ color: var(--brand);
+ display: inline-flex;
+ align-items: center;
}
/* hover volume popup */
@@ -327,39 +378,94 @@ body {
.howto-list { display: flex; flex-direction: column; gap: 8px; padding-left: 18px; font-size: 13px; color: var(--text-1); }
.howto-list li { line-height: 1.4; }
-/* matrix text chat panel */
+/* matrix text chat panel (main view) */
#mx-panel {
- position: fixed; top: 0; right: 0; bottom: 0; width: 340px; max-width: 90vw;
- background: var(--bg-1); border-left: 1px solid rgba(0,0,0,.35);
- display: flex; flex-direction: column; z-index: 40;
- box-shadow: -8px 0 24px rgba(0,0,0,.45);
+ flex: 1;
+ width: 100%;
+ height: 100%;
+ background: var(--bg-2);
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
}
#mx-panel.hidden { display: none; }
.mx-header {
- padding: 12px 14px; font-weight: 700; font-size: 14px;
- display: flex; align-items: center; justify-content: space-between;
- border-bottom: 1px solid rgba(0,0,0,.25); flex-shrink: 0;
+ padding: 14px 16px;
+ font-weight: 700;
+ font-size: 15px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ border-bottom: 1px solid rgba(0,0,0,.25);
+ box-shadow: 0 1px 0 rgba(0,0,0,.06);
+ flex-shrink: 0;
+ background: var(--bg-2);
+}
+.mx-header-info {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+.mx-header-title {
+ color: var(--text-0);
+ font-weight: 700;
}
.mx-header button.ghost {
- background: transparent; border: none; color: var(--text-1);
- font-size: 20px; line-height: 1; cursor: pointer; padding: 0 4px;
+ background: transparent;
+ border: none;
+ color: var(--text-1);
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 32px;
+ height: 32px;
+ border-radius: 6px;
+ cursor: pointer;
+ padding: 0;
}
-.mx-header button.ghost:hover { color: var(--text-0); }
-#mx-messages { flex: 1; overflow: auto; padding: 10px 14px; display: flex; flex-direction: column; gap: 8px; }
-.mx-msg { font-size: 13px; line-height: 1.4; }
-.mx-sender { font-weight: 700; color: var(--text-0); margin-right: 6px; }
+.mx-header button.ghost:hover {
+ background: var(--bg-3);
+ color: var(--text-0);
+}
+#mx-messages {
+ flex: 1;
+ overflow-y: auto;
+ padding: 16px 20px;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+.mx-msg { font-size: 14px; line-height: 1.45; }
+.mx-sender { font-weight: 700; color: var(--text-0); margin-right: 8px; }
.mx-body { color: var(--text-1); white-space: pre-wrap; overflow-wrap: anywhere; }
.mx-composer {
- display: flex; gap: 8px; padding: 10px 14px; border-top: 1px solid rgba(0,0,0,.25); flex-shrink: 0;
+ display: flex;
+ gap: 10px;
+ padding: 14px 20px;
+ border-top: 1px solid rgba(0,0,0,.2);
+ background: var(--bg-2);
+ flex-shrink: 0;
}
.mx-composer textarea {
- flex: 1; resize: none; background: var(--bg-0); border: 1px solid var(--bg-4);
- border-radius: 6px; color: var(--text-0); padding: 8px 10px; font: inherit; max-height: 100px;
+ flex: 1;
+ resize: none;
+ background: var(--bg-0);
+ border: 1px solid var(--bg-4);
+ border-radius: 8px;
+ color: var(--text-0);
+ padding: 10px 14px;
+ font: inherit;
+ max-height: 120px;
}
.mx-composer textarea:focus { outline: none; border-color: var(--brand); }
.mx-composer button {
- background: var(--brand); border: none; color: #fff; border-radius: 6px;
- padding: 0 14px; font-weight: 600; cursor: pointer;
+ background: var(--brand);
+ border: none;
+ color: #fff;
+ border-radius: 8px;
+ padding: 0 18px;
+ font-weight: 600;
+ cursor: pointer;
}
/* reconnect banner */
@@ -467,7 +573,5 @@ code { font-family: ui-monospace, monospace; font-size: 12px; }
/* modals already clamp to 92vw; just tighten padding a bit */
#settings-modal, #welcome-modal { padding: 16px; }
- #mx-panel { width: 100vw; }
-
#toasts { left: 12px; right: 12px; max-width: none; }
}