10 Commits

Author SHA1 Message Date
Siavash Sameni
217567383d fix(ui): timestamps in logs, proper call debounce, no cross-calling
- Copy/Share log now includes HH:MM:SS timestamps
- callInProgress stays true until call resolves (setup or hangup),
  preventing multiple taps from firing multiple place_call offers
- Block place_call when there's a pending incoming call
- leaveVoice clears all call state (callInProgress, pendingCallId)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:16:20 +04:00
Siavash Sameni
98ed981805 fix(ui): self-call prevention, debounce, codec in stats
- 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) <noreply@anthropic.com>
2026-04-14 19:10:31 +04:00
Siavash Sameni
01a3133544 fix(ui): drawer buttons, stats fields, nicknames
- Buttons: use text labels (Mic/Spk/End) instead of emoji HTML
  entities that rendered as raw text on Android WebView
- Stats: match Rust CallStatus fields (tx_codec, rx_codec,
  encode_fps, recv_fps, audio_level, spk_muted)
- Nicknames: register_signal sends derive_alias() as the alias
  so other users see "Brave Falcon" instead of "a525:e9b2:..."
- Lobby header shows alias from get_app_info instead of raw fp
- pollStatus uses correct field names from Rust struct

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:00:09 +04:00
Siavash Sameni
25471c694f feat(ui): voice drawer replaces full-screen call UI
Discord-style bottom drawer for voice instead of navigating away:

- "Join Voice" hides the FAB, slides up a persistent bottom bar
- Drawer shows: room name, timer, P2P/Relay badge, level meter
- Controls: mic, speaker, end call — all in the drawer
- Direct call info (identicon, name, P2P badge) shown inline
- Lobby stays visible above the drawer at all times
- Stats line shows codec/packet/FEC info
- Leave voice = drawer slides away, FAB returns

Removed: full-screen call-screen, back button, old participant
list, old mic/speaker/hangup buttons. All voice interaction
happens in the 15% bottom drawer while the lobby stays live.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 18:47:40 +04:00
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
cc23e829b2 feat(ui): handle PresenceList in lobby — show online users
The lobby now populates from PresenceList signal events:
- Relay broadcasts user list on register/deregister
- JS receives "presence_list" signal-event
- Updates lobbyUsers map (excluding self)
- Renders user rows with identicon, name, fingerprint

Users appear in the lobby as soon as they register their
signal channel — no need to join voice first.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 18:13:45 +04:00
Siavash Sameni
18c204c1ff merge main: PresenceList signal for lobby 2026-04-14 18:13:15 +04:00
Siavash Sameni
7e7391fdbb feat(ui): lobby-first main.ts rewrite for experimental-ui
Complete JS rewrite for IRC-style lobby flow:

- Auto-connect signal channel on app launch (no connect button)
- Lobby shows online users with identicon, name, voice status
- "Join Voice" FAB toggles room voice on/off
- Tap user → context menu → Direct Call
- Incoming call banner slides up from bottom
- Back button returns from call to lobby
- Settings panel preserved with all debug toggles

~500 lines (down from 1786) — focused on the lobby experience.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 17:52:51 +04:00
Siavash Sameni
aa0362f318 feat(ui): lobby-first HTML/CSS layout for experimental-ui
New IRC-style lobby layout:
- Auto-connect on launch, drop into user list
- User rows with identicon, name, fingerprint, voice status
- Speaking indicator (green highlight + pulsing)
- Join Voice FAB (green, toggles to Leave/red)
- Incoming call banner (slides up from bottom)
- User context menu (tap user → Call / Message)
- Settings panel preserved from original

The old connect-screen HTML is removed. The call-screen is kept
intact. JS adaptation next.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 17:43:15 +04:00
4 changed files with 1121 additions and 1807 deletions

View File

