From 98ed981805e43e78f04b16923035aecc56cb1397 Mon Sep 17 00:00:00 2001 From: Siavash Sameni Date: Tue, 14 Apr 2026 19:10:31 +0400 Subject: [PATCH] fix(ui): self-call prevention, debounce, codec in stats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Filter self from lobby list (double-check in renderLobbyUsers) - Disable "Direct Call" button when tapping own user - Debounce call button (callInProgress flag prevents double-tap) - Block calling own fingerprint - Stats line shows codec names + fps + audio level The direct call to the other phone failing is likely because both phones share the same reflexive addr:port on the same NAT, making determine_role return None (equal addrs). This is an existing edge case in reflect.rs — not a UI bug. Co-Authored-By: Claude Opus 4.6 (1M context) --- desktop/src/main.ts | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/desktop/src/main.ts b/desktop/src/main.ts index 2009d2b..f302648 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -216,7 +216,9 @@ sQuality?.addEventListener("input", () => updateQualityUI(parseInt(sQuality.valu // ── Lobby rendering ─────────────────────────────────────────────── function renderLobbyUsers() { lobbyUserList.innerHTML = ""; - const users = Array.from(lobbyUsers.values()).sort((a, b) => { + const users = Array.from(lobbyUsers.values()) + .filter((u) => u.fingerprint !== myFingerprint) // always exclude self + .sort((a, b) => { // Voice users first, then alphabetical if (a.inVoice !== b.inVoice) return a.inVoice ? -1 : 1; return (a.alias || a.fingerprint).localeCompare(b.alias || b.fingerprint); @@ -269,14 +271,25 @@ function openContextMenu(user: LobbyUser) { ctxIdenticon.appendChild(createIdenticonEl(user.fingerprint, 40, true)); ctxName.textContent = user.alias || user.fingerprint.substring(0, 16); ctxFp.textContent = user.fingerprint; + // Hide call button for self + const isSelf = user.fingerprint === myFingerprint; + (ctxCallBtn as HTMLButtonElement).disabled = isSelf; + (ctxCallBtn as HTMLElement).style.opacity = isSelf ? "0.3" : "1"; ctxMenu.classList.remove("hidden"); } ctxCloseBtn.addEventListener("click", () => ctxMenu.classList.add("hidden")); ctxMenu.addEventListener("click", (e) => { if (e.target === ctxMenu) ctxMenu.classList.add("hidden"); }); +let callInProgress = false; ctxCallBtn.addEventListener("click", async () => { - if (!contextUser) return; + if (!contextUser || callInProgress) return; + // Don't allow calling self + if (contextUser.fingerprint === myFingerprint) { + ctxMenu.classList.add("hidden"); + return; + } + callInProgress = true; ctxMenu.classList.add("hidden"); directCallPeer = { fingerprint: contextUser.fingerprint, alias: contextUser.alias }; try { @@ -284,6 +297,8 @@ ctxCallBtn.addEventListener("click", async () => { } catch (e: any) { console.error("place_call failed:", e); directCallPeer = null; + } finally { + callInProgress = false; } }); @@ -415,8 +430,8 @@ async function pollStatus() { } } - // Stats - vdStats.textContent = `TX: ${st.tx_codec} ${st.encode_fps}fps | RX: ${st.rx_codec} ${st.recv_fps}fps`; + // Stats with codec + vdStats.textContent = `TX: ${st.tx_codec || "?"} ${st.encode_fps || 0}fps | RX: ${st.rx_codec || "?"} ${st.recv_fps || 0}fps | Level: ${st.audio_level || 0}`; } catch {} }