1 Commits

Author SHA1 Message Date
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
4 changed files with 1751 additions and 901 deletions

View File

@@ -1016,10 +1016,16 @@ async fn main() -> anyhow::Result<()> {
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 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;
}

View File

@@ -11,71 +11,97 @@
</head>
<body>
<div id="app">
<!-- Connect screen -->
<div id="connect-screen">
<h1>WarzonePhone</h1>
<p class="subtitle">Encrypted Voice</p>
<div class="form">
<label>Relay
<button id="relay-selected" class="relay-selected" type="button">
<span id="relay-dot" class="dot"></span>
<span id="relay-label">Select relay...</span>
<span class="arrow">&#9881;</span>
</button>
</label>
<label>Room
<input id="room" type="text" value="general" />
</label>
<label>Alias
<input id="alias" type="text" placeholder="your name" />
</label>
<div class="form-row">
<label class="checkbox">
<input id="os-aec" type="checkbox" checked />
OS Echo Cancel
</label>
<button id="settings-btn-home" class="icon-btn" title="Settings (Cmd+,)">&#9881;</button>
</div>
<!-- Mode toggle -->
<div class="mode-toggle" style="display:flex;gap:8px;margin-bottom:8px;">
<button id="mode-room" class="mode-btn active" style="flex:1">Room</button>
<button id="mode-direct" class="mode-btn" style="flex:1">Direct Call</button>
</div>
<!-- ═══════════════════════════════════════════════════════
LOBBY — default view, auto-connects signal on launch
═══════════════════════════════════════════════════════ -->
<div id="lobby-screen">
<header class="lobby-header">
<div class="lobby-title-row">
<h1>WarzonePhone</h1>
<button id="settings-btn" class="icon-btn" title="Settings">&#9881;</button>
<!-- Room mode (default) -->
<div id="room-mode">
<button id="connect-btn" class="primary">Connect</button>
</div>
<div class="lobby-status-row">
<span id="lobby-dot" class="dot"></span>
<span id="lobby-relay-label" class="lobby-relay">Connecting...</span>
<span id="lobby-room-label" class="lobby-room">general</span>
</div>
<div class="lobby-identity">
<span id="lobby-identicon"></span>
<span id="lobby-fp" class="fp-display"></span>
</div>
</header>
<!-- User list -->
<div class="lobby-users-section">
<div class="lobby-users-header">
<span>Online</span>
<span id="lobby-user-count" class="badge">0</span>
</div>
<div id="lobby-user-list" class="lobby-user-list">
<div class="lobby-empty">No one else is here yet</div>
</div>
</div>
<!-- Direct call mode -->
<div id="direct-mode" class="hidden">
<button id="register-btn" class="primary" style="background:#2196F3">Register on Relay</button>
<div id="direct-registered" class="hidden" style="margin-top:12px">
<div class="direct-registered-header">
<p id="registered-status" style="color:var(--green);font-size:13px;margin:0">&#x2705; Registered — waiting for calls</p>
<button id="deregister-btn" class="secondary-btn small">Deregister</button>
</div>
<div id="incoming-call-panel" class="hidden" style="background:#1B5E20;padding:12px;border-radius:8px;margin:8px 0">
<p style="font-weight:bold;margin:0 0 4px 0">Incoming Call</p>
<p id="incoming-caller" style="font-size:12px;opacity:0.8;margin:0 0 8px 0">From: unknown</p>
<div style="display:flex;gap:8px">
<button id="accept-call-btn" style="flex:1;background:var(--green);color:white;border:none;padding:8px;border-radius:6px;cursor:pointer">Accept</button>
<button id="reject-call-btn" style="flex:1;background:var(--red);color:white;border:none;padding:8px;border-radius:6px;cursor:pointer">Reject</button>
</div>
</div>
<!-- Voice join FAB -->
<div class="lobby-fab-row">
<button id="join-voice-btn" class="fab" title="Join Voice Chat">
<span class="fab-icon">&#x1F3A7;</span>
<span class="fab-label">Join Voice</span>
</button>
</div>
<!-- Recent contacts -->
<div id="recent-contacts-section" class="hidden">
<div class="history-header">Recent contacts</div>
<div id="recent-contacts-list" class="history-list"></div>
</div>
<!-- Incoming call banner -->
<div id="incoming-call-banner" class="incoming-banner hidden">
<div class="incoming-info">
<span id="incoming-identicon" class="incoming-identicon"></span>
<div>
<div id="incoming-caller-name" class="incoming-name">Unknown</div>
<div class="incoming-subtitle">Incoming call...</div>
<!-- Call history -->
<div id="call-history-section" class="hidden">
<div class="history-header">
History
<button id="clear-history-btn" class="link-btn">clear</button>
</div>
<div id="call-history-list" class="history-list"></div>
</div>
<label style="margin-top:8px">Call by fingerprint
<input id="target-fp" type="text" placeholder="xxxx:xxxx:xxxx:..." />
</label>
<button id="call-btn" class="primary" style="margin-top:8px">Call</button>
<p id="call-status-text" style="color:var(--yellow);font-size:13px;margin-top:4px"></p>
</div>
</div>
<div class="incoming-actions">
<button id="accept-call-btn" class="btn-accept">Accept</button>
<button id="reject-call-btn" class="btn-reject">Reject</button>
</div>
<p id="connect-error" class="error"></p>
</div>
<div class="identity-info">
<span id="my-identicon"></span>
<span id="my-fingerprint" class="fp-display"></span>
</div>
<div class="recent-rooms" id="recent-rooms"></div>
</div>
<!-- ═══════════════════════════════════════════════════════
IN-CALL — voice active (room or direct)
═══════════════════════════════════════════════════════ -->
<!-- In-call screen -->
<div id="call-screen" class="hidden">
<div class="call-header">
<div class="call-header-row">
<button id="back-to-lobby-btn" class="icon-btn small" title="Back to lobby">&#x2190;</button>
<div id="room-name" class="room-name"></div>
<button id="settings-btn-call" class="icon-btn small" title="Settings">&#9881;</button>
<button id="settings-btn-call" class="icon-btn small" title="Settings (Cmd+,)">&#9881;</button>
</div>
<div class="call-meta">
<span id="call-status" class="status-dot"></span>
@@ -85,14 +111,16 @@
<div class="level-meter">
<div id="level-bar" class="level-bar-fill"></div>
</div>
<!-- Direct-call phone layout -->
<!-- Direct-call phone layout — shown instead of the group
participant list when directCallPeer is set. Centered
identicon, name, fp, connection badge. Hidden for
room calls (directCallPeer == null). -->
<div id="direct-call-view" class="direct-call-view hidden">
<div id="dc-identicon" class="dc-identicon"></div>
<div id="dc-name" class="dc-name">Unknown</div>
<div id="dc-fp" class="dc-fp"></div>
<div id="dc-badge" class="dc-badge">Connecting...</div>
</div>
<!-- Room participants -->
<div id="participants" class="participants"></div>
<div class="controls">
<button id="mic-btn" class="control-btn" title="Toggle Mic (m)">
@@ -108,29 +136,7 @@
<div id="stats" class="stats"></div>
</div>
<!-- ═══════════════════════════════════════════════════════
USER CONTEXT MENU (tap on user in lobby)
═══════════════════════════════════════════════════════ -->
<div id="user-context-menu" class="context-menu hidden">
<div class="context-header">
<span id="ctx-identicon" class="ctx-identicon"></span>
<div>
<div id="ctx-name" class="ctx-name">User</div>
<div id="ctx-fp" class="ctx-fp"></div>
</div>
</div>
<button id="ctx-call-btn" class="context-action">
<span>&#x1F4DE;</span> Direct Call
</button>
<button id="ctx-message-btn" class="context-action" disabled>
<span>&#x1F4AC;</span> Message (coming soon)
</button>
<button id="ctx-close-btn" class="context-action dim">Close</button>
</div>
<!-- ═══════════════════════════════════════════════════════
SETTINGS PANEL (overlay)
═══════════════════════════════════════════════════════ -->
<!-- Settings panel -->
<div id="settings-panel" class="hidden">
<div class="settings-card">
<div class="settings-header">
@@ -151,53 +157,28 @@
<div class="quality-control">
<div class="quality-header">
<span class="setting-label">QUALITY</span>
<span id="s-quality-label" class="quality-value">Auto</span>
<span id="s-quality-label" class="quality-label">Auto</span>
</div>
<input id="s-quality" type="range" min="0" max="6" step="1" value="6" />
<div class="quality-labels">
<span>Codec2 1.2k</span>
<input id="s-quality" type="range" min="0" max="7" step="1" value="3" class="quality-slider" />
<div class="quality-ticks">
<span>64k</span>
<span>48k</span>
<span>32k</span>
<span>Auto</span>
<span>24k</span>
<span>6k</span>
<span>C2</span>
<span>1.2k</span>
</div>
</div>
<label class="checkbox">
<input id="s-os-aec" type="checkbox" checked />
OS Echo Cancellation
<input id="s-os-aec" type="checkbox" />
OS Echo Cancellation (macOS VoiceProcessingIO)
</label>
<label class="checkbox">
<input id="s-agc" type="checkbox" checked />
Automatic Gain Control
</label>
</div>
<div class="settings-section">
<h3>Relays</h3>
<div id="s-relay-list"></div>
<div class="relay-add">
<input id="s-relay-name" type="text" placeholder="Name" style="flex:1" />
<input id="s-relay-addr" type="text" placeholder="host:port" style="flex:2" />
<button id="s-relay-add" class="secondary-btn small">Add</button>
</div>
</div>
<div class="settings-section">
<h3>Identity</h3>
<div>
<span class="setting-label">FINGERPRINT</span>
<div id="s-fingerprint" class="fp-display" style="margin-top:4px"></div>
</div>
<div style="margin-top:8px">
<span class="setting-label">IDENTITY FILE</span>
<div style="font-size:12px;opacity:0.6;margin-top:2px">~/.wzp/identity</div>
</div>
</div>
<div class="settings-section">
<h3>Network</h3>
<div>
<span class="setting-label">PUBLIC ADDRESS</span>
<span id="s-public-addr" style="color:var(--green);font-size:13px;margin-left:8px"></span>
<button id="s-reflect-btn" class="secondary-btn small" style="margin-left:8px">Detect</button>
</div>
<div style="margin-top:8px">
<button id="s-nat-detect-btn" class="secondary-btn" style="width:100%">Detect NAT</button>
<div id="s-nat-result" style="font-size:11px;margin-top:4px;opacity:0.7;white-space:pre-wrap"></div>
</div>
</div>
<div class="settings-section">
<h3>Debug</h3>
<label class="checkbox">
<input id="s-dred-debug" type="checkbox" />
DRED debug logs (verbose, dev only)
@@ -208,11 +189,11 @@
</label>
<label class="checkbox">
<input id="s-direct-only" type="checkbox" />
Direct-only mode (no relay fallback)
Direct-only mode (no relay fallback — fails if P2P can't connect)
</label>
<label class="checkbox">
<input id="s-birthday-attack" type="checkbox" />
Birthday attack (extra ports for hard NAT — adds ~3s)
Birthday attack (opens extra ports for hard NAT — adds ~3s to setup)
</label>
</div>
<div class="settings-section" id="s-call-debug-section" style="display:none">
@@ -224,8 +205,92 @@
<button id="s-call-debug-clear" class="secondary-btn" style="flex:1">Clear log</button>
</div>
<small id="s-call-debug-copy-status" style="display:block;margin-top:4px;color:var(--text-dim);font-size:10px"></small>
<small style="color:var(--text-dim);display:block;margin-top:4px">
Rolling buffer of the last 200 call-flow events. Turned off by
default — the GUI overlay only populates when the checkbox above
is on, but logcat (adb) always keeps a copy regardless.
</small>
</div>
<div class="settings-section">
<h3>Identity</h3>
<div class="setting-row">
<span class="setting-label">Fingerprint</span>
<span id="s-fingerprint" class="fp-display-large"></span>
</div>
<div class="setting-row">
<span class="setting-label">Identity file</span>
<span class="fp-display">~/.wzp/identity</span>
</div>
</div>
<div class="settings-section">
<h3>Network</h3>
<div class="setting-row">
<span class="setting-label">Public address</span>
<span id="s-reflected-addr" class="fp-display">(not queried)</span>
<button id="s-reflect-btn" class="secondary-btn">Detect</button>
</div>
<small style="color:var(--text-dim);display:block;margin-top:4px">
Asks the registered relay to echo back the IP:port it sees for this
connection (QUIC-native NAT reflection, replaces STUN).
</small>
<div class="setting-row" style="margin-top:10px">
<span class="setting-label">NAT type</span>
<span id="s-nat-type" class="fp-display">(not detected)</span>
<button id="s-nat-detect-btn" class="secondary-btn">Detect NAT</button>
</div>
<div id="s-nat-probes" style="margin-top:6px;font-size:11px;color:var(--text-dim)"></div>
<small style="color:var(--text-dim);display:block;margin-top:4px">
Probes every configured relay in parallel and compares the results
to classify the NAT: cone (P2P viable), symmetric (must relay),
multiple, or unknown.
</small>
</div>
<div class="settings-section">
<h3>Recent Rooms</h3>
<div id="s-recent-rooms" class="recent-rooms-list"></div>
<button id="s-clear-recent" class="secondary-btn">Clear History</button>
</div>
<button id="settings-save" class="primary">Save</button>
</div>
</div>
<!-- Manage Relays dialog -->
<div id="relay-dialog" class="hidden">
<div class="settings-card relay-dialog-card">
<div class="settings-header">
<h2>Manage Relays</h2>
<button id="relay-dialog-close" class="icon-btn">&times;</button>
</div>
<div id="relay-dialog-list" class="relay-dialog-list"></div>
<div class="relay-add-row">
<div class="relay-add-inputs">
<input id="relay-add-name" type="text" placeholder="Name" />
<input id="relay-add-addr" type="text" placeholder="host:port" />
</div>
<button id="relay-add-btn" class="primary">Add Relay</button>
</div>
</div>
</div>
<!-- Key changed warning dialog -->
<div id="key-warning" class="hidden">
<div class="settings-card key-warning-card">
<div class="key-warning-icon">&#9888;</div>
<h2>Server Key Changed</h2>
<p class="key-warning-text">The relay's identity has changed since you last connected. This usually happens when the server was restarted, but could also indicate a security issue.</p>
<div class="key-warning-fps">
<div class="key-fp-row">
<span class="key-fp-label">Previously known</span>
<code id="kw-old-fp" class="key-fp"></code>
</div>
<div class="key-fp-row">
<span class="key-fp-label">New key</span>
<code id="kw-new-fp" class="key-fp"></code>
</div>
</div>
<div class="key-warning-actions">
<button id="kw-accept" class="primary">Accept New Key</button>
<button id="kw-cancel" class="secondary-btn">Cancel</button>
</div>
<button id="settings-save" class="primary" style="margin-top:12px">Save</button>
</div>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -32,333 +32,7 @@ body {
.hidden { display: none !important; }
/* ── Lobby screen (IRC-style) ── */
#lobby-screen {
display: flex;
flex-direction: column;
flex: 1;
gap: 0;
max-width: 480px;
margin: 0 auto;
width: 100%;
}
.lobby-header {
padding: 12px 0;
border-bottom: 1px solid var(--surface2);
}
.lobby-title-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.lobby-title-row h1 {
font-size: 20px;
font-weight: 700;
letter-spacing: 0.5px;
}
.lobby-status-row {
display: flex;
align-items: center;
gap: 6px;
margin-top: 6px;
font-size: 12px;
color: var(--text-dim);
}
.lobby-relay { opacity: 0.7; }
.lobby-room { color: var(--green); font-weight: 500; }
.lobby-identity {
display: flex;
align-items: center;
gap: 6px;
margin-top: 6px;
font-size: 11px;
opacity: 0.5;
}
/* User list */
.lobby-users-section {
flex: 1;
display: flex;
flex-direction: column;
margin-top: 8px;
min-height: 0;
}
.lobby-users-header {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 0;
font-size: 13px;
font-weight: 600;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 1px;
}
.badge {
background: var(--surface2);
color: var(--text-dim);
font-size: 11px;
padding: 1px 7px;
border-radius: 10px;
font-weight: 600;
}
.lobby-user-list {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 2px;
}
.lobby-empty {
color: var(--text-dim);
font-size: 13px;
text-align: center;
padding: 40px 20px;
opacity: 0.6;
}
/* Single user row */
.user-row {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: 8px;
cursor: pointer;
transition: background 0.15s;
}
.user-row:hover, .user-row:active {
background: var(--surface);
}
.user-identicon {
width: 36px;
height: 36px;
border-radius: 50%;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
.user-info {
flex: 1;
min-width: 0;
}
.user-name {
font-size: 14px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.user-fp {
font-size: 10px;
color: var(--text-dim);
font-family: ui-monospace, monospace;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.user-status {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 4px;
}
.user-status-icon {
font-size: 16px;
}
/* Speaking indicator */
.user-row.speaking {
background: rgba(74, 222, 128, 0.08);
}
.user-row.speaking .user-name {
color: var(--green);
}
/* In-voice indicator */
.user-row.in-voice .user-status-icon {
color: var(--green);
}
/* Voice join FAB */
.lobby-fab-row {
padding: 12px 0;
display: flex;
justify-content: center;
}
.fab {
display: flex;
align-items: center;
gap: 8px;
background: var(--green);
color: #111;
border: none;
padding: 12px 28px;
border-radius: 24px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
box-shadow: 0 4px 16px rgba(74, 222, 128, 0.3);
transition: transform 0.15s, box-shadow 0.15s;
}
.fab:hover {
transform: scale(1.03);
box-shadow: 0 6px 20px rgba(74, 222, 128, 0.4);
}
.fab:active {
transform: scale(0.97);
}
.fab.active {
background: var(--red);
box-shadow: 0 4px 16px rgba(239, 68, 68, 0.3);
}
.fab-icon { font-size: 18px; }
/* Incoming call banner */
.incoming-banner {
position: fixed;
bottom: 20px;
left: 20px;
right: 20px;
max-width: 440px;
margin: 0 auto;
background: var(--surface);
border: 1px solid var(--green);
border-radius: 16px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
z-index: 100;
animation: slideUp 0.3s ease-out;
}
@keyframes slideUp {
from { transform: translateY(100%); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.incoming-info {
display: flex;
align-items: center;
gap: 12px;
}
.incoming-identicon { width: 40px; height: 40px; border-radius: 50%; }
.incoming-name { font-weight: 600; font-size: 15px; }
.incoming-subtitle { font-size: 12px; color: var(--green); }
.incoming-actions {
display: flex;
gap: 8px;
}
.btn-accept {
flex: 1;
background: var(--green);
color: #111;
border: none;
padding: 10px;
border-radius: 10px;
font-weight: 600;
cursor: pointer;
}
.btn-reject {
flex: 1;
background: var(--red);
color: white;
border: none;
padding: 10px;
border-radius: 10px;
font-weight: 600;
cursor: pointer;
}
/* Context menu */
.context-menu {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--surface);
border: 1px solid var(--surface2);
border-radius: 16px;
padding: 20px;
min-width: 260px;
z-index: 200;
box-shadow: 0 16px 48px rgba(0,0,0,0.6);
}
.context-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid var(--surface2);
}
.ctx-identicon { width: 40px; height: 40px; border-radius: 50%; }
.ctx-name { font-weight: 600; font-size: 15px; }
.ctx-fp { font-size: 10px; color: var(--text-dim); font-family: monospace; }
.context-action {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
background: none;
border: none;
color: var(--text);
padding: 10px 8px;
border-radius: 8px;
font-size: 14px;
cursor: pointer;
text-align: left;
}
.context-action:hover:not(:disabled) {
background: var(--surface2);
}
.context-action:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.context-action.dim {
color: var(--text-dim);
font-size: 13px;
}
/* Legacy compat — keep old connect-screen ID working for JS that
references it (the old connect screen is now the lobby). */
/* ── Connect screen ── */
#connect-screen {
display: flex;
flex-direction: column;