Move contacts panel features into collab_ui

Antonio Scandurra created

Change summary

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 
crates/collab_ui/src/contact_finder.rs       |   26 
crates/collab_ui/src/contact_notification.rs |    5 
crates/collab_ui/src/contacts_popover.rs     |   69 +
crates/collab_ui/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(+), 1,134 deletions(-)

Detailed changes

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",

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"
         }
     },

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" }

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<Workspace>,
+    user_store: ModelHandle<UserStore>,
     contacts_popover: Option<ViewHandle<ContactsPopover>>,
     _subscriptions: Vec<Subscription>,
 }
@@ -71,7 +72,11 @@ impl View for CollabTitlebarItem {
 }
 
 impl CollabTitlebarItem {
-    pub fn new(workspace: &ViewHandle<Workspace>, cx: &mut ViewContext<Self>) -> Self {
+    pub fn new(
+        workspace: &ViewHandle<Workspace>,
+        user_store: &ModelHandle<UserStore>,
+        cx: &mut ViewContext<Self>,
+    ) -> 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<Self>,
     ) -> 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::<ToggleContactsPopover>::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)

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<AppState>, 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);

crates/contacts_panel/src/contact_finder.rs → 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()

crates/contacts_panel/src/contact_notification.rs → 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",

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>) {
+        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>,
+    ) {
+        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<Self>) {
         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(),
                     )

crates/contacts_panel/src/notifications.rs → 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<V: View, A: Action + Clone>(
                 )
                 .with_child(
                     MouseEventHandler::<Dismiss>::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.))

crates/contacts_panel/Cargo.toml 🔗

@@ -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"] }

crates/contacts_panel/src/contacts_panel.rs 🔗

@@ -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<User>),
-    OutgoingRequest(Arc<User>),
-    Contact(Arc<Contact>),
-}
-
-#[derive(Clone, PartialEq)]
-struct ToggleExpanded(Section);
-
-pub struct ContactsPanel {
-    entries: Vec<ContactEntry>,
-    match_candidates: Vec<StringMatchCandidate>,
-    list_state: ListState,
-    user_store: ModelHandle<UserStore>,
-    filter_editor: ViewHandle<Editor>,
-    collapsed_sections: Vec<Section>,
-    selection: Option<usize>,
-    _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<UserStore>,
-        workspace: WeakViewHandle<Workspace>,
-        cx: &mut ViewContext<Self>,
-    ) -> 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::<Settings>().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<Self>,
-    ) -> 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::<Header>::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>,
-        user_store: ModelHandle<UserStore>,
-        theme: &theme::ContactsPanel,
-        is_incoming: bool,
-        is_selected: bool,
-        cx: &mut RenderContext<ContactsPanel>,
-    ) -> 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::<Decline>::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::<Accept>::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::<Cancel>::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<Self>) {
-        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::<Vec<_>, _>(|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(&section) {
-                        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>) {
-        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>) {
-        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>,
-    ) {
-        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<Self>) {
-        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<Self>) {
-        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<Self>) {
-        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<Self>) {
-        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<Self>) {
-        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<Self>) -> ElementBox {
-        enum AddContact {}
-
-        let theme = cx.global::<Settings>().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::<AddContact>::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::<InviteLink>::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<Self>) {
-        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::<HashSet<_>>().len() == entries.len(),
-                    "Duplicate contact panel entries {:?}",
-                    entries
-                )
-            })
-            .detach();
-        });
-
-        let get_users_request = server.receive::<proto::GetUsers>().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<ContactsPanel>, cx: &AppContext) -> Vec<String> {
-        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
-    }
-}

crates/theme/src/theme.rs 🔗

@@ -78,6 +78,7 @@ pub struct Titlebar {
     pub outdated_warning: ContainedText,
     pub share_button: Interactive<ContainedText>,
     pub toggle_contacts_button: Interactive<IconButton>,
+    pub toggle_contacts_badge: ContainerStyle,
     pub contacts_popover: AddParticipantPopover,
 }
 

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" }

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);

crates/zed/src/menus.rs 🔗

@@ -244,10 +244,6 @@ pub fn menus() -> Vec<Menu<'static>> {
                     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),

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<AppState>, 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>| {
-            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));

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
       },