|
|
|
|
@@ -67,29 +67,35 @@ const incomingCallerName = document.getElementById("incoming-caller-name")!;
|
|
|
|
|
const incomingIdenticon = document.getElementById("incoming-identicon")!;
|
|
|
|
|
const acceptCallBtn = document.getElementById("accept-call-btn")!;
|
|
|
|
|
const rejectCallBtn = document.getElementById("reject-call-btn")!;
|
|
|
|
|
const backToLobbyBtn = document.getElementById("back-to-lobby-btn")!;
|
|
|
|
|
const roomName = document.getElementById("room-name")!;
|
|
|
|
|
const callTimer = document.getElementById("call-timer")!;
|
|
|
|
|
const callStatus = document.getElementById("call-status")!;
|
|
|
|
|
const levelBar = document.getElementById("level-bar")!;
|
|
|
|
|
const participantsDiv = document.getElementById("participants")!;
|
|
|
|
|
const directCallView = document.getElementById("direct-call-view")!;
|
|
|
|
|
const dcIdenticon = document.getElementById("dc-identicon")!;
|
|
|
|
|
const dcName = document.getElementById("dc-name")!;
|
|
|
|
|
const dcFp = document.getElementById("dc-fp")!;
|
|
|
|
|
const dcBadge = document.getElementById("dc-badge")!;
|
|
|
|
|
const micBtn = document.getElementById("mic-btn")!;
|
|
|
|
|
const micIcon = document.getElementById("mic-icon")!;
|
|
|
|
|
const spkBtn = document.getElementById("spk-btn")!;
|
|
|
|
|
const spkIcon = document.getElementById("spk-icon")!;
|
|
|
|
|
const hangupBtn = document.getElementById("hangup-btn")!;
|
|
|
|
|
const statsDiv = document.getElementById("stats")!;
|
|
|
|
|
// Voice drawer elements
|
|
|
|
|
const voiceDrawer = document.getElementById("voice-drawer")!;
|
|
|
|
|
const vdRoom = document.getElementById("vd-room")!;
|
|
|
|
|
const vdTimer = document.getElementById("vd-timer")!;
|
|
|
|
|
const vdStatus = document.getElementById("vd-status")!;
|
|
|
|
|
const vdBadge = document.getElementById("vd-badge")!;
|
|
|
|
|
const vdLevelBar = document.getElementById("vd-level-bar")!;
|
|
|
|
|
const vdMicBtn = document.getElementById("vd-mic-btn")!;
|
|
|
|
|
const vdMicIcon = document.getElementById("vd-mic-icon")!;
|
|
|
|
|
const vdSpkBtn = document.getElementById("vd-spk-btn")!;
|
|
|
|
|
const vdSpkIcon = document.getElementById("vd-spk-icon")!;
|
|
|
|
|
const vdEndBtn = document.getElementById("vd-end-btn")!;
|
|
|
|
|
const vdDirectInfo = document.getElementById("vd-direct-info")!;
|
|
|
|
|
const vdDcIdenticon = document.getElementById("vd-dc-identicon")!;
|
|
|
|
|
const vdDcName = document.getElementById("vd-dc-name")!;
|
|
|
|
|
const vdDcBadge = document.getElementById("vd-dc-badge")!;
|
|
|
|
|
const vdStats = document.getElementById("vd-stats")!;
|
|
|
|
|
const ctxMenu = document.getElementById("user-context-menu")!;
|
|
|
|
|
const ctxIdenticon = document.getElementById("ctx-identicon")!;
|
|
|
|
|
const ctxName = document.getElementById("ctx-name")!;
|
|
|
|
|
const ctxFp = document.getElementById("ctx-fp")!;
|
|
|
|
|
const ctxCallBtn = document.getElementById("ctx-call-btn")!;
|
|
|
|
|
const ctxCloseBtn = document.getElementById("ctx-close-btn")!;
|
|
|
|
|
// Relay management
|
|
|
|
|
const sRelayList = document.getElementById("s-relay-list")!;
|
|
|
|
|
const sRelayName = document.getElementById("s-relay-name") as HTMLInputElement;
|
|
|
|
|
const sRelayAddr = document.getElementById("s-relay-addr") as HTMLInputElement;
|
|
|
|
|
const sRelayAdd = document.getElementById("s-relay-add")!;
|
|
|
|
|
|
|
|
|
|
// Settings
|
|
|
|
|
const settingsPanel = document.getElementById("settings-panel")!;
|
|
|
|
|
const settingsBtn = document.getElementById("settings-btn")!;
|
|
|
|
|
@@ -210,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);
|
|
|
|
|
@@ -263,115 +271,109 @@ 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;
|
|
|
|
|
if (contextUser.fingerprint === myFingerprint) {
|
|
|
|
|
ctxMenu.classList.add("hidden");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// Don't place a call if there's already a pending incoming call
|
|
|
|
|
if (pendingCallId) {
|
|
|
|
|
ctxMenu.classList.add("hidden");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
callInProgress = true;
|
|
|
|
|
ctxMenu.classList.add("hidden");
|
|
|
|
|
directCallPeer = { fingerprint: contextUser.fingerprint, alias: contextUser.alias };
|
|
|
|
|
try {
|
|
|
|
|
await invoke("place_call", { targetFp: contextUser.fingerprint });
|
|
|
|
|
// Keep callInProgress true until the call resolves (setup/hangup)
|
|
|
|
|
// — it's cleared in leaveVoice() or when the call connects
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
console.error("place_call failed:", e);
|
|
|
|
|
directCallPeer = null;
|
|
|
|
|
callInProgress = false;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── Voice join/leave ──────────────────────────────────────────────
|
|
|
|
|
// ── Voice join/leave (drawer-based) ───────────────────────────────
|
|
|
|
|
joinVoiceBtn.addEventListener("click", async () => {
|
|
|
|
|
if (inVoice) {
|
|
|
|
|
// Leave voice
|
|
|
|
|
try { await invoke("disconnect"); } catch {}
|
|
|
|
|
inVoice = false;
|
|
|
|
|
joinVoiceBtn.innerHTML = '<span class="fab-icon">🎧</span><span class="fab-label">Join Voice</span>';
|
|
|
|
|
joinVoiceBtn.classList.remove("active");
|
|
|
|
|
if (statusInterval) { clearInterval(statusInterval); statusInterval = null; }
|
|
|
|
|
showLobby();
|
|
|
|
|
} else {
|
|
|
|
|
// Join voice
|
|
|
|
|
const relay = getRelay();
|
|
|
|
|
const s = loadSettings();
|
|
|
|
|
if (!relay) return;
|
|
|
|
|
try {
|
|
|
|
|
await invoke("connect", {
|
|
|
|
|
relay: relay.address,
|
|
|
|
|
room: s.room || "general",
|
|
|
|
|
alias: s.alias || "",
|
|
|
|
|
osAec: s.osAec,
|
|
|
|
|
quality: s.quality || "auto",
|
|
|
|
|
});
|
|
|
|
|
inVoice = true;
|
|
|
|
|
joinVoiceBtn.innerHTML = '<span class="fab-icon">🎧</span><span class="fab-label">Leave Voice</span>';
|
|
|
|
|
joinVoiceBtn.classList.add("active");
|
|
|
|
|
showCallScreen(false);
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
console.error("connect failed:", e);
|
|
|
|
|
}
|
|
|
|
|
if (inVoice) return;
|
|
|
|
|
const relay = getRelay();
|
|
|
|
|
const s = loadSettings();
|
|
|
|
|
if (!relay) return;
|
|
|
|
|
try {
|
|
|
|
|
await invoke("connect", {
|
|
|
|
|
relay: relay.address,
|
|
|
|
|
room: s.room || "general",
|
|
|
|
|
alias: s.alias || "",
|
|
|
|
|
osAec: s.osAec,
|
|
|
|
|
quality: s.quality || "auto",
|
|
|
|
|
});
|
|
|
|
|
enterVoice(false);
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
console.error("connect failed:", e);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── Screen transitions ────────────────────────────────────────────
|
|
|
|
|
function showLobby() {
|
|
|
|
|
callScreen.classList.add("hidden");
|
|
|
|
|
lobbyScreen.classList.remove("hidden");
|
|
|
|
|
directCallPeer = null;
|
|
|
|
|
levelBar.style.width = "0%";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function showCallScreen(isDirect: boolean) {
|
|
|
|
|
lobbyScreen.classList.add("hidden");
|
|
|
|
|
callScreen.classList.remove("hidden");
|
|
|
|
|
function enterVoice(isDirect: boolean) {
|
|
|
|
|
inVoice = true;
|
|
|
|
|
const s = loadSettings();
|
|
|
|
|
joinVoiceBtn.classList.add("hidden");
|
|
|
|
|
voiceDrawer.classList.remove("hidden");
|
|
|
|
|
vdRoom.textContent = isDirect && directCallPeer
|
|
|
|
|
? (directCallPeer.alias || directCallPeer.fingerprint.substring(0, 16))
|
|
|
|
|
: (s.room || "general");
|
|
|
|
|
vdTimer.textContent = "0:00";
|
|
|
|
|
vdBadge.classList.add("hidden");
|
|
|
|
|
vdBadge.textContent = "";
|
|
|
|
|
|
|
|
|
|
if (isDirect && directCallPeer) {
|
|
|
|
|
roomName.textContent = directCallPeer.alias || directCallPeer.fingerprint.substring(0, 16);
|
|
|
|
|
dcName.textContent = directCallPeer.alias || "Unknown";
|
|
|
|
|
dcFp.textContent = directCallPeer.fingerprint;
|
|
|
|
|
dcIdenticon.innerHTML = "";
|
|
|
|
|
dcIdenticon.appendChild(createIdenticonEl(directCallPeer.fingerprint, 96, true));
|
|
|
|
|
dcBadge.textContent = "Connecting...";
|
|
|
|
|
dcBadge.className = "dc-badge connecting";
|
|
|
|
|
directCallView.classList.remove("hidden");
|
|
|
|
|
participantsDiv.classList.add("hidden");
|
|
|
|
|
vdDirectInfo.classList.remove("hidden");
|
|
|
|
|
vdDcIdenticon.innerHTML = "";
|
|
|
|
|
vdDcIdenticon.appendChild(createIdenticonEl(directCallPeer.fingerprint, 32, true));
|
|
|
|
|
vdDcName.textContent = directCallPeer.alias || directCallPeer.fingerprint.substring(0, 16);
|
|
|
|
|
vdDcBadge.textContent = "Connecting...";
|
|
|
|
|
vdDcBadge.className = "vd-dc-badge connecting";
|
|
|
|
|
} else {
|
|
|
|
|
const s = loadSettings();
|
|
|
|
|
roomName.textContent = s.room || "general";
|
|
|
|
|
directCallView.classList.add("hidden");
|
|
|
|
|
participantsDiv.classList.remove("hidden");
|
|
|
|
|
vdDirectInfo.classList.add("hidden");
|
|
|
|
|
}
|
|
|
|
|
callStatus.className = "status-dot";
|
|
|
|
|
|
|
|
|
|
statusInterval = window.setInterval(pollStatus, 250);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Back button from call to lobby
|
|
|
|
|
backToLobbyBtn.addEventListener("click", async () => {
|
|
|
|
|
try { await invoke("disconnect"); } catch {}
|
|
|
|
|
function leaveVoice() {
|
|
|
|
|
inVoice = false;
|
|
|
|
|
joinVoiceBtn.innerHTML = '<span class="fab-icon">🎧</span><span class="fab-label">Join Voice</span>';
|
|
|
|
|
joinVoiceBtn.classList.remove("active");
|
|
|
|
|
callInProgress = false;
|
|
|
|
|
directCallPeer = null;
|
|
|
|
|
pendingCallId = null;
|
|
|
|
|
voiceDrawer.classList.add("hidden");
|
|
|
|
|
joinVoiceBtn.classList.remove("hidden");
|
|
|
|
|
vdLevelBar.style.width = "0%";
|
|
|
|
|
if (statusInterval) { clearInterval(statusInterval); statusInterval = null; }
|
|
|
|
|
showLobby();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Hangup
|
|
|
|
|
hangupBtn.addEventListener("click", async () => {
|
|
|
|
|
// Drawer controls
|
|
|
|
|
vdEndBtn.addEventListener("click", async () => {
|
|
|
|
|
try { await invoke("hangup_call"); } catch {}
|
|
|
|
|
try { await invoke("disconnect"); } catch {}
|
|
|
|
|
inVoice = false;
|
|
|
|
|
joinVoiceBtn.innerHTML = '<span class="fab-icon">🎧</span><span class="fab-label">Join Voice</span>';
|
|
|
|
|
joinVoiceBtn.classList.remove("active");
|
|
|
|
|
if (statusInterval) { clearInterval(statusInterval); statusInterval = null; }
|
|
|
|
|
showLobby();
|
|
|
|
|
leaveVoice();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Mic/speaker toggles
|
|
|
|
|
micBtn.addEventListener("click", async () => {
|
|
|
|
|
vdMicBtn.addEventListener("click", async () => {
|
|
|
|
|
try { await invoke("toggle_mic"); } catch {}
|
|
|
|
|
});
|
|
|
|
|
spkBtn.addEventListener("click", async () => {
|
|
|
|
|
vdSpkBtn.addEventListener("click", async () => {
|
|
|
|
|
try { await invoke("toggle_speaker"); } catch {}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@@ -379,38 +381,40 @@ spkBtn.addEventListener("click", async () => {
|
|
|
|
|
interface CallStatusI {
|
|
|
|
|
active: boolean;
|
|
|
|
|
mic_muted: boolean;
|
|
|
|
|
speaker_muted: boolean;
|
|
|
|
|
send_rms: number;
|
|
|
|
|
recv_rms: number;
|
|
|
|
|
codec_tx: string;
|
|
|
|
|
codec_rx: string;
|
|
|
|
|
fec_ratio: number;
|
|
|
|
|
send_packets: number;
|
|
|
|
|
recv_packets: number;
|
|
|
|
|
spk_muted: boolean;
|
|
|
|
|
participants: any[];
|
|
|
|
|
encode_fps: number;
|
|
|
|
|
recv_fps: number;
|
|
|
|
|
audio_level: number;
|
|
|
|
|
call_duration_secs: number;
|
|
|
|
|
fingerprint: string;
|
|
|
|
|
tx_codec: string;
|
|
|
|
|
rx_codec: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function pollStatus() {
|
|
|
|
|
try {
|
|
|
|
|
const st: CallStatusI = await invoke("get_status");
|
|
|
|
|
if (!st.active) {
|
|
|
|
|
showLobby();
|
|
|
|
|
leaveVoice();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (st.fingerprint) myFingerprint = st.fingerprint;
|
|
|
|
|
micBtn.classList.toggle("muted", st.mic_muted);
|
|
|
|
|
micIcon.textContent = st.mic_muted ? "Mic Off" : "Mic";
|
|
|
|
|
spkBtn.classList.toggle("muted", st.speaker_muted);
|
|
|
|
|
spkIcon.textContent = st.speaker_muted ? "Spk Off" : "Spk";
|
|
|
|
|
|
|
|
|
|
const pct = Math.min(100, (st.send_rms / 10000) * 100);
|
|
|
|
|
levelBar.style.width = `${pct}%`;
|
|
|
|
|
// Update drawer controls
|
|
|
|
|
vdMicBtn.classList.toggle("muted", st.mic_muted);
|
|
|
|
|
vdMicIcon.textContent = st.mic_muted ? "Muted" : "Mic";
|
|
|
|
|
vdSpkBtn.classList.toggle("muted", st.spk_muted);
|
|
|
|
|
vdSpkIcon.textContent = st.spk_muted ? "Off" : "Spk";
|
|
|
|
|
|
|
|
|
|
// Level meter
|
|
|
|
|
const pct = Math.min(100, (st.audio_level / 10000) * 100);
|
|
|
|
|
vdLevelBar.style.width = `${pct}%`;
|
|
|
|
|
|
|
|
|
|
// Duration
|
|
|
|
|
const m = Math.floor(st.call_duration_secs / 60);
|
|
|
|
|
const s = Math.floor(st.call_duration_secs % 60);
|
|
|
|
|
callTimer.textContent = `${m}:${s.toString().padStart(2, "0")}`;
|
|
|
|
|
vdTimer.textContent = `${m}:${s.toString().padStart(2, "0")}`;
|
|
|
|
|
|
|
|
|
|
// P2P badge for direct calls
|
|
|
|
|
if (directCallPeer) {
|
|
|
|
|
@@ -418,16 +422,23 @@ async function pollStatus() {
|
|
|
|
|
const engineOk = [...callDebugBuffer].reverse().find((e) => e.step === "connect:call_engine_started");
|
|
|
|
|
if (engineOk) {
|
|
|
|
|
if (pathNeg?.details?.use_direct === true) {
|
|
|
|
|
dcBadge.textContent = "P2P Direct";
|
|
|
|
|
dcBadge.className = "dc-badge";
|
|
|
|
|
vdDcBadge.textContent = "P2P Direct";
|
|
|
|
|
vdDcBadge.className = "vd-dc-badge direct";
|
|
|
|
|
vdBadge.textContent = "P2P";
|
|
|
|
|
vdBadge.className = "vd-badge direct";
|
|
|
|
|
vdBadge.classList.remove("hidden");
|
|
|
|
|
} else {
|
|
|
|
|
dcBadge.textContent = "Via Relay";
|
|
|
|
|
dcBadge.className = "dc-badge relay";
|
|
|
|
|
vdDcBadge.textContent = "Via Relay";
|
|
|
|
|
vdDcBadge.className = "vd-dc-badge relay";
|
|
|
|
|
vdBadge.textContent = "Relay";
|
|
|
|
|
vdBadge.className = "vd-badge relay";
|
|
|
|
|
vdBadge.classList.remove("hidden");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
statsDiv.textContent = `TX: ${st.codec_tx} ${st.send_packets}pkt | RX: ${st.codec_rx} ${st.recv_packets}pkt | FEC: ${(st.fec_ratio * 100).toFixed(0)}%`;
|
|
|
|
|
// 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 {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -484,7 +495,7 @@ listen("signal-event", (event: any) => {
|
|
|
|
|
directOnly: s.directOnly || false,
|
|
|
|
|
birthdayAttack: s.birthdayAttack || false,
|
|
|
|
|
});
|
|
|
|
|
showCallScreen(true);
|
|
|
|
|
enterVoice(true);
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
console.error("connect failed:", e);
|
|
|
|
|
}
|
|
|
|
|
@@ -495,7 +506,7 @@ listen("signal-event", (event: any) => {
|
|
|
|
|
incomingBanner.classList.add("hidden");
|
|
|
|
|
(async () => {
|
|
|
|
|
try { await invoke("disconnect"); } catch {}
|
|
|
|
|
showLobby();
|
|
|
|
|
leaveVoice();
|
|
|
|
|
})();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
@@ -547,6 +558,80 @@ listen("call-event", (event: any) => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── Settings ──────────────────────────────────────────────────────
|
|
|
|
|
// ── Relay list management ──────────────────────────────────────
|
|
|
|
|
function renderRelayList() {
|
|
|
|
|
const s = loadSettings();
|
|
|
|
|
sRelayList.innerHTML = "";
|
|
|
|
|
for (let i = 0; i < s.relays.length; i++) {
|
|
|
|
|
const r = s.relays[i];
|
|
|
|
|
const isActive = i === s.selectedRelay;
|
|
|
|
|
const row = document.createElement("div");
|
|
|
|
|
row.style.cssText = "display:flex;align-items:center;gap:6px;padding:8px;border-radius:6px;margin-bottom:4px;cursor:pointer;" +
|
|
|
|
|
(isActive ? "background:rgba(74,222,128,0.12);border:1px solid var(--green);" : "background:var(--surface);border:1px solid transparent;");
|
|
|
|
|
row.innerHTML = `
|
|
|
|
|
<span style="flex:1;font-size:13px;font-weight:${isActive ? '600' : '400'}">
|
|
|
|
|
<span style="color:${isActive ? 'var(--green)' : 'var(--text)'}">${r.name}</span>
|
|
|
|
|
<span style="color:var(--text-dim);font-size:11px;margin-left:4px">${r.address}</span>
|
|
|
|
|
</span>
|
|
|
|
|
${isActive ? '<span style="color:var(--green);font-size:11px">ACTIVE</span>' : ''}
|
|
|
|
|
<button class="relay-rm-btn" data-idx="${i}" style="background:none;border:none;color:var(--text-dim);cursor:pointer;font-size:16px;padding:2px 6px">×</button>
|
|
|
|
|
`;
|
|
|
|
|
// Click to select (not on the X button)
|
|
|
|
|
row.addEventListener("click", (e) => {
|
|
|
|
|
if ((e.target as HTMLElement).classList.contains("relay-rm-btn")) return;
|
|
|
|
|
const settings = loadSettings();
|
|
|
|
|
if (i !== settings.selectedRelay) {
|
|
|
|
|
settings.selectedRelay = i;
|
|
|
|
|
saveSettings(settings);
|
|
|
|
|
renderRelayList();
|
|
|
|
|
// Reconnect to new relay
|
|
|
|
|
reconnectSignal();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
sRelayList.appendChild(row);
|
|
|
|
|
}
|
|
|
|
|
// Wire remove buttons
|
|
|
|
|
sRelayList.querySelectorAll(".relay-rm-btn").forEach((btn) => {
|
|
|
|
|
btn.addEventListener("click", (e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
const idx = parseInt((btn as HTMLElement).dataset.idx || "0");
|
|
|
|
|
const settings = loadSettings();
|
|
|
|
|
if (settings.relays.length <= 1) return; // keep at least one
|
|
|
|
|
settings.relays.splice(idx, 1);
|
|
|
|
|
if (settings.selectedRelay >= settings.relays.length) {
|
|
|
|
|
settings.selectedRelay = 0;
|
|
|
|
|
}
|
|
|
|
|
saveSettings(settings);
|
|
|
|
|
renderRelayList();
|
|
|
|
|
reconnectSignal();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sRelayAdd.addEventListener("click", () => {
|
|
|
|
|
const name = sRelayName.value.trim();
|
|
|
|
|
const addr = sRelayAddr.value.trim();
|
|
|
|
|
if (!name || !addr) return;
|
|
|
|
|
if (!addr.includes(":")) return; // must be host:port
|
|
|
|
|
const s = loadSettings();
|
|
|
|
|
s.relays.push({ name, address: addr });
|
|
|
|
|
saveSettings(s);
|
|
|
|
|
sRelayName.value = "";
|
|
|
|
|
sRelayAddr.value = "";
|
|
|
|
|
renderRelayList();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
async function reconnectSignal() {
|
|
|
|
|
// Deregister from current relay, then auto-connect to new one
|
|
|
|
|
try { await invoke("deregister"); } catch {}
|
|
|
|
|
lobbyUsers.clear();
|
|
|
|
|
renderLobbyUsers();
|
|
|
|
|
lobbyDot.style.background = "var(--yellow)";
|
|
|
|
|
lobbyRelayLabel.textContent = "Reconnecting...";
|
|
|
|
|
// Short delay to let deregister complete
|
|
|
|
|
setTimeout(() => autoConnect(), 500);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openSettings() {
|
|
|
|
|
const s = loadSettings();
|
|
|
|
|
sRoom.value = s.room;
|
|
|
|
|
@@ -562,6 +647,7 @@ function openSettings() {
|
|
|
|
|
sQuality.value = String(qi);
|
|
|
|
|
updateQualityUI(qi);
|
|
|
|
|
sFingerprint.textContent = myFingerprint || "(loading...)";
|
|
|
|
|
renderRelayList();
|
|
|
|
|
settingsPanel.classList.remove("hidden");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -584,6 +670,8 @@ settingsSave.addEventListener("click", () => {
|
|
|
|
|
invoke("set_dred_verbose_logs", { enabled: s.dredDebugLogs }).catch(() => {});
|
|
|
|
|
invoke("set_call_debug_logs", { enabled: s.callDebugLogs }).catch(() => {});
|
|
|
|
|
sCallDebugSection.style.display = s.callDebugLogs ? "" : "none";
|
|
|
|
|
// Update lobby room label
|
|
|
|
|
lobbyRoomLabel.textContent = s.room || "general";
|
|
|
|
|
settingsPanel.classList.add("hidden");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@@ -593,11 +681,17 @@ sCallDebugClearBtn?.addEventListener("click", () => {
|
|
|
|
|
sCallDebugLogEl.textContent = "";
|
|
|
|
|
});
|
|
|
|
|
sCallDebugCopyBtn?.addEventListener("click", () => {
|
|
|
|
|
const text = callDebugBuffer.map((e) => `${e.step} ${JSON.stringify(e.details)}`).join("\n");
|
|
|
|
|
const text = callDebugBuffer.map((e) => {
|
|
|
|
|
const t = new Date(e.ts_ms).toLocaleTimeString("en-GB", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
|
|
|
return `${t} ${e.step} ${JSON.stringify(e.details)}`;
|
|
|
|
|
}).join("\n");
|
|
|
|
|
navigator.clipboard?.writeText(text).catch(() => {});
|
|
|
|
|
});
|
|
|
|
|
sCallDebugShareBtn?.addEventListener("click", async () => {
|
|
|
|
|
const text = callDebugBuffer.map((e) => `${e.step} ${JSON.stringify(e.details)}`).join("\n");
|
|
|
|
|
const text = callDebugBuffer.map((e) => {
|
|
|
|
|
const t = new Date(e.ts_ms).toLocaleTimeString("en-GB", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
|
|
|
return `${t} ${e.step} ${JSON.stringify(e.details)}`;
|
|
|
|
|
}).join("\n");
|
|
|
|
|
try { await (navigator as any).share({ text }); } catch {}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@@ -651,13 +745,13 @@ async function autoConnect() {
|
|
|
|
|
lobbyDot.style.background = "var(--green)";
|
|
|
|
|
lobbyRelayLabel.textContent = `${relay.name} — connected`;
|
|
|
|
|
|
|
|
|
|
// Get identity
|
|
|
|
|
const fp: string = await invoke("get_identity");
|
|
|
|
|
if (fp) {
|
|
|
|
|
myFingerprint = fp;
|
|
|
|
|
lobbyFp.textContent = fp;
|
|
|
|
|
// Get identity + alias
|
|
|
|
|
const appInfo: any = await invoke("get_app_info");
|
|
|
|
|
if (appInfo?.fingerprint) {
|
|
|
|
|
myFingerprint = appInfo.fingerprint;
|
|
|
|
|
lobbyFp.textContent = appInfo.alias || appInfo.fingerprint;
|
|
|
|
|
lobbyIdenticon.innerHTML = "";
|
|
|
|
|
lobbyIdenticon.appendChild(createIdenticonEl(fp, 20, true));
|
|
|
|
|
lobbyIdenticon.appendChild(createIdenticonEl(appInfo.fingerprint, 20, true));
|
|
|
|
|
}
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
lobbyDot.style.background = "var(--red)";
|
|
|
|
|
@@ -671,9 +765,9 @@ invoke("set_call_debug_logs", { enabled: !!loadSettings().callDebugLogs }).catch
|
|
|
|
|
// Keyboard shortcuts
|
|
|
|
|
document.addEventListener("keydown", (e) => {
|
|
|
|
|
if ((e.target as HTMLElement).tagName === "INPUT") return;
|
|
|
|
|
if (e.key === "m") micBtn.click();
|
|
|
|
|
if (e.key === "q") hangupBtn.click();
|
|
|
|
|
if (e.key === "s") spkBtn.click();
|
|
|
|
|
if (e.key === "m") vdMicBtn.click();
|
|
|
|
|
if (e.key === "q") vdEndBtn.click();
|
|
|
|
|
if (e.key === "s") vdSpkBtn.click();
|
|
|
|
|
if (e.key === "," && (e.metaKey || e.ctrlKey)) { e.preventDefault(); openSettings(); }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|