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"); 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

@@ -11,71 +11,97 @@
</head> </head>
<body> <body>
<div id="app"> <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>
<!-- ═══════════════════════════════════════════════════════ <!-- Room mode (default) -->
LOBBY — default view, auto-connects signal on launch <div id="room-mode">
═══════════════════════════════════════════════════════ --> <button id="connect-btn" class="primary">Connect</button>
<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>
</div> </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 --> <!-- Direct call mode -->
<div class="lobby-users-section"> <div id="direct-mode" class="hidden">
<div class="lobby-users-header"> <button id="register-btn" class="primary" style="background:#2196F3">Register on Relay</button>
<span>Online</span> <div id="direct-registered" class="hidden" style="margin-top:12px">
<span id="lobby-user-count" class="badge">0</span> <div class="direct-registered-header">
</div> <p id="registered-status" style="color:var(--green);font-size:13px;margin:0">&#x2705; Registered — waiting for calls</p>
<div id="lobby-user-list" class="lobby-user-list"> <button id="deregister-btn" class="secondary-btn small">Deregister</button>
<div class="lobby-empty">No one else is here yet</div> </div>
</div> <div id="incoming-call-panel" class="hidden" style="background:#1B5E20;padding:12px;border-radius:8px;margin:8px 0">
</div> <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 --> <!-- Recent contacts -->
<div class="lobby-fab-row"> <div id="recent-contacts-section" class="hidden">
<button id="join-voice-btn" class="fab" title="Join Voice Chat"> <div class="history-header">Recent contacts</div>
<span class="fab-icon">&#x1F3A7;</span> <div id="recent-contacts-list" class="history-list"></div>
<span class="fab-label">Join Voice</span> </div>
</button>
</div>
<!-- Incoming call banner --> <!-- Call history -->
<div id="incoming-call-banner" class="incoming-banner hidden"> <div id="call-history-section" class="hidden">
<div class="incoming-info"> <div class="history-header">
<span id="incoming-identicon" class="incoming-identicon"></span> History
<div> <button id="clear-history-btn" class="link-btn">clear</button>
<div id="incoming-caller-name" class="incoming-name">Unknown</div> </div>
<div class="incoming-subtitle">Incoming call...</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> </div>
<div class="incoming-actions">
<button id="accept-call-btn" class="btn-accept">Accept</button> <p id="connect-error" class="error"></p>
<button id="reject-call-btn" class="btn-reject">Reject</button>
</div>
</div> </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> </div>
<!-- ═══════════════════════════════════════════════════════ <!-- In-call screen -->
IN-CALL — voice active (room or direct)
═══════════════════════════════════════════════════════ -->
<div id="call-screen" class="hidden"> <div id="call-screen" class="hidden">
<div class="call-header"> <div class="call-header">
<div class="call-header-row"> <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> <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>
<div class="call-meta"> <div class="call-meta">
<span id="call-status" class="status-dot"></span> <span id="call-status" class="status-dot"></span>
@@ -85,14 +111,16 @@
<div class="level-meter"> <div class="level-meter">
<div id="level-bar" class="level-bar-fill"></div> <div id="level-bar" class="level-bar-fill"></div>
</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="direct-call-view" class="direct-call-view hidden">
<div id="dc-identicon" class="dc-identicon"></div> <div id="dc-identicon" class="dc-identicon"></div>
<div id="dc-name" class="dc-name">Unknown</div> <div id="dc-name" class="dc-name">Unknown</div>
<div id="dc-fp" class="dc-fp"></div> <div id="dc-fp" class="dc-fp"></div>
<div id="dc-badge" class="dc-badge">Connecting...</div> <div id="dc-badge" class="dc-badge">Connecting...</div>
</div> </div>
<!-- Room participants -->
<div id="participants" class="participants"></div> <div id="participants" class="participants"></div>
<div class="controls"> <div class="controls">
<button id="mic-btn" class="control-btn" title="Toggle Mic (m)"> <button id="mic-btn" class="control-btn" title="Toggle Mic (m)">
@@ -108,29 +136,7 @@
<div id="stats" class="stats"></div> <div id="stats" class="stats"></div>
</div> </div>
<!-- ═══════════════════════════════════════════════════════ <!-- Settings panel -->
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)
═══════════════════════════════════════════════════════ -->
<div id="settings-panel" class="hidden"> <div id="settings-panel" class="hidden">
<div class="settings-card"> <div class="settings-card">
<div class="settings-header"> <div class="settings-header">
@@ -151,53 +157,28 @@
<div class="quality-control"> <div class="quality-control">
<div class="quality-header"> <div class="quality-header">
<span class="setting-label">QUALITY</span> <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> </div>
<input id="s-quality" type="range" min="0" max="6" step="1" value="6" /> <input id="s-quality" type="range" min="0" max="7" step="1" value="3" class="quality-slider" />
<div class="quality-labels"> <div class="quality-ticks">
<span>Codec2 1.2k</span> <span>64k</span>
<span>48k</span>
<span>32k</span>
<span>Auto</span> <span>Auto</span>
<span>24k</span>
<span>6k</span>
<span>C2</span>
<span>1.2k</span>
</div> </div>
</div> </div>
<label class="checkbox"> <label class="checkbox">
<input id="s-os-aec" type="checkbox" checked /> <input id="s-os-aec" type="checkbox" />
OS Echo Cancellation OS Echo Cancellation (macOS VoiceProcessingIO)
</label>
<label class="checkbox">
<input id="s-agc" type="checkbox" checked />
Automatic Gain Control
</label> </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"> <label class="checkbox">
<input id="s-dred-debug" type="checkbox" /> <input id="s-dred-debug" type="checkbox" />
DRED debug logs (verbose, dev only) DRED debug logs (verbose, dev only)
@@ -208,11 +189,11 @@
</label> </label>
<label class="checkbox"> <label class="checkbox">
<input id="s-direct-only" type="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>
<label class="checkbox"> <label class="checkbox">
<input id="s-birthday-attack" type="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> </label>
</div> </div>
<div class="settings-section" id="s-call-debug-section" style="display:none"> <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> <button id="s-call-debug-clear" class="secondary-btn" style="flex:1">Clear log</button>
</div> </div>
<small id="s-call-debug-copy-status" style="display:block;margin-top:4px;color:var(--text-dim);font-size:10px"></small> <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> </div>
<button id="settings-save" class="primary" style="margin-top:12px">Save</button>
</div> </div>
</div> </div>
</div> </div>

File diff suppressed because it is too large Load Diff

View File

@@ -32,333 +32,7 @@ body {
.hidden { display: none !important; } .hidden { display: none !important; }
/* ── Lobby screen (IRC-style) ── */ /* ── Connect screen ── */
#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; display: flex;
flex-direction: column; flex-direction: column;