fix(friends): self-heal presence + add manual Rescan button
The friends list only ever updated a friend's status on a *successful* presence probe, so it could ratchet a status up (offline -> online -> in a room) but never down. A friend who dropped, left a room, or went invisible kept showing a stale "online"/"in a room" status until PeerSpeak was relaunched (which cleared the in-memory presence map back to offline). The 60s auto-refresh scheduler already existed; the bug was that `probe_friends_once` emitted nothing on a failed probe. Now every pass reports a *definitive* status for every friend: a failed probe (or a friend with no known address) is mapped to a new `FriendPresence::Offline` via the pure, tested `presence::presence_from_probe`, so the list self-heals each cycle. Also adds a manual "⟳ Rescan" button to the Friends panel (new `CoreCommand::RefreshFriends` -> immediate probe pass) for instant feedback instead of waiting up to 60s. 469 lib tests pass, clippy clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+31
-2
@@ -366,6 +366,9 @@ pub enum AppMessage {
|
||||
AddFriendFromRoom(EndpointId),
|
||||
RemoveFriend(EndpointId),
|
||||
RenameFriend(EndpointId, String),
|
||||
/// Manually rescan all friends' presence now (the ⟳ button), instead of waiting
|
||||
/// for the 60s scheduler tick. Statuses update as the probe replies arrive.
|
||||
RefreshFriends,
|
||||
/// Join the gathering a friend is in (W7 B2), via the member ticket their
|
||||
/// presence reply carried. Mirrors a manual ticket join.
|
||||
JoinFriendRoom(String),
|
||||
@@ -1894,6 +1897,11 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
||||
let _ = state.controller.send(CoreCommand::RenameFriend(id, new_name));
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
AppMessage::JoinFriendRoom(ticket) => {
|
||||
// Join via the friend's member ticket (same path as a manual join).
|
||||
let input_device = state.selected_input.as_ref().map(|d| d.name.clone());
|
||||
@@ -3684,7 +3692,11 @@ fn friends_panel(state: &AppState) -> Element<'_, AppMessage> {
|
||||
Some(crate::presence::FriendPresence::Online) => {
|
||||
text("● online").size(11).color(color_green).into()
|
||||
}
|
||||
None => text("○ offline").size(11).color(color_subtext).into(),
|
||||
// An explicit Offline (probe failed / no address) and a missing
|
||||
// entry (not yet probed) both render as offline.
|
||||
Some(crate::presence::FriendPresence::Offline) | None => {
|
||||
text("○ offline").size(11).color(color_subtext).into()
|
||||
}
|
||||
};
|
||||
friend_rows = friend_rows.push(
|
||||
row![
|
||||
@@ -3768,10 +3780,27 @@ fn friends_panel(state: &AppState) -> Element<'_, AppMessage> {
|
||||
} else {
|
||||
column![].into()
|
||||
};
|
||||
// Header: title + a manual "Rescan" button. Presence is also auto-refreshed
|
||||
// every 60s, but the button forces an immediate pass for instant feedback.
|
||||
// Only shown when there are friends to scan.
|
||||
let title_row: Element<'_, AppMessage> = if has_friends {
|
||||
row![
|
||||
text("FRIENDS").size(18).color(color_text),
|
||||
horizontal_space(),
|
||||
button(text("⟳ Rescan").size(12))
|
||||
.on_press(AppMessage::RefreshFriends)
|
||||
.style(b_style(color_surface, color_blue, color_text, 6.0))
|
||||
.padding(6),
|
||||
]
|
||||
.align_y(iced::alignment::Vertical::Center)
|
||||
.into()
|
||||
} else {
|
||||
text("FRIENDS").size(14).color(color_text).into()
|
||||
};
|
||||
|
||||
container(
|
||||
column![
|
||||
text("FRIENDS").size(if has_friends { 18 } else { 14 }).color(color_text),
|
||||
title_row,
|
||||
intro,
|
||||
vertical_space(if has_friends { 10.0 } else { 4.0 }),
|
||||
readonly_warning,
|
||||
|
||||
Reference in New Issue
Block a user