@@ -11,132 +11,119 @@
</head> </head>
<body> <body>
<div id="app"> <div id="app">
<!-- Connect screen -->
<div id="connect-screen"> <!-- ═══════════════════════════════════════════════════════
<h1>WarzonePhone</h1> LOBBY — default view, auto-connects signal on launch
<p class="subtitle">Encrypted Voice</p> ═══════════════════════════════════════════════════════ -->
<div class="form"> <div id="lobby-screen">
<label>Relay <header class="lobby-header">
<button id="relay-selected" class="relay-selected" type="button"> <div class="lobby-title-row">
<span id="relay-dot" class="dot"></span> <h1>WarzonePhone</h1>
<span id="relay-label">Select relay...</span> <button id="settings-btn" class="icon-btn" title="Settings">&#9881;</button>
<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> </div>
<!-- Mode toggle --> <div class="lobby-status-row">
<div class="mode-toggle" style="display:flex;gap:8px;margin-bottom:8px;"> <span id="lobby-dot" class="dot"></span>
<button id="mode-room" class="mode-btn active" style="flex:1">Room</button> <span id="lobby-relay-label" class="lobby-relay">Connecting...</span>
<button id="mode-direct" class="mode-btn" style="flex:1">Direct Call</button> <span id="lobby-room-label" class="lobby-room">general</span>
</div> </div>
<div class="lobby-identity">
<!-- Room mode (default) --> <span id="lobby-identicon"></span>
<div id="room-mode"> <span id="lobby-fp" class="fp-display"></span>
<button id="connect-btn" class="primary">Connect</button>
</div> </div>
</header>
<!-- Direct call mode --> <!-- User list -->
<div id="direct-mode" class="hidden"> <div class="lobby-users-section">
<button id="register-btn" class="primary" style="background:#2196F3">Register on Relay</button> <div class="lobby-users-header">
<div id="direct-registered" class="hidden" style="margin-top:12px"> <span>Online</span>
<div class="direct-registered-header"> <span id="lobby-user-count" class="badge">0</span>
<p id="registered-status" style="color:var(--green);font-size:13px;margin:0">&#x2705; Registered — waiting for calls</p> </div>
<button id="deregister-btn" class="secondary-btn small">Deregister</button> <div id="lobby-user-list" class="lobby-user-list">
</div> <div class="lobby-empty">No one else is here yet</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> </div>
<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>
<!-- Recent contacts --> <!-- Voice join FAB -->
<div id="recent-contacts-section" class="hidden"> <div class="lobby-fab-row">
<div class="history-header">Recent contacts</div> <button id="join-voice-btn" class="fab" title="Join Voice Chat">
<div id="recent-contacts-list" class="history-list"></div> <span class="fab-icon">&#x1F3A7;</span>
</div> <span class="fab-label">Join Voice</span>
</button>
</div>
<!-- Call history --> <!-- Incoming call banner -->
<div id="call-history-section" class="hidden"> <div id="incoming-call-banner" class="incoming-banner hidden">
<div class="history-header"> <div class="incoming-info">
History <span id="incoming-identicon" class="incoming-identicon"></span>
<button id="clear-history-btn" class="link-btn">clear</button> <div>
</div> <div id="incoming-caller-name" class="incoming-name">Unknown</div>
<div id="call-history-list" class="history-list"></div> <div class="incoming-subtitle">Incoming call...</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">
<p id="connect-error" class="error"></p> <button id="accept-call-btn" class="btn-accept">Accept</button>
</div> <button id="reject-call-btn" class="btn-reject">Reject</button>
<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 screen -->
<div id="call-screen" class="hidden">
<div class="call-header">
<div class="call-header-row">
<div id="room-name" class="room-name"></div>
<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>
<span id="call-timer" class="call-timer">0:00</span>
</div> </div>
</div> </div>
<div class="level-meter"> <!-- ═════ Voice Drawer (bottom bar, stays on lobby) ═════ -->
<div id="level-bar" class="level-bar-fill"></div> <div id="voice-drawer" class="voice-drawer hidden">
<div class="voice-drawer-bar" id="voice-drawer-bar">
<div class="vd-info">
<span id="vd-status" class="vd-status-dot"></span>
<span id="vd-room" class="vd-room">general</span>
<span id="vd-timer" class="vd-timer">0:00</span>
<span id="vd-badge" class="vd-badge hidden"></span>
</div>
<div class="vd-level">
<div id="vd-level-bar" class="vd-level-fill"></div>
</div>
<div class="vd-controls">
<button id="vd-mic-btn" class="vd-btn" title="Mic (m)">
<span id="vd-mic-icon">Mic</span>
</button>
<button id="vd-spk-btn" class="vd-btn" title="Speaker (s)">
<span id="vd-spk-icon">Spk</span>
</button>
<button id="vd-end-btn" class="vd-btn vd-end" title="Leave voice (q)">
<span>End</span>
</button>
</div>
</div>
<!-- Direct call info (shown during P2P calls) -->
<div id="vd-direct-info" class="vd-direct-info hidden">
<span id="vd-dc-identicon" class="vd-dc-identicon"></span>
<div class="vd-dc-details">
<div id="vd-dc-name" class="vd-dc-name">Unknown</div>
<div id="vd-dc-badge" class="vd-dc-badge">Connecting...</div>
</div>
</div>
<div id="vd-stats" class="vd-stats"></div>
</div> </div>
<!-- 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>
<div id="participants" class="participants"></div>
<div class="controls">
<button id="mic-btn" class="control-btn" title="Toggle Mic (m)">
<span class="icon" id="mic-icon">Mic</span>
</button>
<button id="hangup-btn" class="control-btn hangup" title="Hang Up (q)">
<span class="icon">End</span>
</button>
<button id="spk-btn" class="control-btn" title="Toggle Speaker (s)">
<span class="icon" id="spk-icon">Spk</span>
</button>
</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">
@@ -157,28 +144,53 @@
<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-label">Auto</span> <span id="s-quality-label" class="quality-value">Auto</span>
</div> </div>
<input id="s-quality" type="range" min="0" max="7" step="1" value="3" class="quality-slider" /> <input id="s-quality" type="range" min="0" max="6" step="1" value="6" />
<div class="quality-ticks"> <div class="quality-labels">
<span>64k</span> <span>Codec2 1.2k</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" /> <input id="s-os-aec" type="checkbox" checked />
OS Echo Cancellation (macOS VoiceProcessingIO) OS Echo Cancellation
</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)
@@ -189,11 +201,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 — fails if P2P can't connect) Direct-only mode (no relay fallback)
</label> </label>
<label class="checkbox"> <label class="checkbox">
<input id="s-birthday-attack" type="checkbox" /> <input id="s-birthday-attack" type="checkbox" />
Birthday attack (opens extra ports for hard NAT — adds ~3s to setup) Birthday attack (extra ports for hard NAT — adds ~3s)
</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">
@@ -205,92 +217,8 @@
<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>

