From a6a88d15c0a6742522da174c3619c07bb058bf3c Mon Sep 17 00:00:00 2001 From: Mollusk Date: Mon, 22 Jun 2026 16:27:16 -0400 Subject: [PATCH] fix(app): don't wipe early-arriving peers on RoomJoined Field-test regression from the F-06 fix (a17b930): joining a room via the friends-list / Recents Join button sometimes landed in an empty roster even though the peer was fully connected at the gossip layer. Cause: `PeerJoined` (gossip event task) and `RoomJoined` (core command loop) ride the same UI channel from different senders. The core emits `RoomJoined` only after audio + echo-cancel setup, so `PeerJoined` for the new room routinely arrives first. F-06 had added `reset_room_state()` to the `RoomJoined` handler, which then cleared the peer that had already announced. Echo cancellation widened the window and made it reliable; the roster "self-healed" only on the peer's next periodic re-announce (`PeerUpdated`). Fix: reset room-scoped UI state at join *initiation* (JoinPressed, CreatePressed, JoinFriendRoom, JoinRecent) instead of on `RoomJoined`. From Home that's a no-op (already cleared on leave), so nothing leaks, and an early `PeerJoined` for the new room now survives. The in-call switch path F-06 targeted is unreachable from the current UI (friends list + Recents render only on the Home screen), so this fully covers the reachable case. Field-verified on a 2-machine desktop<->dopedart call. 407 lib tests pass, clippy --all-targets clean. Co-Authored-By: Claude Opus 4.8 --- src/app/mod.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index d606d23..1ed7cc5 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1006,6 +1006,10 @@ fn update(state: &mut AppState, message: AppMessage) -> Task { let output_device = state.selected_output.as_ref().map(|d| d.name.clone()); if !state.ticket_input.is_empty() { state.status_message = "Joining room...".to_string(); + // Clear any prior room's UI state now, at initiation, so an early + // `PeerJoined` for the new room (which can beat `RoomJoined`) isn't + // wiped. From Home this is a no-op (already cleared on leave). + state.reset_room_state(); // Remember this nickname for next launch. state.config.username = state.name.clone(); state.config.save(); @@ -1026,6 +1030,8 @@ fn update(state: &mut AppState, message: AppMessage) -> Task { let input_device = state.selected_input.as_ref().map(|d| d.name.clone()); let output_device = state.selected_output.as_ref().map(|d| d.name.clone()); state.status_message = "Creating room...".to_string(); + // Clear any prior room's UI state at initiation (see JoinPressed). + state.reset_room_state(); // Remember this nickname for next launch. state.config.username = state.name.clone(); state.config.save(); @@ -1065,7 +1071,12 @@ fn update(state: &mut AppState, message: AppMessage) -> Task { AppMessage::UiEventReceived(event) => { match event { UiEvent::RoomJoined { ticket, self_id } => { - state.reset_room_state(); + // NB: do NOT clear peers here. `PeerJoined` rides a separate + // channel sender (the gossip event task) and routinely arrives + // BEFORE this `RoomJoined` (which the core emits only after audio + // + echo-cancel setup), so clearing here would wipe a peer that + // already announced → an empty roster. Room-scoped state is reset + // at join *initiation* instead (see the Join* handlers). // Remember this gathering for one-click rejoin (W7 P5). The // emitted ticket is the canonical room door (topic + member // addr + label); push_recent de-dupes by topic and persists. @@ -1423,6 +1434,8 @@ fn update(state: &mut AppState, message: AppMessage) -> Task { let input_device = state.selected_input.as_ref().map(|d| d.name.clone()); let output_device = state.selected_output.as_ref().map(|d| d.name.clone()); state.status_message = "Joining your friend's room...".to_string(); + // Clear any prior room's UI state at initiation (see JoinPressed). + state.reset_room_state(); state.config.username = state.name.clone(); state.config.save(); state.mic_test_active = false; @@ -1442,6 +1455,8 @@ fn update(state: &mut AppState, message: AppMessage) -> Task { let input_device = state.selected_input.as_ref().map(|d| d.name.clone()); let output_device = state.selected_output.as_ref().map(|d| d.name.clone()); state.status_message = "Rejoining a recent room...".to_string(); + // Clear any prior room's UI state at initiation (see JoinPressed). + state.reset_room_state(); state.config.username = state.name.clone(); state.config.save(); state.mic_test_active = false;