From ed2f1ddd2d6d099c09e5a1889666554876414893 Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 26 Jan 2023 14:01:05 -0500 Subject: [PATCH 01/36] Move workspace title into collaboration titlebar item render --- crates/collab_ui/src/collab_titlebar_item.rs | 20 +++++++++++++++++++- crates/gpui/src/elements.rs | 1 + crates/workspace/src/workspace.rs | 17 +---------------- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index a767e5056564ac42cc004ec2b6047d3919f17eb1..d4f56c54f434d9abf688714a171508ec436de9b9 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -47,6 +47,15 @@ impl View for CollabTitlebarItem { return Empty::new().boxed(); }; + let project = workspace.read(cx).project().read(cx); + let mut worktree_root_names = String::new(); + for (i, name) in project.worktree_root_names(cx).enumerate() { + if i > 0 { + worktree_root_names.push_str(", "); + } + worktree_root_names.push_str(name); + } + let theme = cx.global::().theme.clone(); let mut container = Flex::row(); @@ -67,7 +76,16 @@ impl View for CollabTitlebarItem { container.add_children(self.render_collaborators(&workspace, &theme, cx)); container.add_children(self.render_current_user(&workspace, &theme, cx)); container.add_children(self.render_connection_status(&workspace, cx)); - container.boxed() + + Stack::new() + .with_child( + Label::new(worktree_root_names, theme.workspace.titlebar.title.clone()) + .aligned() + .left() + .boxed(), + ) + .with_child(container.aligned().right().boxed()) + .boxed() } } diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index 41a802feb37fcda12c8b68b76759f173b350a793..b77d46536de2da0c59c901d913146704a764bcba 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -363,6 +363,7 @@ impl AnyElement for Lifecycle { value } } + _ => panic!("invalid element lifecycle state"), } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index d61fc3774c45b02c0077481a172cc78f150244bf..e6a89045d23082601a60798f6b0eb428240e1b9a 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1837,15 +1837,6 @@ impl Workspace { } fn render_titlebar(&self, theme: &Theme, cx: &mut RenderContext) -> ElementBox { - let project = &self.project.read(cx); - let mut worktree_root_names = String::new(); - for (i, name) in project.worktree_root_names(cx).enumerate() { - if i > 0 { - worktree_root_names.push_str(", "); - } - worktree_root_names.push_str(name); - } - // TODO: There should be a better system in place for this // (https://github.com/zed-industries/zed/issues/1290) let is_fullscreen = cx.window_is_fullscreen(cx.window_id()); @@ -1862,16 +1853,10 @@ impl Workspace { MouseEventHandler::::new(0, cx, |_, cx| { Container::new( Stack::new() - .with_child( - Label::new(worktree_root_names, theme.workspace.titlebar.title.clone()) - .aligned() - .left() - .boxed(), - ) .with_children( self.titlebar_item .as_ref() - .map(|item| ChildView::new(item, cx).aligned().right().boxed()), + .map(|item| ChildView::new(item, cx).boxed()), ) .boxed(), ) From e96d52f35a20f1eaefcb415e5de2739b8136a7f8 Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 26 Jan 2023 23:59:19 -0500 Subject: [PATCH 02/36] Move share button and support unsharing --- crates/call/src/call.rs | 12 ++++ crates/call/src/room.rs | 13 ++++ crates/collab_ui/src/collab_titlebar_item.rs | 75 ++++++++++++++------ 3 files changed, 80 insertions(+), 20 deletions(-) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 64584e61400b414620ba640f2fbc0b79825c535e..dfe4f39e0e85bef14a52a22a3c8c0f1b9bfa84b6 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -284,6 +284,18 @@ impl ActiveCall { } } + pub fn unshare_project( + &mut self, + project: ModelHandle, + cx: &mut ModelContext, + ) -> Result<()> { + if let Some((room, _)) = self.room.as_ref() { + room.update(cx, |room, cx| room.unshare_project(project, cx)) + } else { + Err(anyhow!("no active call")) + } + } + pub fn set_location( &mut self, project: Option<&ModelHandle>, diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 7527a693269725f0a6eae3bea48790e7c79dfb00..f84341e5dd1080a36d0f7f86556ffdff5f636d7b 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -793,6 +793,19 @@ impl Room { }) } + pub(crate) fn unshare_project( + &mut self, + project: ModelHandle, + cx: &mut ModelContext, + ) -> Result<()> { + let project_id = match project.read(cx).remote_id() { + Some(project_id) => project_id, + None => return Ok(()), + }; + + self.client.send(proto::UnshareProject { project_id }) + } + pub(crate) fn set_location( &mut self, project: Option<&ModelHandle>, diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index d4f56c54f434d9abf688714a171508ec436de9b9..9f360fc3f506a6dc708427f649f4197de62c470b 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -15,13 +15,18 @@ use gpui::{ use settings::Settings; use std::ops::Range; use theme::Theme; +use util::ResultExt; use workspace::{FollowNextCollaborator, JoinProject, ToggleFollow, Workspace}; -actions!(collab, [ToggleCollaborationMenu, ShareProject]); +actions!( + collab, + [ToggleCollaborationMenu, ShareProject, UnshareProject] +); pub fn init(cx: &mut MutableAppContext) { cx.add_action(CollabTitlebarItem::toggle_contacts_popover); cx.add_action(CollabTitlebarItem::share_project); + cx.add_action(CollabTitlebarItem::unshare_project); } pub struct CollabTitlebarItem { @@ -58,6 +63,19 @@ impl View for CollabTitlebarItem { let theme = cx.global::().theme.clone(); + let mut left_container = Flex::row(); + + left_container.add_child( + Label::new(worktree_root_names, theme.workspace.titlebar.title.clone()) + .aligned() + .left() + .boxed(), + ); + + if ActiveCall::global(cx).read(cx).room().is_some() { + left_container.add_child(self.render_share_unshare_button(&workspace, &theme, cx)); + } + let mut container = Flex::row(); container.add_children(self.render_toggle_screen_sharing_button(&theme, cx)); @@ -69,8 +87,6 @@ impl View for CollabTitlebarItem { || ActiveCall::global(cx).read(cx).room().is_none() { container.add_child(self.render_toggle_contacts_button(&theme, cx)); - } else { - container.add_child(self.render_share_button(&theme, cx)); } } container.add_children(self.render_collaborators(&workspace, &theme, cx)); @@ -78,12 +94,7 @@ impl View for CollabTitlebarItem { container.add_children(self.render_connection_status(&workspace, cx)); Stack::new() - .with_child( - Label::new(worktree_root_names, theme.workspace.titlebar.title.clone()) - .aligned() - .left() - .boxed(), - ) + .with_child(left_container.boxed()) .with_child(container.aligned().right().boxed()) .boxed() } @@ -157,6 +168,16 @@ impl CollabTitlebarItem { } } + fn unshare_project(&mut self, _: &UnshareProject, cx: &mut ViewContext) { + if let Some(workspace) = self.workspace.upgrade(cx) { + let active_call = ActiveCall::global(cx); + let project = workspace.read(cx).project().clone(); + active_call + .update(cx, |call, cx| call.unshare_project(project, cx)) + .log_err(); + } + } + pub fn toggle_contacts_popover( &mut self, _: &ToggleCollaborationMenu, @@ -305,26 +326,40 @@ impl CollabTitlebarItem { ) } - fn render_share_button(&self, theme: &Theme, cx: &mut RenderContext) -> ElementBox { - enum Share {} + fn render_share_unshare_button( + &self, + workspace: &ViewHandle, + theme: &Theme, + cx: &mut RenderContext, + ) -> ElementBox { + let is_shared = workspace.read(cx).project().read(cx).is_shared(); + let label = if is_shared { "Unshare" } else { "Share" }; + let tooltip = if is_shared { + "Unshare project from call participants" + } else { + "Share project with call participants" + }; let titlebar = &theme.workspace.titlebar; + + enum Share {} MouseEventHandler::::new(0, cx, |state, _| { + //TODO: Ensure this button has consistant width for both text variations let style = titlebar.share_button.style_for(state, false); - Label::new("Share", style.text.clone()) + Label::new(label, style.text.clone()) .contained() .with_style(style.container) .boxed() }) .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, cx| cx.dispatch_action(ShareProject)) - .with_tooltip::( - 0, - "Share project with call participants".into(), - None, - theme.tooltip.clone(), - cx, - ) + .on_click(MouseButton::Left, move |_, cx| { + if is_shared { + cx.dispatch_action(UnshareProject); + } else { + cx.dispatch_action(ShareProject); + } + }) + .with_tooltip::(0, tooltip.to_owned(), None, theme.tooltip.clone(), cx) .aligned() .contained() .with_margin_left(theme.workspace.titlebar.avatar_margin) From 2b6aa3f5d1eb5e6ec6f2941f7991a106891c8dc2 Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 27 Jan 2023 14:48:03 -0500 Subject: [PATCH 03/36] Begin adding collaborator list popover Co-Authored-By: Antonio Scandurra --- crates/collab_ui/src/collab_titlebar_item.rs | 109 ++++++++++++++++-- crates/collab_ui/src/collab_ui.rs | 1 + .../src/collaborator_list_popover.rs | 71 ++++++++++++ crates/collab_ui/src/contact_list.rs | 5 +- 4 files changed, 175 insertions(+), 11 deletions(-) create mode 100644 crates/collab_ui/src/collaborator_list_popover.rs diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 9f360fc3f506a6dc708427f649f4197de62c470b..96217a22cb19f64aadf0fc2bd30a1194b53e6102 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -1,4 +1,7 @@ -use crate::{contact_notification::ContactNotification, contacts_popover, ToggleScreenSharing}; +use crate::{ + collaborator_list_popover, collaborator_list_popover::CollaboratorListPopover, + contact_notification::ContactNotification, contacts_popover, ToggleScreenSharing, +}; use call::{ActiveCall, ParticipantLocation}; use client::{proto::PeerId, Authenticate, ContactEventKind, User, UserStore}; use clock::ReplicaId; @@ -20,10 +23,16 @@ use workspace::{FollowNextCollaborator, JoinProject, ToggleFollow, Workspace}; actions!( collab, - [ToggleCollaborationMenu, ShareProject, UnshareProject] + [ + ToggleCollaboratorList, + ToggleCollaborationMenu, + ShareProject, + UnshareProject + ] ); pub fn init(cx: &mut MutableAppContext) { + cx.add_action(CollabTitlebarItem::toggle_collaborator_list_popover); cx.add_action(CollabTitlebarItem::toggle_contacts_popover); cx.add_action(CollabTitlebarItem::share_project); cx.add_action(CollabTitlebarItem::unshare_project); @@ -33,6 +42,7 @@ pub struct CollabTitlebarItem { workspace: WeakViewHandle, user_store: ModelHandle, contacts_popover: Option>, + collaborator_list_popover: Option>, _subscriptions: Vec, } @@ -76,9 +86,11 @@ impl View for CollabTitlebarItem { left_container.add_child(self.render_share_unshare_button(&workspace, &theme, cx)); } - let mut container = Flex::row(); + left_container.add_child(self.render_toggle_collaborator_list_button(&theme, cx)); + + let mut right_container = Flex::row(); - container.add_children(self.render_toggle_screen_sharing_button(&theme, cx)); + right_container.add_children(self.render_toggle_screen_sharing_button(&theme, cx)); if workspace.read(cx).client().status().borrow().is_connected() { let project = workspace.read(cx).project().read(cx); @@ -86,16 +98,16 @@ impl View for CollabTitlebarItem { || project.is_remote() || ActiveCall::global(cx).read(cx).room().is_none() { - container.add_child(self.render_toggle_contacts_button(&theme, cx)); + right_container.add_child(self.render_toggle_contacts_button(&theme, cx)); } } - container.add_children(self.render_collaborators(&workspace, &theme, cx)); - container.add_children(self.render_current_user(&workspace, &theme, cx)); - container.add_children(self.render_connection_status(&workspace, cx)); + right_container.add_children(self.render_collaborators(&workspace, &theme, cx)); + right_container.add_children(self.render_current_user(&workspace, &theme, cx)); + right_container.add_children(self.render_connection_status(&workspace, cx)); Stack::new() .with_child(left_container.boxed()) - .with_child(container.aligned().right().boxed()) + .with_child(right_container.aligned().right().boxed()) .boxed() } } @@ -141,6 +153,7 @@ impl CollabTitlebarItem { workspace: workspace.downgrade(), user_store: user_store.clone(), contacts_popover: None, + collaborator_list_popover: None, _subscriptions: subscriptions, } } @@ -178,6 +191,82 @@ impl CollabTitlebarItem { } } + fn render_toggle_collaborator_list_button( + &self, + theme: &Theme, + cx: &mut RenderContext, + ) -> ElementBox { + let titlebar = &theme.workspace.titlebar; + + Stack::new() + .with_child( + MouseEventHandler::::new(0, cx, |state, _| { + let style = titlebar + .toggle_contacts_button + .style_for(state, self.collaborator_list_popover.is_some()); + Svg::new("icons/plus_8.svg") + .with_color(style.color) + .constrained() + .with_width(style.icon_width) + .aligned() + .constrained() + .with_width(style.button_width) + .with_height(style.button_width) + .contained() + .with_style(style.container) + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, cx| { + cx.dispatch_action(ToggleCollaboratorList); + }) + .aligned() + .boxed(), + ) + .with_children(self.collaborator_list_popover.as_ref().map(|popover| { + Overlay::new( + ChildView::new(popover, cx) + .contained() + .with_margin_top(titlebar.height) + .with_margin_left(titlebar.toggle_contacts_button.default.button_width) + .with_margin_right(-titlebar.toggle_contacts_button.default.button_width) + .boxed(), + ) + .with_fit_mode(OverlayFitMode::SwitchAnchor) + .with_anchor_corner(AnchorCorner::BottomLeft) + .with_z_index(999) + .boxed() + })) + .boxed() + } + + pub fn toggle_collaborator_list_popover( + &mut self, + _: &ToggleCollaboratorList, + cx: &mut ViewContext, + ) { + match self.collaborator_list_popover.take() { + Some(_) => {} + None => { + let view = cx.add_view(|cx| CollaboratorListPopover::new(cx)); + + cx.subscribe(&view, |this, _, event, cx| { + match event { + collaborator_list_popover::Event::Dismissed => { + this.collaborator_list_popover = None; + } + } + + cx.notify(); + }) + .detach(); + + self.collaborator_list_popover = Some(view); + } + } + cx.notify(); + } + pub fn toggle_contacts_popover( &mut self, _: &ToggleCollaborationMenu, @@ -213,6 +302,7 @@ impl CollabTitlebarItem { cx: &mut RenderContext, ) -> ElementBox { let titlebar = &theme.workspace.titlebar; + let badge = if self .user_store .read(cx) @@ -233,6 +323,7 @@ impl CollabTitlebarItem { .boxed(), ) }; + Stack::new() .with_child( MouseEventHandler::::new(0, cx, |state, _| { diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index d26e2c99ccfb53d9b5e83160464df1a5c73aeafe..d036e10dae0c838e0bc362ff4f5e99f07c271b9c 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -1,4 +1,5 @@ mod collab_titlebar_item; +mod collaborator_list_popover; mod contact_finder; mod contact_list; mod contact_notification; diff --git a/crates/collab_ui/src/collaborator_list_popover.rs b/crates/collab_ui/src/collaborator_list_popover.rs new file mode 100644 index 0000000000000000000000000000000000000000..fa36e202cd7a464648bc98f65adccc04f9ab4198 --- /dev/null +++ b/crates/collab_ui/src/collaborator_list_popover.rs @@ -0,0 +1,71 @@ +use call::ActiveCall; +use gpui::{elements::*, Entity, MouseButton, RenderContext, View, ViewContext}; +use settings::Settings; + +use crate::collab_titlebar_item::ToggleCollaboratorList; + +pub(crate) enum Event { + Dismissed, +} + +pub(crate) struct CollaboratorListPopover { + list_state: ListState, +} + +impl Entity for CollaboratorListPopover { + type Event = Event; +} + +impl View for CollaboratorListPopover { + fn ui_name() -> &'static str { + "CollaboratorListPopover" + } + + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + let theme = cx.global::().theme.clone(); + + MouseEventHandler::::new(0, cx, |_, _| { + List::new(self.list_state.clone()) + .contained() + .with_style(theme.contacts_popover.container) //TODO: Change the name of this theme key + .constrained() + .with_width(theme.contacts_popover.width) + .with_height(theme.contacts_popover.height) + .boxed() + }) + .on_down_out(MouseButton::Left, move |_, cx| { + cx.dispatch_action(ToggleCollaboratorList); + }) + .boxed() + } + + fn focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { + cx.emit(Event::Dismissed); + } +} + +impl CollaboratorListPopover { + pub fn new(cx: &mut ViewContext) -> Self { + let active_call = ActiveCall::global(cx); + let collaborator_count = active_call + .read(cx) + .room() + .map_or(0, |room| room.read(cx).remote_participants().len()); + Self { + list_state: ListState::new( + collaborator_count, + Orientation::Top, + 0., + cx, + |_, index, cx| { + let theme = &cx.global::().theme; + Label::new( + format!("Participant {index}"), + theme.contact_list.contact_username.text.clone(), + ) + .boxed() + }, + ), + } + } +} diff --git a/crates/collab_ui/src/contact_list.rs b/crates/collab_ui/src/contact_list.rs index a1607750c902abd84ba4b7f55a97cd2fc85e4a1a..66b754931ad51b2349706da4dd1f53a7fa81c21d 100644 --- a/crates/collab_ui/src/contact_list.rs +++ b/crates/collab_ui/src/contact_list.rs @@ -27,7 +27,7 @@ impl_internal_actions!(contact_list, [ToggleExpanded, Call, LeaveCall]); pub fn init(cx: &mut MutableAppContext) { cx.add_action(ContactList::remove_contact); cx.add_action(ContactList::respond_to_contact_request); - cx.add_action(ContactList::clear_filter); + cx.add_action(ContactList::cancel); cx.add_action(ContactList::select_next); cx.add_action(ContactList::select_prev); cx.add_action(ContactList::confirm); @@ -326,7 +326,7 @@ impl ContactList { .detach(); } - fn clear_filter(&mut self, _: &Cancel, cx: &mut ViewContext) { + fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { let did_clear = self.filter_editor.update(cx, |editor, cx| { if editor.buffer().read(cx).len(cx) > 0 { editor.set_text("", cx); @@ -335,6 +335,7 @@ impl ContactList { false } }); + if !did_clear { cx.emit(Event::Dismissed); } From c697c1a96ac46ba14b7bfbdb4b4fdf16a1c464a1 Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 30 Jan 2023 10:18:04 -0500 Subject: [PATCH 04/36] Switch collaborator list to using own style --- crates/collab_ui/src/collab_titlebar_item.rs | 10 ++++++---- crates/theme/src/theme.rs | 1 + styles/src/styleTree/workspace.ts | 15 +++++++++++++++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 96217a22cb19f64aadf0fc2bd30a1194b53e6102..59098e3492db0b133258d6121f50d17aae0319cd 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -202,9 +202,9 @@ impl CollabTitlebarItem { .with_child( MouseEventHandler::::new(0, cx, |state, _| { let style = titlebar - .toggle_contacts_button + .toggle_collaborators_button .style_for(state, self.collaborator_list_popover.is_some()); - Svg::new("icons/plus_8.svg") + Svg::new("icons/user_group_12.svg") .with_color(style.color) .constrained() .with_width(style.icon_width) @@ -228,8 +228,10 @@ impl CollabTitlebarItem { ChildView::new(popover, cx) .contained() .with_margin_top(titlebar.height) - .with_margin_left(titlebar.toggle_contacts_button.default.button_width) - .with_margin_right(-titlebar.toggle_contacts_button.default.button_width) + .with_margin_left(titlebar.toggle_collaborators_button.default.button_width) + .with_margin_right( + -titlebar.toggle_collaborators_button.default.button_width, + ) .boxed(), ) .with_fit_mode(OverlayFitMode::SwitchAnchor) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index bc338bbe269369f29fdea1d5794d30c172d2e316..26f071167e630e7eb64b711d0ef2cefd8028465c 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -84,6 +84,7 @@ pub struct Titlebar { pub outdated_warning: ContainedText, pub share_button: Interactive, pub call_control: Interactive, + pub toggle_collaborators_button: Interactive, pub toggle_contacts_button: Interactive, pub toggle_contacts_badge: ContainerStyle, } diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 769ea85a00276cc410b3165b326e4360e59f59f2..e81ce608f42e33fd59dfffa492eccb98e9c2305a 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -147,6 +147,21 @@ export default function workspace(colorScheme: ColorScheme) { color: foreground(layer, "variant", "hovered"), }, }, + toggleCollaboratorsButton: { + margin: { left: 6 }, + cornerRadius: 6, + color: foreground(layer, "variant"), + iconWidth: 12, + buttonWidth: 20, + active: { + background: background(layer, "variant", "active"), + color: foreground(layer, "variant", "active"), + }, + hover: { + background: background(layer, "variant", "hovered"), + color: foreground(layer, "variant", "hovered"), + }, + }, toggleContactsButton: { margin: { left: 6 }, cornerRadius: 6, From bf8658067f0259f208f6dc20688b7ce6adc448dc Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 30 Jan 2023 17:07:30 -0500 Subject: [PATCH 05/36] Add collaborator count to collaborator list button --- crates/collab_ui/src/collab_titlebar_item.rs | 49 ++++++++++++++------ crates/theme/src/theme.rs | 5 +- styles/src/styleTree/workspace.ts | 10 +++- 3 files changed, 45 insertions(+), 19 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 59098e3492db0b133258d6121f50d17aae0319cd..5ca242cd0b062937558e133de24a26eb2387ac40 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -84,10 +84,9 @@ impl View for CollabTitlebarItem { if ActiveCall::global(cx).read(cx).room().is_some() { left_container.add_child(self.render_share_unshare_button(&workspace, &theme, cx)); + left_container.add_child(self.render_toggle_collaborator_list_button(&theme, cx)); } - left_container.add_child(self.render_toggle_collaborator_list_button(&theme, cx)); - let mut right_container = Flex::row(); right_container.add_children(self.render_toggle_screen_sharing_button(&theme, cx)); @@ -200,18 +199,40 @@ impl CollabTitlebarItem { Stack::new() .with_child( - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::::new(0, cx, |state, cx| { + let collaborator_count_style = titlebar.collaborator_count.clone(); let style = titlebar .toggle_collaborators_button .style_for(state, self.collaborator_list_popover.is_some()); - Svg::new("icons/user_group_12.svg") - .with_color(style.color) - .constrained() - .with_width(style.icon_width) - .aligned() - .constrained() - .with_width(style.button_width) - .with_height(style.button_width) + + let active_call = ActiveCall::global(cx); + let collaborator_count = active_call + .read(cx) + .room() + .map_or(0, |room| room.read(cx).remote_participants().len()) + + 1; + + Flex::row() + .with_child( + Svg::new("icons/user_group_12.svg") + .with_color(style.color) + .constrained() + .with_width(style.width) + .aligned() + .contained() + .with_style(style.container) + .boxed(), + ) + .with_child( + Label::new( + format!("{collaborator_count}"), + collaborator_count_style.text, + ) + .contained() + .with_style(collaborator_count_style.container) + .aligned() + .boxed(), + ) .contained() .with_style(style.container) .boxed() @@ -228,10 +249,8 @@ impl CollabTitlebarItem { ChildView::new(popover, cx) .contained() .with_margin_top(titlebar.height) - .with_margin_left(titlebar.toggle_collaborators_button.default.button_width) - .with_margin_right( - -titlebar.toggle_collaborators_button.default.button_width, - ) + .with_margin_left(titlebar.toggle_collaborators_button.default.width) + .with_margin_right(-titlebar.toggle_collaborators_button.default.width) .boxed(), ) .with_fit_mode(OverlayFitMode::SwitchAnchor) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 26f071167e630e7eb64b711d0ef2cefd8028465c..b4d3883b4a0c2ce68c7679714ffb478a3e2cbe6c 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -84,7 +84,8 @@ pub struct Titlebar { pub outdated_warning: ContainedText, pub share_button: Interactive, pub call_control: Interactive, - pub toggle_collaborators_button: Interactive, + pub toggle_collaborators_button: Interactive, + pub collaborator_count: ContainedText, pub toggle_contacts_button: Interactive, pub toggle_contacts_badge: ContainerStyle, } @@ -382,7 +383,7 @@ pub struct InviteLink { pub icon: Icon, } -#[derive(Deserialize, Default)] +#[derive(Deserialize, Clone, Copy, Default)] pub struct Icon { #[serde(flatten)] pub container: ContainerStyle, diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index e81ce608f42e33fd59dfffa492eccb98e9c2305a..22aa361591a2bbbd3c6efab37d1d1ab404b31043 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -151,8 +151,7 @@ export default function workspace(colorScheme: ColorScheme) { margin: { left: 6 }, cornerRadius: 6, color: foreground(layer, "variant"), - iconWidth: 12, - buttonWidth: 20, + width: 12, active: { background: background(layer, "variant", "active"), color: foreground(layer, "variant", "active"), @@ -162,6 +161,13 @@ export default function workspace(colorScheme: ColorScheme) { color: foreground(layer, "variant", "hovered"), }, }, + collaboratorCount: { + ...text(layer, "mono", { size: "sm" }), + margin: { + left: 4, + right: 6, + }, + }, toggleContactsButton: { margin: { left: 6 }, cornerRadius: 6, From 99236f1875d63a8b0489e79773a3a7e979fbf91a Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 30 Jan 2023 23:55:13 -0500 Subject: [PATCH 06/36] Add collaborators to collaborator list, including self user --- crates/collab_ui/src/collab_titlebar_item.rs | 23 ++++++++------- .../src/collaborator_list_popover.rs | 28 +++++++++++++------ 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 5ca242cd0b062937558e133de24a26eb2387ac40..39bcb99f8e7000574e35c77b6eb35826d0e4004d 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -269,20 +269,23 @@ impl CollabTitlebarItem { match self.collaborator_list_popover.take() { Some(_) => {} None => { - let view = cx.add_view(|cx| CollaboratorListPopover::new(cx)); + if let Some(workspace) = self.workspace.upgrade(cx) { + let user_store = workspace.read(cx).user_store().clone(); + let view = cx.add_view(|cx| CollaboratorListPopover::new(user_store, cx)); - cx.subscribe(&view, |this, _, event, cx| { - match event { - collaborator_list_popover::Event::Dismissed => { - this.collaborator_list_popover = None; + cx.subscribe(&view, |this, _, event, cx| { + match event { + collaborator_list_popover::Event::Dismissed => { + this.collaborator_list_popover = None; + } } - } - cx.notify(); - }) - .detach(); + cx.notify(); + }) + .detach(); - self.collaborator_list_popover = Some(view); + self.collaborator_list_popover = Some(view); + } } } cx.notify(); diff --git a/crates/collab_ui/src/collaborator_list_popover.rs b/crates/collab_ui/src/collaborator_list_popover.rs index fa36e202cd7a464648bc98f65adccc04f9ab4198..cdc053d83fb00bbf962ac41eafe6f949f78f9162 100644 --- a/crates/collab_ui/src/collaborator_list_popover.rs +++ b/crates/collab_ui/src/collaborator_list_popover.rs @@ -1,5 +1,6 @@ use call::ActiveCall; -use gpui::{elements::*, Entity, MouseButton, RenderContext, View, ViewContext}; +use client::UserStore; +use gpui::{elements::*, Entity, ModelHandle, MouseButton, RenderContext, View, ViewContext}; use settings::Settings; use crate::collab_titlebar_item::ToggleCollaboratorList; @@ -45,22 +46,33 @@ impl View for CollaboratorListPopover { } impl CollaboratorListPopover { - pub fn new(cx: &mut ViewContext) -> Self { + pub fn new(user_store: ModelHandle, cx: &mut ViewContext) -> Self { let active_call = ActiveCall::global(cx); - let collaborator_count = active_call + + let mut collaborators = user_store .read(cx) - .room() - .map_or(0, |room| room.read(cx).remote_participants().len()); + .current_user() + .map(|u| u.github_login.clone()) + .into_iter() + .collect::>(); + + //TODO: What should the canonical sort here look like, consult contacts list implementation + if let Some(room) = active_call.read(cx).room() { + for participant in room.read(cx).remote_participants() { + collaborators.push(participant.1.user.github_login.clone()); + } + } + Self { list_state: ListState::new( - collaborator_count, + collaborators.len(), Orientation::Top, 0., cx, - |_, index, cx| { + move |_, index, cx| { let theme = &cx.global::().theme; Label::new( - format!("Participant {index}"), + collaborators[index].clone(), theme.contact_list.contact_username.text.clone(), ) .boxed() From f68eda97fba2bef9c280c6fe098a636f6dffbef8 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 31 Jan 2023 13:31:26 -0500 Subject: [PATCH 07/36] Tell project when it has been unshared Co-Authored-By: Petros Amoiridis --- crates/call/src/room.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index f84341e5dd1080a36d0f7f86556ffdff5f636d7b..9c22298946d947b41475fc1ab885872c5e8fd462 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -803,7 +803,8 @@ impl Room { None => return Ok(()), }; - self.client.send(proto::UnshareProject { project_id }) + self.client.send(proto::UnshareProject { project_id })?; + project.update(cx, |this, cx| this.unshare(cx)) } pub(crate) fn set_location( From a5fd0250ab6c3cdd451e04730192702b17dca085 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 31 Jan 2023 18:13:35 -0500 Subject: [PATCH 08/36] Start fleshing out layout of collaborator list entries Co-Authored-By: Petros Amoiridis --- .../src/collaborator_list_popover.rs | 102 ++++++++++++++++-- 1 file changed, 92 insertions(+), 10 deletions(-) diff --git a/crates/collab_ui/src/collaborator_list_popover.rs b/crates/collab_ui/src/collaborator_list_popover.rs index cdc053d83fb00bbf962ac41eafe6f949f78f9162..e6bebf861b596d8b4518c7a5c2f4fb1595266e4b 100644 --- a/crates/collab_ui/src/collaborator_list_popover.rs +++ b/crates/collab_ui/src/collaborator_list_popover.rs @@ -1,6 +1,9 @@ use call::ActiveCall; use client::UserStore; -use gpui::{elements::*, Entity, ModelHandle, MouseButton, RenderContext, View, ViewContext}; +use gpui::Action; +use gpui::{ + actions, elements::*, Entity, ModelHandle, MouseButton, RenderContext, View, ViewContext, +}; use settings::Settings; use crate::collab_titlebar_item::ToggleCollaboratorList; @@ -9,6 +12,13 @@ pub(crate) enum Event { Dismissed, } +enum Collaborator { + SelfUser { username: String }, + RemoteUser { username: String }, +} + +actions!(collaborator_list_popover, [NoOp]); + pub(crate) struct CollaboratorListPopover { list_state: ListState, } @@ -52,14 +62,18 @@ impl CollaboratorListPopover { let mut collaborators = user_store .read(cx) .current_user() - .map(|u| u.github_login.clone()) + .map(|u| Collaborator::SelfUser { + username: u.github_login.clone(), + }) .into_iter() .collect::>(); //TODO: What should the canonical sort here look like, consult contacts list implementation if let Some(room) = active_call.read(cx).room() { for participant in room.read(cx).remote_participants() { - collaborators.push(participant.1.user.github_login.clone()); + collaborators.push(Collaborator::RemoteUser { + username: participant.1.user.github_login.clone(), + }); } } @@ -69,15 +83,83 @@ impl CollaboratorListPopover { Orientation::Top, 0., cx, - move |_, index, cx| { - let theme = &cx.global::().theme; - Label::new( - collaborators[index].clone(), - theme.contact_list.contact_username.text.clone(), - ) - .boxed() + move |_, index, cx| match &collaborators[index] { + Collaborator::SelfUser { username } => render_collaborator_list_entry( + index, + username, + None::, + None, + Svg::new("icons/chevron_right_12.svg"), + NoOp, + "Leave call".to_owned(), + cx, + ), + + Collaborator::RemoteUser { username } => render_collaborator_list_entry( + index, + username, + Some(NoOp), + Some(format!("Follow {username}")), + Svg::new("icons/x_mark_12.svg"), + NoOp, + format!("Remove {username} from call"), + cx, + ), }, ), } } } + +fn render_collaborator_list_entry( + index: usize, + username: &str, + username_action: Option, + username_tooltip: Option, + icon: Svg, + icon_action: IA, + icon_tooltip: String, + cx: &mut RenderContext, +) -> ElementBox { + enum Username {} + enum UsernameTooltip {} + enum Icon {} + enum IconTooltip {} + + let theme = &cx.global::().theme; + let username_theme = theme.contact_list.contact_username.text.clone(); + let tooltip_theme = theme.tooltip.clone(); + + let username = MouseEventHandler::::new(index, cx, |_, _| { + Label::new(username.to_owned(), username_theme.clone()).boxed() + }) + .on_click(MouseButton::Left, move |_, cx| { + if let Some(username_action) = username_action.clone() { + cx.dispatch_action(username_action); + } + }); + + Flex::row() + .with_child(if let Some(username_tooltip) = username_tooltip { + username + .with_tooltip::( + index, + username_tooltip, + None, + tooltip_theme.clone(), + cx, + ) + .boxed() + } else { + username.boxed() + }) + .with_child( + MouseEventHandler::::new(index, cx, |_, _| icon.boxed()) + .on_click(MouseButton::Left, move |_, cx| { + cx.dispatch_action(icon_action.clone()) + }) + .with_tooltip::(index, icon_tooltip, None, tooltip_theme, cx) + .boxed(), + ) + .boxed() +} From 28786a3c183e0c24df598be25cb8ccf956553c98 Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 1 Feb 2023 19:18:23 -0500 Subject: [PATCH 09/36] Add Flex `with_reversed_paint_order` & initially move face piles to left Co-Authored-By: Petros Amoiridis --- crates/collab_ui/src/collab_titlebar_item.rs | 11 +++++----- crates/gpui/src/elements/flex.rs | 21 +++++++++++++++++++- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 39bcb99f8e7000574e35c77b6eb35826d0e4004d..3cb34d451457960aeb1ea0678557aac7f09b5056 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -87,6 +87,9 @@ impl View for CollabTitlebarItem { left_container.add_child(self.render_toggle_collaborator_list_button(&theme, cx)); } + left_container.add_children(self.render_current_user(&workspace, &theme, cx)); + left_container.add_children(self.render_collaborators(&workspace, &theme, cx)); + let mut right_container = Flex::row(); right_container.add_children(self.render_toggle_screen_sharing_button(&theme, cx)); @@ -100,8 +103,6 @@ impl View for CollabTitlebarItem { right_container.add_child(self.render_toggle_contacts_button(&theme, cx)); } } - right_container.add_children(self.render_collaborators(&workspace, &theme, cx)); - right_container.add_children(self.render_current_user(&workspace, &theme, cx)); right_container.add_children(self.render_connection_status(&workspace, cx)); Stack::new() @@ -506,7 +507,7 @@ impl CollabTitlebarItem { .get(&participant.peer_id) .map(|collaborator| collaborator.replica_id); let user = participant.user.clone(); - Some(self.render_avatar( + Some(self.render_face_pile( &user, replica_id, Some(( @@ -535,7 +536,7 @@ impl CollabTitlebarItem { let replica_id = workspace.read(cx).project().read(cx).replica_id(); let status = *workspace.read(cx).client().status().borrow(); if let Some(user) = user { - Some(self.render_avatar(&user, Some(replica_id), None, workspace, theme, cx)) + Some(self.render_face_pile(&user, Some(replica_id), None, workspace, theme, cx)) } else if matches!(status, client::Status::UpgradeRequired) { None } else { @@ -559,7 +560,7 @@ impl CollabTitlebarItem { } } - fn render_avatar( + fn render_face_pile( &self, user: &User, replica_id: Option, diff --git a/crates/gpui/src/elements/flex.rs b/crates/gpui/src/elements/flex.rs index f6a1a5d8e6e52ec572835d34fe7cc9e0655f664a..4e52032f67a5bccae6f2ae0300e0f628e5309671 100644 --- a/crates/gpui/src/elements/flex.rs +++ b/crates/gpui/src/elements/flex.rs @@ -20,6 +20,7 @@ struct ScrollState { pub struct Flex { axis: Axis, + paint_reversed: bool, children: Vec, scroll_state: Option<(ElementStateHandle>, usize)>, } @@ -28,6 +29,7 @@ impl Flex { pub fn new(axis: Axis) -> Self { Self { axis, + paint_reversed: false, children: Default::default(), scroll_state: None, } @@ -41,6 +43,11 @@ impl Flex { Self::new(Axis::Vertical) } + pub fn with_reversed_paint_order(mut self) -> Self { + self.paint_reversed = true; + self + } + pub fn scrollable( mut self, element_id: usize, @@ -296,7 +303,7 @@ impl Element for Flex { } } - for child in &mut self.children { + let mut child_action = |child: &mut ElementBox| { if remaining_space > 0. { if let Some(metadata) = child.metadata::() { if metadata.float { @@ -308,11 +315,23 @@ impl Element for Flex { } } } + child.paint(child_origin, visible_bounds, cx); + match self.axis { Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0), Axis::Vertical => child_origin += vec2f(0.0, child.size().y()), } + }; + + if self.paint_reversed { + for child in self.children.iter_mut().rev() { + child_action(child); + } + } else { + for child in &mut self.children { + child_action(child); + } } if overflowing { From d6462c611cc8256411b0f3c44402d5283b1fe25d Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 3 Feb 2023 13:13:38 -0500 Subject: [PATCH 10/36] Begin tracking follow states on collab server Co-Authored-By: Antonio Scandurra --- crates/call/src/room.rs | 22 ++++++++ .../migrations/20230202155735_followers.sql | 15 ++++++ crates/collab/src/db.rs | 42 +++++++++++++++ crates/collab/src/db/follower.rs | 51 +++++++++++++++++++ crates/collab/src/db/room.rs | 8 +++ crates/collab/src/rpc.rs | 10 ++++ crates/rpc/proto/zed.proto | 10 +++- crates/rpc/src/rpc.rs | 2 +- 8 files changed, 157 insertions(+), 3 deletions(-) create mode 100644 crates/collab/migrations/20230202155735_followers.sql create mode 100644 crates/collab/src/db/follower.rs diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 9c22298946d947b41475fc1ab885872c5e8fd462..bf82fb4c7325dc32c28ac2f48f7e5086b84cb2b8 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -55,6 +55,7 @@ pub struct Room { leave_when_empty: bool, client: Arc, user_store: ModelHandle, + follows_by_leader_id: HashMap>, subscriptions: Vec, pending_room_update: Option>, maintain_connection: Option>>, @@ -148,6 +149,7 @@ impl Room { pending_room_update: None, client, user_store, + follows_by_leader_id: Default::default(), maintain_connection: Some(maintain_connection), } } @@ -487,11 +489,13 @@ impl Room { .iter() .map(|p| p.user_id) .collect::>(); + let remote_participant_user_ids = room .participants .iter() .map(|p| p.user_id) .collect::>(); + let (remote_participants, pending_participants) = self.user_store.update(cx, move |user_store, cx| { ( @@ -499,6 +503,7 @@ impl Room { user_store.get_users(pending_participant_user_ids, cx), ) }); + self.pending_room_update = Some(cx.spawn(|this, mut cx| async move { let (remote_participants, pending_participants) = futures::join!(remote_participants, pending_participants); @@ -620,6 +625,23 @@ impl Room { } } + this.follows_by_leader_id.clear(); + for follower in room.followers { + let (leader, follower) = match (follower.leader_id, follower.follower_id) { + (Some(leader), Some(follower)) => (leader, follower), + + _ => { + log::error!("Follower message {follower:?} missing some state"); + continue; + } + }; + + this.follows_by_leader_id + .entry(leader) + .or_insert(Default::default()) + .insert(follower); + } + this.pending_room_update.take(); if this.should_leave() { log::info!("room is empty, leaving"); diff --git a/crates/collab/migrations/20230202155735_followers.sql b/crates/collab/migrations/20230202155735_followers.sql new file mode 100644 index 0000000000000000000000000000000000000000..c82d6ba3bdaa4f2b2a60771bca7401c47678f247 --- /dev/null +++ b/crates/collab/migrations/20230202155735_followers.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS "followers" ( + "id" SERIAL PRIMARY KEY, + "room_id" INTEGER NOT NULL REFERENCES rooms (id) ON DELETE CASCADE, + "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE, + "leader_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE, + "leader_connection_id" INTEGER NOT NULL, + "follower_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE, + "follower_connection_id" INTEGER NOT NULL +); + +CREATE UNIQUE INDEX + "index_followers_on_project_id_and_leader_connection_server_id_and_leader_connection_id_and_follower_connection_server_id_and_follower_connection_id" +ON "followers" ("project_id", "leader_connection_server_id", "leader_connection_id", "follower_connection_server_id", "follower_connection_id"); + +CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id"); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index af30073ab4ef424b8c9f84557cdd692cdfbfcf46..e60cc9914562cbb68a87d209bdccdfcf51fc56ce 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1,5 +1,6 @@ mod access_token; mod contact; +mod follower; mod language_server; mod project; mod project_collaborator; @@ -1717,6 +1718,35 @@ impl Database { .await } + pub async fn follow( + &self, + room_id: RoomId, + project_id: ProjectId, + leader_connection: ConnectionId, + follower_connection: ConnectionId, + ) -> Result> { + self.room_transaction(|tx| async move { + follower::ActiveModel { + room_id: ActiveValue::set(room_id), + project_id: ActiveValue::set(project_id), + leader_connection_server_id: ActiveValue::set(ServerId( + leader_connection.owner_id as i32, + )), + leader_connection_id: ActiveValue::set(leader_connection.id as i32), + follower_connection_server_id: ActiveValue::set(ServerId( + follower_connection.owner_id as i32, + )), + follower_connection_id: ActiveValue::set(follower_connection.id as i32), + ..Default::default() + } + .insert(&*tx) + .await?; + + Ok((room_id, self.get_room(room_id, &*tx).await?)) + }) + .await + } + pub async fn update_room_participant_location( &self, room_id: RoomId, @@ -1927,11 +1957,22 @@ impl Database { } } + let mut db_followers = db_room.find_related(follower::Entity).stream(tx).await?; + let mut followers = Vec::new(); + while let Some(db_follower) = db_followers.next().await { + let db_follower = db_follower?; + followers.push(proto::Follower { + leader_id: Some(db_follower.leader_connection().into()), + follower_id: Some(db_follower.follower_connection().into()), + }); + } + Ok(proto::Room { id: db_room.id.to_proto(), live_kit_room: db_room.live_kit_room, participants: participants.into_values().collect(), pending_participants, + followers, }) } @@ -3011,6 +3052,7 @@ macro_rules! id_type { id_type!(AccessTokenId); id_type!(ContactId); +id_type!(FollowerId); id_type!(RoomId); id_type!(RoomParticipantId); id_type!(ProjectId); diff --git a/crates/collab/src/db/follower.rs b/crates/collab/src/db/follower.rs new file mode 100644 index 0000000000000000000000000000000000000000..f1243dc99eb34371bdd433c431b8a12eafeadbb6 --- /dev/null +++ b/crates/collab/src/db/follower.rs @@ -0,0 +1,51 @@ +use super::{FollowerId, ProjectId, RoomId, ServerId}; +use rpc::ConnectionId; +use sea_orm::entity::prelude::*; +use serde::Serialize; + +#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel, Serialize)] +#[sea_orm(table_name = "followers")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: FollowerId, + pub room_id: RoomId, + pub project_id: ProjectId, + pub leader_connection_server_id: ServerId, + pub leader_connection_id: i32, + pub follower_connection_server_id: ServerId, + pub follower_connection_id: i32, +} + +impl Model { + pub fn leader_connection(&self) -> ConnectionId { + ConnectionId { + owner_id: self.leader_connection_server_id.0 as u32, + id: self.leader_connection_id as u32, + } + } + + pub fn follower_connection(&self) -> ConnectionId { + ConnectionId { + owner_id: self.follower_connection_server_id.0 as u32, + id: self.follower_connection_id as u32, + } + } +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::room::Entity", + from = "Column::RoomId", + to = "super::room::Column::Id" + )] + Room, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Room.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/room.rs b/crates/collab/src/db/room.rs index 7dbf03a780adbd69c1d3b492e4bcf82557ae70ab..c3e88670ebe00afa523b008e6bf669de01ec1d1e 100644 --- a/crates/collab/src/db/room.rs +++ b/crates/collab/src/db/room.rs @@ -15,6 +15,8 @@ pub enum Relation { RoomParticipant, #[sea_orm(has_many = "super::project::Entity")] Project, + #[sea_orm(has_many = "super::follower::Entity")] + Follower, } impl Related for Entity { @@ -29,4 +31,10 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::Follower.def() + } +} + impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 32cce1e6815451ce9ac7b8e6e261ed60bca41e3d..fad34497eb578d0e54108274e65f99d6d36cbef9 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1312,6 +1312,7 @@ async fn join_project( .filter(|collaborator| collaborator.connection_id != session.connection_id) .map(|collaborator| collaborator.to_proto()) .collect::>(); + let worktrees = project .worktrees .iter() @@ -1719,6 +1720,7 @@ async fn follow( session: Session, ) -> Result<()> { let project_id = ProjectId::from_proto(request.project_id); + let room_id = RoomId::from_proto(request.project_id); let leader_id = request .leader_id .ok_or_else(|| anyhow!("invalid leader id"))? @@ -1744,6 +1746,14 @@ async fn follow( .views .retain(|view| view.leader_id != Some(follower_id.into())); response.send(response_payload)?; + + let room = session + .db() + .await + .follow(room_id, project_id, leader_id, follower_id) + .await?; + room_updated(&room, &session.peer); + Ok(()) } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index ba481ce45ba7ab148d2f9b347d5d483e448b44f6..6b46f09e268ca96c75235200b26249cb523dda4f 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -16,7 +16,7 @@ message Envelope { Error error = 6; Ping ping = 7; Test test = 8; - + CreateRoom create_room = 9; CreateRoomResponse create_room_response = 10; JoinRoom join_room = 11; @@ -206,7 +206,8 @@ message Room { uint64 id = 1; repeated Participant participants = 2; repeated PendingParticipant pending_participants = 3; - string live_kit_room = 4; + repeated Follower followers = 4; + string live_kit_room = 5; } message Participant { @@ -227,6 +228,11 @@ message ParticipantProject { repeated string worktree_root_names = 2; } +message Follower { + PeerId leader_id = 1; + PeerId follower_id = 2; +} + message ParticipantLocation { oneof variant { SharedProject shared_project = 1; diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index b05bc17906063fe51dc4ec349496f26a1338a9dc..439ed8774637af088bd5bea72cd0ee56e76db4ac 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 46; +pub const PROTOCOL_VERSION: u32 = 47; From 2592ec726596e36052855b5fed10ea91022d93ba Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 3 Feb 2023 15:37:24 -0500 Subject: [PATCH 11/36] Initial tracking of unfollows on collab server --- crates/collab/src/db.rs | 41 ++++++++++++++++++++++++++++++++++++++++ crates/collab/src/rpc.rs | 22 +++++++++++++++++---- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index e60cc9914562cbb68a87d209bdccdfcf51fc56ce..ec89a3dfacea919d8acbc908e13fc348e90905c4 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1747,6 +1747,47 @@ impl Database { .await } + pub async fn unfollow( + &self, + room_id: RoomId, + project_id: ProjectId, + leader_connection: ConnectionId, + follower_connection: ConnectionId, + ) -> Result> { + self.room_transaction(|tx| async move { + follower::Entity::delete_many() + .filter( + Condition::all() + .add(follower::Column::RoomId.eq(room_id)) + .add(follower::Column::ProjectId.eq(project_id)) + .add( + Condition::any() + .add( + follower::Column::LeaderConnectionServerId + .eq(leader_connection.owner_id) + .and( + follower::Column::LeaderConnectionId + .eq(leader_connection.id), + ), + ) + .add( + follower::Column::FollowerConnectionServerId + .eq(follower_connection.owner_id) + .and( + follower::Column::FollowerConnectionId + .eq(follower_connection.id), + ), + ), + ), + ) + .exec(&*tx) + .await?; + + Ok((room_id, self.get_room(room_id, &*tx).await?)) + }) + .await + } + pub async fn update_room_participant_location( &self, room_id: RoomId, diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index fad34497eb578d0e54108274e65f99d6d36cbef9..2260c10605057ff75e93ce9850bd3003775e46c1 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1719,13 +1719,14 @@ async fn follow( response: Response, session: Session, ) -> Result<()> { - let project_id = ProjectId::from_proto(request.project_id); let room_id = RoomId::from_proto(request.project_id); + let project_id = ProjectId::from_proto(request.project_id); let leader_id = request .leader_id .ok_or_else(|| anyhow!("invalid leader id"))? .into(); let follower_id = session.connection_id; + { let project_connection_ids = session .db() @@ -1758,22 +1759,35 @@ async fn follow( } async fn unfollow(request: proto::Unfollow, session: Session) -> Result<()> { + let room_id = RoomId::from_proto(request.project_id); let project_id = ProjectId::from_proto(request.project_id); let leader_id = request .leader_id .ok_or_else(|| anyhow!("invalid leader id"))? .into(); - let project_connection_ids = session + let follower_id = session.connection_id; + + if !session .db() .await .project_connection_ids(project_id, session.connection_id) - .await?; - if !project_connection_ids.contains(&leader_id) { + .await? + .contains(&leader_id) + { Err(anyhow!("no such peer"))?; } + session .peer .forward_send(session.connection_id, leader_id, request)?; + + let room = session + .db() + .await + .unfollow(room_id, project_id, leader_id, follower_id) + .await?; + room_updated(&room, &session.peer); + Ok(()) } From 58c41778e7638c3db75a426499ee0a4ef29621a2 Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 3 Feb 2023 18:20:24 -0500 Subject: [PATCH 12/36] Absolute pain of the iterator kind (start laying out a user's followers) --- crates/call/src/room.rs | 17 ++++- crates/collab_ui/src/collab_titlebar_item.rs | 77 ++++++++++++++------ crates/workspace/src/workspace.rs | 2 +- 3 files changed, 70 insertions(+), 26 deletions(-) diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index bf82fb4c7325dc32c28ac2f48f7e5086b84cb2b8..d3d7facf3b570a35decd3071ee306008f4980f05 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -55,7 +55,7 @@ pub struct Room { leave_when_empty: bool, client: Arc, user_store: ModelHandle, - follows_by_leader_id: HashMap>, + follows_by_leader_id: HashMap>, subscriptions: Vec, pending_room_update: Option>, maintain_connection: Option>>, @@ -459,6 +459,12 @@ impl Room { self.participant_user_ids.contains(&user_id) } + pub fn follows(&self, leader_id: PeerId) -> &[PeerId] { + self.follows_by_leader_id + .get(&leader_id) + .map_or(&[], |v| v.as_slice()) + } + async fn handle_room_updated( this: ModelHandle, envelope: TypedEnvelope, @@ -636,10 +642,13 @@ impl Room { } }; - this.follows_by_leader_id + let list = this + .follows_by_leader_id .entry(leader) - .or_insert(Default::default()) - .insert(follower); + .or_insert(Vec::new()); + if !list.contains(&follower) { + list.push(follower); + } } this.pending_room_update.take(); diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 3cb34d451457960aeb1ea0678557aac7f09b5056..518df161694ca94db84da71ab314c8fc017bbb5b 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -12,11 +12,11 @@ use gpui::{ elements::*, geometry::{rect::RectF, vector::vec2f, PathBuilder}, json::{self, ToJson}, - Border, CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, - Subscription, View, ViewContext, ViewHandle, WeakViewHandle, + Border, CursorStyle, Entity, ImageData, ModelHandle, MouseButton, MutableAppContext, + RenderContext, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, }; use settings::Settings; -use std::ops::Range; +use std::{ops::Range, sync::Arc}; use theme::Theme; use util::ResultExt; use workspace::{FollowNextCollaborator, JoinProject, ToggleFollow, Workspace}; @@ -510,11 +510,7 @@ impl CollabTitlebarItem { Some(self.render_face_pile( &user, replica_id, - Some(( - participant.peer_id, - &user.github_login, - participant.location, - )), + Some((participant.peer_id, participant.location)), workspace, theme, cx, @@ -564,18 +560,23 @@ impl CollabTitlebarItem { &self, user: &User, replica_id: Option, - peer: Option<(PeerId, &str, ParticipantLocation)>, + peer_id_and_location: Option<(PeerId, ParticipantLocation)>, workspace: &ViewHandle, theme: &Theme, cx: &mut RenderContext, ) -> ElementBox { - let is_followed = peer.map_or(false, |(peer_id, _, _)| { + let is_followed = peer_id_and_location.map_or(false, |(peer_id, _)| { workspace.read(cx).is_following(peer_id) }); + let room = ActiveCall::global(cx).read(cx).room(); + let get_followers = |leader_id: PeerId| -> &[PeerId] { + room.map_or(&[], |room| room.read(cx).follows(leader_id)) + }; + let mut avatar_style; - if let Some((_, _, location)) = peer.as_ref() { - if let ParticipantLocation::SharedProject { project_id } = *location { + if let Some((_, location)) = peer_id_and_location { + if let ParticipantLocation::SharedProject { project_id } = location { if Some(project_id) == workspace.read(cx).project().read(cx).remote_id() { avatar_style = theme.workspace.titlebar.avatar; } else { @@ -599,11 +600,36 @@ impl CollabTitlebarItem { let content = Stack::new() .with_children(user.avatar.as_ref().map(|avatar| { - Image::new(avatar.clone()) - .with_style(avatar_style) - .constrained() - .with_width(theme.workspace.titlebar.avatar_width) - .aligned() + Flex::row() + .with_child(Self::render_face(avatar.clone(), avatar_style, theme)) + .with_children( + peer_id_and_location + .map(|(peer_id, _)| { + get_followers(peer_id) + .into_iter() + .map(|&follower| { + room.map(|room| { + room.read(cx) + .remote_participant_for_peer_id(follower) + .map(|participant| { + participant.user.avatar.as_ref().map(|avatar| { + Self::render_face( + avatar.clone(), + avatar_style, + theme, + ) + }) + }) + .flatten() + }) + .flatten() + }) + .flatten() + }) + .into_iter() + .flatten(), + ) + .with_reversed_paint_order() .boxed() })) .with_children(replica_color.map(|replica_color| { @@ -621,7 +647,7 @@ impl CollabTitlebarItem { .with_margin_left(theme.workspace.titlebar.avatar_margin) .boxed(); - if let Some((peer_id, peer_github_login, location)) = peer { + if let Some((peer_id, location)) = peer_id_and_location { if let Some(replica_id) = replica_id { MouseEventHandler::::new(replica_id.into(), cx, move |_, _| content) .with_cursor_style(CursorStyle::PointingHand) @@ -631,9 +657,9 @@ impl CollabTitlebarItem { .with_tooltip::( peer_id.as_u64() as usize, if is_followed { - format!("Unfollow {}", peer_github_login) + format!("Unfollow {}", user.github_login) } else { - format!("Follow {}", peer_github_login) + format!("Follow {}", user.github_login) }, Some(Box::new(FollowNextCollaborator)), theme.tooltip.clone(), @@ -654,7 +680,7 @@ impl CollabTitlebarItem { }) .with_tooltip::( peer_id.as_u64() as usize, - format!("Follow {} into external project", peer_github_login), + format!("Follow {} into external project", user.github_login), Some(Box::new(FollowNextCollaborator)), theme.tooltip.clone(), cx, @@ -668,6 +694,15 @@ impl CollabTitlebarItem { } } + fn render_face(avatar: Arc, avatar_style: ImageStyle, theme: &Theme) -> ElementBox { + Image::new(avatar) + .with_style(avatar_style) + .constrained() + .with_width(theme.workspace.titlebar.avatar_width) + .aligned() + .boxed() + } + fn render_connection_status( &self, workspace: &ViewHandle, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index e6a89045d23082601a60798f6b0eb428240e1b9a..82f22a1a404577961c9e524341567796472b4e89 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -837,7 +837,7 @@ impl Workspace { &self.project } - pub fn client(&self) -> &Arc { + pub fn client(&self) -> &Client { &self.client } From 33c265d3cf268d97594ae6248adef8c8ff026f33 Mon Sep 17 00:00:00 2001 From: Julia Date: Sat, 4 Feb 2023 01:30:24 -0500 Subject: [PATCH 13/36] Abuse a closure instead of abusing options/iterators quite so much --- crates/call/src/room.rs | 4 +- crates/collab_ui/src/collab_titlebar_item.rs | 45 +++++++------------- 2 files changed, 17 insertions(+), 32 deletions(-) diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index d3d7facf3b570a35decd3071ee306008f4980f05..2f4fd56e4703753c76f2b3b467cc0c9abca51e19 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -459,10 +459,10 @@ impl Room { self.participant_user_ids.contains(&user_id) } - pub fn follows(&self, leader_id: PeerId) -> &[PeerId] { + pub fn follows(&self, leader_id: PeerId) -> Option<&[PeerId]> { self.follows_by_leader_id .get(&leader_id) - .map_or(&[], |v| v.as_slice()) + .map(|v| v.as_slice()) } async fn handle_room_updated( diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 518df161694ca94db84da71ab314c8fc017bbb5b..f9b8df348851fcaadd101788640115cf5d547bf6 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -569,11 +569,6 @@ impl CollabTitlebarItem { workspace.read(cx).is_following(peer_id) }); - let room = ActiveCall::global(cx).read(cx).room(); - let get_followers = |leader_id: PeerId| -> &[PeerId] { - room.map_or(&[], |room| room.read(cx).follows(leader_id)) - }; - let mut avatar_style; if let Some((_, location)) = peer_id_and_location { if let ParticipantLocation::SharedProject { project_id } = location { @@ -603,31 +598,21 @@ impl CollabTitlebarItem { Flex::row() .with_child(Self::render_face(avatar.clone(), avatar_style, theme)) .with_children( - peer_id_and_location - .map(|(peer_id, _)| { - get_followers(peer_id) - .into_iter() - .map(|&follower| { - room.map(|room| { - room.read(cx) - .remote_participant_for_peer_id(follower) - .map(|participant| { - participant.user.avatar.as_ref().map(|avatar| { - Self::render_face( - avatar.clone(), - avatar_style, - theme, - ) - }) - }) - .flatten() - }) - .flatten() - }) - .flatten() - }) - .into_iter() - .flatten(), + (|| { + let peer_id_and_location = peer_id_and_location?; + let peer_id = peer_id_and_location.0; + + let room = ActiveCall::global(cx).read(cx).room()?.read(cx); + let followers = room.follows(peer_id)?; + + Some(followers.into_iter().flat_map(|&follower| { + let participant = room.remote_participant_for_peer_id(follower)?; + let avatar = participant.user.avatar.as_ref()?; + Some(Self::render_face(avatar.clone(), avatar_style, theme)) + })) + })() + .into_iter() + .flatten(), ) .with_reversed_paint_order() .boxed() From 4ffc8cd9fd2ec537373d99146e95c227b4c3f56a Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 6 Feb 2023 14:44:17 -0500 Subject: [PATCH 14/36] Fix deadlock in db `get_room` Co-Authored-By: Max Brunsfeld --- crates/collab/src/db.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index ec89a3dfacea919d8acbc908e13fc348e90905c4..1059b2801016b275678172b539f823ec51490d26 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1996,7 +1996,8 @@ impl Database { } } } - } + } + drop(db_projects); let mut db_followers = db_room.find_related(follower::Entity).stream(tx).await?; let mut followers = Vec::new(); From 4513c409933d94f2d25a88ec11443254e0998bd1 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 7 Feb 2023 19:08:35 -0500 Subject: [PATCH 15/36] Following face piles finally take their first breath --- crates/call/src/room.rs | 4 +-- crates/collab/src/db.rs | 2 +- crates/collab_ui/src/collab_titlebar_item.rs | 33 +++++++++++--------- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 2f4fd56e4703753c76f2b3b467cc0c9abca51e19..d3d7facf3b570a35decd3071ee306008f4980f05 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -459,10 +459,10 @@ impl Room { self.participant_user_ids.contains(&user_id) } - pub fn follows(&self, leader_id: PeerId) -> Option<&[PeerId]> { + pub fn follows(&self, leader_id: PeerId) -> &[PeerId] { self.follows_by_leader_id .get(&leader_id) - .map(|v| v.as_slice()) + .map_or(&[], |v| v.as_slice()) } async fn handle_room_updated( diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 1059b2801016b275678172b539f823ec51490d26..d7953e1494ce3d4ad8ff9895ab1ecf9989cd125f 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1996,7 +1996,7 @@ impl Database { } } } - } + } drop(db_projects); let mut db_followers = db_room.find_related(follower::Entity).stream(tx).await?; diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index f9b8df348851fcaadd101788640115cf5d547bf6..d4c667b1c47d8383ebd3f3167a165e0e069d6cef 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -510,7 +510,8 @@ impl CollabTitlebarItem { Some(self.render_face_pile( &user, replica_id, - Some((participant.peer_id, participant.location)), + participant.peer_id, + Some(participant.location), workspace, theme, cx, @@ -530,9 +531,18 @@ impl CollabTitlebarItem { ) -> Option { let user = workspace.read(cx).user_store().read(cx).current_user(); let replica_id = workspace.read(cx).project().read(cx).replica_id(); + let peer_id = workspace.read(cx).client().peer_id()?; let status = *workspace.read(cx).client().status().borrow(); if let Some(user) = user { - Some(self.render_face_pile(&user, Some(replica_id), None, workspace, theme, cx)) + Some(self.render_face_pile( + &user, + Some(replica_id), + peer_id, + None, + workspace, + theme, + cx, + )) } else if matches!(status, client::Status::UpgradeRequired) { None } else { @@ -560,17 +570,16 @@ impl CollabTitlebarItem { &self, user: &User, replica_id: Option, - peer_id_and_location: Option<(PeerId, ParticipantLocation)>, + peer_id: PeerId, + location: Option, workspace: &ViewHandle, theme: &Theme, cx: &mut RenderContext, ) -> ElementBox { - let is_followed = peer_id_and_location.map_or(false, |(peer_id, _)| { - workspace.read(cx).is_following(peer_id) - }); + let is_followed = workspace.read(cx).is_following(peer_id); let mut avatar_style; - if let Some((_, location)) = peer_id_and_location { + if let Some(location) = location { if let ParticipantLocation::SharedProject { project_id } = location { if Some(project_id) == workspace.read(cx).project().read(cx).remote_id() { avatar_style = theme.workspace.titlebar.avatar; @@ -599,11 +608,8 @@ impl CollabTitlebarItem { .with_child(Self::render_face(avatar.clone(), avatar_style, theme)) .with_children( (|| { - let peer_id_and_location = peer_id_and_location?; - let peer_id = peer_id_and_location.0; - let room = ActiveCall::global(cx).read(cx).room()?.read(cx); - let followers = room.follows(peer_id)?; + let followers = room.follows(peer_id); Some(followers.into_iter().flat_map(|&follower| { let participant = room.remote_participant_for_peer_id(follower)?; @@ -614,7 +620,6 @@ impl CollabTitlebarItem { .into_iter() .flatten(), ) - .with_reversed_paint_order() .boxed() })) .with_children(replica_color.map(|replica_color| { @@ -626,13 +631,11 @@ impl CollabTitlebarItem { .bottom() .boxed() })) - .constrained() - .with_width(theme.workspace.titlebar.avatar_width) .contained() .with_margin_left(theme.workspace.titlebar.avatar_margin) .boxed(); - if let Some((peer_id, location)) = peer_id_and_location { + if let Some(location) = location { if let Some(replica_id) = replica_id { MouseEventHandler::::new(replica_id.into(), cx, move |_, _| content) .with_cursor_style(CursorStyle::PointingHand) From 3fb83950856e715de3b70b49e31c73639fcda38d Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 9 Feb 2023 12:05:07 -0500 Subject: [PATCH 16/36] Make things a bit more infallible --- crates/collab_ui/src/collab_titlebar_item.rs | 138 +++++++++---------- 1 file changed, 65 insertions(+), 73 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index d4c667b1c47d8383ebd3f3167a165e0e069d6cef..d89bc51569ec4f4580582b7d458d0d491b32b32f 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -2,7 +2,7 @@ use crate::{ collaborator_list_popover, collaborator_list_popover::CollaboratorListPopover, contact_notification::ContactNotification, contacts_popover, ToggleScreenSharing, }; -use call::{ActiveCall, ParticipantLocation}; +use call::{ActiveCall, ParticipantLocation, Room}; use client::{proto::PeerId, Authenticate, ContactEventKind, User, UserStore}; use clock::ReplicaId; use contacts_popover::ContactsPopover; @@ -82,13 +82,13 @@ impl View for CollabTitlebarItem { .boxed(), ); - if ActiveCall::global(cx).read(cx).room().is_some() { + if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { left_container.add_child(self.render_share_unshare_button(&workspace, &theme, cx)); left_container.add_child(self.render_toggle_collaborator_list_button(&theme, cx)); - } - left_container.add_children(self.render_current_user(&workspace, &theme, cx)); - left_container.add_children(self.render_collaborators(&workspace, &theme, cx)); + left_container.add_child(self.render_current_user(&workspace, &theme, cx)); + left_container.add_children(self.render_collaborators(&workspace, &theme, room, cx)); + } let mut right_container = Flex::row(); @@ -486,41 +486,37 @@ impl CollabTitlebarItem { &self, workspace: &ViewHandle, theme: &Theme, + room: ModelHandle, cx: &mut RenderContext, ) -> Vec { - let active_call = ActiveCall::global(cx); - if let Some(room) = active_call.read(cx).room().cloned() { - let project = workspace.read(cx).project().read(cx); - let mut participants = room - .read(cx) - .remote_participants() - .values() - .cloned() - .collect::>(); - participants.sort_by_key(|p| Some(project.collaborators().get(&p.peer_id)?.replica_id)); - participants - .into_iter() - .filter_map(|participant| { - let project = workspace.read(cx).project().read(cx); - let replica_id = project - .collaborators() - .get(&participant.peer_id) - .map(|collaborator| collaborator.replica_id); - let user = participant.user.clone(); - Some(self.render_face_pile( - &user, - replica_id, - participant.peer_id, - Some(participant.location), - workspace, - theme, - cx, - )) - }) - .collect() - } else { - Default::default() - } + let project = workspace.read(cx).project().read(cx); + let mut participants = room + .read(cx) + .remote_participants() + .values() + .cloned() + .collect::>(); + participants.sort_by_key(|p| Some(project.collaborators().get(&p.peer_id)?.replica_id)); + participants + .into_iter() + .filter_map(|participant| { + let project = workspace.read(cx).project().read(cx); + let replica_id = project + .collaborators() + .get(&participant.peer_id) + .map(|collaborator| collaborator.replica_id); + let user = participant.user.clone(); + Some(self.render_face_pile( + &user, + replica_id, + participant.peer_id, + Some(participant.location), + workspace, + theme, + cx, + )) + }) + .collect() } fn render_current_user( @@ -528,42 +524,38 @@ impl CollabTitlebarItem { workspace: &ViewHandle, theme: &Theme, cx: &mut RenderContext, - ) -> Option { - let user = workspace.read(cx).user_store().read(cx).current_user(); + ) -> ElementBox { + let user = workspace + .read(cx) + .user_store() + .read(cx) + .current_user() + .expect("Active call without user"); let replica_id = workspace.read(cx).project().read(cx).replica_id(); - let peer_id = workspace.read(cx).client().peer_id()?; - let status = *workspace.read(cx).client().status().borrow(); - if let Some(user) = user { - Some(self.render_face_pile( - &user, - Some(replica_id), - peer_id, - None, - workspace, - theme, - cx, - )) - } else if matches!(status, client::Status::UpgradeRequired) { - None - } else { - Some( - MouseEventHandler::::new(0, cx, |state, _| { - let style = theme - .workspace - .titlebar - .sign_in_prompt - .style_for(state, false); - Label::new("Sign in", style.text.clone()) - .contained() - .with_style(style.container) - .boxed() - }) - .on_click(MouseButton::Left, |_, cx| cx.dispatch_action(Authenticate)) - .with_cursor_style(CursorStyle::PointingHand) - .aligned() - .boxed(), - ) - } + let peer_id = workspace + .read(cx) + .client() + .peer_id() + .expect("Active call without peer id"); + self.render_face_pile(&user, Some(replica_id), peer_id, None, workspace, theme, cx) + } + + fn render_authenticate(theme: &Theme, cx: &mut RenderContext) -> ElementBox { + MouseEventHandler::::new(0, cx, |state, _| { + let style = theme + .workspace + .titlebar + .sign_in_prompt + .style_for(state, false); + Label::new("Sign in", style.text.clone()) + .contained() + .with_style(style.container) + .boxed() + }) + .on_click(MouseButton::Left, |_, cx| cx.dispatch_action(Authenticate)) + .with_cursor_style(CursorStyle::PointingHand) + .aligned() + .boxed() } fn render_face_pile( From 50e681bbb1e5ed3c6c80132bf347c3def07695d2 Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 9 Feb 2023 12:19:40 -0500 Subject: [PATCH 17/36] Add username to right side --- crates/collab_ui/src/collab_titlebar_item.rs | 27 ++++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index d89bc51569ec4f4580582b7d458d0d491b32b32f..4b8cd816e14500862cf99d84715aa9fffdad710e 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -72,6 +72,7 @@ impl View for CollabTitlebarItem { } let theme = cx.global::().theme.clone(); + let user = workspace.read(cx).user_store().read(cx).current_user(); let mut left_container = Flex::row(); @@ -86,7 +87,7 @@ impl View for CollabTitlebarItem { left_container.add_child(self.render_share_unshare_button(&workspace, &theme, cx)); left_container.add_child(self.render_toggle_collaborator_list_button(&theme, cx)); - left_container.add_child(self.render_current_user(&workspace, &theme, cx)); + left_container.add_child(self.render_current_user(&workspace, &theme, &user, cx)); left_container.add_children(self.render_collaborators(&workspace, &theme, room, cx)); } @@ -105,6 +106,20 @@ impl View for CollabTitlebarItem { } right_container.add_children(self.render_connection_status(&workspace, cx)); + if let Some(user) = user { + //TODO: Add style + right_container.add_child( + Label::new( + user.github_login.clone(), + theme.workspace.titlebar.title.clone(), + ) + .aligned() + .boxed(), + ); + } else { + right_container.add_child(Self::render_authenticate(&theme, cx)); + } + Stack::new() .with_child(left_container.boxed()) .with_child(right_container.aligned().right().boxed()) @@ -523,21 +538,17 @@ impl CollabTitlebarItem { &self, workspace: &ViewHandle, theme: &Theme, + user: &Option>, cx: &mut RenderContext, ) -> ElementBox { - let user = workspace - .read(cx) - .user_store() - .read(cx) - .current_user() - .expect("Active call without user"); + let user = user.as_ref().expect("Active call without user"); let replica_id = workspace.read(cx).project().read(cx).replica_id(); let peer_id = workspace .read(cx) .client() .peer_id() .expect("Active call without peer id"); - self.render_face_pile(&user, Some(replica_id), peer_id, None, workspace, theme, cx) + self.render_face_pile(user, Some(replica_id), peer_id, None, workspace, theme, cx) } fn render_authenticate(theme: &Theme, cx: &mut RenderContext) -> ElementBox { From 1abb7794cb7da590a3669e1a8a9bf80f0e3e560c Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 9 Feb 2023 13:32:07 -0500 Subject: [PATCH 18/36] Handle case where follower is local user --- crates/collab_ui/src/collab_titlebar_item.rs | 21 ++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 4b8cd816e14500862cf99d84715aa9fffdad710e..5a283ba52926355089c810a572ccc1ec887ede58 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -505,6 +505,7 @@ impl CollabTitlebarItem { cx: &mut RenderContext, ) -> Vec { let project = workspace.read(cx).project().read(cx); + let mut participants = room .read(cx) .remote_participants() @@ -512,6 +513,7 @@ impl CollabTitlebarItem { .cloned() .collect::>(); participants.sort_by_key(|p| Some(project.collaborators().get(&p.peer_id)?.replica_id)); + participants .into_iter() .filter_map(|participant| { @@ -615,8 +617,23 @@ impl CollabTitlebarItem { let followers = room.follows(peer_id); Some(followers.into_iter().flat_map(|&follower| { - let participant = room.remote_participant_for_peer_id(follower)?; - let avatar = participant.user.avatar.as_ref()?; + let avatar = room + .remote_participant_for_peer_id(follower) + .and_then(|participant| participant.user.avatar.clone()) + .or_else(|| { + if follower == workspace.read(cx).client().peer_id()? { + workspace + .read(cx) + .user_store() + .read(cx) + .current_user()? + .avatar + .clone() + } else { + None + } + })?; + Some(Self::render_face(avatar.clone(), avatar_style, theme)) })) })() From 05e9615507daf4f917fd38d82ce47a9add15254d Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 9 Feb 2023 14:38:43 -0500 Subject: [PATCH 19/36] Highlight face pile which local user is following --- crates/call/src/room.rs | 2 +- crates/collab_ui/src/collab_titlebar_item.rs | 70 ++++++++++++-------- crates/collab_ui/src/collab_ui.rs | 2 +- crates/workspace/src/workspace.rs | 4 +- 4 files changed, 46 insertions(+), 32 deletions(-) diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index d3d7facf3b570a35decd3071ee306008f4980f05..51b125577db11d8c0e8d48d9c2f146a2cec2f630 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -459,7 +459,7 @@ impl Room { self.participant_user_ids.contains(&user_id) } - pub fn follows(&self, leader_id: PeerId) -> &[PeerId] { + pub fn followers_for(&self, leader_id: PeerId) -> &[PeerId] { self.follows_by_leader_id .get(&leader_id) .map_or(&[], |v| v.as_slice()) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 5a283ba52926355089c810a572ccc1ec887ede58..a4112164e348c9e0ce00261816b2fd228996dde3 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -12,8 +12,8 @@ use gpui::{ elements::*, geometry::{rect::RectF, vector::vec2f, PathBuilder}, json::{self, ToJson}, - Border, CursorStyle, Entity, ImageData, ModelHandle, MouseButton, MutableAppContext, - RenderContext, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, + CursorStyle, Entity, ImageData, ModelHandle, MouseButton, MutableAppContext, RenderContext, + Subscription, View, ViewContext, ViewHandle, WeakViewHandle, }; use settings::Settings; use std::{ops::Range, sync::Arc}; @@ -581,9 +581,10 @@ impl CollabTitlebarItem { theme: &Theme, cx: &mut RenderContext, ) -> ElementBox { - let is_followed = workspace.read(cx).is_following(peer_id); + let room = ActiveCall::global(cx).read(cx).room(); + let is_being_followed = workspace.read(cx).is_being_followed(peer_id); - let mut avatar_style; + let avatar_style; if let Some(location) = location { if let ParticipantLocation::SharedProject { project_id } = location { if Some(project_id) == workspace.read(cx).project().read(cx).remote_id() { @@ -598,23 +599,14 @@ impl CollabTitlebarItem { avatar_style = theme.workspace.titlebar.avatar; } - let mut replica_color = None; - if let Some(replica_id) = replica_id { - let color = theme.editor.replica_selection_style(replica_id).cursor; - replica_color = Some(color); - if is_followed { - avatar_style.border = Border::all(1.0, color); - } - } - let content = Stack::new() .with_children(user.avatar.as_ref().map(|avatar| { - Flex::row() + let flex = Flex::row() .with_child(Self::render_face(avatar.clone(), avatar_style, theme)) .with_children( (|| { - let room = ActiveCall::global(cx).read(cx).room()?.read(cx); - let followers = room.follows(peer_id); + let room = room?.read(cx); + let followers = room.followers_for(peer_id); Some(followers.into_iter().flat_map(|&follower| { let avatar = room @@ -639,18 +631,40 @@ impl CollabTitlebarItem { })() .into_iter() .flatten(), - ) - .boxed() - })) - .with_children(replica_color.map(|replica_color| { - AvatarRibbon::new(replica_color) - .constrained() - .with_width(theme.workspace.titlebar.avatar_ribbon.width) - .with_height(theme.workspace.titlebar.avatar_ribbon.height) - .aligned() - .bottom() - .boxed() + ); + + let room = ActiveCall::global(cx).read(cx).room(); + if let (Some(replica_id), Some(room)) = (replica_id, room) { + let followed_by_self = is_being_followed + && room + .read(cx) + .followers_for(peer_id) + .iter() + .any(|&follower| { + Some(follower) == workspace.read(cx).client().peer_id() + }); + + if followed_by_self { + let color = theme.editor.replica_selection_style(replica_id).selection; + return flex.contained().with_background_color(color).boxed(); + } + } + + flex.boxed() })) + .with_children((|| { + let replica_id = replica_id?; + let color = theme.editor.replica_selection_style(replica_id).cursor; + Some( + AvatarRibbon::new(color) + .constrained() + .with_width(theme.workspace.titlebar.avatar_ribbon.width) + .with_height(theme.workspace.titlebar.avatar_ribbon.height) + .aligned() + .bottom() + .boxed(), + ) + })()) .contained() .with_margin_left(theme.workspace.titlebar.avatar_margin) .boxed(); @@ -664,7 +678,7 @@ impl CollabTitlebarItem { }) .with_tooltip::( peer_id.as_u64() as usize, - if is_followed { + if is_being_followed { format!("Unfollow {}", user.github_login) } else { format!("Follow {}", user.github_login) diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index d036e10dae0c838e0bc362ff4f5e99f07c271b9c..a99a7c4f0bea50d9a93d7ac9f944e7d0bd36d1da 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -117,7 +117,7 @@ fn join_project(action: &JoinProject, app_state: Arc, cx: &mut Mutable }); if let Some(follow_peer_id) = follow_peer_id { - if !workspace.is_following(follow_peer_id) { + if !workspace.is_being_followed(follow_peer_id) { workspace .toggle_follow(&ToggleFollow(follow_peer_id), cx) .map(|follow| follow.detach_and_log_err(cx)); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 82f22a1a404577961c9e524341567796472b4e89..d78129bac923dada7c1afa38bc3bc23c6d0e96c2 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1828,11 +1828,11 @@ impl Workspace { None } - pub fn is_following(&self, peer_id: PeerId) -> bool { + pub fn is_being_followed(&self, peer_id: PeerId) -> bool { self.follower_states_by_leader.contains_key(&peer_id) } - pub fn is_followed(&self, peer_id: PeerId) -> bool { + pub fn is_followed_by(&self, peer_id: PeerId) -> bool { self.leader_state.followers.contains(&peer_id) } From 3105ecd0bde5d92e3a07296facb053fd497f3623 Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 10 Feb 2023 08:56:41 -0500 Subject: [PATCH 20/36] Shuffle share/unshare button back to the right side --- crates/collab_ui/src/collab_titlebar_item.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index a4112164e348c9e0ce00261816b2fd228996dde3..42ddd3c5daa880f647ebb979fcb2ed03e043a190 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -84,9 +84,7 @@ impl View for CollabTitlebarItem { ); if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { - left_container.add_child(self.render_share_unshare_button(&workspace, &theme, cx)); left_container.add_child(self.render_toggle_collaborator_list_button(&theme, cx)); - left_container.add_child(self.render_current_user(&workspace, &theme, &user, cx)); left_container.add_children(self.render_collaborators(&workspace, &theme, room, cx)); } @@ -104,6 +102,11 @@ impl View for CollabTitlebarItem { right_container.add_child(self.render_toggle_contacts_button(&theme, cx)); } } + + if ActiveCall::global(cx).read(cx).room().is_some() { + right_container.add_child(self.render_share_unshare_button(&workspace, &theme, cx)); + } + right_container.add_children(self.render_connection_status(&workspace, cx)); if let Some(user) = user { From e403b868b77683334d67ef30f45ddc13e67c30be Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 10 Feb 2023 09:15:36 -0500 Subject: [PATCH 21/36] Add followers table to sqlite scheme for tests Co-Authored-By: Antonio Scandurra --- .../20221109000000_test_schema.sql | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 32254d5757da77f7b90f6c675b0a432418d32624..89b924087ef987c89ec58e65f2b165a7d11b4afa 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -143,3 +143,17 @@ CREATE TABLE "servers" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "environment" VARCHAR NOT NULL ); + +CREATE TABLE "followers" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "room_id" INTEGER NOT NULL REFERENCES rooms (id) ON DELETE CASCADE, + "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE, + "leader_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE, + "leader_connection_id" INTEGER NOT NULL, + "follower_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE, + "follower_connection_id" INTEGER NOT NULL +); +CREATE UNIQUE INDEX + "index_followers_on_project_id_and_leader_connection_server_id_and_leader_connection_id_and_follower_connection_server_id_and_follower_connection_id" +ON "followers" ("project_id", "leader_connection_server_id", "leader_connection_id", "follower_connection_server_id", "follower_connection_id"); +CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id"); From dd02bc7748adf23a1b898e1a8ccb77c437cae7cd Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 13 Feb 2023 14:09:31 -0500 Subject: [PATCH 22/36] Initial adding of contact menu to call-less share button --- crates/collab_ui/src/collab_titlebar_item.rs | 92 +++++++++++++++----- crates/collab_ui/src/collab_ui.rs | 2 +- crates/collab_ui/src/contacts_popover.rs | 4 +- crates/zed/src/zed.rs | 6 +- 4 files changed, 74 insertions(+), 30 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 42ddd3c5daa880f647ebb979fcb2ed03e043a190..3dda0b2e22ad82d4da841419d8f41dc11c09a101 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -25,7 +25,7 @@ actions!( collab, [ ToggleCollaboratorList, - ToggleCollaborationMenu, + ToggleContactsMenu, ShareProject, UnshareProject ] @@ -87,24 +87,19 @@ impl View for CollabTitlebarItem { left_container.add_child(self.render_toggle_collaborator_list_button(&theme, cx)); left_container.add_child(self.render_current_user(&workspace, &theme, &user, cx)); left_container.add_children(self.render_collaborators(&workspace, &theme, room, cx)); + left_container.add_child(self.render_toggle_contacts_button(&theme, cx)); } let mut right_container = Flex::row(); right_container.add_children(self.render_toggle_screen_sharing_button(&theme, cx)); - if workspace.read(cx).client().status().borrow().is_connected() { - let project = workspace.read(cx).project().read(cx); - if project.is_shared() - || project.is_remote() - || ActiveCall::global(cx).read(cx).room().is_none() - { - right_container.add_child(self.render_toggle_contacts_button(&theme, cx)); - } - } - if ActiveCall::global(cx).read(cx).room().is_some() { - right_container.add_child(self.render_share_unshare_button(&workspace, &theme, cx)); + right_container + .add_child(self.render_in_call_share_unshare_button(&workspace, &theme, cx)); + } else { + right_container + .add_child(self.render_outside_call_share_button(&workspace, &theme, cx)); } right_container.add_children(self.render_connection_status(&workspace, cx)); @@ -310,11 +305,7 @@ impl CollabTitlebarItem { cx.notify(); } - pub fn toggle_contacts_popover( - &mut self, - _: &ToggleCollaborationMenu, - cx: &mut ViewContext, - ) { + pub fn toggle_contacts_popover(&mut self, _: &ToggleContactsMenu, cx: &mut ViewContext) { match self.contacts_popover.take() { Some(_) => {} None => { @@ -369,7 +360,7 @@ impl CollabTitlebarItem { Stack::new() .with_child( - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::::new(0, cx, |state, _| { let style = titlebar .toggle_contacts_button .style_for(state, self.contacts_popover.is_some()); @@ -387,7 +378,7 @@ impl CollabTitlebarItem { }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, cx| { - cx.dispatch_action(ToggleCollaborationMenu); + cx.dispatch_action(ToggleContactsMenu); }) .aligned() .boxed(), @@ -460,7 +451,7 @@ impl CollabTitlebarItem { ) } - fn render_share_unshare_button( + fn render_in_call_share_unshare_button( &self, workspace: &ViewHandle, theme: &Theme, @@ -476,8 +467,8 @@ impl CollabTitlebarItem { let titlebar = &theme.workspace.titlebar; - enum Share {} - MouseEventHandler::::new(0, cx, |state, _| { + enum ShareUnshare {} + MouseEventHandler::::new(0, cx, |state, _| { //TODO: Ensure this button has consistant width for both text variations let style = titlebar.share_button.style_for(state, false); Label::new(label, style.text.clone()) @@ -493,13 +484,68 @@ impl CollabTitlebarItem { cx.dispatch_action(ShareProject); } }) - .with_tooltip::(0, tooltip.to_owned(), None, theme.tooltip.clone(), cx) + .with_tooltip::(0, tooltip.to_owned(), None, theme.tooltip.clone(), cx) .aligned() .contained() .with_margin_left(theme.workspace.titlebar.avatar_margin) + .with_margin_right(theme.workspace.titlebar.avatar_margin) .boxed() } + fn render_outside_call_share_button( + &self, + workspace: &ViewHandle, + theme: &Theme, + cx: &mut RenderContext, + ) -> ElementBox { + let tooltip = "Share project with new call"; + let titlebar = &theme.workspace.titlebar; + + enum OutsideCallShare {} + Stack::new() + .with_child( + MouseEventHandler::::new(0, cx, |state, _| { + //TODO: Ensure this button has consistant width for both text variations + let style = titlebar.share_button.style_for(state, false); + Label::new("Share".to_owned(), style.text.clone()) + .contained() + .with_style(style.container) + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, cx| { + cx.dispatch_action(ToggleContactsMenu); + }) + .with_tooltip::( + 0, + tooltip.to_owned(), + None, + theme.tooltip.clone(), + cx, + ) + .boxed(), + ) + .with_children(self.contacts_popover.as_ref().map(|popover| { + Overlay::new( + ChildView::new(popover, cx) + .contained() + .with_margin_top(titlebar.height) + .with_margin_left(titlebar.toggle_contacts_button.default.button_width) + .with_margin_right(-titlebar.toggle_contacts_button.default.button_width) + .boxed(), + ) + .with_fit_mode(OverlayFitMode::SwitchAnchor) + .with_anchor_corner(AnchorCorner::BottomLeft) + .with_z_index(999) + .boxed() + })) + .aligned() + .contained() + .with_margin_left(theme.workspace.titlebar.avatar_margin) + .with_margin_right(theme.workspace.titlebar.avatar_margin) + .boxed() + } + fn render_collaborators( &self, workspace: &ViewHandle, diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index a99a7c4f0bea50d9a93d7ac9f944e7d0bd36d1da..d250ce557672126dddff6bfa9bd3b2b4ee8fd3a5 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -11,7 +11,7 @@ mod sharing_status_indicator; use anyhow::anyhow; use call::ActiveCall; -pub use collab_titlebar_item::{CollabTitlebarItem, ToggleCollaborationMenu}; +pub use collab_titlebar_item::{CollabTitlebarItem, ToggleContactsMenu}; use gpui::{actions, MutableAppContext, Task}; use std::sync::Arc; use workspace::{AppState, JoinProject, ToggleFollow, Workspace}; diff --git a/crates/collab_ui/src/contacts_popover.rs b/crates/collab_ui/src/contacts_popover.rs index 37280f929e7c9a5536588a4543d26f8fb8214282..0c67ef4c7c094a17758284bc5b2e8aeac8d30586 100644 --- a/crates/collab_ui/src/contacts_popover.rs +++ b/crates/collab_ui/src/contacts_popover.rs @@ -1,4 +1,4 @@ -use crate::{contact_finder::ContactFinder, contact_list::ContactList, ToggleCollaborationMenu}; +use crate::{contact_finder::ContactFinder, contact_list::ContactList, ToggleContactsMenu}; use client::UserStore; use gpui::{ actions, elements::*, ClipboardItem, CursorStyle, Entity, ModelHandle, MouseButton, @@ -155,7 +155,7 @@ impl View for ContactsPopover { .boxed() }) .on_down_out(MouseButton::Left, move |_, cx| { - cx.dispatch_action(ToggleCollaborationMenu); + cx.dispatch_action(ToggleContactsMenu); }) .boxed() } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 754195e09943c59d61140430ca99550130614f44..19721547bda187bb3be53083cf52edfacf37e0cb 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -6,7 +6,7 @@ use anyhow::{anyhow, Context, Result}; use assets::Assets; use breadcrumbs::Breadcrumbs; pub use client; -use collab_ui::{CollabTitlebarItem, ToggleCollaborationMenu}; +use collab_ui::{CollabTitlebarItem, ToggleContactsMenu}; use collections::VecDeque; pub use editor; use editor::{Editor, MultiBuffer}; @@ -99,9 +99,7 @@ pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { }, ); cx.add_action( - |workspace: &mut Workspace, - _: &ToggleCollaborationMenu, - cx: &mut ViewContext| { + |workspace: &mut Workspace, _: &ToggleContactsMenu, cx: &mut ViewContext| { if let Some(item) = workspace .titlebar_item() .and_then(|item| item.downcast::()) From 1ed47663ef23f00c9bba48e319c070ca9e73a48b Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 20 Feb 2023 13:15:04 -0500 Subject: [PATCH 23/36] Avoid moving contacts popover during call start & add button style state Co-Authored-By: Nathan Sobo --- crates/collab_ui/src/collab_titlebar_item.rs | 170 +++++++++++-------- styles/src/styleTree/workspace.ts | 14 ++ 2 files changed, 114 insertions(+), 70 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 3dda0b2e22ad82d4da841419d8f41dc11c09a101..b450ee4a2727de5af8ecae9931748ca7393dd067 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -31,6 +31,12 @@ actions!( ] ); +#[derive(PartialEq, Eq)] +enum ContactsPopoverSide { + Left, + Right, +} + pub fn init(cx: &mut MutableAppContext) { cx.add_action(CollabTitlebarItem::toggle_collaborator_list_popover); cx.add_action(CollabTitlebarItem::toggle_contacts_popover); @@ -42,6 +48,7 @@ pub struct CollabTitlebarItem { workspace: WeakViewHandle, user_store: ModelHandle, contacts_popover: Option>, + contacts_popover_side: ContactsPopoverSide, collaborator_list_popover: Option>, _subscriptions: Vec, } @@ -98,8 +105,7 @@ impl View for CollabTitlebarItem { right_container .add_child(self.render_in_call_share_unshare_button(&workspace, &theme, cx)); } else { - right_container - .add_child(self.render_outside_call_share_button(&workspace, &theme, cx)); + right_container.add_child(self.render_outside_call_share_button(&theme, cx)); } right_container.add_children(self.render_connection_status(&workspace, cx)); @@ -166,6 +172,7 @@ impl CollabTitlebarItem { workspace: workspace.downgrade(), user_store: user_store.clone(), contacts_popover: None, + contacts_popover_side: ContactsPopoverSide::Right, collaborator_list_popover: None, _subscriptions: subscriptions, } @@ -306,27 +313,31 @@ impl CollabTitlebarItem { } pub fn toggle_contacts_popover(&mut self, _: &ToggleContactsMenu, cx: &mut ViewContext) { - match self.contacts_popover.take() { - Some(_) => {} - None => { - if let Some(workspace) = self.workspace.upgrade(cx) { - let project = workspace.read(cx).project().clone(); - let user_store = workspace.read(cx).user_store().clone(); - let view = cx.add_view(|cx| ContactsPopover::new(project, user_store, cx)); - cx.subscribe(&view, |this, _, event, cx| { - match event { - contacts_popover::Event::Dismissed => { - this.contacts_popover = None; - } + if self.contacts_popover.take().is_none() { + if let Some(workspace) = self.workspace.upgrade(cx) { + let project = workspace.read(cx).project().clone(); + let user_store = workspace.read(cx).user_store().clone(); + let view = cx.add_view(|cx| ContactsPopover::new(project, user_store, cx)); + cx.subscribe(&view, |this, _, event, cx| { + match event { + contacts_popover::Event::Dismissed => { + this.contacts_popover = None; } + } - cx.notify(); - }) - .detach(); - self.contacts_popover = Some(view); - } + cx.notify(); + }) + .detach(); + + self.contacts_popover_side = match ActiveCall::global(cx).read(cx).room() { + Some(_) => ContactsPopoverSide::Left, + None => ContactsPopoverSide::Right, + }; + + self.contacts_popover = Some(view); } } + cx.notify(); } @@ -361,9 +372,11 @@ impl CollabTitlebarItem { Stack::new() .with_child( MouseEventHandler::::new(0, cx, |state, _| { - let style = titlebar - .toggle_contacts_button - .style_for(state, self.contacts_popover.is_some()); + let style = titlebar.toggle_contacts_button.style_for( + state, + self.contacts_popover.is_some() + && self.contacts_popover_side == ContactsPopoverSide::Left, + ); Svg::new("icons/plus_8.svg") .with_color(style.color) .constrained() @@ -384,20 +397,7 @@ impl CollabTitlebarItem { .boxed(), ) .with_children(badge) - .with_children(self.contacts_popover.as_ref().map(|popover| { - Overlay::new( - ChildView::new(popover, cx) - .contained() - .with_margin_top(titlebar.height) - .with_margin_left(titlebar.toggle_contacts_button.default.button_width) - .with_margin_right(-titlebar.toggle_contacts_button.default.button_width) - .boxed(), - ) - .with_fit_mode(OverlayFitMode::SwitchAnchor) - .with_anchor_corner(AnchorCorner::BottomLeft) - .with_z_index(999) - .boxed() - })) + .with_children(self.contacts_popover_host(ContactsPopoverSide::Left, titlebar, cx)) .boxed() } @@ -468,33 +468,47 @@ impl CollabTitlebarItem { let titlebar = &theme.workspace.titlebar; enum ShareUnshare {} - MouseEventHandler::::new(0, cx, |state, _| { - //TODO: Ensure this button has consistant width for both text variations - let style = titlebar.share_button.style_for(state, false); - Label::new(label, style.text.clone()) - .contained() - .with_style(style.container) - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, cx| { - if is_shared { - cx.dispatch_action(UnshareProject); - } else { - cx.dispatch_action(ShareProject); - } - }) - .with_tooltip::(0, tooltip.to_owned(), None, theme.tooltip.clone(), cx) - .aligned() - .contained() - .with_margin_left(theme.workspace.titlebar.avatar_margin) - .with_margin_right(theme.workspace.titlebar.avatar_margin) - .boxed() + Stack::new() + .with_child( + MouseEventHandler::::new(0, cx, |state, _| { + //TODO: Ensure this button has consistant width for both text variations + let style = titlebar.share_button.style_for( + state, + self.contacts_popover.is_some() + && self.contacts_popover_side == ContactsPopoverSide::Right, + ); + Label::new(label, style.text.clone()) + .contained() + .with_style(style.container) + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, cx| { + if is_shared { + cx.dispatch_action(UnshareProject); + } else { + cx.dispatch_action(ShareProject); + } + }) + .with_tooltip::( + 0, + tooltip.to_owned(), + None, + theme.tooltip.clone(), + cx, + ) + .boxed(), + ) + .with_children(self.contacts_popover_host(ContactsPopoverSide::Right, titlebar, cx)) + .aligned() + .contained() + .with_margin_left(theme.workspace.titlebar.avatar_margin) + .with_margin_right(theme.workspace.titlebar.avatar_margin) + .boxed() } fn render_outside_call_share_button( &self, - workspace: &ViewHandle, theme: &Theme, cx: &mut RenderContext, ) -> ElementBox { @@ -506,7 +520,11 @@ impl CollabTitlebarItem { .with_child( MouseEventHandler::::new(0, cx, |state, _| { //TODO: Ensure this button has consistant width for both text variations - let style = titlebar.share_button.style_for(state, false); + let style = titlebar.share_button.style_for( + state, + self.contacts_popover.is_some() + && self.contacts_popover_side == ContactsPopoverSide::Right, + ); Label::new("Share".to_owned(), style.text.clone()) .contained() .with_style(style.container) @@ -525,25 +543,37 @@ impl CollabTitlebarItem { ) .boxed(), ) - .with_children(self.contacts_popover.as_ref().map(|popover| { + .with_children(self.contacts_popover_host(ContactsPopoverSide::Right, titlebar, cx)) + .aligned() + .contained() + .with_margin_left(theme.workspace.titlebar.avatar_margin) + .with_margin_right(theme.workspace.titlebar.avatar_margin) + .boxed() + } + + fn contacts_popover_host<'a>( + &'a self, + side: ContactsPopoverSide, + theme: &'a theme::Titlebar, + cx: &'a RenderContext, + ) -> impl Iterator + 'a { + self.contacts_popover + .iter() + .filter(move |_| self.contacts_popover_side == side) + .map(|popover| { Overlay::new( ChildView::new(popover, cx) .contained() - .with_margin_top(titlebar.height) - .with_margin_left(titlebar.toggle_contacts_button.default.button_width) - .with_margin_right(-titlebar.toggle_contacts_button.default.button_width) + .with_margin_top(theme.height) + .with_margin_left(theme.toggle_contacts_button.default.button_width) + .with_margin_right(-theme.toggle_contacts_button.default.button_width) .boxed(), ) .with_fit_mode(OverlayFitMode::SwitchAnchor) .with_anchor_corner(AnchorCorner::BottomLeft) .with_z_index(999) .boxed() - })) - .aligned() - .contained() - .with_margin_left(theme.workspace.titlebar.avatar_margin) - .with_margin_right(theme.workspace.titlebar.avatar_margin) - .boxed() + }) } fn render_collaborators( diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 22aa361591a2bbbd3c6efab37d1d1ab404b31043..072fdab673eb57d5b3c59a54bd870c67d0f63172 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -29,6 +29,16 @@ export default function workspace(colorScheme: ColorScheme) { background: background(layer, "variant", "hovered"), border: border(layer, "variant", "hovered"), }, + clicked: { + ...text(layer, "sans", "variant", "pressed", { size: "xs" }), + background: background(layer, "variant", "pressed"), + border: border(layer, "variant", "pressed"), + }, + active: { + ...text(layer, "sans", "variant", "active", { size: "xs" }), + background: background(layer, "variant", "active"), + border: border(layer, "variant", "active"), + }, }; const avatarWidth = 18; @@ -178,6 +188,10 @@ export default function workspace(colorScheme: ColorScheme) { background: background(layer, "variant", "active"), color: foreground(layer, "variant", "active"), }, + clicked: { + background: background(layer, "variant", "pressed"), + color: foreground(layer, "variant", "pressed"), + }, hover: { background: background(layer, "variant", "hovered"), color: foreground(layer, "variant", "hovered"), From e9464815e057a929f8f789873c7e54a23978c550 Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 20 Feb 2023 13:55:25 -0500 Subject: [PATCH 24/36] Make tooltip lie less --- crates/collab_ui/src/contact_list.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/collab_ui/src/contact_list.rs b/crates/collab_ui/src/contact_list.rs index 66b754931ad51b2349706da4dd1f53a7fa81c21d..0e129674d871b0d9785cba927b25464f1b1efd26 100644 --- a/crates/collab_ui/src/contact_list.rs +++ b/crates/collab_ui/src/contact_list.rs @@ -1335,7 +1335,7 @@ impl View for ContactList { }) .with_tooltip::( 0, - "Add contact".into(), + "Search for new contact".into(), None, theme.tooltip.clone(), cx, From ae87961a773501decc3b02dc084fbc102c050540 Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 20 Feb 2023 14:18:04 -0500 Subject: [PATCH 25/36] Close contacts popover when call ends Co-Authored-By: Nathan Sobo --- crates/collab_ui/src/collab_titlebar_item.rs | 29 ++++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index b450ee4a2727de5af8ecae9931748ca7393dd067..9893a047b04b022a982ea006020cffe7d3de331b 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -140,7 +140,7 @@ impl CollabTitlebarItem { let active_call = ActiveCall::global(cx); let mut subscriptions = Vec::new(); subscriptions.push(cx.observe(workspace, |_, _, cx| cx.notify())); - subscriptions.push(cx.observe(&active_call, |_, _, cx| cx.notify())); + subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx))); subscriptions.push(cx.observe_window_activation(|this, active, cx| { this.window_activation_changed(active, cx) })); @@ -191,6 +191,13 @@ impl CollabTitlebarItem { } } + fn active_call_changed(&mut self, cx: &mut ViewContext) { + if ActiveCall::global(cx).read(cx).room().is_none() { + self.contacts_popover = None; + } + cx.notify(); + } + fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext) { if let Some(workspace) = self.workspace.upgrade(cx) { let active_call = ActiveCall::global(cx); @@ -397,7 +404,11 @@ impl CollabTitlebarItem { .boxed(), ) .with_children(badge) - .with_children(self.contacts_popover_host(ContactsPopoverSide::Left, titlebar, cx)) + .with_children(self.render_contacts_popover_host( + ContactsPopoverSide::Left, + titlebar, + cx, + )) .boxed() } @@ -499,7 +510,11 @@ impl CollabTitlebarItem { ) .boxed(), ) - .with_children(self.contacts_popover_host(ContactsPopoverSide::Right, titlebar, cx)) + .with_children(self.render_contacts_popover_host( + ContactsPopoverSide::Right, + titlebar, + cx, + )) .aligned() .contained() .with_margin_left(theme.workspace.titlebar.avatar_margin) @@ -543,7 +558,11 @@ impl CollabTitlebarItem { ) .boxed(), ) - .with_children(self.contacts_popover_host(ContactsPopoverSide::Right, titlebar, cx)) + .with_children(self.render_contacts_popover_host( + ContactsPopoverSide::Right, + titlebar, + cx, + )) .aligned() .contained() .with_margin_left(theme.workspace.titlebar.avatar_margin) @@ -551,7 +570,7 @@ impl CollabTitlebarItem { .boxed() } - fn contacts_popover_host<'a>( + fn render_contacts_popover_host<'a>( &'a self, side: ContactsPopoverSide, theme: &'a theme::Titlebar, From c75aca25b626fb3168babb23240d4cc7c4495cdc Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 20 Feb 2023 17:06:16 -0500 Subject: [PATCH 26/36] Remove left side collaborator list Co-Authored-By: Max Brunsfeld --- crates/collab_ui/src/collab_titlebar_item.rs | 72 -------------------- crates/theme/src/theme.rs | 2 - styles/src/styleTree/workspace.ts | 21 ------ 3 files changed, 95 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 9893a047b04b022a982ea006020cffe7d3de331b..dc449e9d9663e7c29a77e6e2456a06e389da8f14 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -91,7 +91,6 @@ impl View for CollabTitlebarItem { ); if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { - left_container.add_child(self.render_toggle_collaborator_list_button(&theme, cx)); left_container.add_child(self.render_current_user(&workspace, &theme, &user, cx)); left_container.add_children(self.render_collaborators(&workspace, &theme, room, cx)); left_container.add_child(self.render_toggle_contacts_button(&theme, cx)); @@ -218,77 +217,6 @@ impl CollabTitlebarItem { } } - fn render_toggle_collaborator_list_button( - &self, - theme: &Theme, - cx: &mut RenderContext, - ) -> ElementBox { - let titlebar = &theme.workspace.titlebar; - - Stack::new() - .with_child( - MouseEventHandler::::new(0, cx, |state, cx| { - let collaborator_count_style = titlebar.collaborator_count.clone(); - let style = titlebar - .toggle_collaborators_button - .style_for(state, self.collaborator_list_popover.is_some()); - - let active_call = ActiveCall::global(cx); - let collaborator_count = active_call - .read(cx) - .room() - .map_or(0, |room| room.read(cx).remote_participants().len()) - + 1; - - Flex::row() - .with_child( - Svg::new("icons/user_group_12.svg") - .with_color(style.color) - .constrained() - .with_width(style.width) - .aligned() - .contained() - .with_style(style.container) - .boxed(), - ) - .with_child( - Label::new( - format!("{collaborator_count}"), - collaborator_count_style.text, - ) - .contained() - .with_style(collaborator_count_style.container) - .aligned() - .boxed(), - ) - .contained() - .with_style(style.container) - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, cx| { - cx.dispatch_action(ToggleCollaboratorList); - }) - .aligned() - .boxed(), - ) - .with_children(self.collaborator_list_popover.as_ref().map(|popover| { - Overlay::new( - ChildView::new(popover, cx) - .contained() - .with_margin_top(titlebar.height) - .with_margin_left(titlebar.toggle_collaborators_button.default.width) - .with_margin_right(-titlebar.toggle_collaborators_button.default.width) - .boxed(), - ) - .with_fit_mode(OverlayFitMode::SwitchAnchor) - .with_anchor_corner(AnchorCorner::BottomLeft) - .with_z_index(999) - .boxed() - })) - .boxed() - } - pub fn toggle_collaborator_list_popover( &mut self, _: &ToggleCollaboratorList, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index b4d3883b4a0c2ce68c7679714ffb478a3e2cbe6c..904e6c2bd3222ee252a61ea7bdec5ffefa6f75f0 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -84,8 +84,6 @@ pub struct Titlebar { pub outdated_warning: ContainedText, pub share_button: Interactive, pub call_control: Interactive, - pub toggle_collaborators_button: Interactive, - pub collaborator_count: ContainedText, pub toggle_contacts_button: Interactive, pub toggle_contacts_badge: ContainerStyle, } diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 072fdab673eb57d5b3c59a54bd870c67d0f63172..f0a11db1f9d2e2ac3af0609fe83ddf3f6564fa72 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -157,27 +157,6 @@ export default function workspace(colorScheme: ColorScheme) { color: foreground(layer, "variant", "hovered"), }, }, - toggleCollaboratorsButton: { - margin: { left: 6 }, - cornerRadius: 6, - color: foreground(layer, "variant"), - width: 12, - active: { - background: background(layer, "variant", "active"), - color: foreground(layer, "variant", "active"), - }, - hover: { - background: background(layer, "variant", "hovered"), - color: foreground(layer, "variant", "hovered"), - }, - }, - collaboratorCount: { - ...text(layer, "mono", { size: "sm" }), - margin: { - left: 4, - right: 6, - }, - }, toggleContactsButton: { margin: { left: 6 }, cornerRadius: 6, From 0dc92bec5cefee42c693ade6a48b2edf127612da Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 20 Feb 2023 16:34:55 -0800 Subject: [PATCH 27/36] Retrieve room id from the project when following/unfollowing Previously, we were accidentally using the project id as the room id. --- crates/collab/src/db.rs | 24 +++++++++++++++++++++--- crates/collab/src/rpc.rs | 6 ++---- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index d7953e1494ce3d4ad8ff9895ab1ecf9989cd125f..6b74c47d3e9b567ab6c3d7343c6946967c1fefe7 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1720,12 +1720,12 @@ impl Database { pub async fn follow( &self, - room_id: RoomId, project_id: ProjectId, leader_connection: ConnectionId, follower_connection: ConnectionId, ) -> Result> { self.room_transaction(|tx| async move { + let room_id = self.room_id_for_project(project_id, &*tx).await?; follower::ActiveModel { room_id: ActiveValue::set(room_id), project_id: ActiveValue::set(project_id), @@ -1749,16 +1749,15 @@ impl Database { pub async fn unfollow( &self, - room_id: RoomId, project_id: ProjectId, leader_connection: ConnectionId, follower_connection: ConnectionId, ) -> Result> { self.room_transaction(|tx| async move { + let room_id = self.room_id_for_project(project_id, &*tx).await?; follower::Entity::delete_many() .filter( Condition::all() - .add(follower::Column::RoomId.eq(room_id)) .add(follower::Column::ProjectId.eq(project_id)) .add( Condition::any() @@ -1788,6 +1787,25 @@ impl Database { .await } + async fn room_id_for_project( + &self, + project_id: ProjectId, + tx: &DatabaseTransaction, + ) -> Result { + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryAs { + RoomId, + } + + Ok(project::Entity::find_by_id(project_id) + .select_only() + .column(project::Column::RoomId) + .into_values::<_, QueryAs>() + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such project"))?) + } + pub async fn update_room_participant_location( &self, room_id: RoomId, diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 2260c10605057ff75e93ce9850bd3003775e46c1..dbfdf748651441d5c57adf4f6d9015f4137b4494 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1719,7 +1719,6 @@ async fn follow( response: Response, session: Session, ) -> Result<()> { - let room_id = RoomId::from_proto(request.project_id); let project_id = ProjectId::from_proto(request.project_id); let leader_id = request .leader_id @@ -1751,7 +1750,7 @@ async fn follow( let room = session .db() .await - .follow(room_id, project_id, leader_id, follower_id) + .follow(project_id, leader_id, follower_id) .await?; room_updated(&room, &session.peer); @@ -1759,7 +1758,6 @@ async fn follow( } async fn unfollow(request: proto::Unfollow, session: Session) -> Result<()> { - let room_id = RoomId::from_proto(request.project_id); let project_id = ProjectId::from_proto(request.project_id); let leader_id = request .leader_id @@ -1784,7 +1782,7 @@ async fn unfollow(request: proto::Unfollow, session: Session) -> Result<()> { let room = session .db() .await - .unfollow(room_id, project_id, leader_id, follower_id) + .unfollow(project_id, leader_id, follower_id) .await?; room_updated(&room, &session.peer); From b7cf426908fcad16f6b28e1fb348d29657fa4b26 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 20 Feb 2023 23:20:22 -0800 Subject: [PATCH 28/36] Start work on styling of follower avatars in facepiles * Make follower avatars smaller than top-level avatars * Make avatars in facepiles overlap * Render an opaque background behind avatars in facepiles. --- crates/collab_ui/src/collab_titlebar_item.rs | 35 +++++++++++++------- crates/theme/src/theme.rs | 14 ++++++-- styles/src/styleTree/workspace.ts | 24 +++++++++----- 3 files changed, 50 insertions(+), 23 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index dc449e9d9663e7c29a77e6e2456a06e389da8f14..72e885b84e624d8c7cdbbcee0e516b53904369da 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -17,7 +17,7 @@ use gpui::{ }; use settings::Settings; use std::{ops::Range, sync::Arc}; -use theme::Theme; +use theme::{AvatarStyle, Theme}; use util::ResultExt; use workspace::{FollowNextCollaborator, JoinProject, ToggleFollow, Workspace}; @@ -614,21 +614,22 @@ impl CollabTitlebarItem { if let Some(location) = location { if let ParticipantLocation::SharedProject { project_id } = location { if Some(project_id) == workspace.read(cx).project().read(cx).remote_id() { - avatar_style = theme.workspace.titlebar.avatar; + avatar_style = &theme.workspace.titlebar.avatar; } else { - avatar_style = theme.workspace.titlebar.inactive_avatar; + avatar_style = &theme.workspace.titlebar.inactive_avatar; } } else { - avatar_style = theme.workspace.titlebar.inactive_avatar; + avatar_style = &theme.workspace.titlebar.inactive_avatar; } } else { - avatar_style = theme.workspace.titlebar.avatar; + avatar_style = &theme.workspace.titlebar.avatar; } let content = Stack::new() .with_children(user.avatar.as_ref().map(|avatar| { let flex = Flex::row() - .with_child(Self::render_face(avatar.clone(), avatar_style, theme)) + // .with_reversed_paint_order() + .with_child(Self::render_face(avatar.clone(), avatar_style.clone())) .with_children( (|| { let room = room?.read(cx); @@ -652,14 +653,22 @@ impl CollabTitlebarItem { } })?; - Some(Self::render_face(avatar.clone(), avatar_style, theme)) + Some( + Container::new(Self::render_face( + avatar.clone(), + theme.workspace.titlebar.follower_avatar.clone(), + )) + .with_margin_left( + -1.0 * theme.workspace.titlebar.follower_avatar_overlap, + ) + .boxed(), + ) })) })() .into_iter() .flatten(), ); - let room = ActiveCall::global(cx).read(cx).room(); if let (Some(replica_id), Some(room)) = (replica_id, room) { let followed_by_self = is_being_followed && room @@ -669,7 +678,6 @@ impl CollabTitlebarItem { .any(|&follower| { Some(follower) == workspace.read(cx).client().peer_id() }); - if followed_by_self { let color = theme.editor.replica_selection_style(replica_id).selection; return flex.contained().with_background_color(color).boxed(); @@ -742,11 +750,14 @@ impl CollabTitlebarItem { } } - fn render_face(avatar: Arc, avatar_style: ImageStyle, theme: &Theme) -> ElementBox { + fn render_face(avatar: Arc, avatar_style: AvatarStyle) -> ElementBox { Image::new(avatar) - .with_style(avatar_style) + .with_style(avatar_style.image) .constrained() - .with_width(theme.workspace.titlebar.avatar_width) + .with_width(avatar_style.width) + .contained() + .with_background_color(Color::white()) + .with_corner_radius(avatar_style.image.corner_radius) .aligned() .boxed() } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 904e6c2bd3222ee252a61ea7bdec5ffefa6f75f0..f4802f5eca6e015a0153a7cd52c08d3bac5290f3 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -74,12 +74,13 @@ pub struct Titlebar { pub container: ContainerStyle, pub height: f32, pub title: TextStyle, - pub avatar_width: f32, pub avatar_margin: f32, pub avatar_ribbon: AvatarRibbon, + pub follower_avatar_overlap: f32, pub offline_icon: OfflineIcon, - pub avatar: ImageStyle, - pub inactive_avatar: ImageStyle, + pub avatar: AvatarStyle, + pub inactive_avatar: AvatarStyle, + pub follower_avatar: AvatarStyle, pub sign_in_prompt: Interactive, pub outdated_warning: ContainedText, pub share_button: Interactive, @@ -88,6 +89,13 @@ pub struct Titlebar { pub toggle_contacts_badge: ContainerStyle, } +#[derive(Clone, Deserialize, Default)] +pub struct AvatarStyle { + #[serde(flatten)] + pub image: ImageStyle, + pub width: f32, +} + #[derive(Deserialize, Default)] pub struct ContactsPopover { #[serde(flatten)] diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index f0a11db1f9d2e2ac3af0609fe83ddf3f6564fa72..42f5540194e37995b9c7a2e46361cb84d5ab8a12 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -41,6 +41,7 @@ export default function workspace(colorScheme: ColorScheme) { }, }; const avatarWidth = 18; + const followerAvatarWidth = 14; return { background: background(layer), @@ -80,7 +81,6 @@ export default function workspace(colorScheme: ColorScheme) { }, statusBar: statusBar(colorScheme), titlebar: { - avatarWidth, avatarMargin: 8, height: 33, // 32px + 1px for overlaid border background: background(layer), @@ -95,6 +95,7 @@ export default function workspace(colorScheme: ColorScheme) { // Collaborators avatar: { + width: avatarWidth, cornerRadius: avatarWidth / 2, border: { color: "#00000088", @@ -102,6 +103,7 @@ export default function workspace(colorScheme: ColorScheme) { }, }, inactiveAvatar: { + width: avatarWidth, cornerRadius: avatarWidth / 2, border: { color: "#00000088", @@ -109,6 +111,15 @@ export default function workspace(colorScheme: ColorScheme) { }, grayscale: true, }, + followerAvatar: { + width: followerAvatarWidth, + cornerRadius: followerAvatarWidth / 2, + border: { + color: "#00000088", + width: 1, + }, + }, + followerAvatarOverlap: 4, avatarRibbon: { height: 3, width: 12, @@ -118,7 +129,7 @@ export default function workspace(colorScheme: ColorScheme) { // Sign in buttom // FlatButton, Variant signInPrompt: { - ...titlebarButton + ...titlebarButton, }, // Offline Indicator @@ -184,8 +195,8 @@ export default function workspace(colorScheme: ColorScheme) { background: foreground(layer, "accent"), }, shareButton: { - ...titlebarButton - } + ...titlebarButton, + }, }, toolbar: { @@ -241,9 +252,6 @@ export default function workspace(colorScheme: ColorScheme) { shadow: colorScheme.modalShadow, }, }, - dropTargetOverlayColor: withOpacity( - foreground(layer, "variant"), - 0.5 - ), + dropTargetOverlayColor: withOpacity(foreground(layer, "variant"), 0.5), }; } From 6a731233c5c9b88e623a823b439a239a8a4d52da Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 21 Feb 2023 11:40:48 -0500 Subject: [PATCH 29/36] Remove `Flex::with_reversed_pain_order` --- crates/collab_ui/src/collab_titlebar_item.rs | 1 - crates/gpui/src/elements/flex.rs | 19 +------------------ 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 72e885b84e624d8c7cdbbcee0e516b53904369da..48a1a0f26337c71fce6bfa168c41a948b0cfddf3 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -628,7 +628,6 @@ impl CollabTitlebarItem { let content = Stack::new() .with_children(user.avatar.as_ref().map(|avatar| { let flex = Flex::row() - // .with_reversed_paint_order() .with_child(Self::render_face(avatar.clone(), avatar_style.clone())) .with_children( (|| { diff --git a/crates/gpui/src/elements/flex.rs b/crates/gpui/src/elements/flex.rs index 4e52032f67a5bccae6f2ae0300e0f628e5309671..ce595222f3f8a3c2e95019f19f790114f05653fd 100644 --- a/crates/gpui/src/elements/flex.rs +++ b/crates/gpui/src/elements/flex.rs @@ -20,7 +20,6 @@ struct ScrollState { pub struct Flex { axis: Axis, - paint_reversed: bool, children: Vec, scroll_state: Option<(ElementStateHandle>, usize)>, } @@ -29,7 +28,6 @@ impl Flex { pub fn new(axis: Axis) -> Self { Self { axis, - paint_reversed: false, children: Default::default(), scroll_state: None, } @@ -43,11 +41,6 @@ impl Flex { Self::new(Axis::Vertical) } - pub fn with_reversed_paint_order(mut self) -> Self { - self.paint_reversed = true; - self - } - pub fn scrollable( mut self, element_id: usize, @@ -303,7 +296,7 @@ impl Element for Flex { } } - let mut child_action = |child: &mut ElementBox| { + for child in &mut self.children { if remaining_space > 0. { if let Some(metadata) = child.metadata::() { if metadata.float { @@ -322,16 +315,6 @@ impl Element for Flex { Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0), Axis::Vertical => child_origin += vec2f(0.0, child.size().y()), } - }; - - if self.paint_reversed { - for child in self.children.iter_mut().rev() { - child_action(child); - } - } else { - for child in &mut self.children { - child_action(child); - } } if overflowing { From 7f4da8038632780f08d6b5e8f4f166f2fef90b11 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 21 Feb 2023 16:12:11 -0500 Subject: [PATCH 30/36] Initial dedicated face pile element Rather than overload Flex with yet another special case, add a dedicated element to handle this overlap instead Co-Authored-By: Max Brunsfeld --- crates/collab_ui/src/collab_titlebar_item.rs | 23 ++--- crates/collab_ui/src/collab_ui.rs | 1 + crates/collab_ui/src/face_pile.rs | 99 ++++++++++++++++++++ styles/src/styleTree/workspace.ts | 2 +- 4 files changed, 110 insertions(+), 15 deletions(-) create mode 100644 crates/collab_ui/src/face_pile.rs diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 48a1a0f26337c71fce6bfa168c41a948b0cfddf3..a2414069c7300647c4e5fad1904ae2f2989db473 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -1,6 +1,7 @@ use crate::{ collaborator_list_popover, collaborator_list_popover::CollaboratorListPopover, - contact_notification::ContactNotification, contacts_popover, ToggleScreenSharing, + contact_notification::ContactNotification, contacts_popover, face_pile::FacePile, + ToggleScreenSharing, }; use call::{ActiveCall, ParticipantLocation, Room}; use client::{proto::PeerId, Authenticate, ContactEventKind, User, UserStore}; @@ -627,7 +628,7 @@ impl CollabTitlebarItem { let content = Stack::new() .with_children(user.avatar.as_ref().map(|avatar| { - let flex = Flex::row() + let face_pile = FacePile::new(theme.workspace.titlebar.follower_avatar_overlap) .with_child(Self::render_face(avatar.clone(), avatar_style.clone())) .with_children( (|| { @@ -652,16 +653,10 @@ impl CollabTitlebarItem { } })?; - Some( - Container::new(Self::render_face( - avatar.clone(), - theme.workspace.titlebar.follower_avatar.clone(), - )) - .with_margin_left( - -1.0 * theme.workspace.titlebar.follower_avatar_overlap, - ) - .boxed(), - ) + Some(Self::render_face( + avatar.clone(), + theme.workspace.titlebar.follower_avatar.clone(), + )) })) })() .into_iter() @@ -679,11 +674,11 @@ impl CollabTitlebarItem { }); if followed_by_self { let color = theme.editor.replica_selection_style(replica_id).selection; - return flex.contained().with_background_color(color).boxed(); + return face_pile.contained().with_background_color(color).boxed(); } } - flex.boxed() + face_pile.boxed() })) .with_children((|| { let replica_id = replica_id?; diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index d250ce557672126dddff6bfa9bd3b2b4ee8fd3a5..6abfec21f74f0918e6338bc77a2e8aa8acfe783b 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -4,6 +4,7 @@ mod contact_finder; mod contact_list; mod contact_notification; mod contacts_popover; +mod face_pile; mod incoming_call_notification; mod notifications; mod project_shared_notification; diff --git a/crates/collab_ui/src/face_pile.rs b/crates/collab_ui/src/face_pile.rs new file mode 100644 index 0000000000000000000000000000000000000000..a24cc466463d29497ce628eb44f5bd230988ea7d --- /dev/null +++ b/crates/collab_ui/src/face_pile.rs @@ -0,0 +1,99 @@ +use std::ops::Range; + +use gpui::{ + geometry::{ + rect::RectF, + vector::{vec2f, Vector2F}, + }, + json::ToJson, + serde_json::{self, json}, + Axis, DebugContext, Element, ElementBox, MeasurementContext, PaintContext, +}; + +pub(crate) struct FacePile { + overlap: f32, + faces: Vec, +} + +impl FacePile { + pub fn new(overlap: f32) -> FacePile { + FacePile { + overlap, + faces: Vec::new(), + } + } +} + +impl Element for FacePile { + type LayoutState = (); + type PaintState = (); + + fn layout( + &mut self, + constraint: gpui::SizeConstraint, + cx: &mut gpui::LayoutContext, + ) -> (Vector2F, Self::LayoutState) { + debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY); + + let mut width = 0.; + for face in &mut self.faces { + width += face.layout(constraint, cx).x(); + } + width -= self.overlap * self.faces.len().saturating_sub(1) as f32; + + (Vector2F::new(width, constraint.max.y()), ()) + } + + fn paint( + &mut self, + bounds: RectF, + visible_bounds: RectF, + _layout: &mut Self::LayoutState, + cx: &mut PaintContext, + ) -> Self::PaintState { + let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default(); + + let origin_y = bounds.upper_right().y(); + let mut origin_x = bounds.upper_right().x(); + + for face in self.faces.iter_mut().rev() { + let size = face.size(); + origin_x -= size.x(); + face.paint(vec2f(origin_x, origin_y), visible_bounds, cx); + origin_x += self.overlap; + } + + () + } + + fn rect_for_text_range( + &self, + _: Range, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &MeasurementContext, + ) -> Option { + None + } + + fn debug( + &self, + bounds: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &DebugContext, + ) -> serde_json::Value { + json!({ + "type": "FacePile", + "bounds": bounds.to_json() + }) + } +} + +impl Extend for FacePile { + fn extend>(&mut self, children: T) { + self.faces.extend(children); + } +} diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 42f5540194e37995b9c7a2e46361cb84d5ab8a12..49884d7e309daaba6bd92ad0e5340283d8a894b0 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -119,7 +119,7 @@ export default function workspace(colorScheme: ColorScheme) { width: 1, }, }, - followerAvatarOverlap: 4, + followerAvatarOverlap: 6, avatarRibbon: { height: 3, width: 12, From 9b92a8e3fe9cdcd371d6dbc35d32fbf1e6b351d6 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 21 Feb 2023 17:14:32 -0500 Subject: [PATCH 31/36] Add mockup accurate avatar background Co-Authored-By: Max Brunsfeld --- crates/collab_ui/src/collab_titlebar_item.rs | 57 ++++++++++++++------ crates/collab_ui/src/face_pile.rs | 4 +- crates/theme/src/theme.rs | 3 +- styles/src/styleTree/workspace.ts | 22 ++++---- 4 files changed, 56 insertions(+), 30 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index a2414069c7300647c4e5fad1904ae2f2989db473..04457c7233a8f035b83a0c87efb733a50cbb8f6f 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -610,6 +610,16 @@ impl CollabTitlebarItem { ) -> ElementBox { let room = ActiveCall::global(cx).read(cx).room(); let is_being_followed = workspace.read(cx).is_being_followed(peer_id); + let followed_by_self = room + .map(|room| { + is_being_followed + && room + .read(cx) + .followers_for(peer_id) + .iter() + .any(|&follower| Some(follower) == workspace.read(cx).client().peer_id()) + }) + .unwrap_or(false); let avatar_style; if let Some(location) = location { @@ -626,10 +636,28 @@ impl CollabTitlebarItem { avatar_style = &theme.workspace.titlebar.avatar; } + let mut background_color = theme + .workspace + .titlebar + .container + .background_color + .unwrap_or_default(); + if let Some(replica_id) = replica_id { + if followed_by_self { + let selection = dbg!(theme.editor.replica_selection_style(replica_id).selection); + background_color = dbg!(Color::blend(selection, background_color)); + background_color.a = 255; + } + } + let content = Stack::new() .with_children(user.avatar.as_ref().map(|avatar| { let face_pile = FacePile::new(theme.workspace.titlebar.follower_avatar_overlap) - .with_child(Self::render_face(avatar.clone(), avatar_style.clone())) + .with_child(Self::render_face( + avatar.clone(), + avatar_style.clone(), + background_color, + )) .with_children( (|| { let room = room?.read(cx); @@ -656,6 +684,7 @@ impl CollabTitlebarItem { Some(Self::render_face( avatar.clone(), theme.workspace.titlebar.follower_avatar.clone(), + background_color, )) })) })() @@ -663,15 +692,7 @@ impl CollabTitlebarItem { .flatten(), ); - if let (Some(replica_id), Some(room)) = (replica_id, room) { - let followed_by_self = is_being_followed - && room - .read(cx) - .followers_for(peer_id) - .iter() - .any(|&follower| { - Some(follower) == workspace.read(cx).client().peer_id() - }); + if let Some(replica_id) = replica_id { if followed_by_self { let color = theme.editor.replica_selection_style(replica_id).selection; return face_pile.contained().with_background_color(color).boxed(); @@ -744,14 +765,20 @@ impl CollabTitlebarItem { } } - fn render_face(avatar: Arc, avatar_style: AvatarStyle) -> ElementBox { + fn render_face( + avatar: Arc, + avatar_style: AvatarStyle, + background_color: Color, + ) -> ElementBox { Image::new(avatar) .with_style(avatar_style.image) - .constrained() - .with_width(avatar_style.width) + .aligned() .contained() - .with_background_color(Color::white()) - .with_corner_radius(avatar_style.image.corner_radius) + .with_background_color(background_color) + .with_corner_radius(avatar_style.outer_corner_radius) + .constrained() + .with_width(avatar_style.outer_width) + .with_height(avatar_style.outer_width) .aligned() .boxed() } diff --git a/crates/collab_ui/src/face_pile.rs b/crates/collab_ui/src/face_pile.rs index a24cc466463d29497ce628eb44f5bd230988ea7d..3b95443fee822a32b79c15617e13689aa8f6fac4 100644 --- a/crates/collab_ui/src/face_pile.rs +++ b/crates/collab_ui/src/face_pile.rs @@ -59,7 +59,9 @@ impl Element for FacePile { for face in self.faces.iter_mut().rev() { let size = face.size(); origin_x -= size.x(); - face.paint(vec2f(origin_x, origin_y), visible_bounds, cx); + cx.paint_layer(None, |cx| { + face.paint(vec2f(origin_x, origin_y), visible_bounds, cx); + }); origin_x += self.overlap; } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index f4802f5eca6e015a0153a7cd52c08d3bac5290f3..a425ce3b9b34110634b0bf4b44009c8c3ce84228 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -93,7 +93,8 @@ pub struct Titlebar { pub struct AvatarStyle { #[serde(flatten)] pub image: ImageStyle, - pub width: f32, + pub outer_width: f32, + pub outer_corner_radius: f32, } #[derive(Deserialize, Default)] diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 49884d7e309daaba6bd92ad0e5340283d8a894b0..7f793161253201c7f54551f7c8c731e944db17a6 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -41,7 +41,9 @@ export default function workspace(colorScheme: ColorScheme) { }, }; const avatarWidth = 18; + const avatarOuterWidth = avatarWidth + 4; const followerAvatarWidth = 14; + const followerAvatarOuterWidth = followerAvatarWidth + 4; return { background: background(layer), @@ -96,30 +98,24 @@ export default function workspace(colorScheme: ColorScheme) { // Collaborators avatar: { width: avatarWidth, + outerWidth: avatarOuterWidth, cornerRadius: avatarWidth / 2, - border: { - color: "#00000088", - width: 1, - }, + outerCornerRadius: avatarOuterWidth / 2, }, inactiveAvatar: { width: avatarWidth, + outerWidth: avatarOuterWidth, cornerRadius: avatarWidth / 2, - border: { - color: "#00000088", - width: 1, - }, + outerCornerRadius: avatarOuterWidth / 2, grayscale: true, }, followerAvatar: { width: followerAvatarWidth, + outerWidth: followerAvatarOuterWidth, cornerRadius: followerAvatarWidth / 2, - border: { - color: "#00000088", - width: 1, - }, + outerCornerRadius: followerAvatarOuterWidth / 2, }, - followerAvatarOverlap: 6, + followerAvatarOverlap: 8, avatarRibbon: { height: 3, width: 12, From dbe5b0205c71d83cadbc303beb48084993e24b2a Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 22 Feb 2023 13:36:06 -0500 Subject: [PATCH 32/36] Add style leader selection container Co-Authored-By: Max Brunsfeld --- crates/collab_ui/src/collab_titlebar_item.rs | 12 ++++++++---- crates/theme/src/theme.rs | 1 + styles/src/styleTree/workspace.ts | 13 +++++++++++++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 04457c7233a8f035b83a0c87efb733a50cbb8f6f..b8f319970d192094f4b442408228be2659e72d29 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -644,8 +644,8 @@ impl CollabTitlebarItem { .unwrap_or_default(); if let Some(replica_id) = replica_id { if followed_by_self { - let selection = dbg!(theme.editor.replica_selection_style(replica_id).selection); - background_color = dbg!(Color::blend(selection, background_color)); + let selection = theme.editor.replica_selection_style(replica_id).selection; + background_color = Color::blend(selection, background_color); background_color.a = 255; } } @@ -692,14 +692,18 @@ impl CollabTitlebarItem { .flatten(), ); + let mut container = face_pile + .contained() + .with_style(theme.workspace.titlebar.leader_selection); + if let Some(replica_id) = replica_id { if followed_by_self { let color = theme.editor.replica_selection_style(replica_id).selection; - return face_pile.contained().with_background_color(color).boxed(); + container = container.with_background_color(color); } } - face_pile.boxed() + container.boxed() })) .with_children((|| { let replica_id = replica_id?; diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index a425ce3b9b34110634b0bf4b44009c8c3ce84228..96391c943ac28c07652ffda9d1350af851cf036f 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -77,6 +77,7 @@ pub struct Titlebar { pub avatar_margin: f32, pub avatar_ribbon: AvatarRibbon, pub follower_avatar_overlap: f32, + pub leader_selection: ContainerStyle, pub offline_icon: OfflineIcon, pub avatar: AvatarStyle, pub inactive_avatar: AvatarStyle, diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 7f793161253201c7f54551f7c8c731e944db17a6..6770d3bd7ec1c8f3fdf093ad7cf66c08e4c0066d 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -116,6 +116,19 @@ export default function workspace(colorScheme: ColorScheme) { outerCornerRadius: followerAvatarOuterWidth / 2, }, followerAvatarOverlap: 8, + leaderSelection: { + margin: { + top: 4, + bottom: 4, + }, + padding: { + left: 2, + right: 2, + top: 4, + bottom: 4, + }, + cornerRadius: 6, + }, avatarRibbon: { height: 3, width: 12, From 812145f9ab2ee2d190a1af783fa7271fb2a01f9a Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 22 Feb 2023 13:40:43 -0500 Subject: [PATCH 33/36] Only show in-call share/unshare button if own project Co-Authored-By: Max Brunsfeld --- crates/collab_ui/src/collab_titlebar_item.rs | 91 +++++++++++--------- 1 file changed, 49 insertions(+), 42 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index b8f319970d192094f4b442408228be2659e72d29..07aeb74547c253b98cb4798494cb088fd762e84d 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -103,7 +103,7 @@ impl View for CollabTitlebarItem { if ActiveCall::global(cx).read(cx).room().is_some() { right_container - .add_child(self.render_in_call_share_unshare_button(&workspace, &theme, cx)); + .add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx)); } else { right_container.add_child(self.render_outside_call_share_button(&theme, cx)); } @@ -396,8 +396,13 @@ impl CollabTitlebarItem { workspace: &ViewHandle, theme: &Theme, cx: &mut RenderContext, - ) -> ElementBox { - let is_shared = workspace.read(cx).project().read(cx).is_shared(); + ) -> Option { + let project = workspace.read(cx).project(); + if project.read(cx).is_remote() { + return None; + } + + let is_shared = project.read(cx).is_shared(); let label = if is_shared { "Unshare" } else { "Share" }; let tooltip = if is_shared { "Unshare project from call participants" @@ -408,47 +413,49 @@ impl CollabTitlebarItem { let titlebar = &theme.workspace.titlebar; enum ShareUnshare {} - Stack::new() - .with_child( - MouseEventHandler::::new(0, cx, |state, _| { - //TODO: Ensure this button has consistant width for both text variations - let style = titlebar.share_button.style_for( - state, - self.contacts_popover.is_some() - && self.contacts_popover_side == ContactsPopoverSide::Right, - ); - Label::new(label, style.text.clone()) - .contained() - .with_style(style.container) - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, cx| { - if is_shared { - cx.dispatch_action(UnshareProject); - } else { - cx.dispatch_action(ShareProject); - } - }) - .with_tooltip::( - 0, - tooltip.to_owned(), - None, - theme.tooltip.clone(), - cx, + Some( + Stack::new() + .with_child( + MouseEventHandler::::new(0, cx, |state, _| { + //TODO: Ensure this button has consistant width for both text variations + let style = titlebar.share_button.style_for( + state, + self.contacts_popover.is_some() + && self.contacts_popover_side == ContactsPopoverSide::Right, + ); + Label::new(label, style.text.clone()) + .contained() + .with_style(style.container) + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, cx| { + if is_shared { + cx.dispatch_action(UnshareProject); + } else { + cx.dispatch_action(ShareProject); + } + }) + .with_tooltip::( + 0, + tooltip.to_owned(), + None, + theme.tooltip.clone(), + cx, + ) + .boxed(), ) + .with_children(self.render_contacts_popover_host( + ContactsPopoverSide::Right, + titlebar, + cx, + )) + .aligned() + .contained() + .with_margin_left(theme.workspace.titlebar.avatar_margin) + .with_margin_right(theme.workspace.titlebar.avatar_margin) .boxed(), - ) - .with_children(self.render_contacts_popover_host( - ContactsPopoverSide::Right, - titlebar, - cx, - )) - .aligned() - .contained() - .with_margin_left(theme.workspace.titlebar.avatar_margin) - .with_margin_right(theme.workspace.titlebar.avatar_margin) - .boxed() + ) } fn render_outside_call_share_button( From a07867d6282c82da6574a6022d8abca0a77e9679 Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 22 Feb 2023 14:13:29 -0500 Subject: [PATCH 34/36] Fiddle with titlebar item spacing Co-Authored-By: Max Brunsfeld --- crates/collab_ui/src/collab_titlebar_item.rs | 46 ++++++++++++-------- crates/theme/src/theme.rs | 3 +- styles/src/styleTree/workspace.ts | 13 +++--- 3 files changed, 36 insertions(+), 26 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 07aeb74547c253b98cb4798494cb088fd762e84d..ffe5c161fd78ece6838bbcabc0397e2a64386441 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -71,12 +71,15 @@ impl View for CollabTitlebarItem { }; let project = workspace.read(cx).project().read(cx); - let mut worktree_root_names = String::new(); + let mut project_title = String::new(); for (i, name) in project.worktree_root_names(cx).enumerate() { if i > 0 { - worktree_root_names.push_str(", "); + project_title.push_str(", "); } - worktree_root_names.push_str(name); + project_title.push_str(name); + } + if project_title.is_empty() { + project_title = "empty project".to_owned(); } let theme = cx.global::().theme.clone(); @@ -85,7 +88,9 @@ impl View for CollabTitlebarItem { let mut left_container = Flex::row(); left_container.add_child( - Label::new(worktree_root_names, theme.workspace.titlebar.title.clone()) + Label::new(project_title, theme.workspace.titlebar.title.clone()) + .contained() + .with_margin_right(theme.workspace.titlebar.item_spacing) .aligned() .left() .boxed(), @@ -118,6 +123,8 @@ impl View for CollabTitlebarItem { theme.workspace.titlebar.title.clone(), ) .aligned() + .contained() + .with_margin_left(theme.workspace.titlebar.item_spacing) .boxed(), ); } else { @@ -452,8 +459,7 @@ impl CollabTitlebarItem { )) .aligned() .contained() - .with_margin_left(theme.workspace.titlebar.avatar_margin) - .with_margin_right(theme.workspace.titlebar.avatar_margin) + .with_margin_left(theme.workspace.titlebar.item_spacing) .boxed(), ) } @@ -501,8 +507,7 @@ impl CollabTitlebarItem { )) .aligned() .contained() - .with_margin_left(theme.workspace.titlebar.avatar_margin) - .with_margin_right(theme.workspace.titlebar.avatar_margin) + .with_margin_left(theme.workspace.titlebar.item_spacing) .boxed() } @@ -557,15 +562,19 @@ impl CollabTitlebarItem { .get(&participant.peer_id) .map(|collaborator| collaborator.replica_id); let user = participant.user.clone(); - Some(self.render_face_pile( - &user, - replica_id, - participant.peer_id, - Some(participant.location), - workspace, - theme, - cx, - )) + Some( + Container::new(self.render_face_pile( + &user, + replica_id, + participant.peer_id, + Some(participant.location), + workspace, + theme, + cx, + )) + .with_margin_left(theme.workspace.titlebar.face_pile_spacing) + .boxed(), + ) }) .collect() } @@ -597,6 +606,7 @@ impl CollabTitlebarItem { Label::new("Sign in", style.text.clone()) .contained() .with_style(style.container) + .with_margin_left(theme.workspace.titlebar.item_spacing) .boxed() }) .on_click(MouseButton::Left, |_, cx| cx.dispatch_action(Authenticate)) @@ -725,8 +735,6 @@ impl CollabTitlebarItem { .boxed(), ) })()) - .contained() - .with_margin_left(theme.workspace.titlebar.avatar_margin) .boxed(); if let Some(location) = location { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 96391c943ac28c07652ffda9d1350af851cf036f..17a7c876bb79691ef9503593064cd06acc523951 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -74,7 +74,8 @@ pub struct Titlebar { pub container: ContainerStyle, pub height: f32, pub title: TextStyle, - pub avatar_margin: f32, + pub item_spacing: f32, + pub face_pile_spacing: f32, pub avatar_ribbon: AvatarRibbon, pub follower_avatar_overlap: f32, pub leader_selection: ContainerStyle, diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 6770d3bd7ec1c8f3fdf093ad7cf66c08e4c0066d..659a0e67455fbad45f5eb939f8bf7a982887a41e 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -12,7 +12,7 @@ import tabBar from "./tabBar"; export default function workspace(colorScheme: ColorScheme) { const layer = colorScheme.lowest; - const titlebarPadding = 6; + const itemSpacing = 8; const titlebarButton = { cornerRadius: 6, padding: { @@ -83,13 +83,14 @@ export default function workspace(colorScheme: ColorScheme) { }, statusBar: statusBar(colorScheme), titlebar: { - avatarMargin: 8, + itemSpacing, + facePileSpacing: 2, height: 33, // 32px + 1px for overlaid border background: background(layer), border: border(layer, { bottom: true, overlay: true }), padding: { left: 80, - right: titlebarPadding, + right: itemSpacing, }, // Project @@ -146,7 +147,7 @@ export default function workspace(colorScheme: ColorScheme) { color: foreground(layer, "variant"), width: 16, margin: { - left: titlebarPadding, + left: itemSpacing, }, padding: { right: 4, @@ -159,7 +160,7 @@ export default function workspace(colorScheme: ColorScheme) { background: withOpacity(background(layer, "warning"), 0.3), border: border(layer, "warning"), margin: { - left: titlebarPadding, + left: itemSpacing, }, padding: { left: 8, @@ -178,7 +179,7 @@ export default function workspace(colorScheme: ColorScheme) { }, }, toggleContactsButton: { - margin: { left: 6 }, + margin: { left: itemSpacing }, cornerRadius: 6, color: foreground(layer, "variant"), iconWidth: 8, From 36040cd0e17176da366046d77baf585306842093 Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 22 Feb 2023 14:44:23 -0500 Subject: [PATCH 35/36] Add top level leave call button Co-Authored-By: Max Brunsfeld --- assets/icons/leave_12.svg | 3 + crates/collab_ui/src/collab_titlebar_item.rs | 113 +++++++++++++------ crates/collab_ui/src/contact_list.rs | 19 +--- 3 files changed, 86 insertions(+), 49 deletions(-) create mode 100644 assets/icons/leave_12.svg diff --git a/assets/icons/leave_12.svg b/assets/icons/leave_12.svg new file mode 100644 index 0000000000000000000000000000000000000000..84491384b8cc7f80d4a727e75c142ee509b451ac --- /dev/null +++ b/assets/icons/leave_12.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index ffe5c161fd78ece6838bbcabc0397e2a64386441..e190deef84f4965af1dd2d94fe1be9963d1683ec 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -12,6 +12,7 @@ use gpui::{ color::Color, elements::*, geometry::{rect::RectF, vector::vec2f, PathBuilder}, + impl_internal_actions, json::{self, ToJson}, CursorStyle, Entity, ImageData, ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, @@ -32,6 +33,11 @@ actions!( ] ); +impl_internal_actions!(collab, [LeaveCall]); + +#[derive(Copy, Clone, PartialEq)] +pub(crate) struct LeaveCall; + #[derive(PartialEq, Eq)] enum ContactsPopoverSide { Left, @@ -43,6 +49,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(CollabTitlebarItem::toggle_contacts_popover); cx.add_action(CollabTitlebarItem::share_project); cx.add_action(CollabTitlebarItem::unshare_project); + cx.add_action(CollabTitlebarItem::leave_call); } pub struct CollabTitlebarItem { @@ -104,9 +111,9 @@ impl View for CollabTitlebarItem { let mut right_container = Flex::row(); - right_container.add_children(self.render_toggle_screen_sharing_button(&theme, cx)); - - if ActiveCall::global(cx).read(cx).room().is_some() { + if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { + right_container.add_child(self.render_toggle_screen_sharing_button(&theme, &room, cx)); + right_container.add_child(self.render_leave_call_button(&theme, cx)); right_container .add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx)); } else { @@ -284,6 +291,12 @@ impl CollabTitlebarItem { cx.notify(); } + fn leave_call(&mut self, _: &LeaveCall, cx: &mut ViewContext) { + ActiveCall::global(cx) + .update(cx, |call, cx| call.hang_up(cx)) + .log_err(); + } + fn render_toggle_contacts_button( &self, theme: &Theme, @@ -351,13 +364,11 @@ impl CollabTitlebarItem { fn render_toggle_screen_sharing_button( &self, theme: &Theme, + room: &ModelHandle, cx: &mut RenderContext, - ) -> Option { - let active_call = ActiveCall::global(cx); - let room = active_call.read(cx).room().cloned()?; + ) -> ElementBox { let icon; let tooltip; - if room.read(cx).is_screen_sharing() { icon = "icons/disable_screen_sharing_12.svg"; tooltip = "Stop Sharing Screen" @@ -367,35 +378,67 @@ impl CollabTitlebarItem { } let titlebar = &theme.workspace.titlebar; - Some( - MouseEventHandler::::new(0, cx, |state, _| { - let style = titlebar.call_control.style_for(state, false); - Svg::new(icon) - .with_color(style.color) - .constrained() - .with_width(style.icon_width) - .aligned() - .constrained() - .with_width(style.button_width) - .with_height(style.button_width) - .contained() - .with_style(style.container) - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, cx| { - cx.dispatch_action(ToggleScreenSharing); - }) - .with_tooltip::( - 0, - tooltip.into(), - Some(Box::new(ToggleScreenSharing)), - theme.tooltip.clone(), - cx, - ) - .aligned() - .boxed(), + MouseEventHandler::::new(0, cx, |state, _| { + let style = titlebar.call_control.style_for(state, false); + Svg::new(icon) + .with_color(style.color) + .constrained() + .with_width(style.icon_width) + .aligned() + .constrained() + .with_width(style.button_width) + .with_height(style.button_width) + .contained() + .with_style(style.container) + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, cx| { + cx.dispatch_action(ToggleScreenSharing); + }) + .with_tooltip::( + 0, + tooltip.into(), + Some(Box::new(ToggleScreenSharing)), + theme.tooltip.clone(), + cx, ) + .aligned() + .boxed() + } + + fn render_leave_call_button(&self, theme: &Theme, cx: &mut RenderContext) -> ElementBox { + let titlebar = &theme.workspace.titlebar; + + MouseEventHandler::::new(0, cx, |state, _| { + let style = titlebar.call_control.style_for(state, false); + Svg::new("icons/leave_12.svg") + .with_color(style.color) + .constrained() + .with_width(style.icon_width) + .aligned() + .constrained() + .with_width(style.button_width) + .with_height(style.button_width) + .contained() + .with_style(style.container) + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, cx| { + cx.dispatch_action(LeaveCall); + }) + .with_tooltip::( + 0, + "Leave call".to_owned(), + Some(Box::new(LeaveCall)), + theme.tooltip.clone(), + cx, + ) + .contained() + .with_margin_left(theme.workspace.titlebar.item_spacing) + .aligned() + .boxed() } fn render_in_call_share_unshare_button( diff --git a/crates/collab_ui/src/contact_list.rs b/crates/collab_ui/src/contact_list.rs index 0e129674d871b0d9785cba927b25464f1b1efd26..ba9bc8ad63dd75188a646ae2fc5ff875191889a5 100644 --- a/crates/collab_ui/src/contact_list.rs +++ b/crates/collab_ui/src/contact_list.rs @@ -1,3 +1,4 @@ +use super::collab_titlebar_item::LeaveCall; use crate::contacts_popover; use call::ActiveCall; use client::{proto::PeerId, Contact, User, UserStore}; @@ -18,11 +19,10 @@ use serde::Deserialize; use settings::Settings; use std::{mem, sync::Arc}; use theme::IconButton; -use util::ResultExt; use workspace::{JoinProject, OpenSharedScreen}; impl_actions!(contact_list, [RemoveContact, RespondToContactRequest]); -impl_internal_actions!(contact_list, [ToggleExpanded, Call, LeaveCall]); +impl_internal_actions!(contact_list, [ToggleExpanded, Call]); pub fn init(cx: &mut MutableAppContext) { cx.add_action(ContactList::remove_contact); @@ -33,7 +33,6 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(ContactList::confirm); cx.add_action(ContactList::toggle_expanded); cx.add_action(ContactList::call); - cx.add_action(ContactList::leave_call); } #[derive(Clone, PartialEq)] @@ -45,9 +44,6 @@ struct Call { initial_project: Option>, } -#[derive(Copy, Clone, PartialEq)] -struct LeaveCall; - #[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)] enum Section { ActiveCall, @@ -981,6 +977,7 @@ impl ContactList { cx: &mut RenderContext, ) -> ElementBox { enum Header {} + enum LeaveCallContactList {} let header_style = theme .header_row @@ -993,9 +990,9 @@ impl ContactList { }; let leave_call = if section == Section::ActiveCall { Some( - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::::new(0, cx, |state, _| { let style = theme.leave_call.style_for(state, false); - Label::new("Leave Session", style.text.clone()) + Label::new("Leave Call", style.text.clone()) .contained() .with_style(style.container) .boxed() @@ -1284,12 +1281,6 @@ impl ContactList { }) .detach_and_log_err(cx); } - - fn leave_call(&mut self, _: &LeaveCall, cx: &mut ViewContext) { - ActiveCall::global(cx) - .update(cx, |call, cx| call.hang_up(cx)) - .log_err(); - } } impl Entity for ContactList { From 0324ca3b081f4785c4cbbad385e0615e0c4826e4 Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 22 Feb 2023 15:29:20 -0500 Subject: [PATCH 36/36] Be more specific about clearing (leader, follower) row Previously anyone unfollowing someone would clear all other rows for other followers leading to an incorrect state, fix and test Co-Authored-By: Max Brunsfeld --- crates/collab/src/db.rs | 26 ++---- crates/collab/src/tests/integration_tests.rs | 98 +++++++++++++++++--- 2 files changed, 96 insertions(+), 28 deletions(-) diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 6b74c47d3e9b567ab6c3d7343c6946967c1fefe7..97c9b6d344f7c3c3e57182b58185ad4ca8f5869d 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1760,22 +1760,16 @@ impl Database { Condition::all() .add(follower::Column::ProjectId.eq(project_id)) .add( - Condition::any() - .add( - follower::Column::LeaderConnectionServerId - .eq(leader_connection.owner_id) - .and( - follower::Column::LeaderConnectionId - .eq(leader_connection.id), - ), - ) - .add( - follower::Column::FollowerConnectionServerId - .eq(follower_connection.owner_id) - .and( - follower::Column::FollowerConnectionId - .eq(follower_connection.id), - ), + follower::Column::LeaderConnectionServerId + .eq(leader_connection.owner_id) + .and(follower::Column::LeaderConnectionId.eq(leader_connection.id)), + ) + .add( + follower::Column::FollowerConnectionServerId + .eq(follower_connection.owner_id) + .and( + follower::Column::FollowerConnectionId + .eq(follower_connection.id), ), ), ) diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 2fc19b005b7ded5a1212fb09e41f545d7191503c..7c4087f54036302c589f75ff85bc9a25340be58d 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -5786,6 +5786,7 @@ async fn test_following( deterministic: Arc, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, ) { deterministic.forbid_parking(); cx_a.update(editor::init); @@ -5794,9 +5795,13 @@ async fn test_following( let mut server = TestServer::start(&deterministic).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; + let client_c = server.create_client(cx_c, "user_c").await; server .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; + server + .make_contacts(&mut [(&client_a, cx_a), (&client_c, cx_c)]) + .await; let active_call_a = cx_a.read(ActiveCall::global); let active_call_b = cx_b.read(ActiveCall::global); @@ -5827,8 +5832,10 @@ async fn test_following( .await .unwrap(); - // Client A opens some editors. let workspace_a = client_a.build_workspace(&project_a, cx_a); + let workspace_b = client_b.build_workspace(&project_b, cx_b); + + // Client A opens some editors. let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone()); let editor_a1 = workspace_a .update(cx_a, |workspace, cx| { @@ -5848,7 +5855,6 @@ async fn test_following( .unwrap(); // Client B opens an editor. - let workspace_b = client_b.build_workspace(&project_b, cx_b); let editor_b1 = workspace_b .update(cx_b, |workspace, cx| { workspace.open_path((worktree_id, "1.txt"), None, true, cx) @@ -5858,29 +5864,97 @@ async fn test_following( .downcast::() .unwrap(); - let client_a_id = project_b.read_with(cx_b, |project, _| { - project.collaborators().values().next().unwrap().peer_id - }); - let client_b_id = project_a.read_with(cx_a, |project, _| { - project.collaborators().values().next().unwrap().peer_id - }); + let peer_id_a = client_a.peer_id().unwrap(); + let peer_id_b = client_b.peer_id().unwrap(); + let peer_id_c = client_c.peer_id().unwrap(); - // When client B starts following client A, all visible view states are replicated to client B. + // Client A updates their selections in those editors editor_a1.update(cx_a, |editor, cx| { editor.change_selections(None, cx, |s| s.select_ranges([0..1])) }); editor_a2.update(cx_a, |editor, cx| { editor.change_selections(None, cx, |s| s.select_ranges([2..3])) }); + + // When client B starts following client A, all visible view states are replicated to client B. workspace_b .update(cx_b, |workspace, cx| { workspace - .toggle_follow(&ToggleFollow(client_a_id), cx) + .toggle_follow(&ToggleFollow(peer_id_a), cx) + .unwrap() + }) + .await + .unwrap(); + + // Client A invites client C to the call. + active_call_a + .update(cx_a, |call, cx| { + call.invite(client_c.current_user_id(cx_c).to_proto(), None, cx) + }) + .await + .unwrap(); + cx_c.foreground().run_until_parked(); + let active_call_c = cx_c.read(ActiveCall::global); + active_call_c + .update(cx_c, |call, cx| call.accept_incoming(cx)) + .await + .unwrap(); + let project_c = client_c.build_remote_project(project_id, cx_c).await; + let workspace_c = client_c.build_workspace(&project_c, cx_c); + active_call_c + .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx)) + .await + .unwrap(); + + // Client C also follows client A. + workspace_c + .update(cx_c, |workspace, cx| { + workspace + .toggle_follow(&ToggleFollow(peer_id_a), cx) .unwrap() }) .await .unwrap(); + // All clients see that clients B and C are following client A. + cx_c.foreground().run_until_parked(); + for (name, active_call, cx) in [ + ("A", &active_call_a, &cx_a), + ("B", &active_call_b, &cx_b), + ("C", &active_call_c, &cx_c), + ] { + active_call.read_with(*cx, |call, cx| { + let room = call.room().unwrap().read(cx); + assert_eq!( + room.followers_for(peer_id_a), + &[peer_id_b, peer_id_c], + "checking followers for A as {name}" + ); + }); + } + + // Client C unfollows client A. + workspace_c.update(cx_c, |workspace, cx| { + workspace.toggle_follow(&ToggleFollow(peer_id_a), cx); + }); + + // All clients see that clients B is following client A. + cx_c.foreground().run_until_parked(); + for (name, active_call, cx) in [ + ("A", &active_call_a, &cx_a), + ("B", &active_call_b, &cx_b), + ("C", &active_call_c, &cx_c), + ] { + active_call.read_with(*cx, |call, cx| { + let room = call.room().unwrap().read(cx); + assert_eq!( + room.followers_for(peer_id_a), + &[peer_id_b], + "checking followers for A as {name}" + ); + }); + } + let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| { workspace .active_item(cx) @@ -6033,14 +6107,14 @@ async fn test_following( workspace_a .update(cx_a, |workspace, cx| { workspace - .toggle_follow(&ToggleFollow(client_b_id), cx) + .toggle_follow(&ToggleFollow(peer_id_b), cx) .unwrap() }) .await .unwrap(); assert_eq!( workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), - Some(client_b_id) + Some(peer_id_b) ); assert_eq!( workspace_a.read_with(cx_a, |workspace, cx| workspace