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 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 16:27:16 -04:00
co-authored by Claude Opus 4.8
parent a17b930524
commit ed565a68c3
+16 -1
View File
@@ -1006,6 +1006,10 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
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<AppMessage> {
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> {
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<AppMessage> {
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<AppMessage> {
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;