Show a different message when participant is active on unshared project

Antonio Scandurra and Nathan Sobo created

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

Change summary

crates/call/src/participant.rs               | 19 ++++++++++---
crates/call/src/room.rs                      | 26 ++++++++++++++++---
crates/collab/src/integration_tests.rs       | 29 ++++++++++++---------
crates/collab/src/rpc/store.rs               |  2 
crates/collab_ui/src/collab_titlebar_item.rs | 25 ++++--------------
crates/gpui/src/app.rs                       |  6 ++++
crates/rpc/proto/zed.proto                   |  9 ++++--
crates/workspace/src/pane_group.rs           | 17 ++++++++++++
8 files changed, 88 insertions(+), 45 deletions(-)

Detailed changes

crates/call/src/participant.rs 🔗

@@ -1,28 +1,37 @@
 use anyhow::{anyhow, Result};
 use client::{proto, User};
+use gpui::WeakModelHandle;
+use project::Project;
 use std::sync::Arc;
 
 #[derive(Copy, Clone, Debug, Eq, PartialEq)]
 pub enum ParticipantLocation {
-    Project { project_id: u64 },
+    SharedProject { project_id: u64 },
+    UnsharedProject,
     External,
 }
 
 impl ParticipantLocation {
     pub fn from_proto(location: Option<proto::ParticipantLocation>) -> Result<Self> {
         match location.and_then(|l| l.variant) {
-            Some(proto::participant_location::Variant::Project(project)) => Ok(Self::Project {
-                project_id: project.id,
-            }),
+            Some(proto::participant_location::Variant::SharedProject(project)) => {
+                Ok(Self::SharedProject {
+                    project_id: project.id,
+                })
+            }
+            Some(proto::participant_location::Variant::UnsharedProject(_)) => {
+                Ok(Self::UnsharedProject)
+            }
             Some(proto::participant_location::Variant::External(_)) => Ok(Self::External),
             None => Err(anyhow!("participant location was not provided")),
         }
     }
 }
 
-#[derive(Clone, Debug, Default)]
+#[derive(Clone, Default)]
 pub struct LocalParticipant {
     pub projects: Vec<proto::ParticipantProject>,
+    pub active_project: Option<WeakModelHandle<Project>>,
 }
 
 #[derive(Clone, Debug)]

crates/call/src/room.rs 🔗

@@ -395,13 +395,26 @@ impl Room {
                 })
                 .collect(),
         });
