Show contacts panel the first time a new user connects to collab

Antonio Scandurra and Nathan Sobo created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

Cargo.lock                                        |  1 
crates/client/src/user.rs                         | 29 +++++++++---
crates/contacts_panel/Cargo.toml                  |  1 
crates/contacts_panel/src/contact_notification.rs | 36 ++++++++++------
crates/contacts_panel/src/contacts_panel.rs       | 34 ++++++++++++---
crates/workspace/src/sidebar.rs                   | 27 +++++++++++-
6 files changed, 95 insertions(+), 33 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -935,6 +935,7 @@ checksum = "f92cfa0fd5690b3cf8c1ef2cabbd9b7ef22fa53cf5e1f92b05103f6d5d1cf6e7"
 name = "contacts_panel"
 version = "0.1.0"
 dependencies = [
+ "anyhow",
  "client",
  "editor",
  "futures",

crates/client/src/user.rs 🔗

@@ -78,10 +78,12 @@ pub struct InviteInfo {
     pub url: Arc<str>,
 }
 
-#[derive(Clone)]
-pub struct ContactEvent {
-    pub user: Arc<User>,
-    pub kind: ContactEventKind,
+pub enum Event {
+    Contact {
+        user: Arc<User>,
+        kind: ContactEventKind,
+    },
+    ShowContacts,
 }
 
 #[derive(Clone, Copy)]
@@ -92,7 +94,7 @@ pub enum ContactEventKind {
 }
 
 impl Entity for UserStore {
-    type Event = ContactEvent;
+    type Event = Event;
 }
 
 enum UpdateContacts {
@@ -111,6 +113,7 @@ impl UserStore {
         let rpc_subscriptions = vec![
             client.add_message_handler(cx.handle(), Self::handle_update_contacts),
             client.add_message_handler(cx.handle(), Self::handle_update_invite_info),
+            client.add_message_handler(cx.handle(), Self::handle_show_contacts),
         ];
         Self {
             users: Default::default(),
@@ -180,6 +183,16 @@ impl UserStore {
         Ok(())
     }
 
+    async fn handle_show_contacts(
+        this: ModelHandle<Self>,
+        _: TypedEnvelope<proto::ShowContacts>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<()> {
+        this.update(&mut cx, |_, cx| cx.emit(Event::ShowContacts));
+        Ok(())
+    }
+
     pub fn invite_info(&self) -> Option<&InviteInfo> {
         self.invite_info.as_ref()
     }
@@ -274,7 +287,7 @@ impl UserStore {
                         // Update existing contacts and insert new ones
                         for (updated_contact, should_notify) in updated_contacts {
                             if should_notify {
-                                cx.emit(ContactEvent {
+                                cx.emit(Event::Contact {
                                     user: updated_contact.user.clone(),
                                     kind: ContactEventKind::Accepted,
                                 });
@@ -291,7 +304,7 @@ impl UserStore {
                         // Remove incoming contact requests
                         this.incoming_contact_requests.retain(|user| {
                             if removed_incoming_requests.contains(&user.id) {
-                                cx.emit(ContactEvent {
+                                cx.emit(Event::Contact {
                                     user: user.clone(),
                                     kind: ContactEventKind::Cancelled,
                                 });
@@ -303,7 +316,7 @@ impl UserStore {
                         // Update existing incoming requests and insert new ones
                         for (user, should_notify) in incoming_requests {
                             if should_notify {
-                                cx.emit(ContactEvent {
+                                cx.emit(Event::Contact {
                                     user: user.clone(),
                                     kind: ContactEventKind::Requested,
                                 });

crates/contacts_panel/Cargo.toml 🔗

@@ -18,6 +18,7 @@ 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"] }

crates/contacts_panel/src/contact_notification.rs 🔗

@@ -1,5 +1,7 @@
+use std::sync::Arc;
+
 use crate::notifications::render_user_notification;
-use client::{ContactEvent, ContactEventKind, UserStore};
+use client::{ContactEventKind, User, UserStore};
 use gpui::{
     elements::*, impl_internal_actions, Entity, ModelHandle, MutableAppContext, RenderContext,
     View, ViewContext,
@@ -15,7 +17,8 @@ pub fn init(cx: &mut MutableAppContext) {
 
 pub struct ContactNotification {
     user_store: ModelHandle<UserStore>,
-    event: ContactEvent,
+    user: Arc<User>,
+    kind: client::ContactEventKind,
 }
 
 #[derive(Clone)]
@@ -41,27 +44,27 @@ impl View for ContactNotification {
     }
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
-        match self.event.kind {
+        match self.kind {
             ContactEventKind::Requested => render_user_notification(
-                self.event.user.clone(),
+                self.user.clone(),
                 "wants to add you as a contact",
                 Some("They won't know if you decline."),
                 RespondToContactRequest {
-                    user_id: self.event.user.id,
+                    user_id: self.user.id,
                     accept: false,
                 },
                 vec![
                     (
                         "Decline",
                         Box::new(RespondToContactRequest {
-                            user_id: self.event.user.id,
+                            user_id: self.user.id,
                             accept: false,
                         }),
                     ),
                     (
                         "Accept",
                         Box::new(RespondToContactRequest {
-                            user_id: self.event.user.id,
+                            user_id: self.user.id,
                             accept: true,
                         }),
                     ),
@@ -69,10 +72,10 @@ impl View for ContactNotification {
                 cx,
             ),
             ContactEventKind::Accepted => render_user_notification(
-                self.event.user.clone(),
+                self.user.clone(),
                 "accepted your contact request",
                 None,
-                Dismiss(self.event.user.id),
+                Dismiss(self.user.id),
                 vec![],
                 cx,
             ),
@@ -89,30 +92,35 @@ impl Notification for ContactNotification {
 
 impl ContactNotification {
     pub fn new(
-        event: ContactEvent,
+        user: Arc<User>,
+        kind: client::ContactEventKind,
         user_store: ModelHandle<UserStore>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
         cx.subscribe(&user_store, move |this, _, event, cx| {
-            if let client::ContactEvent {
+            if let client::Event::Contact {
                 kind: ContactEventKind::Cancelled,
                 user,
             } = event
             {
-                if user.id == this.event.user.id {
+                if user.id == this.user.id {
                     cx.emit(Event::Dismiss);
                 }
             }
         })
         .detach();
 
-        Self { event, user_store }
+        Self {
+            user,
+            kind,
+            user_store,
+        }
     }
 
     fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
         self.user_store.update(cx, |store, cx| {
             store
-                .dismiss_contact_request(self.event.user.id, cx)
+                .dismiss_contact_request(self.user.id, cx)
                 .detach_and_log_err(cx);
         });
         cx.emit(Event::Dismiss);

crates/contacts_panel/src/contacts_panel.rs 🔗

@@ -158,16 +158,28 @@ impl ContactsPanel {
                 if let Some((workspace, user_store)) =
                     workspace.upgrade(cx).zip(user_store.upgrade(cx))
                 {
-                    workspace.update(cx, |workspace, cx| match event.kind {
-                        ContactEventKind::Requested | ContactEventKind::Accepted => workspace
-                            .show_notification(event.user.id as usize, cx, |cx| {
-                                cx.add_view(|cx| {
-                                    ContactNotification::new(event.clone(), user_store, cx)
-                                })
-                            }),
+                    workspace.update(cx, |workspace, cx| match event {
+                        client::Event::Contact { user, kind } => match kind {
+                            ContactEventKind::Requested | ContactEventKind::Accepted => 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();
@@ -801,6 +813,10 @@ impl SidebarItem for ContactsPanel {
     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 {
@@ -816,7 +832,9 @@ fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Elemen
         .with_height(style.button_width)
 }
 
-pub enum Event {}
+pub enum Event {
+    Activate,
+}
 
 impl Entity for ContactsPanel {
     type Event = Event;

crates/workspace/src/sidebar.rs 🔗

@@ -9,6 +9,9 @@ use std::{cell::RefCell, rc::Rc};
 use theme::Theme;
 
 pub trait SidebarItem: View {
+    fn should_activate_item_on_event(&self, _: &Self::Event, _: &AppContext) -> bool {
+        false
+    }
     fn should_show_badge(&self, cx: &AppContext) -> bool;
     fn contains_focused_view(&self, _: &AppContext) -> bool {
         false
@@ -16,6 +19,7 @@ pub trait SidebarItem: View {
 }
 
 pub trait SidebarItemHandle {
+    fn id(&self) -> usize;
     fn should_show_badge(&self, cx: &AppContext) -> bool;
     fn is_focused(&self, cx: &AppContext) -> bool;
     fn to_any(&self) -> AnyViewHandle;
@@ -25,6 +29,10 @@ impl<T> SidebarItemHandle for ViewHandle<T>
 where
     T: SidebarItem,
 {
+    fn id(&self) -> usize {
+        self.id()
+    }
+
     fn should_show_badge(&self, cx: &AppContext) -> bool {
         self.read(cx).should_show_badge(cx)
     }
@@ -61,7 +69,7 @@ pub enum Side {
 struct Item {
     icon_path: &'static str,
     view: Rc<dyn SidebarItemHandle>,
-    _observation: Subscription,
+    _subscriptions: [Subscription; 2],
 }
 
 pub struct SidebarButtons {
@@ -99,11 +107,24 @@ impl Sidebar {
         view: ViewHandle<T>,
         cx: &mut ViewContext<Self>,
     ) {
-        let subscription = cx.observe(&view, |_, _, cx| cx.notify());
+        let subscriptions = [
+            cx.observe(&view, |_, _, cx| cx.notify()),
+            cx.subscribe(&view, |this, view, event, cx| {
+                if view.read(cx).should_activate_item_on_event(event, cx) {
+                    if let Some(ix) = this
+                        .items
+                        .iter()
+                        .position(|item| item.view.id() == view.id())
+                    {
+                        this.activate_item(ix, cx);
+                    }
+                }
+            }),
+        ];
         self.items.push(Item {
             icon_path,
             view: Rc::new(view),
-            _observation: subscription,
+            _subscriptions: subscriptions,
         });
         cx.notify()
     }