Start on `Presence` as the backing model for the people panel

Antonio Scandurra created

Change summary

server/src/rpc.rs       |  14 ++++
zed/src/lib.rs          |   4 +
zed/src/main.rs         |   5 +
zed/src/people_panel.rs |  31 ++++++++++
zed/src/presence.rs     | 129 +++++++++++++++++++++++++++++++++++++++++++
zed/src/test.rs         |   2 
zed/src/workspace.rs    |   7 ++
zrpc/proto/zed.proto    |   5 +
8 files changed, 191 insertions(+), 6 deletions(-)

Detailed changes

server/src/rpc.rs 🔗

@@ -1,6 +1,6 @@
 use super::{
     auth,
-    db::{ChannelId, MessageId, User, UserId},
+    db::{ChannelId, MessageId, UserId},
     AppState,
 };
 use anyhow::anyhow;
@@ -280,11 +280,20 @@ impl Server {
         request: TypedEnvelope<proto::OpenWorktree>,
     ) -> tide::Result<()> {
         let receipt = request.receipt();
+        let user_id = self
+            .state
+            .read()
+            .await
+            .user_id_for_connection(request.sender_id)?;
 
         let mut collaborator_user_ids = Vec::new();
         for github_login in request.payload.collaborator_logins {
             match self.app_state.db.create_user(&github_login, false).await {
-                Ok(user_id) => collaborator_user_ids.push(user_id),
+                Ok(collaborator_user_id) => {
+                    if collaborator_user_id != user_id {
+                        collaborator_user_ids.push(collaborator_user_id);
+                    }
+                }
                 Err(err) => {
                     let message = err.to_string();
                     self.peer
@@ -717,6 +726,7 @@ impl Server {
                             is_online: true,
                         });
                 host.worktrees.push(proto::CollaboratorWorktree {
+                    root_name: worktree.root_name.clone(),
                     is_shared: worktree.share().is_ok(),
                     participants,
                 });

zed/src/lib.rs 🔗

@@ -8,6 +8,8 @@ mod fuzzy;
 pub mod http;
 pub mod language;
 pub mod menus;
+pub mod people_panel;
+pub mod presence;
 pub mod project_browser;
 pub mod rpc;
 pub mod settings;
@@ -26,6 +28,7 @@ use channel::ChannelList;
 use gpui::{action, keymap::Binding, ModelHandle};
 use parking_lot::Mutex;
 use postage::watch;
+use presence::Presence;
 use std::sync::Arc;
 
 pub use settings::Settings;
@@ -46,6 +49,7 @@ pub struct AppState {
     pub user_store: Arc<user::UserStore>,
     pub fs: Arc<dyn fs::Fs>,
     pub channel_list: ModelHandle<ChannelList>,
+    pub presence: ModelHandle<Presence>,
 }
 
 pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {

zed/src/main.rs 🔗

@@ -13,7 +13,9 @@ use zed::{
     channel::ChannelList,
     chat_panel, editor, file_finder,
     fs::RealFs,
-    http, language, menus, rpc, settings, theme_selector,
+    http, language, menus,
+    presence::Presence,
+    rpc, settings, theme_selector,
     user::UserStore,
     workspace::{self, OpenNew, OpenParams, OpenPaths},
     AppState,
@@ -45,6 +47,7 @@ fn main() {
             settings,
             themes,
             channel_list: cx.add_model(|cx| ChannelList::new(user_store.clone(), rpc.clone(), cx)),
+            presence: cx.add_model(|cx| Presence::new(user_store.clone(), rpc.clone(), cx)),
             rpc,
             user_store,
             fs: Arc::new(RealFs),

zed/src/people_panel.rs 🔗

@@ -0,0 +1,31 @@
+use crate::presence::Presence;
+use gpui::{
+    elements::Empty, Element, ElementBox, Entity, ModelHandle, RenderContext, View, ViewContext,
+};
+
+pub struct PeoplePanel {
+    presence: ModelHandle<Presence>,
+}
+
+impl PeoplePanel {
+    pub fn new(presence: ModelHandle<Presence>, cx: &mut ViewContext<Self>) -> Self {
+        cx.observe(&presence, |_, _, cx| cx.notify());
+        Self { presence }
+    }
+}
+
+pub enum Event {}
+
+impl Entity for PeoplePanel {
+    type Event = Event;
+}
+
+impl View for PeoplePanel {
+    fn ui_name() -> &'static str {
+        "PeoplePanel"
+    }
+
+    fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
+        Empty::new().boxed()
+    }
+}

zed/src/presence.rs 🔗

@@ -0,0 +1,129 @@
+use crate::{
+    rpc::Client,
+    user::{User, UserStore},
+    util::TryFutureExt,
+};
+use anyhow::Result;
+use gpui::{Entity, ModelContext, Task};
+use postage::prelude::Stream;
+use smol::future::FutureExt;
+use std::{collections::HashSet, sync::Arc, time::Duration};
+use zrpc::proto;
+
+pub struct Presence {
+    collaborators: Vec<Collaborator>,
+    user_store: Arc<UserStore>,
+    rpc: Arc<Client>,
+    _maintain_people: Task<()>,
+}
+
+#[derive(Debug)]
+struct Collaborator {
+    user: Arc<User>,
+    worktrees: Vec<CollaboratorWorktree>,
+}
+
+#[derive(Debug)]
+struct CollaboratorWorktree {
+    root_name: String,
+    is_shared: bool,
+    participants: Vec<Arc<User>>,
+}
+
+impl Presence {
+    pub fn new(user_store: Arc<UserStore>, rpc: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
+        let _maintain_collaborators = cx.spawn_weak(|this, mut cx| {
+            let user_store = user_store.clone();
+            let foreground = cx.foreground();
+            async move {
+                let mut current_user = user_store.watch_current_user();
+                loop {
+                    let timer = foreground.timer(Duration::from_secs(2));
+                    let next_current_user = async {
+                        current_user.recv().await;
+                    };
+
+                    next_current_user.race(timer).await;
+                    if current_user.borrow().is_some() {
+                        if let Some(this) = cx.read(|cx| this.upgrade(cx)) {
+                            this.update(&mut cx, |this, cx| this.refresh(cx))
+                                .log_err()
+                                .await;
+                        }
+                    }
+                }
+            }
+        });
+
+        Self {
+            collaborators: Vec::new(),
+            user_store,
+            rpc,
+            _maintain_people: _maintain_collaborators,
+        }
+    }
+
+    fn refresh(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
+        cx.spawn(|this, mut cx| {
+            let rpc = self.rpc.clone();
+            let user_store = self.user_store.clone();
+            async move {
+                let response = rpc.request(proto::GetCollaborators {}).await?;
+                let mut user_ids = HashSet::new();
+                for collaborator in &response.collaborators {
+                    user_ids.insert(collaborator.user_id);
+                    user_ids.extend(
+                        collaborator
+                            .worktrees
+                            .iter()
+                            .flat_map(|w| &w.participants)
+                            .copied(),
+                    );
+                }
+                user_store
+                    .load_users(user_ids.into_iter().collect())
+                    .await?;
+
+                let mut collaborators = Vec::new();
+                for collaborator in response.collaborators {
+                    collaborators.push(Collaborator::from_proto(collaborator, &user_store).await?);
+                }
+
+                this.update(&mut cx, |this, cx| {
+                    this.collaborators = collaborators;
+                    cx.notify();
+                });
+
+                Ok(())
+            }
+        })
+    }
+}
+
+pub enum Event {}
+
+impl Entity for Presence {
+    type Event = Event;
+}
+
+impl Collaborator {
+    async fn from_proto(
+        collaborator: proto::Collaborator,
+        user_store: &Arc<UserStore>,
+    ) -> Result<Self> {
+        let user = user_store.fetch_user(collaborator.user_id).await?;
+        let mut worktrees = Vec::new();
+        for worktree in collaborator.worktrees {
+            let mut participants = Vec::new();
+            for participant_id in worktree.participants {
+                participants.push(user_store.fetch_user(participant_id).await?);
+            }
+            worktrees.push(CollaboratorWorktree {
+                root_name: worktree.root_name,
+                is_shared: worktree.is_shared,
+                participants,
+            });
+        }
+        Ok(Self { user, worktrees })
+    }
+}

zed/src/test.rs 🔗

@@ -4,6 +4,7 @@ use crate::{
     fs::RealFs,
     http::{HttpClient, Request, Response, ServerResponse},
     language::LanguageRegistry,
+    presence::Presence,
     rpc::{self, Client, Credentials, EstablishConnectionError},
     settings::{self, ThemeRegistry},
     time::ReplicaId,
@@ -175,6 +176,7 @@ pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> {
         themes,
         languages: languages.clone(),
         channel_list: cx.add_model(|cx| ChannelList::new(user_store.clone(), rpc.clone(), cx)),
+        presence: cx.add_model(|cx| Presence::new(user_store.clone(), rpc.clone(), cx)),
         rpc,
         user_store,
         fs: Arc::new(RealFs),

zed/src/workspace.rs 🔗

@@ -7,6 +7,7 @@ use crate::{
     editor::Buffer,
     fs::Fs,
     language::LanguageRegistry,
+    people_panel::PeoplePanel,
     project_browser::ProjectBrowser,
     rpc,
     settings::Settings,
@@ -378,7 +379,11 @@ impl Workspace {
             })
             .into(),
         );
-        right_sidebar.add_item("icons/user-16.svg", cx.add_view(|_| ProjectBrowser).into());
+        right_sidebar.add_item(
+            "icons/user-16.svg",
+            cx.add_view(|cx| PeoplePanel::new(app_state.presence.clone(), cx))
+                .into(),
+        );
 
         let mut current_user = app_state.user_store.watch_current_user().clone();
         let mut connection_status = app_state.rpc.status().clone();

zrpc/proto/zed.proto 🔗

@@ -342,6 +342,7 @@ message Collaborator {
 }
 
 message CollaboratorWorktree {
-    bool is_shared = 1;
-    repeated uint64 participants = 2;
+    string root_name = 1;
+    bool is_shared = 2;
+    repeated uint64 participants = 3;
 }