-        cx.spawn_weak(|_, mut cx| async move {
+        cx.spawn(|this, mut cx| async move {
             let response = request.await?;
+
             project
                 .update(&mut cx, |project, cx| {
                     project.shared(response.project_id, cx)
                 })
                 .await?;
+
+            // If the user's location is in this project, it changes from UnsharedProject to SharedProject.
+            this.update(&mut cx, |this, cx| {
+                let active_project = this.local_participant.active_project.as_ref();
+                if active_project.map_or(false, |location| *location == project) {
+                    this.set_location(Some(&project), cx)
+                } else {
+                    Task::ready(Ok(()))
+                }
+            })
+            .await?;
+
             Ok(response.project_id)
         })
     }
@@ -418,17 +431,22 @@ impl Room {
         let client = self.client.clone();
         let room_id = self.id;
         let location = if let Some(project) = project {
+            self.local_participant.active_project = Some(project.downgrade());
             if let Some(project_id) = project.read(cx).remote_id() {
-                proto::participant_location::Variant::Project(
-                    proto::participant_location::Project { id: project_id },
+                proto::participant_location::Variant::SharedProject(
+                    proto::participant_location::SharedProject { id: project_id },
                 )
             } else {
-                return Task::ready(Err(anyhow!("project is not shared")));
+                proto::participant_location::Variant::UnsharedProject(
+                    proto::participant_location::UnsharedProject {},
+                )
             }
         } else {
+            self.local_participant.active_project = None;
             proto::participant_location::Variant::External(proto::participant_location::External {})
         };
 
+        cx.notify();
         cx.foreground().spawn(async move {
             client
                 .request(proto::UpdateParticipantLocation {

crates/collab/src/integration_tests.rs 🔗

@@ -946,8 +946,8 @@ async fn test_room_location(
         }
     });
 
-    let project_a_id = active_call_a
-        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+    room_a
+        .update(cx_a, |room, cx| room.set_location(Some(&project_a), cx))
         .await
         .unwrap();
     deterministic.run_until_parked();
@@ -959,11 +959,11 @@ async fn test_room_location(
     assert!(b_notified.take());
     assert_eq!(
         participant_locations(&room_b, cx_b),
-        vec![("user_a".to_string(), ParticipantLocation::External)]
+        vec![("user_a".to_string(), ParticipantLocation::UnsharedProject)]
     );
 
-    let project_b_id = active_call_b
-        .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
+    let project_a_id = active_call_a
+        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
         .await
         .unwrap();
     deterministic.run_until_parked();
@@ -975,11 +975,16 @@ async fn test_room_location(
     assert!(b_notified.take());
     assert_eq!(
         participant_locations(&room_b, cx_b),
-        vec![("user_a".to_string(), ParticipantLocation::External)]
+        vec![(
+            "user_a".to_string(),
+            ParticipantLocation::SharedProject {
+                project_id: project_a_id
+            }
+        )]
     );
 
-    room_a
-        .update(cx_a, |room, cx| room.set_location(Some(&project_a), cx))
+    let project_b_id = active_call_b
+        .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
         .await
         .unwrap();
     deterministic.run_until_parked();
@@ -993,7 +998,7 @@ async fn test_room_location(
         participant_locations(&room_b, cx_b),
         vec![(
             "user_a".to_string(),
-            ParticipantLocation::Project {
+            ParticipantLocation::SharedProject {
                 project_id: project_a_id
             }
         )]
@@ -1009,7 +1014,7 @@ async fn test_room_location(
         participant_locations(&room_a, cx_a),
         vec![(
             "user_b".to_string(),
-            ParticipantLocation::Project {
+            ParticipantLocation::SharedProject {
                 project_id: project_b_id
             }
         )]
@@ -1019,7 +1024,7 @@ async fn test_room_location(
         participant_locations(&room_b, cx_b),
         vec![(
             "user_a".to_string(),
-            ParticipantLocation::Project {
+            ParticipantLocation::SharedProject {
                 project_id: project_a_id
             }
         )]
@@ -1040,7 +1045,7 @@ async fn test_room_location(
         participant_locations(&room_b, cx_b),
         vec![(
             "user_a".to_string(),
-            ParticipantLocation::Project {
+            ParticipantLocation::SharedProject {
                 project_id: project_a_id
             }
         )]

crates/collab/src/rpc/store.rs 🔗

@@ -684,7 +684,7 @@ impl Store {
             .rooms
             .get_mut(&room_id)
             .ok_or_else(|| anyhow!("no such room"))?;
-        if let Some(proto::participant_location::Variant::Project(project)) =
+        if let Some(proto::participant_location::Variant::SharedProject(project)) =
             location.variant.as_ref()
         {
             anyhow::ensure!(

crates/collab_ui/src/collab_titlebar_item.rs 🔗

@@ -121,14 +121,11 @@ impl CollabTitlebarItem {
         let room = ActiveCall::global(cx).read(cx).room().cloned();
         if let Some((workspace, room)) = workspace.zip(room) {
             let workspace = workspace.read(cx);
-            let project = if !active {
-                None
-            } else if workspace.project().read(cx).remote_id().is_some() {
+            let project = if active {
                 Some(workspace.project().clone())
             } else {
                 None
             };
-
             room.update(cx, |room, cx| {
                 room.set_location(project.as_ref(), cx)
                     .detach_and_log_err(cx);
@@ -139,20 +136,10 @@ impl CollabTitlebarItem {
     fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext<Self>) {
         if let Some(workspace) = self.workspace.upgrade(cx) {
             let active_call = ActiveCall::global(cx);
-
-            let window_id = cx.window_id();
             let project = workspace.read(cx).project().clone();
-            let share = active_call.update(cx, |call, cx| call.share_project(project.clone(), cx));
-            cx.spawn_weak(|_, mut cx| async move {
-                share.await?;
-                if cx.update(|cx| cx.window_is_active(window_id)) {
-                    active_call.update(&mut cx, |call, cx| {
-                        call.set_location(Some(&project), cx).detach_and_log_err(cx);
-                    });
-                }
-                anyhow::Ok(())
-            })
-            .detach_and_log_err(cx);
+            active_call
+                .update(cx, |call, cx| call.share_project(project, cx))
+                .detach_and_log_err(cx);
         }
     }
 
@@ -363,7 +350,7 @@ impl CollabTitlebarItem {
 
         let mut avatar_style;
         if let Some((_, _, location)) = peer.as_ref() {
-            if let ParticipantLocation::Project { project_id } = *location {
+            if let ParticipantLocation::SharedProject { project_id } = *location {
                 if Some(project_id) == workspace.read(cx).project().read(cx).remote_id() {
                     avatar_style = theme.workspace.titlebar.avatar;
                 } else {
@@ -428,7 +415,7 @@ impl CollabTitlebarItem {
                         cx,
                     )
                     .boxed()
-            } else if let ParticipantLocation::Project { project_id } = location {
+            } else if let ParticipantLocation::SharedProject { project_id } = location {
                 let user_id = user.id;
                 MouseEventHandler::<JoinProject>::new(peer_id.0 as usize, cx, move |_, _| content)
                     .with_cursor_style(CursorStyle::PointingHand)

crates/gpui/src/app.rs 🔗

@@ -4687,6 +4687,12 @@ impl<T> PartialEq for WeakModelHandle<T> {
 
 impl<T> Eq for WeakModelHandle<T> {}
 
+impl<T: Entity> PartialEq<ModelHandle<T>> for WeakModelHandle<T> {
+    fn eq(&self, other: &ModelHandle<T>) -> bool {
+        self.model_id == other.model_id
+    }
+}
+
 impl<T> Clone for WeakModelHandle<T> {
     fn clone(&self) -> Self {
         Self {

crates/rpc/proto/zed.proto 🔗

@@ -174,14 +174,17 @@ message ParticipantProject {
 
 message ParticipantLocation {
     oneof variant {
-        Project project = 1;
-        External external = 2;
+        SharedProject shared_project = 1;
+        UnsharedProject unshared_project = 2;
+        External external = 3;
     }
     
-    message Project {
+    message SharedProject {
         uint64 id = 1;
     }
     
+    message UnsharedProject {}
+    
     message External {}
 }
 

crates/workspace/src/pane_group.rs 🔗

@@ -138,7 +138,7 @@ impl Member {
                     border.overlay = true;
 
                     match leader.location {
-                        call::ParticipantLocation::Project {
+                        call::ParticipantLocation::SharedProject {
                             project_id: leader_project_id,
                         } => {
                             if Some(leader_project_id) == project.read(cx).remote_id() {
@@ -183,6 +183,21 @@ impl Member {
                                 )
                             }
                         }
+                        call::ParticipantLocation::UnsharedProject => Some(
+                            Label::new(
+                                format!(
+                                    "{} is viewing an unshared Zed project",
+                                    leader.user.github_login
+                                ),
+                                theme.workspace.external_location_message.text.clone(),
+                            )
+                            .contained()
+                            .with_style(theme.workspace.external_location_message.container)
+                            .aligned()
+                            .bottom()
+                            .right()
+                            .boxed(),
+                        ),
                         call::ParticipantLocation::External => Some(
                             Label::new(
                                 format!(