From fb17fd1850a5758bf7b9d6c0c86ea8e6d53be00c Mon Sep 17 00:00:00 2001 From: Mollusk Date: Tue, 16 Jun 2026 14:42:04 -0400 Subject: [PATCH] =?UTF-8?q?feat(w7):=20add-friend-from-room=20=E2=80=94=20?= =?UTF-8?q?friend=20a=20co-participant=20with=20one=20click=20(P5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/app/mod.rs | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/app/mod.rs b/src/app/mod.rs index 7d231bd..3283367 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -145,6 +145,9 @@ pub enum AppMessage { FriendAddIdChanged(String), FriendAddNameChanged(String), 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), RenameFriend(EndpointId, String), /// 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::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) => { if !state.friends_read_only { let _ = state.controller.send(CoreCommand::RemoveFriend(id)); @@ -2405,6 +2428,36 @@ fn view(state: &AppState) -> Element<'_, AppMessage> { 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![ row![ 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(format!("ID: {}", short_id(&peer_id.to_string()))).size(11).color(color_subtext) ], + add_friend_el, horizontal_space(), share_el, mute_btn,