From feb17c29ec1e9fd041918ddff34c6f8c4b95c3d9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 11 Oct 2022 12:23:15 +0200 Subject: [PATCH] Show participant projects in contacts popover --- crates/call/src/participant.rs | 5 + crates/call/src/room.rs | 25 +++- crates/collab_ui/src/contact_list.rs | 202 +++++++++++++++++++++++++-- crates/theme/src/theme.rs | 15 ++ styles/src/styleTree/contactList.ts | 51 ++++++- 5 files changed, 279 insertions(+), 19 deletions(-) diff --git a/crates/call/src/participant.rs b/crates/call/src/participant.rs index b124d920a3..7e031f907f 100644 --- a/crates/call/src/participant.rs +++ b/crates/call/src/participant.rs @@ -20,6 +20,11 @@ impl ParticipantLocation { } } +#[derive(Clone, Debug, Default)] +pub struct LocalParticipant { + pub projects: Vec, +} + #[derive(Clone, Debug)] pub struct RemoteParticipant { pub user: Arc, diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index ebe2d4284b..a3b49c2a4f 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -1,5 +1,5 @@ use crate::{ - participant::{ParticipantLocation, RemoteParticipant}, + participant::{LocalParticipant, ParticipantLocation, RemoteParticipant}, IncomingCall, }; use anyhow::{anyhow, Result}; @@ -27,6 +27,7 @@ pub enum Event { pub struct Room { id: u64, status: RoomStatus, + local_participant: LocalParticipant, remote_participants: BTreeMap, pending_participants: Vec>, participant_user_ids: HashSet, @@ -72,6 +73,7 @@ impl Room { id, status: RoomStatus::Online, participant_user_ids: Default::default(), + local_participant: Default::default(), remote_participants: Default::default(), pending_participants: Default::default(), pending_call_count: 0, @@ -170,6 +172,10 @@ impl Room { self.status } + pub fn local_participant(&self) -> &LocalParticipant { + &self.local_participant + } + pub fn remote_participants(&self) -> &BTreeMap { &self.remote_participants } @@ -201,8 +207,11 @@ impl Room { cx: &mut ModelContext, ) -> Result<()> { // Filter ourselves out from the room's participants. - room.participants - .retain(|participant| Some(participant.user_id) != self.client.user_id()); + let local_participant_ix = room + .participants + .iter() + .position(|participant| Some(participant.user_id) == self.client.user_id()); + let local_participant = local_participant_ix.map(|ix| room.participants.swap_remove(ix)); let remote_participant_user_ids = room .participants @@ -223,6 +232,12 @@ impl Room { this.update(&mut cx, |this, cx| { this.participant_user_ids.clear(); + if let Some(participant) = local_participant { + this.local_participant.projects = participant.projects; + } else { + this.local_participant.projects.clear(); + } + if let Some(participants) = remote_participants.log_err() { for (participant, user) in room.participants.into_iter().zip(participants) { let peer_id = PeerId(participant.peer_id); @@ -280,8 +295,6 @@ impl Room { false } }); - - cx.notify(); } if let Some(pending_participants) = pending_participants.log_err() { @@ -289,7 +302,6 @@ impl Room { for participant in &this.pending_participants { this.participant_user_ids.insert(participant.id); } - cx.notify(); } this.pending_room_update.take(); @@ -298,6 +310,7 @@ impl Room { } this.check_invariants(); + cx.notify(); }); })); diff --git a/crates/collab_ui/src/contact_list.rs b/crates/collab_ui/src/contact_list.rs index a539f8ffac..357b3c65e0 100644 --- a/crates/collab_ui/src/contact_list.rs +++ b/crates/collab_ui/src/contact_list.rs @@ -6,9 +6,11 @@ use client::{Contact, PeerId, User, UserStore}; use editor::{Cancel, Editor}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ - elements::*, impl_actions, impl_internal_actions, keymap, AppContext, ClipboardItem, - CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, - View, ViewContext, ViewHandle, + elements::*, + geometry::{rect::RectF, vector::vec2f}, + impl_actions, impl_internal_actions, keymap, AppContext, ClipboardItem, CursorStyle, Entity, + ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, View, ViewContext, + ViewHandle, }; use menu::{Confirm, SelectNext, SelectPrev}; use project::Project; @@ -16,6 +18,7 @@ use serde::Deserialize; use settings::Settings; use theme::IconButton; use util::ResultExt; +use workspace::JoinProject; impl_actions!(contact_list, [RemoveContact, RespondToContactRequest]); impl_internal_actions!(contact_list, [ToggleExpanded, Call, LeaveCall]); @@ -55,7 +58,17 @@ enum Section { #[derive(Clone)] enum ContactEntry { Header(Section), - CallParticipant { user: Arc, is_pending: bool }, + CallParticipant { + user: Arc, + is_pending: bool, + }, + ParticipantProject { + project_id: u64, + worktree_root_names: Vec, + host_user_id: u64, + is_host: bool, + is_last: bool, + }, IncomingRequest(Arc), OutgoingRequest(Arc), Contact(Arc), @@ -74,6 +87,18 @@ impl PartialEq for ContactEntry { return user_1.id == user_2.id; } } + ContactEntry::ParticipantProject { + project_id: project_id_1, + .. + } => { + if let ContactEntry::ParticipantProject { + project_id: project_id_2, + .. + } = other + { + return project_id_1 == project_id_2; + } + } ContactEntry::IncomingRequest(user_1) => { if let ContactEntry::IncomingRequest(user_2) = other { return user_1.id == user_2.id; @@ -177,6 +202,22 @@ impl ContactList { &theme.contact_list, ) } + ContactEntry::ParticipantProject { + project_id, + worktree_root_names, + host_user_id, + is_host, + is_last, + } => Self::render_participant_project( + *project_id, + worktree_root_names, + *host_user_id, + *is_host, + *is_last, + is_selected, + &theme.contact_list, + cx, + ), ContactEntry::IncomingRequest(user) => Self::render_contact_request( user.clone(), this.user_store.clone(), @@ -298,6 +339,19 @@ impl ContactList { ); } } + ContactEntry::ParticipantProject { + project_id, + host_user_id, + is_host, + .. + } => { + if !is_host { + cx.dispatch_global_action(JoinProject { + project_id: *project_id, + follow_user_id: *host_user_id, + }); + } + } _ => {} } } @@ -324,7 +378,7 @@ impl ContactList { if let Some(room) = ActiveCall::global(cx).read(cx).room() { let room = room.read(cx); - let mut call_participants = Vec::new(); + let mut participant_entries = Vec::new(); // Populate the active user. if let Some(user) = user_store.current_user() { @@ -343,10 +397,21 @@ impl ContactList { executor.clone(), )); if !matches.is_empty() { - call_participants.push(ContactEntry::CallParticipant { + let user_id = user.id; + participant_entries.push(ContactEntry::CallParticipant { user, is_pending: false, }); + let mut projects = room.local_participant().projects.iter().peekable(); + while let Some(project) = projects.next() { + participant_entries.push(ContactEntry::ParticipantProject { + project_id: project.id, + worktree_root_names: project.worktree_root_names.clone(), + host_user_id: user_id, + is_host: true, + is_last: projects.peek().is_none(), + }); + } } } @@ -370,14 +435,25 @@ impl ContactList { &Default::default(), executor.clone(), )); - call_participants.extend(matches.iter().map(|mat| { - ContactEntry::CallParticipant { + for mat in matches { + let participant = &room.remote_participants()[&PeerId(mat.candidate_id as u32)]; + participant_entries.push(ContactEntry::CallParticipant { user: room.remote_participants()[&PeerId(mat.candidate_id as u32)] .user .clone(), is_pending: false, + }); + let mut projects = participant.projects.iter().peekable(); + while let Some(project) = projects.next() { + participant_entries.push(ContactEntry::ParticipantProject { + project_id: project.id, + worktree_root_names: project.worktree_root_names.clone(), + host_user_id: participant.user.id, + is_host: false, + is_last: projects.peek().is_none(), + }); } - })); + } // Populate pending participants. self.match_candidates.clear(); @@ -400,15 +476,15 @@ impl ContactList { &Default::default(), executor.clone(), )); - call_participants.extend(matches.iter().map(|mat| ContactEntry::CallParticipant { + participant_entries.extend(matches.iter().map(|mat| ContactEntry::CallParticipant { user: room.pending_participants()[mat.candidate_id].clone(), is_pending: true, })); - if !call_participants.is_empty() { + if !participant_entries.is_empty() { self.entries.push(ContactEntry::Header(Section::ActiveCall)); if !self.collapsed_sections.contains(&Section::ActiveCall) { - self.entries.extend(call_participants); + self.entries.extend(participant_entries); } } } @@ -588,6 +664,108 @@ impl ContactList { .boxed() } + fn render_participant_project( + project_id: u64, + worktree_root_names: &[String], + host_user_id: u64, + is_host: bool, + is_last: bool, + is_selected: bool, + theme: &theme::ContactList, + cx: &mut RenderContext, + ) -> ElementBox { + let font_cache = cx.font_cache(); + let host_avatar_height = theme + .contact_avatar + .width + .or(theme.contact_avatar.height) + .unwrap_or(0.); + let row = &theme.project_row.default; + let tree_branch = theme.tree_branch; + let line_height = row.name.text.line_height(font_cache); + let cap_height = row.name.text.cap_height(font_cache); + let baseline_offset = + row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.; + let project_name = if worktree_root_names.is_empty() { + "untitled".to_string() + } else { + worktree_root_names.join(", ") + }; + + MouseEventHandler::::new(project_id as usize, cx, |mouse_state, _| { + let tree_branch = *tree_branch.style_for(mouse_state, is_selected); + let row = theme.project_row.style_for(mouse_state, is_selected); + + Flex::row() + .with_child( + Stack::new() + .with_child( + Canvas::new(move |bounds, _, cx| { + let start_x = bounds.min_x() + (bounds.width() / 2.) + - (tree_branch.width / 2.); + let end_x = bounds.max_x(); + let start_y = bounds.min_y(); + let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.); + + cx.scene.push_quad(gpui::Quad { + bounds: RectF::from_points( + vec2f(start_x, start_y), + vec2f( + start_x + tree_branch.width, + if is_last { end_y } else { bounds.max_y() }, + ), + ), + background: Some(tree_branch.color), + border: gpui::Border::default(), + corner_radius: 0., + }); + cx.scene.push_quad(gpui::Quad { + bounds: RectF::from_points( + vec2f(start_x, end_y), + vec2f(end_x, end_y + tree_branch.width), + ), + background: Some(tree_branch.color), + border: gpui::Border::default(), + corner_radius: 0., + }); + }) + .boxed(), + ) + .constrained() + .with_width(host_avatar_height) + .boxed(), + ) + .with_child( + Label::new(project_name, row.name.text.clone()) + .aligned() + .left() + .contained() + .with_style(row.name.container) + .flex(1., false) + .boxed(), + ) + .constrained() + .with_height(theme.row_height) + .contained() + .with_style(row.container) + .boxed() + }) + .with_cursor_style(if !is_host { + CursorStyle::PointingHand + } else { + CursorStyle::Arrow + }) + .on_click(MouseButton::Left, move |_, cx| { + if !is_host { + cx.dispatch_global_action(JoinProject { + project_id, + follow_user_id: host_user_id, + }); + } + }) + .boxed() + } + fn render_header( section: Section, theme: &theme::ContactList, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 9a836864bf..503645d6bc 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -100,6 +100,8 @@ pub struct ContactList { pub leave_call: Interactive, pub contact_row: Interactive, pub row_height: f32, + pub project_row: Interactive, + pub tree_branch: Interactive, pub contact_avatar: ImageStyle, pub contact_status_free: ContainerStyle, pub contact_status_busy: ContainerStyle, @@ -112,6 +114,19 @@ pub struct ContactList { pub calling_indicator: ContainedText, } +#[derive(Deserialize, Default)] +pub struct ProjectRow { + #[serde(flatten)] + pub container: ContainerStyle, + pub name: ContainedText, +} + +#[derive(Deserialize, Default, Clone, Copy)] +pub struct TreeBranch { + pub width: f32, + pub color: Color, +} + #[derive(Deserialize, Default)] pub struct ContactFinder { pub picker: Picker, diff --git a/styles/src/styleTree/contactList.ts b/styles/src/styleTree/contactList.ts index 8b8b6024cf..633d634196 100644 --- a/styles/src/styleTree/contactList.ts +++ b/styles/src/styleTree/contactList.ts @@ -12,6 +12,31 @@ export default function contactList(theme: Theme) { buttonWidth: 16, cornerRadius: 8, }; + const projectRow = { + guestAvatarSpacing: 4, + height: 24, + guestAvatar: { + cornerRadius: 8, + width: 14, + }, + name: { + ...text(theme, "mono", "placeholder", { size: "sm" }), + margin: { + left: nameMargin, + right: 6, + }, + }, + guests: { + margin: { + left: nameMargin, + right: nameMargin, + }, + }, + padding: { + left: sidePadding, + right: sidePadding, + }, + }; return { userQueryEditor: { @@ -129,6 +154,30 @@ export default function contactList(theme: Theme) { }, callingIndicator: { ...text(theme, "mono", "muted", { size: "xs" }) - } + }, + treeBranch: { + color: borderColor(theme, "active"), + width: 1, + hover: { + color: borderColor(theme, "active"), + }, + active: { + color: borderColor(theme, "active"), + }, + }, + projectRow: { + ...projectRow, + background: backgroundColor(theme, 300), + name: { + ...projectRow.name, + ...text(theme, "mono", "secondary", { size: "sm" }), + }, + hover: { + background: backgroundColor(theme, 300, "hovered"), + }, + active: { + background: backgroundColor(theme, 300, "active"), + }, + }, } }