feat(w7): add-friend-from-room — friend a co-participant with one click (P5)

Each participant card gets a star affordance: a clickable outline star (☆) that
adds that peer to your friends list, or a non-interactive gold filled star (★)
once they're already a friend (hidden while the friends store is read-only). The
add pulls the peer's live presence name + address from the room roster and passes
addr: Some(..) to CoreCommand::AddFriend, so the new friend is reachable
immediately — no waiting for a future call to seed last_addr the way a bare
add-by-id does. Name sanitized, short-id fallback; idempotent in core; no-op if
already a friend. New AppMessage::AddFriendFromRoom(EndpointId).

clippy --all-targets clean, 257 lib tests green. UI wiring screenshot-pending: the
star + click need a live 2-machine call (a peer in the room) to verify visually.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-16 14:42:04 -04:00
co-authored by Claude Opus 4.8
parent b97b8eb7ca
commit fb17fd1850
+54
View File
@@ -145,6 +145,9 @@ pub enum AppMessage {
FriendAddIdChanged(String), FriendAddIdChanged(String),
FriendAddNameChanged(String), FriendAddNameChanged(String),
AddFriend, AddFriend,
/// Add a peer you're in a call with to your friends list (W7), using their
/// live presence name + address so they're reachable immediately.
AddFriendFromRoom(EndpointId),
RemoveFriend(EndpointId), RemoveFriend(EndpointId),
RenameFriend(EndpointId, String), RenameFriend(EndpointId, String),
/// Join the gathering a friend is in (W7 B2), via the member ticket their /// Join the gathering a friend is in (W7 B2), via the member ticket their
@@ -837,6 +840,26 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
} }
} }
} }
AppMessage::AddFriendFromRoom(id) => {
// Add a co-participant: pull their name + live address from the room
// roster so the friend is reachable right away (no wait for a future
// call to seed last_addr, unlike a bare add-by-id). No-op if read-only
// or already a friend; idempotent in core regardless.
if !state.friends_read_only
&& !state.friends.contains(&id)
&& let Some(peer) = state.peers.get(&id)
{
let name = {
let n = crate::sanitize::sanitize_name(&peer.name);
if n.is_empty() { short_id(&id.to_string()) } else { n }
};
let _ = state.controller.send(CoreCommand::AddFriend {
id,
name,
addr: Some(peer.addr.clone()),
});
}
}
AppMessage::RemoveFriend(id) => { AppMessage::RemoveFriend(id) => {
if !state.friends_read_only { if !state.friends_read_only {
let _ = state.controller.send(CoreCommand::RemoveFriend(id)); let _ = state.controller.send(CoreCommand::RemoveFriend(id));
@@ -2405,6 +2428,36 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
color_surface color_surface
}; };
// Add-friend affordance (W7): a star you can click to friend this
// co-participant; a filled gold star (non-interactive) once they're a
// friend. Hidden while their friend state is read-only-degraded.
let add_friend_el: Element<'_, AppMessage> = if state.friends_read_only {
iced::widget::Space::new().width(0.0).height(0.0).into()
} else if state.friends.contains(peer_id) {
tooltip(
text("").size(16).color(color_yellow),
container(text("In your friends list").size(11).color(color_text))
.padding(8)
.style(c_style(color_crust, color_surface, 6.0)),
iced::widget::tooltip::Position::Bottom,
)
.gap(6)
.into()
} else {
tooltip(
button(text("").size(16).color(color_subtext))
.on_press(AppMessage::AddFriendFromRoom(peer_id_clone))
.style(b_style(color_surface, color_blue, color_text, 6.0))
.padding([2, 6]),
container(text("Add to your friends list").size(11).color(color_text))
.padding(8)
.style(c_style(color_crust, color_surface, 6.0)),
iced::widget::tooltip::Position::Bottom,
)
.gap(6)
.into()
};
let mut card_content = column![ let mut card_content = column![
row![ row![
avatar_view(&peer.avatar, &peer.name, &peer_id.to_string(), 38.0), avatar_view(&peer.avatar, &peer.name, &peer_id.to_string(), 38.0),
@@ -2412,6 +2465,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
text(&peer.name).size(16).color(color_text), text(&peer.name).size(16).color(color_text),
text(format!("ID: {}", short_id(&peer_id.to_string()))).size(11).color(color_subtext) text(format!("ID: {}", short_id(&peer_id.to_string()))).size(11).color(color_subtext)
], ],
add_friend_el,
horizontal_space(), horizontal_space(),
share_el, share_el,
mute_btn, mute_btn,