View File

@@ -1215,8 +1215,11 @@ fn do_register_signal(
let transport = Arc::new(wzp_transport::QuinnTransport::new(conn)); let transport = Arc::new(wzp_transport::QuinnTransport::new(conn));
emit_call_debug(&app, "register_signal:quic_connected", serde_json::json!({ "relay": relay })); emit_call_debug(&app, "register_signal:quic_connected", serde_json::json!({ "relay": relay }));
// Send alias from seed-derived adjective+noun so other
// users see a friendly name in the lobby.
let alias = derive_alias(&seed);
transport.send_signal(&SignalMessage::RegisterPresence { transport.send_signal(&SignalMessage::RegisterPresence {
identity_pub, signature: vec![], alias: None, identity_pub, signature: vec![], alias: Some(alias),
}).await.map_err(|e| format!("{e}"))?; }).await.map_err(|e| format!("{e}"))?;
emit_call_debug(&app, "register_signal:register_presence_sent", serde_json::json!({})); emit_call_debug(&app, "register_signal:register_presence_sent", serde_json::json!({}));

File diff suppressed because it is too large Load Diff

View File

@@ -32,7 +32,401 @@ body {
.hidden { display: none !important; } .hidden { display: none !important; }
/* ── Connect screen ── */ /* ── 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; }
/* ── Voice Drawer (bottom bar) ── */
.voice-drawer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: var(--surface);
border-top: 1px solid var(--surface2);
padding: 0 16px;
padding-bottom: env(safe-area-inset-bottom, 8px);
z-index: 50;
animation: drawerUp 0.25s ease-out;
box-shadow: 0 -4px 20px rgba(0,0,0,0.4);
}
@keyframes drawerUp {
from { transform: translateY(100%); }
to { transform: translateY(0); }
}
.voice-drawer-bar {
display: flex; flex-direction: column; gap: 6px; padding: 10px 0 6px;
}
.vd-info {
display: flex; align-items: center; gap: 8px; font-size: 13px;
}
.vd-status-dot {
width: 8px; height: 8px; border-radius: 50%; background: var(--green); flex-shrink: 0;
}
.vd-room { color: var(--green); font-weight: 600; }
.vd-timer { color: var(--text-dim); font-family: ui-monospace, monospace; font-size: 12px; }
.vd-badge {
font-size: 10px; padding: 1px 6px; border-radius: 6px; font-weight: 500;
}
.vd-badge.direct { background: rgba(74,222,128,0.15); color: var(--green); }
.vd-badge.relay { background: rgba(96,165,250,0.15); color: #60a5fa; }
.vd-level { height: 3px; background: var(--surface2); border-radius: 2px; overflow: hidden; }
.vd-level-fill {
height: 100%; width: 0%; background: var(--green); border-radius: 2px; transition: width 0.1s;
}
.vd-controls {
display: flex; align-items: center; justify-content: center; gap: 12px; padding: 4px 0;
}
.vd-btn {
width: 44px; height: 44px; border-radius: 50%; border: none;
background: var(--surface2); color: var(--text); font-size: 18px;
cursor: pointer; display: flex; align-items: center; justify-content: center;
transition: background 0.15s;
}
.vd-btn:hover { background: var(--primary); }
.vd-btn.muted { background: var(--red); color: white; }
.vd-end { background: var(--red); color: white; }
.vd-end:hover { background: #dc2626; }
.vd-direct-info {
display: flex; align-items: center; gap: 10px; padding: 8px 0 4px;
border-top: 1px solid var(--surface2); margin-top: 4px;
}
.vd-dc-identicon { width: 32px; height: 32px; border-radius: 50%; flex-shrink: 0; }
.vd-dc-name { font-size: 13px; font-weight: 600; }
.vd-dc-badge {
font-size: 10px; padding: 1px 6px; border-radius: 6px;
}
.vd-dc-badge.direct { background: rgba(74,222,128,0.15); color: var(--green); }
.vd-dc-badge.relay { background: rgba(96,165,250,0.15); color: #60a5fa; }
.vd-dc-badge.connecting { background: rgba(250,204,21,0.15); color: var(--yellow); }
.vd-stats {
font-size: 10px; color: var(--text-dim); font-family: ui-monospace, monospace;
padding: 2px 0 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* 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;