diff --git a/src/app/mod.rs b/src/app/mod.rs index a102fcf..75e7e70 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1700,6 +1700,14 @@ fn update(state: &mut AppState, message: AppMessage) -> Task { UiEvent::FriendPresence { id, presence } => { state.friend_presence.insert(id, presence); } + UiEvent::FriendsRescanned => { + // The manual pass finished; replace the transient "Rescanning…" + // banner, but only if it's still showing (don't clobber a status + // the user has since triggered, e.g. by joining a room). + if state.status_message == "Rescanning friends…" { + state.status_message = "Friends rescanned.".to_string(); + } + } UiEvent::PresenceModeReverted { mode } => { // Core corrected the committed presence mode. Mirror + persist so // the picker reflects the discovery state the endpoint actually has. @@ -1898,9 +1906,17 @@ fn update(state: &mut AppState, message: AppMessage) -> Task { } } AppMessage::RefreshFriends => { - // Kick an immediate presence pass; the list updates as replies land. - let _ = state.controller.send(CoreCommand::RefreshFriends); - state.status_message = "Rescanning friends…".to_string(); + // Kick an immediate presence pass; the list updates as replies land and + // the banner clears on the FriendsRescanned completion event. While + // Invisible we probe no one, so say why instead of a banner that resolves + // with nothing changed. + if state.config.presence_mode == PresenceMode::Invisible { + state.status_message = + "You're invisible — turn on presence to scan friends.".to_string(); + } else { + let _ = state.controller.send(CoreCommand::RefreshFriends); + state.status_message = "Rescanning friends…".to_string(); + } } AppMessage::JoinFriendRoom(ticket) => { // Join via the friend's member ticket (same path as a manual join). diff --git a/src/core/messages.rs b/src/core/messages.rs index da8df7b..e163a06 100644 --- a/src/core/messages.rs +++ b/src/core/messages.rs @@ -296,6 +296,11 @@ pub enum UiEvent { /// joinable gathering (with a one-click ticket). Emitted by the outbound ping /// scheduler; absence of a recent event = treat as offline. FriendPresence { id: EndpointId, presence: FriendPresence }, + /// A manual "Rescan" pass finished (every friend has been probed and its + /// per-friend `FriendPresence` already emitted). Lets the GUI clear the + /// transient "Rescanning…" status. Sent only for the on-demand button, not the + /// periodic auto-refresh, so the status bar isn't churned every interval. + FriendsRescanned, /// Core corrected the committed presence posture. Usually the Discoverable /// time-box elapsed and the core auto-reverted to `Normal`; on discovery apply /// failure, this carries the previous truthful mode. The GUI must mirror + diff --git a/src/core/mod.rs b/src/core/mod.rs index a225d48..cc56f5a 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -2509,14 +2509,20 @@ async fn run_core_loop( CoreCommand::RefreshFriends => { // Manual "Rescan": run an immediate probe pass (same as a scheduler // tick), detached so it can't block command handling. Honour - // Invisible — stay fully dark and touch no friend's machine. - if *presence_mode.lock().unwrap() != crate::presence::PresenceMode::Invisible { - tokio::spawn(probe_friends_once( - net.endpoint.clone(), - friends.lock().unwrap().clone(), - ui_tx.clone(), - )); - } + // Invisible — stay fully dark and touch no friend's machine. A + // `FriendsRescanned` event always follows so the UI's transient + // "Rescanning…" status clears even when probing was skipped. + let visible = + *presence_mode.lock().unwrap() != crate::presence::PresenceMode::Invisible; + let endpoint = net.endpoint.clone(); + let snapshot = friends.lock().unwrap().clone(); + let tx = ui_tx.clone(); + tokio::spawn(async move { + if visible { + probe_friends_once(endpoint, snapshot, tx.clone()).await; + } + let _ = tx.send(UiEvent::FriendsRescanned).await; + }); } CoreCommand::SetPresenceMode(mode) => {