2 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
Siavash Sameni
1120c7b579 feat(signal): PresenceList broadcast for lobby user discovery
Some checks failed
Build Release Binaries / build-amd64 (push) Failing after 7m21s
Mirror to GitHub / mirror (push) Failing after 27s
New signal infrastructure for the lobby-first UI:

- PresenceUser struct: { fingerprint, alias }
- SignalMessage::PresenceList: relay broadcasts full user list
  to all signal clients on every register/deregister
- SignalHub::presence_list(): builds the list from connected clients
- SignalHub::broadcast(): sends to ALL signal clients
- Relay calls broadcast on register + unregister
- Desktop emits "presence_list" signal-event to JS frontend

This gives clients real-time visibility of who's online via the
signal channel, without needing to join a voice room first.

603 tests pass, 0 regressions.

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

View File

@@ -138,6 +138,7 @@ pub fn signal_to_call_type(signal: &SignalMessage) -> CallSignalType {
| SignalMessage::UpgradeResponse { .. }
| SignalMessage::UpgradeConfirm { .. }
| SignalMessage::QualityCapability { .. } => CallSignalType::Offer, // quality negotiation
SignalMessage::PresenceList { .. } => CallSignalType::Offer, // lobby presence
SignalMessage::QualityDirective { .. } => CallSignalType::Offer, // relay-initiated
}
}

View File

@@ -27,7 +27,7 @@ pub use codec_id::{CodecId, QualityProfile};
pub use error::*;
pub use packet::{
CallAcceptMode, HangupReason, MediaHeader, MediaPacket, MiniFrameContext, MiniHeader,
QualityReport, RoomParticipant, SignalMessage, TrunkEntry, TrunkFrame, FRAME_TYPE_FULL,
PresenceUser, QualityReport, RoomParticipant, SignalMessage, TrunkEntry, TrunkFrame, FRAME_TYPE_FULL,
FRAME_TYPE_MINI,
};
pub use bandwidth::{BandwidthEstimator, CongestionState};

View File

@@ -156,6 +156,14 @@ impl MediaHeader {
}
}
/// A user visible in the signal presence list.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PresenceUser {
pub fingerprint: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub alias: Option<String>,
}
/// Quality report appended to a media packet when Q flag is set (4 bytes).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct QualityReport {
@@ -1020,6 +1028,16 @@ pub enum SignalMessage {
reason: Option<String>,
},
// ── Signal presence ───────────────────────────────────────────
/// Relay broadcasts the list of currently registered signal
/// users to all connected clients. Sent on every register/
/// deregister so clients can maintain a live lobby user list.
PresenceList {
/// List of online users. Each entry is { fingerprint, alias }.
users: Vec<PresenceUser>,
},
// ── Quality upgrade negotiation (#28, #29) ──────────────────
/// Peer proposes upgrading to a higher quality profile.

View File

@@ -1016,6 +1016,19 @@ async fn main() -> anyhow::Result<()> {
info!(%addr, fingerprint = %client_fp, alias = ?client_alias, "signal client registered");
// 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;
}
// Signal recv loop
loop {
match transport.recv_signal().await {
@@ -1560,6 +1573,9 @@ async fn main() -> anyhow::Result<()> {
{
let mut hub = signal_hub.lock().await;
hub.unregister(&client_fp);
// Broadcast updated presence to remaining clients
let presence_msg = hub.presence_list();
hub.broadcast(&presence_msg).await;
}
{
let mut reg = presence.lock().await;

View File

@@ -86,6 +86,26 @@ impl SignalHub {
pub fn alias(&self, fp: &str) -> Option<&str> {
self.clients.get(fp).and_then(|c| c.alias.as_deref())
}
/// Build a PresenceList message with all online users.
pub fn presence_list(&self) -> SignalMessage {
let users: Vec<wzp_proto::PresenceUser> = self
.clients
.values()
.map(|c| wzp_proto::PresenceUser {
fingerprint: c.fingerprint.clone(),
alias: c.alias.clone(),
})
.collect();
SignalMessage::PresenceList { users }
}
/// Broadcast a message to ALL connected signal clients.
pub async fn broadcast(&self, msg: &SignalMessage) {
for client in self.clients.values() {
let _ = client.transport.send_signal(msg).await;
}
}
}
#[cfg(test)]

View File

@@ -1451,6 +1451,20 @@ fn do_register_signal(
});
}
}
Ok(Some(SignalMessage::PresenceList { users })) => {
tracing::info!(count = users.len(), "signal: PresenceList received");
// Emit to JS frontend for lobby user list
let user_list: Vec<serde_json::Value> = users.iter().map(|u| {
serde_json::json!({
"fingerprint": u.fingerprint,
"alias": u.alias,
})
}).collect();
let _ = app_clone.emit("signal-event", serde_json::json!({
"type": "presence_list",
"users": user_list,
}));
}
Ok(Some(SignalMessage::UpgradeProposal { call_id, proposal_id, proposed_profile, local_loss_pct, local_rtt_ms })) => {
tracing::info!(%call_id, %proposal_id, ?proposed_profile, "signal: UpgradeProposal from peer");
emit_call_debug(&app_clone, "recv:UpgradeProposal", serde_json::json!({