diff --git a/src/core/mod.rs b/src/core/mod.rs index 24c8783..d929f98 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -7,7 +7,7 @@ use crate::audio::eq::{Eq, EqSettings}; use crate::codec::{AudioEncoder, opus_impl::OpusEncoder}; use crate::core::jitter::{JitterBuffer, FRAME_SAMPLES}; use crate::network::{ - NetworkTransport, RoomState, PeerState, RoomEvent, ConnEvent, PeerSpeakTicket, + NetworkTransport, RoomState, SelfPresence, RoomEvent, ConnEvent, PeerSpeakTicket, iroh_impl::{IrohTransport, AudioRouter, FileRouter}, gossip::IrohGossipState, }; @@ -917,10 +917,14 @@ async fn run_core_loop( let peer_gate = Arc::new(Mutex::new(HashMap::::new())); // Peers locally muted by us: decoded for level metering but not mixed. let locally_muted = Arc::new(Mutex::new(HashSet::::new())); - let mut current_name = "Anonymous".to_string(); - // Our chosen avatar (W4), set on Join and changeable via SetAvatar; included - // in every self-state we announce over presence. - let mut current_avatar = crate::avatar::Avatar::default(); + // Sticky identity fields of our own presence (display name + W4 avatar), set on + // Join and changed via SetName/SetAvatar. Combined with the volatile per-announce + // fields (mute/addr/share ticket) by `SelfPresence::to_state` — the single place + // our `PeerState` is built. Defaults match the prior `current_name`/`current_avatar`. + let mut presence = SelfPresence { + name: "Anonymous".to_string(), + avatar: crate::avatar::Avatar::default(), + }; let mut network_mode = NetworkMode::default(); // Pixelpass binary override (config), and the ticket of our own active screen // share (rides our presence so the room — incl. late joiners — can watch). @@ -1125,8 +1129,8 @@ async fn run_core_loop( } CoreCommand::Join { name, ticket, room_name, input_device, output_device, echo_cancellation, avatar } => { - current_name = name.clone(); - current_avatar = avatar; + presence.name = name.clone(); + presence.avatar = avatar; // Finalize any recording before tearing down the old session — its // capture/mixer feeders are about to stop. @@ -1206,13 +1210,11 @@ async fn run_core_loop( // Fresh join starts not sharing; clear any stale share ticket. current_sharing = None; - let self_state = PeerState { - name: current_name.clone(), - is_muted: is_muted.load(Ordering::Relaxed), - addr: endpoint.addr(), - sharing: None, - avatar: current_avatar.clone(), - }; + let self_state = presence.to_state( + is_muted.load(Ordering::Relaxed), + endpoint.addr(), + None, + ); // Snapshot THIS room's retained peers (by ticket) as extra bootstrap // targets so a rejoin can dial them (A8) — including after a detour @@ -1917,29 +1919,25 @@ async fn run_core_loop( is_muted.store(new_state, Ordering::Relaxed); if let Some(session) = &active_session { - let self_state = PeerState { - name: current_name.clone(), - is_muted: new_state, - addr: net.endpoint.addr(), - sharing: current_sharing.clone(), - avatar: current_avatar.clone(), - }; + let self_state = presence.to_state( + new_state, + net.endpoint.addr(), + current_sharing.clone(), + ); let _ = session.room_state.update_self_state(self_state).await; } } CoreCommand::SetAvatar(avatar) => { - current_avatar = avatar; + presence.avatar = avatar; // Re-announce presence so the room (incl. late joiners, via the // retained presence) picks up the new avatar (W4). if let Some(session) = &active_session { - let self_state = PeerState { - name: current_name.clone(), - is_muted: is_muted.load(Ordering::Relaxed), - addr: net.endpoint.addr(), - sharing: current_sharing.clone(), - avatar: current_avatar.clone(), - }; + let self_state = presence.to_state( + is_muted.load(Ordering::Relaxed), + net.endpoint.addr(), + current_sharing.clone(), + ); let _ = session.room_state.update_self_state(self_state).await; } } @@ -2337,13 +2335,11 @@ async fn run_core_loop( crate::log_msg("Screen share host started"); session.screenshare_host = Some(child); current_sharing = Some(ticket.clone()); - let self_state = PeerState { - name: current_name.clone(), - is_muted: is_muted.load(Ordering::Relaxed), - addr: net.endpoint.addr(), - sharing: Some(ticket), - avatar: current_avatar.clone(), - }; + let self_state = presence.to_state( + is_muted.load(Ordering::Relaxed), + net.endpoint.addr(), + Some(ticket), + ); let _ = session.room_state.update_self_state(self_state).await; let _ = ui_tx.send(UiEvent::ScreenShareStarted).await; } @@ -2362,13 +2358,11 @@ async fn run_core_loop( let _ = child.kill().await; crate::log_msg("Screen share host stopped"); } - let self_state = PeerState { - name: current_name.clone(), - is_muted: is_muted.load(Ordering::Relaxed), - addr: net.endpoint.addr(), - sharing: None, - avatar: current_avatar.clone(), - }; + let self_state = presence.to_state( + is_muted.load(Ordering::Relaxed), + net.endpoint.addr(), + None, + ); let _ = session.room_state.update_self_state(self_state).await; } let _ = ui_tx.send(UiEvent::ScreenShareStopped).await; diff --git a/src/network/mod.rs b/src/network/mod.rs index 942e5e6..97b21a3 100644 --- a/src/network/mod.rs +++ b/src/network/mod.rs @@ -41,6 +41,43 @@ pub struct PeerState { pub avatar: crate::avatar::Avatar, } +/// The locally-owned, "sticky" pieces of our own presence: the identity fields +/// that change only on explicit user action and persist for the whole core +/// session. The remaining `PeerState` fields are *volatile* — mute state, current +/// `addr`, and the active screen-share ticket are read fresh at each announce — so +/// they are passed into [`SelfPresence::to_state`] rather than stored here. +/// +/// This is the single source of truth for building our own `PeerState`: core +/// reconstructs self-state in several command branches (join, mute toggle, avatar +/// change, screen-share start/stop), and centralizing the `PeerState` literal here +/// means a new presence field is added in exactly one place instead of at every +/// call site. +#[derive(Debug, Clone, Default)] +pub struct SelfPresence { + pub name: String, + pub avatar: crate::avatar::Avatar, +} + +impl SelfPresence { + /// Combine the sticky identity fields with the volatile per-announce fields + /// (`is_muted`, current `addr`, active-share `sharing` ticket) into a full + /// `PeerState` ready to announce over the gossip presence plane. + pub fn to_state( + &self, + is_muted: bool, + addr: iroh::EndpointAddr, + sharing: Option, + ) -> PeerState { + PeerState { + name: self.name.clone(), + is_muted, + addr, + sharing, + avatar: self.avatar.clone(), + } + } +} + #[derive(Debug, Clone)] pub enum RoomEvent { PeerJoined(EndpointId, PeerState), @@ -358,6 +395,27 @@ mod tests { assert_eq!(PeerSpeakTicket::restamp("not-a-ticket", EndpointAddr::from(me)), "not-a-ticket"); } + #[test] + fn self_presence_builds_peer_state_with_volatile_fields() { + let addr = EndpointAddr::from(SecretKey::generate().public()); + let presence = SelfPresence { + name: "Alice".to_string(), + avatar: crate::avatar::Avatar::default(), + }; + // Volatile fields come from the call; sticky fields from the struct. + let muted = presence.to_state(true, addr.clone(), Some("ticket".to_string())); + assert_eq!(muted.name, "Alice"); + assert!(muted.is_muted); + assert_eq!(muted.addr.id, addr.id); + assert_eq!(muted.sharing.as_deref(), Some("ticket")); + assert_eq!(muted.avatar, crate::avatar::Avatar::default()); + // The same sticky presence yields different volatile fields per announce. + let unmuted = presence.to_state(false, addr.clone(), None); + assert!(!unmuted.is_muted); + assert_eq!(unmuted.sharing, None); + assert_eq!(unmuted.name, muted.name); + } + #[test] fn test_peer_state_serde_round_trip() { let original = sample_peer_state();