From 40163da679d9d5a5ca1cf6bca7824e91daa3233f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 6 Oct 2022 14:00:14 +0200 Subject: [PATCH] Move contacts panel features into collab_ui --- Cargo.lock | 26 +- assets/keymaps/default.json | 1 - crates/collab_ui/Cargo.toml | 1 + crates/collab_ui/src/collab_titlebar_item.rs | 60 +- crates/collab_ui/src/collab_ui.rs | 5 + .../src/contact_finder.rs | 26 +- .../src/contact_notification.rs | 5 +- crates/collab_ui/src/contacts_popover.rs | 69 +- .../src/notifications.rs | 23 +- crates/contacts_panel/Cargo.toml | 32 - crates/contacts_panel/src/contacts_panel.rs | 1000 ----------------- crates/theme/src/theme.rs | 1 + crates/zed/Cargo.toml | 1 - crates/zed/src/main.rs | 1 - crates/zed/src/menus.rs | 4 - crates/zed/src/zed.rs | 24 +- styles/src/styleTree/workspace.ts | 7 + 17 files changed, 152 insertions(+), 1134 deletions(-) rename crates/{contacts_panel => collab_ui}/src/contact_finder.rs (88%) rename crates/{contacts_panel => collab_ui}/src/contact_notification.rs (96%) rename crates/{contacts_panel => collab_ui}/src/notifications.rs (83%) delete mode 100644 crates/contacts_panel/Cargo.toml delete mode 100644 crates/contacts_panel/src/contacts_panel.rs diff --git a/Cargo.lock b/Cargo.lock index a971570e2f..2d3ca1f78c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1091,6 +1091,7 @@ dependencies = [ "gpui", "log", "menu", + "picker", "postage", "project", "serde", @@ -1141,30 +1142,6 @@ dependencies = [ "cache-padded", ] -[[package]] -name = "contacts_panel" -version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "collections", - "editor", - "futures", - "fuzzy", - "gpui", - "language", - "log", - "menu", - "picker", - "postage", - "project", - "serde", - "settings", - "theme", - "util", - "workspace", -] - [[package]] name = "context_menu" version = "0.1.0" @@ -7165,7 +7142,6 @@ dependencies = [ "collab_ui", "collections", "command_palette", - "contacts_panel", "context_menu", "ctor", "diagnostics", diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index a0bc8c39e6..e2adfc0f81 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -395,7 +395,6 @@ "context": "Workspace", "bindings": { "shift-escape": "dock::FocusDock", - "cmd-shift-c": "contacts_panel::ToggleFocus", "cmd-shift-b": "workspace::ToggleRightSidebar" } }, diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index cf3a78a0b5..20db066ce7 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -29,6 +29,7 @@ editor = { path = "../editor" } fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } menu = { path = "../menu" } +picker = { path = "../picker" } project = { path = "../project" } settings = { path = "../settings" } theme = { path = "../theme" } diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index cea3654856..c982962042 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -1,6 +1,6 @@ -use crate::contacts_popover; +use crate::{contact_notification::ContactNotification, contacts_popover}; use call::{ActiveCall, ParticipantLocation}; -use client::{Authenticate, PeerId}; +use client::{Authenticate, ContactEventKind, PeerId, UserStore}; use clock::ReplicaId; use contacts_popover::ContactsPopover; use gpui::{ @@ -9,8 +9,8 @@ use gpui::{ elements::*, geometry::{rect::RectF, vector::vec2f, PathBuilder}, json::{self, ToJson}, - Border, CursorStyle, Entity, ImageData, 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, sync::Arc}; @@ -29,6 +29,7 @@ pub fn init(cx: &mut MutableAppContext) { pub struct CollabTitlebarItem { workspace: WeakViewHandle, + user_store: ModelHandle, contacts_popover: Option>, _subscriptions: Vec, } @@ -71,7 +72,11 @@ impl View for CollabTitlebarItem { } impl CollabTitlebarItem { - pub fn new(workspace: &ViewHandle, cx: &mut ViewContext) -> Self { + pub fn new( + workspace: &ViewHandle, + user_store: &ModelHandle, + cx: &mut ViewContext, + ) -> Self { let active_call = ActiveCall::global(cx); let mut subscriptions = Vec::new(); subscriptions.push(cx.observe(workspace, |_, _, cx| cx.notify())); @@ -79,9 +84,33 @@ impl CollabTitlebarItem { subscriptions.push(cx.observe_window_activation(|this, active, cx| { this.window_activation_changed(active, cx) })); + subscriptions.push(cx.observe(user_store, |_, _, cx| cx.notify())); + subscriptions.push( + cx.subscribe(user_store, move |this, user_store, event, cx| { + if let Some(workspace) = this.workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { + if let client::Event::Contact { user, kind } = event { + if let ContactEventKind::Requested | ContactEventKind::Accepted = kind { + workspace.show_notification(user.id as usize, cx, |cx| { + cx.add_view(|cx| { + ContactNotification::new( + user.clone(), + *kind, + user_store, + cx, + ) + }) + }) + } + } + }); + } + }), + ); Self { workspace: workspace.downgrade(), + user_store: user_store.clone(), contacts_popover: None, _subscriptions: subscriptions, } @@ -160,6 +189,26 @@ impl CollabTitlebarItem { cx: &mut RenderContext, ) -> ElementBox { let titlebar = &theme.workspace.titlebar; + let badge = if self + .user_store + .read(cx) + .incoming_contact_requests() + .is_empty() + { + None + } else { + Some( + Empty::new() + .collapsed() + .contained() + .with_style(titlebar.toggle_contacts_badge) + .contained() + .with_margin_left(titlebar.toggle_contacts_button.default.icon_width) + .with_margin_top(titlebar.toggle_contacts_button.default.icon_width) + .aligned() + .boxed(), + ) + }; Stack::new() .with_child( MouseEventHandler::::new(0, cx, |state, _| { @@ -185,6 +234,7 @@ impl CollabTitlebarItem { .aligned() .boxed(), ) + .with_children(badge) .with_children(self.contacts_popover.as_ref().map(|popover| { Overlay::new( ChildView::new(popover) diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 03d1bf6672..438a41ae7d 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -1,6 +1,9 @@ mod collab_titlebar_item; +mod contact_finder; +mod contact_notification; mod contacts_popover; mod incoming_call_notification; +mod notifications; mod project_shared_notification; use call::ActiveCall; @@ -11,6 +14,8 @@ use std::sync::Arc; use workspace::{AppState, JoinProject, ToggleFollow, Workspace}; pub fn init(app_state: Arc, cx: &mut MutableAppContext) { + contact_notification::init(cx); + contact_finder::init(cx); contacts_popover::init(cx); collab_titlebar_item::init(cx); incoming_call_notification::init(cx); diff --git a/crates/contacts_panel/src/contact_finder.rs b/crates/collab_ui/src/contact_finder.rs similarity index 88% rename from crates/contacts_panel/src/contact_finder.rs rename to crates/collab_ui/src/contact_finder.rs index 1831c1ba72..6814b7479f 100644 --- a/crates/contacts_panel/src/contact_finder.rs +++ b/crates/collab_ui/src/contact_finder.rs @@ -9,8 +9,6 @@ use std::sync::Arc; use util::TryFutureExt; use workspace::Workspace; -use crate::render_icon_button; - actions!(contact_finder, [Toggle]); pub fn init(cx: &mut MutableAppContext) { @@ -117,11 +115,10 @@ impl PickerDelegate for ContactFinder { let icon_path = match request_status { ContactRequestStatus::None | ContactRequestStatus::RequestReceived => { - "icons/check_8.svg" - } - ContactRequestStatus::RequestSent | ContactRequestStatus::RequestAccepted => { - "icons/x_mark_8.svg" + Some("icons/check_8.svg") } + ContactRequestStatus::RequestSent => Some("icons/x_mark_8.svg"), + ContactRequestStatus::RequestAccepted => None, }; let button_style = if self.user_store.read(cx).is_contact_request_pending(user) { &theme.contact_finder.disabled_contact_button @@ -145,12 +142,21 @@ impl PickerDelegate for ContactFinder { .left() .boxed(), ) - .with_child( - render_icon_button(button_style, icon_path) + .with_children(icon_path.map(|icon_path| { + Svg::new(icon_path) + .with_color(button_style.color) + .constrained() + .with_width(button_style.icon_width) + .aligned() + .contained() + .with_style(button_style.container) + .constrained() + .with_width(button_style.button_width) + .with_height(button_style.button_width) .aligned() .flex_float() - .boxed(), - ) + .boxed() + })) .contained() .with_style(style.container) .constrained() diff --git a/crates/contacts_panel/src/contact_notification.rs b/crates/collab_ui/src/contact_notification.rs similarity index 96% rename from crates/contacts_panel/src/contact_notification.rs rename to crates/collab_ui/src/contact_notification.rs index c608346d79..f543a01446 100644 --- a/crates/contacts_panel/src/contact_notification.rs +++ b/crates/collab_ui/src/contact_notification.rs @@ -49,10 +49,7 @@ impl View for ContactNotification { self.user.clone(), "wants to add you as a contact", Some("They won't know if you decline."), - RespondToContactRequest { - user_id: self.user.id, - accept: false, - }, + Dismiss(self.user.id), vec![ ( "Decline", diff --git a/crates/collab_ui/src/contacts_popover.rs b/crates/collab_ui/src/contacts_popover.rs index 389fe9fbd2..f3ebf3abea 100644 --- a/crates/collab_ui/src/contacts_popover.rs +++ b/crates/collab_ui/src/contacts_popover.rs @@ -1,22 +1,27 @@ use std::sync::Arc; +use crate::contact_finder; use call::ActiveCall; use client::{Contact, User, UserStore}; use editor::{Cancel, Editor}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ - elements::*, impl_internal_actions, keymap, AppContext, ClipboardItem, CursorStyle, Entity, - ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, View, ViewContext, - ViewHandle, + elements::*, impl_actions, impl_internal_actions, keymap, AppContext, ClipboardItem, + CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, + View, ViewContext, ViewHandle, }; use menu::{Confirm, SelectNext, SelectPrev}; use project::Project; +use serde::Deserialize; use settings::Settings; use theme::IconButton; -impl_internal_actions!(contacts_panel, [ToggleExpanded, Call]); +impl_actions!(contacts_popover, [RemoveContact, RespondToContactRequest]); +impl_internal_actions!(contacts_popover, [ToggleExpanded, Call]); pub fn init(cx: &mut MutableAppContext) { + cx.add_action(ContactsPopover::remove_contact); + cx.add_action(ContactsPopover::respond_to_contact_request); cx.add_action(ContactsPopover::clear_filter); cx.add_action(ContactsPopover::select_next); cx.add_action(ContactsPopover::select_prev); @@ -77,6 +82,18 @@ impl PartialEq for ContactEntry { } } +#[derive(Clone, Deserialize, PartialEq)] +pub struct RequestContact(pub u64); + +#[derive(Clone, Deserialize, PartialEq)] +pub struct RemoveContact(pub u64); + +#[derive(Clone, Deserialize, PartialEq)] +pub struct RespondToContactRequest { + pub user_id: u64, + pub accept: bool, +} + pub enum Event { Dismissed, } @@ -186,6 +203,24 @@ impl ContactsPopover { this } + fn remove_contact(&mut self, request: &RemoveContact, cx: &mut ViewContext) { + self.user_store + .update(cx, |store, cx| store.remove_contact(request.0, cx)) + .detach(); + } + + fn respond_to_contact_request( + &mut self, + action: &RespondToContactRequest, + cx: &mut ViewContext, + ) { + self.user_store + .update(cx, |store, cx| { + store.respond_to_contact_request(action.user_id, action.accept, cx) + }) + .detach(); + } + fn clear_filter(&mut self, _: &Cancel, cx: &mut ViewContext) { let did_clear = self.filter_editor.update(cx, |editor, cx| { if editor.buffer().read(cx).len(cx) > 0 { @@ -574,18 +609,15 @@ impl ContactsPopover { }; render_icon_button(button_style, "icons/x_mark_8.svg") .aligned() - // .flex_float() .boxed() }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, cx| { - todo!(); - // cx.dispatch_action(RespondToContactRequest { - // user_id, - // accept: false, - // }) + cx.dispatch_action(RespondToContactRequest { + user_id, + accept: false, + }) }) - // .flex_float() .contained() .with_margin_right(button_spacing) .boxed(), @@ -602,11 +634,10 @@ impl ContactsPopover { }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, cx| { - todo!() - // cx.dispatch_action(RespondToContactRequest { - // user_id, - // accept: true, - // }) + cx.dispatch_action(RespondToContactRequest { + user_id, + accept: true, + }) }) .boxed(), ]); @@ -626,8 +657,7 @@ impl ContactsPopover { .with_padding(Padding::uniform(2.)) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, cx| { - todo!() - // cx.dispatch_action(RemoveContact(user_id)) + cx.dispatch_action(RemoveContact(user_id)) }) .flex_float() .boxed(), @@ -692,8 +722,7 @@ impl View for ContactsPopover { }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, |_, cx| { - todo!() - // cx.dispatch_action(contact_finder::Toggle) + cx.dispatch_action(contact_finder::Toggle) }) .boxed(), ) diff --git a/crates/contacts_panel/src/notifications.rs b/crates/collab_ui/src/notifications.rs similarity index 83% rename from crates/contacts_panel/src/notifications.rs rename to crates/collab_ui/src/notifications.rs index b9a6dba545..dcb9894006 100644 --- a/crates/contacts_panel/src/notifications.rs +++ b/crates/collab_ui/src/notifications.rs @@ -1,9 +1,7 @@ -use crate::render_icon_button; use client::User; use gpui::{ - elements::{Flex, Image, Label, MouseEventHandler, Padding, ParentElement, Text}, - platform::CursorStyle, - Action, Element, ElementBox, MouseButton, RenderContext, View, + elements::*, platform::CursorStyle, Action, Element, ElementBox, MouseButton, RenderContext, + View, }; use settings::Settings; use std::sync::Arc; @@ -53,11 +51,18 @@ pub fn render_user_notification( ) .with_child( MouseEventHandler::::new(user.id as usize, cx, |state, _| { - render_icon_button( - theme.dismiss_button.style_for(state, false), - "icons/x_mark_thin_8.svg", - ) - .boxed() + let style = theme.dismiss_button.style_for(state, false); + Svg::new("icons/x_mark_thin_8.svg") + .with_color(style.color) + .constrained() + .with_width(style.icon_width) + .aligned() + .contained() + .with_style(style.container) + .constrained() + .with_width(style.button_width) + .with_height(style.button_width) + .boxed() }) .with_cursor_style(CursorStyle::PointingHand) .with_padding(Padding::uniform(5.)) diff --git a/crates/contacts_panel/Cargo.toml b/crates/contacts_panel/Cargo.toml deleted file mode 100644 index b68f48bb97..0000000000 --- a/crates/contacts_panel/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "contacts_panel" -version = "0.1.0" -edition = "2021" - -[lib] -path = "src/contacts_panel.rs" -doctest = false - -[dependencies] -client = { path = "../client" } -collections = { path = "../collections" } -editor = { path = "../editor" } -fuzzy = { path = "../fuzzy" } -gpui = { path = "../gpui" } -menu = { path = "../menu" } -picker = { path = "../picker" } -project = { path = "../project" } -settings = { path = "../settings" } -theme = { path = "../theme" } -util = { path = "../util" } -workspace = { path = "../workspace" } -anyhow = "1.0" -futures = "0.3" -log = "0.4" -postage = { version = "0.4.1", features = ["futures-traits"] } -serde = { version = "1.0", features = ["derive", "rc"] } - -[dev-dependencies] -language = { path = "../language", features = ["test-support"] } -project = { path = "../project", features = ["test-support"] } -workspace = { path = "../workspace", features = ["test-support"] } diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs deleted file mode 100644 index db6d3bd3b0..0000000000 --- a/crates/contacts_panel/src/contacts_panel.rs +++ /dev/null @@ -1,1000 +0,0 @@ -mod contact_finder; -mod contact_notification; -mod notifications; - -use client::{Contact, ContactEventKind, User, UserStore}; -use contact_notification::ContactNotification; -use editor::{Cancel, Editor}; -use fuzzy::{match_strings, StringMatchCandidate}; -use gpui::{ - actions, elements::*, impl_actions, impl_internal_actions, platform::CursorStyle, - AnyViewHandle, AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, - MouseButton, MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle, - WeakViewHandle, -}; -use menu::{Confirm, SelectNext, SelectPrev}; -use serde::Deserialize; -use settings::Settings; -use std::sync::Arc; -use theme::IconButton; -use workspace::{sidebar::SidebarItem, Workspace}; - -actions!(contacts_panel, [ToggleFocus]); - -impl_actions!( - contacts_panel, - [RequestContact, RemoveContact, RespondToContactRequest] -); - -impl_internal_actions!(contacts_panel, [ToggleExpanded]); - -#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)] -enum Section { - Requests, - Online, - Offline, -} - -#[derive(Clone)] -enum ContactEntry { - Header(Section), - IncomingRequest(Arc), - OutgoingRequest(Arc), - Contact(Arc), -} - -#[derive(Clone, PartialEq)] -struct ToggleExpanded(Section); - -pub struct ContactsPanel { - entries: Vec, - match_candidates: Vec, - list_state: ListState, - user_store: ModelHandle, - filter_editor: ViewHandle, - collapsed_sections: Vec
, - selection: Option, - _maintain_contacts: Subscription, -} - -#[derive(Clone, Deserialize, PartialEq)] -pub struct RequestContact(pub u64); - -#[derive(Clone, Deserialize, PartialEq)] -pub struct RemoveContact(pub u64); - -#[derive(Clone, Deserialize, PartialEq)] -pub struct RespondToContactRequest { - pub user_id: u64, - pub accept: bool, -} - -pub fn init(cx: &mut MutableAppContext) { - contact_finder::init(cx); - contact_notification::init(cx); - cx.add_action(ContactsPanel::request_contact); - cx.add_action(ContactsPanel::remove_contact); - cx.add_action(ContactsPanel::respond_to_contact_request); - cx.add_action(ContactsPanel::clear_filter); - cx.add_action(ContactsPanel::select_next); - cx.add_action(ContactsPanel::select_prev); - cx.add_action(ContactsPanel::confirm); - cx.add_action(ContactsPanel::toggle_expanded); -} - -impl ContactsPanel { - pub fn new( - user_store: ModelHandle, - workspace: WeakViewHandle, - cx: &mut ViewContext, - ) -> Self { - let filter_editor = cx.add_view(|cx| { - let mut editor = Editor::single_line( - Some(|theme| theme.contacts_panel.user_query_editor.clone()), - cx, - ); - editor.set_placeholder_text("Filter contacts", cx); - editor - }); - - cx.subscribe(&filter_editor, |this, _, event, cx| { - if let editor::Event::BufferEdited = event { - let query = this.filter_editor.read(cx).text(cx); - if !query.is_empty() { - this.selection.take(); - } - this.update_entries(cx); - if !query.is_empty() { - this.selection = this - .entries - .iter() - .position(|entry| !matches!(entry, ContactEntry::Header(_))); - } - } - }) - .detach(); - - cx.subscribe(&user_store, move |_, user_store, event, cx| { - if let Some(workspace) = workspace.upgrade(cx) { - workspace.update(cx, |workspace, cx| { - if let client::Event::Contact { user, kind } = event { - if let ContactEventKind::Requested | ContactEventKind::Accepted = kind { - workspace.show_notification(user.id as usize, cx, |cx| { - cx.add_view(|cx| { - ContactNotification::new(user.clone(), *kind, user_store, cx) - }) - }) - } - } - }); - } - - if let client::Event::ShowContacts = event { - cx.emit(Event::Activate); - } - }) - .detach(); - - let list_state = ListState::new(0, Orientation::Top, 1000., cx, move |this, ix, cx| { - let theme = cx.global::().theme.clone(); - let is_selected = this.selection == Some(ix); - - match &this.entries[ix] { - ContactEntry::Header(section) => { - let is_collapsed = this.collapsed_sections.contains(section); - Self::render_header( - *section, - &theme.contacts_panel, - is_selected, - is_collapsed, - cx, - ) - } - ContactEntry::IncomingRequest(user) => Self::render_contact_request( - user.clone(), - this.user_store.clone(), - &theme.contacts_panel, - true, - is_selected, - cx, - ), - ContactEntry::OutgoingRequest(user) => Self::render_contact_request( - user.clone(), - this.user_store.clone(), - &theme.contacts_panel, - false, - is_selected, - cx, - ), - ContactEntry::Contact(contact) => { - Self::render_contact(&contact.user, &theme.contacts_panel, is_selected) - } - } - }); - - let mut this = Self { - list_state, - selection: None, - collapsed_sections: Default::default(), - entries: Default::default(), - match_candidates: Default::default(), - filter_editor, - _maintain_contacts: cx.observe(&user_store, |this, _, cx| this.update_entries(cx)), - user_store, - }; - this.update_entries(cx); - this - } - - fn render_header( - section: Section, - theme: &theme::ContactsPanel, - is_selected: bool, - is_collapsed: bool, - cx: &mut RenderContext, - ) -> ElementBox { - enum Header {} - - let header_style = theme.header_row.style_for(Default::default(), is_selected); - let text = match section { - Section::Requests => "Requests", - Section::Online => "Online", - Section::Offline => "Offline", - }; - let icon_size = theme.section_icon_size; - MouseEventHandler::
::new(section as usize, cx, |_, _| { - Flex::row() - .with_child( - Svg::new(if is_collapsed { - "icons/chevron_right_8.svg" - } else { - "icons/chevron_down_8.svg" - }) - .with_color(header_style.text.color) - .constrained() - .with_max_width(icon_size) - .with_max_height(icon_size) - .aligned() - .constrained() - .with_width(icon_size) - .boxed(), - ) - .with_child( - Label::new(text.to_string(), header_style.text.clone()) - .aligned() - .left() - .contained() - .with_margin_left(theme.contact_username.container.margin.left) - .flex(1., true) - .boxed(), - ) - .constrained() - .with_height(theme.row_height) - .contained() - .with_style(header_style.container) - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, cx| { - cx.dispatch_action(ToggleExpanded(section)) - }) - .boxed() - } - - fn render_contact(user: &User, theme: &theme::ContactsPanel, is_selected: bool) -> ElementBox { - Flex::row() - .with_children(user.avatar.clone().map(|avatar| { - Image::new(avatar) - .with_style(theme.contact_avatar) - .aligned() - .left() - .boxed() - })) - .with_child( - Label::new( - user.github_login.clone(), - theme.contact_username.text.clone(), - ) - .contained() - .with_style(theme.contact_username.container) - .aligned() - .left() - .flex(1., true) - .boxed(), - ) - .constrained() - .with_height(theme.row_height) - .contained() - .with_style(*theme.contact_row.style_for(Default::default(), is_selected)) - .boxed() - } - - fn render_contact_request( - user: Arc, - user_store: ModelHandle, - theme: &theme::ContactsPanel, - is_incoming: bool, - is_selected: bool, - cx: &mut RenderContext, - ) -> ElementBox { - enum Decline {} - enum Accept {} - enum Cancel {} - - let mut row = Flex::row() - .with_children(user.avatar.clone().map(|avatar| { - Image::new(avatar) - .with_style(theme.contact_avatar) - .aligned() - .left() - .boxed() - })) - .with_child( - Label::new( - user.github_login.clone(), - theme.contact_username.text.clone(), - ) - .contained() - .with_style(theme.contact_username.container) - .aligned() - .left() - .flex(1., true) - .boxed(), - ); - - let user_id = user.id; - let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user); - let button_spacing = theme.contact_button_spacing; - - if is_incoming { - row.add_children([ - MouseEventHandler::::new(user.id as usize, cx, |mouse_state, _| { - let button_style = if is_contact_request_pending { - &theme.disabled_button - } else { - theme.contact_button.style_for(mouse_state, false) - }; - render_icon_button(button_style, "icons/x_mark_8.svg") - .aligned() - // .flex_float() - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, cx| { - cx.dispatch_action(RespondToContactRequest { - user_id, - accept: false, - }) - }) - // .flex_float() - .contained() - .with_margin_right(button_spacing) - .boxed(), - MouseEventHandler::::new(user.id as usize, cx, |mouse_state, _| { - let button_style = if is_contact_request_pending { - &theme.disabled_button - } else { - theme.contact_button.style_for(mouse_state, false) - }; - render_icon_button(button_style, "icons/check_8.svg") - .aligned() - .flex_float() - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, cx| { - cx.dispatch_action(RespondToContactRequest { - user_id, - accept: true, - }) - }) - .boxed(), - ]); - } else { - row.add_child( - MouseEventHandler::::new(user.id as usize, cx, |mouse_state, _| { - let button_style = if is_contact_request_pending { - &theme.disabled_button - } else { - theme.contact_button.style_for(mouse_state, false) - }; - render_icon_button(button_style, "icons/x_mark_8.svg") - .aligned() - .flex_float() - .boxed() - }) - .with_padding(Padding::uniform(2.)) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, cx| { - cx.dispatch_action(RemoveContact(user_id)) - }) - .flex_float() - .boxed(), - ); - } - - row.constrained() - .with_height(theme.row_height) - .contained() - .with_style(*theme.contact_row.style_for(Default::default(), is_selected)) - .boxed() - } - - fn update_entries(&mut self, cx: &mut ViewContext) { - let user_store = self.user_store.read(cx); - let query = self.filter_editor.read(cx).text(cx); - let executor = cx.background().clone(); - - let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned()); - self.entries.clear(); - - let mut request_entries = Vec::new(); - let incoming = user_store.incoming_contact_requests(); - if !incoming.is_empty() { - self.match_candidates.clear(); - self.match_candidates - .extend( - incoming - .iter() - .enumerate() - .map(|(ix, user)| StringMatchCandidate { - id: ix, - string: user.github_login.clone(), - char_bag: user.github_login.chars().collect(), - }), - ); - let matches = executor.block(match_strings( - &self.match_candidates, - &query, - true, - usize::MAX, - &Default::default(), - executor.clone(), - )); - request_entries.extend( - matches - .iter() - .map(|mat| ContactEntry::IncomingRequest(incoming[mat.candidate_id].clone())), - ); - } - - let outgoing = user_store.outgoing_contact_requests(); - if !outgoing.is_empty() { - self.match_candidates.clear(); - self.match_candidates - .extend( - outgoing - .iter() - .enumerate() - .map(|(ix, user)| StringMatchCandidate { - id: ix, - string: user.github_login.clone(), - char_bag: user.github_login.chars().collect(), - }), - ); - let matches = executor.block(match_strings( - &self.match_candidates, - &query, - true, - usize::MAX, - &Default::default(), - executor.clone(), - )); - request_entries.extend( - matches - .iter() - .map(|mat| ContactEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())), - ); - } - - if !request_entries.is_empty() { - self.entries.push(ContactEntry::Header(Section::Requests)); - if !self.collapsed_sections.contains(&Section::Requests) { - self.entries.append(&mut request_entries); - } - } - - let current_user = user_store.current_user(); - - let contacts = user_store.contacts(); - if !contacts.is_empty() { - // Always put the current user first. - self.match_candidates.clear(); - self.match_candidates.reserve(contacts.len()); - self.match_candidates.push(StringMatchCandidate { - id: 0, - string: Default::default(), - char_bag: Default::default(), - }); - for (ix, contact) in contacts.iter().enumerate() { - let candidate = StringMatchCandidate { - id: ix, - string: contact.user.github_login.clone(), - char_bag: contact.user.github_login.chars().collect(), - }; - if current_user - .as_ref() - .map_or(false, |current_user| current_user.id == contact.user.id) - { - self.match_candidates[0] = candidate; - } else { - self.match_candidates.push(candidate); - } - } - if self.match_candidates[0].string.is_empty() { - self.match_candidates.remove(0); - } - - let matches = executor.block(match_strings( - &self.match_candidates, - &query, - true, - usize::MAX, - &Default::default(), - executor.clone(), - )); - - let (online_contacts, offline_contacts) = matches - .iter() - .partition::, _>(|mat| contacts[mat.candidate_id].online); - - for (matches, section) in [ - (online_contacts, Section::Online), - (offline_contacts, Section::Offline), - ] { - if !matches.is_empty() { - self.entries.push(ContactEntry::Header(section)); - if !self.collapsed_sections.contains(§ion) { - for mat in matches { - let contact = &contacts[mat.candidate_id]; - self.entries.push(ContactEntry::Contact(contact.clone())); - } - } - } - } - } - - if let Some(prev_selected_entry) = prev_selected_entry { - self.selection.take(); - for (ix, entry) in self.entries.iter().enumerate() { - if *entry == prev_selected_entry { - self.selection = Some(ix); - break; - } - } - } - - self.list_state.reset(self.entries.len()); - cx.notify(); - } - - fn request_contact(&mut self, request: &RequestContact, cx: &mut ViewContext) { - self.user_store - .update(cx, |store, cx| store.request_contact(request.0, cx)) - .detach(); - } - - fn remove_contact(&mut self, request: &RemoveContact, cx: &mut ViewContext) { - self.user_store - .update(cx, |store, cx| store.remove_contact(request.0, cx)) - .detach(); - } - - fn respond_to_contact_request( - &mut self, - action: &RespondToContactRequest, - cx: &mut ViewContext, - ) { - self.user_store - .update(cx, |store, cx| { - store.respond_to_contact_request(action.user_id, action.accept, cx) - }) - .detach(); - } - - fn clear_filter(&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); - true - } else { - false - } - }); - if !did_clear { - cx.propagate_action(); - } - } - - fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { - if let Some(ix) = self.selection { - if self.entries.len() > ix + 1 { - self.selection = Some(ix + 1); - } - } else if !self.entries.is_empty() { - self.selection = Some(0); - } - cx.notify(); - self.list_state.reset(self.entries.len()); - } - - fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { - if let Some(ix) = self.selection { - if ix > 0 { - self.selection = Some(ix - 1); - } else { - self.selection = None; - } - } - cx.notify(); - self.list_state.reset(self.entries.len()); - } - - fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { - if let Some(selection) = self.selection { - if let Some(entry) = self.entries.get(selection) { - match entry { - ContactEntry::Header(section) => { - let section = *section; - self.toggle_expanded(&ToggleExpanded(section), cx); - } - _ => {} - } - } - } - } - - fn toggle_expanded(&mut self, action: &ToggleExpanded, cx: &mut ViewContext) { - let section = action.0; - if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) { - self.collapsed_sections.remove(ix); - } else { - self.collapsed_sections.push(section); - } - self.update_entries(cx); - } -} - -impl SidebarItem for ContactsPanel { - fn should_show_badge(&self, cx: &AppContext) -> bool { - !self - .user_store - .read(cx) - .incoming_contact_requests() - .is_empty() - } - - fn contains_focused_view(&self, cx: &AppContext) -> bool { - self.filter_editor.is_focused(cx) - } - - fn should_activate_item_on_event(&self, event: &Event, _: &AppContext) -> bool { - matches!(event, Event::Activate) - } -} - -fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element { - Svg::new(svg_path) - .with_color(style.color) - .constrained() - .with_width(style.icon_width) - .aligned() - .contained() - .with_style(style.container) - .constrained() - .with_width(style.button_width) - .with_height(style.button_width) -} - -pub enum Event { - Activate, -} - -impl Entity for ContactsPanel { - type Event = Event; -} - -impl View for ContactsPanel { - fn ui_name() -> &'static str { - "ContactsPanel" - } - - fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - enum AddContact {} - - let theme = cx.global::().theme.clone(); - let theme = &theme.contacts_panel; - Container::new( - Flex::column() - .with_child( - Flex::row() - .with_child( - ChildView::new(self.filter_editor.clone()) - .contained() - .with_style(theme.user_query_editor.container) - .flex(1., true) - .boxed(), - ) - .with_child( - MouseEventHandler::::new(0, cx, |_, _| { - Svg::new("icons/user_plus_16.svg") - .with_color(theme.add_contact_button.color) - .constrained() - .with_height(16.) - .contained() - .with_style(theme.add_contact_button.container) - .aligned() - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, cx| { - cx.dispatch_action(contact_finder::Toggle) - }) - .boxed(), - ) - .constrained() - .with_height(theme.user_query_editor_height) - .boxed(), - ) - .with_child(List::new(self.list_state.clone()).flex(1., false).boxed()) - .with_children( - self.user_store - .read(cx) - .invite_info() - .cloned() - .and_then(|info| { - enum InviteLink {} - - if info.count > 0 { - Some( - MouseEventHandler::::new(0, cx, |state, cx| { - let style = - theme.invite_row.style_for(state, false).clone(); - - let copied = - cx.read_from_clipboard().map_or(false, |item| { - item.text().as_str() == info.url.as_ref() - }); - - Label::new( - format!( - "{} invite link ({} left)", - if copied { "Copied" } else { "Copy" }, - info.count - ), - style.label.clone(), - ) - .aligned() - .left() - .constrained() - .with_height(theme.row_height) - .contained() - .with_style(style.container) - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, cx| { - cx.write_to_clipboard(ClipboardItem::new( - info.url.to_string(), - )); - cx.notify(); - }) - .boxed(), - ) - } else { - None - } - }), - ) - .boxed(), - ) - .with_style(theme.container) - .boxed() - } - - fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { - cx.focus(&self.filter_editor); - } - - fn keymap_context(&self, _: &gpui::AppContext) -> gpui::keymap::Context { - let mut cx = Self::default_keymap_context(); - cx.set.insert("menu".into()); - cx - } -} - -impl PartialEq for ContactEntry { - fn eq(&self, other: &Self) -> bool { - match self { - ContactEntry::Header(section_1) => { - if let ContactEntry::Header(section_2) = other { - return section_1 == section_2; - } - } - ContactEntry::IncomingRequest(user_1) => { - if let ContactEntry::IncomingRequest(user_2) = other { - return user_1.id == user_2.id; - } - } - ContactEntry::OutgoingRequest(user_1) => { - if let ContactEntry::OutgoingRequest(user_2) = other { - return user_1.id == user_2.id; - } - } - ContactEntry::Contact(contact_1) => { - if let ContactEntry::Contact(contact_2) = other { - return contact_1.user.id == contact_2.user.id; - } - } - } - false - } -} - -#[cfg(test)] -mod tests { - use super::*; - use client::{ - proto, - test::{FakeHttpClient, FakeServer}, - Client, - }; - use collections::HashSet; - use gpui::TestAppContext; - use language::LanguageRegistry; - use project::{FakeFs, Project, ProjectStore}; - - #[gpui::test] - async fn test_contact_panel(cx: &mut TestAppContext) { - Settings::test_async(cx); - let current_user_id = 100; - - let languages = Arc::new(LanguageRegistry::test()); - let http_client = FakeHttpClient::with_404_response(); - let client = Client::new(http_client.clone()); - let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); - let project_store = cx.add_model(|_| ProjectStore::new()); - let server = FakeServer::for_client(current_user_id, &client, cx).await; - let fs = FakeFs::new(cx.background()); - let project = cx.update(|cx| { - Project::local( - client.clone(), - user_store.clone(), - project_store.clone(), - languages, - fs, - cx, - ) - }); - - let (_, workspace) = - cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx)); - let panel = cx.add_view(&workspace, |cx| { - ContactsPanel::new(user_store.clone(), workspace.downgrade(), cx) - }); - - workspace.update(cx, |_, cx| { - cx.observe(&panel, |_, panel, cx| { - let entries = render_to_strings(&panel, cx); - assert!( - entries.iter().collect::>().len() == entries.len(), - "Duplicate contact panel entries {:?}", - entries - ) - }) - .detach(); - }); - - let get_users_request = server.receive::().await.unwrap(); - server - .respond( - get_users_request.receipt(), - proto::UsersResponse { - users: [ - "user_zero", - "user_one", - "user_two", - "user_three", - "user_four", - "user_five", - ] - .into_iter() - .enumerate() - .map(|(id, name)| proto::User { - id: id as u64, - github_login: name.to_string(), - ..Default::default() - }) - .chain([proto::User { - id: current_user_id, - github_login: "the_current_user".to_string(), - ..Default::default() - }]) - .collect(), - }, - ) - .await; - - server.send(proto::UpdateContacts { - incoming_requests: vec![proto::IncomingContactRequest { - requester_id: 1, - should_notify: false, - }], - outgoing_requests: vec![2], - contacts: vec![ - proto::Contact { - user_id: 3, - online: true, - should_notify: false, - }, - proto::Contact { - user_id: 4, - online: true, - should_notify: false, - }, - proto::Contact { - user_id: 5, - online: false, - should_notify: false, - }, - proto::Contact { - user_id: current_user_id, - online: true, - should_notify: false, - }, - ], - ..Default::default() - }); - - cx.foreground().run_until_parked(); - assert_eq!( - cx.read(|cx| render_to_strings(&panel, cx)), - &[ - "v Requests", - " incoming user_one", - " outgoing user_two", - "v Online", - " the_current_user", - " user_four", - " user_three", - "v Offline", - " user_five", - ] - ); - - panel.update(cx, |panel, cx| { - panel - .filter_editor - .update(cx, |editor, cx| editor.set_text("f", cx)) - }); - cx.foreground().run_until_parked(); - assert_eq!( - cx.read(|cx| render_to_strings(&panel, cx)), - &[ - "v Online", - " user_four <=== selected", - "v Offline", - " user_five", - ] - ); - - panel.update(cx, |panel, cx| { - panel.select_next(&Default::default(), cx); - }); - assert_eq!( - cx.read(|cx| render_to_strings(&panel, cx)), - &[ - "v Online", - " user_four", - "v Offline <=== selected", - " user_five", - ] - ); - - panel.update(cx, |panel, cx| { - panel.select_next(&Default::default(), cx); - }); - assert_eq!( - cx.read(|cx| render_to_strings(&panel, cx)), - &[ - "v Online", - " user_four", - "v Offline", - " user_five <=== selected", - ] - ); - } - - fn render_to_strings(panel: &ViewHandle, cx: &AppContext) -> Vec { - let panel = panel.read(cx); - let mut entries = Vec::new(); - entries.extend(panel.entries.iter().enumerate().map(|(ix, entry)| { - let mut string = match entry { - ContactEntry::Header(name) => { - let icon = if panel.collapsed_sections.contains(name) { - ">" - } else { - "v" - }; - format!("{} {:?}", icon, name) - } - ContactEntry::IncomingRequest(user) => { - format!(" incoming {}", user.github_login) - } - ContactEntry::OutgoingRequest(user) => { - format!(" outgoing {}", user.github_login) - } - ContactEntry::Contact(contact) => { - format!(" {}", contact.user.github_login) - } - }; - - if panel.selection == Some(ix) { - string.push_str(" <=== selected"); - } - - string - })); - entries - } -} diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index bc7e6f0995..175c523e53 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -78,6 +78,7 @@ pub struct Titlebar { pub outdated_warning: ContainedText, pub share_button: Interactive, pub toggle_contacts_button: Interactive, + pub toggle_contacts_badge: ContainerStyle, pub contacts_popover: AddParticipantPopover, } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index f2eb765353..c0b43dca8e 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -28,7 +28,6 @@ command_palette = { path = "../command_palette" } context_menu = { path = "../context_menu" } client = { path = "../client" } clock = { path = "../clock" } -contacts_panel = { path = "../contacts_panel" } diagnostics = { path = "../diagnostics" } editor = { path = "../editor" } file_finder = { path = "../file_finder" } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 580493f6d0..dc953bae8c 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -112,7 +112,6 @@ fn main() { go_to_line::init(cx); file_finder::init(cx); chat_panel::init(cx); - contacts_panel::init(cx); outline::init(cx); project_symbols::init(cx); project_panel::init(cx); diff --git a/crates/zed/src/menus.rs b/crates/zed/src/menus.rs index 3a34166ba6..835519fb5c 100644 --- a/crates/zed/src/menus.rs +++ b/crates/zed/src/menus.rs @@ -244,10 +244,6 @@ pub fn menus() -> Vec> { name: "Project Panel", action: Box::new(project_panel::ToggleFocus), }, - MenuItem::Action { - name: "Contacts Panel", - action: Box::new(contacts_panel::ToggleFocus), - }, MenuItem::Action { name: "Command Palette", action: Box::new(command_palette::Toggle), diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index d41b9284c4..28a1249c12 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -12,8 +12,6 @@ use breadcrumbs::Breadcrumbs; pub use client; use collab_ui::CollabTitlebarItem; use collections::VecDeque; -pub use contacts_panel; -use contacts_panel::ContactsPanel; pub use editor; use editor::{Editor, MultiBuffer}; use gpui::{ @@ -208,13 +206,6 @@ pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { workspace.toggle_sidebar_item_focus(SidebarSide::Left, 0, cx); }, ); - cx.add_action( - |workspace: &mut Workspace, - _: &contacts_panel::ToggleFocus, - cx: &mut ViewContext| { - workspace.toggle_sidebar_item_focus(SidebarSide::Right, 0, cx); - }, - ); activity_indicator::init(cx); call::init(app_state.client.clone(), app_state.user_store.clone(), cx); @@ -281,14 +272,11 @@ pub fn initialize_workspace( })); }); - let collab_titlebar_item = cx.add_view(|cx| CollabTitlebarItem::new(&workspace_handle, cx)); + let collab_titlebar_item = + cx.add_view(|cx| CollabTitlebarItem::new(&workspace_handle, &app_state.user_store, cx)); workspace.set_titlebar_item(collab_titlebar_item, cx); let project_panel = ProjectPanel::new(workspace.project().clone(), cx); - let contact_panel = cx.add_view(|cx| { - ContactsPanel::new(app_state.user_store.clone(), workspace.weak_handle(), cx) - }); - workspace.left_sidebar().update(cx, |sidebar, cx| { sidebar.add_item( "icons/folder_tree_16.svg", @@ -297,14 +285,6 @@ pub fn initialize_workspace( cx, ) }); - workspace.right_sidebar().update(cx, |sidebar, cx| { - sidebar.add_item( - "icons/user_group_16.svg", - "Contacts Panel".to_string(), - contact_panel, - cx, - ) - }); let diagnostic_summary = cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace.project(), cx)); diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 0877f131c1..65531e6ec9 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -144,6 +144,13 @@ export default function workspace(theme: Theme) { color: iconColor(theme, "active"), }, }, + toggleContactsBadge: { + cornerRadius: 3, + padding: 2, + margin: { top: 3, left: 3 }, + border: { width: 1, color: workspaceBackground(theme) }, + background: iconColor(theme, "feature"), + }, shareButton: { ...titlebarButton },