3 Commits

Author SHA1 Message Date
Siavash Sameni
a058a83c91 feat(ui): relay list management in settings
Settings now shows relay list with:
- Visual list of all configured relays
- Active relay highlighted in green with "ACTIVE" badge
- Tap a relay to switch (deregisters + reconnects automatically)
- X button to remove a relay (keeps at least 1)
- Add relay with name + address inputs
- Reconnect flow: deregister → clear lobby → auto-connect to new relay

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 18:37:58 +04:00
Siavash Sameni
9b8013ba7f merge main: PresenceList direct send fix 2026-04-14 18:36:01 +04:00
Siavash Sameni
defd8eab07 fix(signal): send PresenceList directly to new client after ack
Some checks failed
Mirror to GitHub / mirror (push) Failing after 24s
Build Release Binaries / build-amd64 (push) Failing after 3m50s
The broadcast alone wasn't reaching the first client because its
recv loop hadn't started yet when the second client registered.
Now the relay sends PresenceList directly to the new client (right
after RegisterPresenceAck) AND broadcasts to all others.

This guarantees every client gets the full user list:
- New client: via direct send (queued before recv loop starts)
- Existing clients: via broadcast

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 18:20:37 +04:00
2 changed files with 90 additions and 1 deletions

View File

@@ -1016,10 +1016,16 @@ async fn main() -> anyhow::Result<()> {
info!(%addr, fingerprint = %client_fp, alias = ?client_alias, "signal client registered"); info!(%addr, fingerprint = %client_fp, alias = ?client_alias, "signal client registered");
// Broadcast updated presence to all signal clients // Send the full presence list directly to the new
// client (guaranteed delivery — their recv loop is
// about to start). Then broadcast to all OTHER
// clients so they learn about the new user.
{ {
let hub = signal_hub.lock().await; let hub = signal_hub.lock().await;
let presence = hub.presence_list(); let presence = hub.presence_list();
// Direct send to new client (arrives right after ack)
let _ = transport.send_signal(&presence).await;
// Broadcast to everyone else
hub.broadcast(&presence).await; hub.broadcast(&presence).await;
} }

View File

@@ -90,6 +90,12 @@ const ctxName = document.getElementById("ctx-name")!;
const ctxFp = document.getElementById("ctx-fp")!; const ctxFp = document.getElementById("ctx-fp")!;
const ctxCallBtn = document.getElementById("ctx-call-btn")!; const ctxCallBtn = document.getElementById("ctx-call-btn")!;
const ctxCloseBtn = document.getElementById("ctx-close-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 // Settings
const settingsPanel = document.getElementById("settings-panel")!; const settingsPanel = document.getElementById("settings-panel")!;
const settingsBtn = document.getElementById("settings-btn")!; const settingsBtn = document.getElementById("settings-btn")!;
@@ -547,6 +553,80 @@ listen("call-event", (event: any) => {
}); });
// ── Settings ────────────────────────────────────────────────────── // ── 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">&times;</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() { function openSettings() {
const s = loadSettings(); const s = loadSettings();
sRoom.value = s.room; sRoom.value = s.room;
@@ -562,6 +642,7 @@ function openSettings() {
sQuality.value = String(qi); sQuality.value = String(qi);
updateQualityUI(qi); updateQualityUI(qi);
sFingerprint.textContent = myFingerprint || "(loading...)"; sFingerprint.textContent = myFingerprint || "(loading...)";
renderRelayList();
settingsPanel.classList.remove("hidden"); settingsPanel.classList.remove("hidden");
} }
@@ -584,6 +665,8 @@ settingsSave.addEventListener("click", () => {
invoke("set_dred_verbose_logs", { enabled: s.dredDebugLogs }).catch(() => {}); invoke("set_dred_verbose_logs", { enabled: s.dredDebugLogs }).catch(() => {});
invoke("set_call_debug_logs", { enabled: s.callDebugLogs }).catch(() => {}); invoke("set_call_debug_logs", { enabled: s.callDebugLogs }).catch(() => {});
sCallDebugSection.style.display = s.callDebugLogs ? "" : "none"; sCallDebugSection.style.display = s.callDebugLogs ? "" : "none";
// Update lobby room label
lobbyRoomLabel.textContent = s.room || "general";
settingsPanel.classList.add("hidden"); settingsPanel.classList.add("hidden");
}); });