Fix joining hosted projects (#9038)

Conrad Irwin created

Release Notes:

- N/A

Change summary

crates/channel/src/channel_store.rs             | 30 +++++-----
crates/client/src/user.rs                       |  2 
crates/collab/src/db/queries/hosted_projects.rs | 23 ++++---
crates/collab/src/db/queries/projects.rs        | 20 +++++-
crates/collab/src/db/tables/hosted_project.rs   | 11 +++
crates/collab/src/db/tables/project.rs          | 12 ++++
crates/collab/src/rpc.rs                        |  8 +-
crates/collab_ui/src/collab_panel.rs            |  6 +-
crates/project/src/project.rs                   | 53 +++++++++---------
crates/rpc/proto/zed.proto                      |  4 
crates/workspace/src/workspace.rs               |  4 
11 files changed, 104 insertions(+), 69 deletions(-)

Detailed changes

crates/channel/src/channel_store.rs 🔗

@@ -3,9 +3,7 @@ mod channel_index;
 use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat, ChannelMessage};
 use anyhow::{anyhow, Result};
 use channel_index::ChannelIndex;
-use client::{
-    ChannelId, Client, ClientSettings, HostedProjectId, Subscription, User, UserId, UserStore,
-};
+use client::{ChannelId, Client, ClientSettings, ProjectId, Subscription, User, UserId, UserStore};
 use collections::{hash_map, HashMap, HashSet};
 use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
 use gpui::{
@@ -37,7 +35,7 @@ struct NotesVersion {
 
 #[derive(Debug, Clone)]
 pub struct HostedProject {
-    id: HostedProjectId,
+    project_id: ProjectId,
     channel_id: ChannelId,
     name: SharedString,
     _visibility: proto::ChannelVisibility,
@@ -46,7 +44,7 @@ pub struct HostedProject {
 impl From<proto::HostedProject> for HostedProject {
     fn from(project: proto::HostedProject) -> Self {
         Self {
-            id: HostedProjectId(project.id),
+            project_id: ProjectId(project.project_id),
             channel_id: ChannelId(project.channel_id),
             _visibility: project.visibility(),
             name: project.name.into(),
@@ -59,7 +57,7 @@ pub struct ChannelStore {
     channel_invitations: Vec<Arc<Channel>>,
     channel_participants: HashMap<ChannelId, Vec<Arc<User>>>,
     channel_states: HashMap<ChannelId, ChannelState>,
-    hosted_projects: HashMap<HostedProjectId, HostedProject>,
+    hosted_projects: HashMap<ProjectId, HostedProject>,
 
     outgoing_invites: HashSet<(ChannelId, UserId)>,
     update_channels_tx: mpsc::UnboundedSender<proto::UpdateChannels>,
@@ -88,7 +86,7 @@ pub struct ChannelState {
     observed_notes_version: NotesVersion,
     observed_chat_message: Option<u64>,
     role: Option<ChannelRole>,
-    projects: HashSet<HostedProjectId>,
+    projects: HashSet<ProjectId>,
 }
 
 impl Channel {
@@ -305,8 +303,8 @@ impl ChannelStore {
         self.channel_index.by_id().get(&channel_id)
     }
 
-    pub fn projects_for_id(&self, channel_id: ChannelId) -> Vec<(SharedString, HostedProjectId)> {
-        let mut projects: Vec<(SharedString, HostedProjectId)> = self
+    pub fn projects_for_id(&self, channel_id: ChannelId) -> Vec<(SharedString, ProjectId)> {
+        let mut projects: Vec<(SharedString, ProjectId)> = self
             .channel_states
             .get(&channel_id)
             .map(|state| state.projects.clone())
@@ -1159,27 +1157,27 @@ impl ChannelStore {
                 let hosted_project: HostedProject = hosted_project.into();
                 if let Some(old_project) = self
                     .hosted_projects
-                    .insert(hosted_project.id, hosted_project.clone())
+                    .insert(hosted_project.project_id, hosted_project.clone())
                 {
                     self.channel_states
                         .entry(old_project.channel_id)
                         .or_default()
-                        .remove_hosted_project(old_project.id);
+                        .remove_hosted_project(old_project.project_id);
                 }
                 self.channel_states
                     .entry(hosted_project.channel_id)
                     .or_default()
-                    .add_hosted_project(hosted_project.id);
+                    .add_hosted_project(hosted_project.project_id);
             }
 
             for hosted_project_id in payload.deleted_hosted_projects {
-                let hosted_project_id = HostedProjectId(hosted_project_id);
+                let hosted_project_id = ProjectId(hosted_project_id);
 
                 if let Some(old_project) = self.hosted_projects.remove(&hosted_project_id) {
                     self.channel_states
                         .entry(old_project.channel_id)
                         .or_default()
-                        .remove_hosted_project(old_project.id);
+                        .remove_hosted_project(old_project.project_id);
                 }
             }
         }
@@ -1289,11 +1287,11 @@ impl ChannelState {
         }
     }
 
-    fn add_hosted_project(&mut self, project_id: HostedProjectId) {
+    fn add_hosted_project(&mut self, project_id: ProjectId) {
         self.projects.insert(project_id);
     }
 
-    fn remove_hosted_project(&mut self, project_id: HostedProjectId) {
+    fn remove_hosted_project(&mut self, project_id: ProjectId) {
         self.projects.remove(&project_id);
     }
 }

crates/client/src/user.rs 🔗

@@ -25,7 +25,7 @@ impl std::fmt::Display for ChannelId {
 }
 
 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
-pub struct HostedProjectId(pub u64);
+pub struct ProjectId(pub u64);
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub struct ParticipantIndex(pub u32);

crates/collab/src/db/queries/hosted_projects.rs 🔗

@@ -9,20 +9,21 @@ impl Database {
         roles: &HashMap<ChannelId, ChannelRole>,
         tx: &DatabaseTransaction,
     ) -> Result<Vec<proto::HostedProject>> {
-        Ok(hosted_project::Entity::find()
+        let projects = hosted_project::Entity::find()
+            .find_also_related(project::Entity)
             .filter(hosted_project::Column::ChannelId.is_in(channel_ids.iter().map(|id| id.0)))
             .all(tx)
             .await?
             .into_iter()
-            .flat_map(|project| {
-                if project.deleted_at.is_some() {
+            .flat_map(|(hosted_project, project)| {
+                if hosted_project.deleted_at.is_some() {
                     return None;
                 }
-                match project.visibility {
+                match hosted_project.visibility {
                     ChannelVisibility::Public => {}
                     ChannelVisibility::Members => {
                         let is_visible = roles
-                            .get(&project.channel_id)
+                            .get(&hosted_project.channel_id)
                             .map(|role| role.can_see_all_descendants())
                             .unwrap_or(false);
                         if !is_visible {
@@ -31,13 +32,15 @@ impl Database {
                     }
                 };
                 Some(proto::HostedProject {
-                    id: project.id.to_proto(),
-                    channel_id: project.channel_id.to_proto(),
-                    name: project.name.clone(),
-                    visibility: project.visibility.into(),
+                    project_id: project?.id.to_proto(),
+                    channel_id: hosted_project.channel_id.to_proto(),
+                    name: hosted_project.name.clone(),
+                    visibility: hosted_project.visibility.into(),
                 })
             })
-            .collect())
+            .collect();
+
+        Ok(projects)
     }
 
     pub async fn get_hosted_project(

crates/collab/src/db/queries/projects.rs 🔗

@@ -512,18 +512,30 @@ impl Database {
     /// Adds the given connection to the specified hosted project
     pub async fn join_hosted_project(
         &self,
-        id: HostedProjectId,
+        id: ProjectId,
         user_id: UserId,
         connection: ConnectionId,
     ) -> Result<(Project, ReplicaId)> {
         self.transaction(|tx| async move {
-            let (hosted_project, role) = self.get_hosted_project(id, user_id, &tx).await?;
-            let project = project::Entity::find()
-                .filter(project::Column::HostedProjectId.eq(hosted_project.id))
+            let (project, hosted_project) = project::Entity::find_by_id(id)
+                .find_also_related(hosted_project::Entity)
                 .one(&*tx)
                 .await?
                 .ok_or_else(|| anyhow!("hosted project is no longer shared"))?;
 
+            let Some(hosted_project) = hosted_project else {
+                return Err(anyhow!("project is not hosted"))?;
+            };
+
+            let channel = channel::Entity::find_by_id(hosted_project.channel_id)
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("no such channel"))?;
+
+            let role = self
+                .check_user_is_channel_participant(&channel, user_id, &tx)
+                .await?;
+
             self.join_project_internal(project, user_id, connection, role, &tx)
                 .await
         })

crates/collab/src/db/tables/hosted_project.rs 🔗

@@ -15,4 +15,13 @@ pub struct Model {
 impl ActiveModelBehavior for ActiveModel {}
 
 #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
-pub enum Relation {}
+pub enum Relation {
+    #[sea_orm(has_one = "super::project::Entity")]
+    Project,
+}
+
+impl Related<super::project::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::Project.def()
+    }
+}

crates/collab/src/db/tables/project.rs 🔗

@@ -50,6 +50,12 @@ pub enum Relation {
     Collaborators,
     #[sea_orm(has_many = "super::language_server::Entity")]
     LanguageServers,
+    #[sea_orm(
+        belongs_to = "super::hosted_project::Entity",
+        from = "Column::HostedProjectId",
+        to = "super::hosted_project::Column::Id"
+    )]
+    HostedProject,
 }
 
 impl Related<super::user::Entity> for Entity {
@@ -82,4 +88,10 @@ impl Related<super::language_server::Entity> for Entity {
     }
 }
 
+impl Related<super::hosted_project::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::HostedProject.def()
+    }
+}
+
 impl ActiveModelBehavior for ActiveModel {}

crates/collab/src/rpc.rs 🔗

@@ -4,9 +4,9 @@ use crate::{
     auth::{self, Impersonator},
     db::{
         self, BufferId, ChannelId, ChannelRole, ChannelsForUser, CreatedChannelMessage, Database,
-        HostedProjectId, InviteMemberResult, MembershipUpdated, MessageId, NotificationId, Project,
-        ProjectId, RemoveChannelMemberResult, ReplicaId, RespondToChannelInvite, RoomId, ServerId,
-        User, UserId,
+        InviteMemberResult, MembershipUpdated, MessageId, NotificationId, Project, ProjectId,
+        RemoveChannelMemberResult, ReplicaId, RespondToChannelInvite, RoomId, ServerId, User,
+        UserId,
     },
     executor::Executor,
     AppState, Error, Result,
@@ -1770,7 +1770,7 @@ async fn join_hosted_project(
         .db()
         .await
         .join_hosted_project(
-            HostedProjectId(request.id as i32),
+            ProjectId(request.project_id as i32),
             session.user_id,
             session.connection_id,
         )

crates/collab_ui/src/collab_panel.rs 🔗

@@ -8,7 +8,7 @@ use crate::{
 };
 use call::ActiveCall;
 use channel::{Channel, ChannelEvent, ChannelStore};
-use client::{ChannelId, Client, Contact, HostedProjectId, User, UserStore};
+use client::{ChannelId, Client, Contact, ProjectId, User, UserStore};
 use contact_finder::ContactFinder;
 use db::kvp::KEY_VALUE_STORE;
 use editor::{Editor, EditorElement, EditorStyle};
@@ -185,7 +185,7 @@ enum ListEntry {
         depth: usize,
     },
     HostedProject {
-        id: HostedProjectId,
+        id: ProjectId,
         name: SharedString,
     },
     Contact {
@@ -1035,7 +1035,7 @@ impl CollabPanel {
 
     fn render_channel_project(
         &self,
-        id: HostedProjectId,
+        id: ProjectId,
         name: &SharedString,
         is_selected: bool,
         cx: &mut ViewContext<Self>,

crates/project/src/project.rs 🔗

@@ -12,8 +12,7 @@ mod project_tests;
 use anyhow::{anyhow, bail, Context as _, Result};
 use async_trait::async_trait;
 use client::{
-    proto, Client, Collaborator, HostedProjectId, PendingEntitySubscription, TypedEnvelope,
-    UserStore,
+    proto, Client, Collaborator, PendingEntitySubscription, ProjectId, TypedEnvelope, UserStore,
 };
 use clock::ReplicaId;
 use collections::{hash_map, BTreeMap, HashMap, HashSet, VecDeque};
@@ -175,7 +174,7 @@ pub struct Project {
     prettiers_per_worktree: HashMap<WorktreeId, HashSet<Option<PathBuf>>>,
     prettier_instances: HashMap<PathBuf, PrettierInstance>,
     tasks: Model<Inventory>,
-    hosted_project_id: Option<HostedProjectId>,
+    hosted_project_id: Option<ProjectId>,
 }
 
 pub enum LanguageServerToQuery {
@@ -780,29 +779,31 @@ impl Project {
     }
 
     pub async fn hosted(
-        _hosted_project_id: HostedProjectId,
-        _user_store: Model<UserStore>,
-        _client: Arc<Client>,
-        _languages: Arc<LanguageRegistry>,
-        _fs: Arc<dyn Fs>,
-        _cx: AsyncAppContext,
+        remote_id: ProjectId,
+        user_store: Model<UserStore>,
+        client: Arc<Client>,
+        languages: Arc<LanguageRegistry>,
+        fs: Arc<dyn Fs>,
+        cx: AsyncAppContext,
     ) -> Result<Model<Self>> {
-        // let response = client
-        //     .request_envelope(proto::JoinHostedProject {
-        //         id: hosted_project_id.0,
-        //     })
-        //     .await?;
-        // Self::from_join_project_response(
-        //     response,
-        //     Some(hosted_project_id),
-        //     client,
-        //     user_store,
-        //     languages,
-        //     fs,
-        //     cx,
-        // )
-        // .await
-        Err(anyhow!("disabled"))
+        client.authenticate_and_connect(true, &cx).await?;
+
+        let subscription = client.subscribe_to_entity(remote_id.0)?;
+        let response = client
+            .request_envelope(proto::JoinHostedProject {
+                project_id: remote_id.0,
+            })
+            .await?;
+        Self::from_join_project_response(
+            response,
+            subscription,
+            client,
+            user_store,
+            languages,
+            fs,
+            cx,
+        )
+        .await
     }
 
     fn release(&mut self, cx: &mut AppContext) {
@@ -1050,7 +1051,7 @@ impl Project {
         }
     }
 
-    pub fn hosted_project_id(&self) -> Option<HostedProjectId> {
+    pub fn hosted_project_id(&self) -> Option<ProjectId> {
         self.hosted_project_id
     }
 

crates/rpc/proto/zed.proto 🔗

@@ -408,7 +408,7 @@ message JoinProject {
 }
 
 message JoinHostedProject {
-    uint64 id = 1;
+    uint64 project_id = 1;
 }
 
 message JoinProjectResponse {
@@ -1067,7 +1067,7 @@ message ChannelParticipants {
 }
 
 message HostedProject {
-    uint64 id = 1;
+    uint64 project_id = 1;
     uint64 channel_id = 2;
     string name = 3;
     ChannelVisibility visibility = 4;

crates/workspace/src/workspace.rs 🔗

@@ -15,7 +15,7 @@ use anyhow::{anyhow, Context as _, Result};
 use call::{call_settings::CallSettings, ActiveCall};
 use client::{
     proto::{self, ErrorCode, PeerId},
-    ChannelId, Client, ErrorExt, HostedProjectId, Status, TypedEnvelope, UserStore,
+    ChannelId, Client, ErrorExt, ProjectId, Status, TypedEnvelope, UserStore,
 };
 use collections::{hash_map, HashMap, HashSet};
 use derive_more::{Deref, DerefMut};
@@ -4462,7 +4462,7 @@ pub fn create_and_open_local_file(
 }
 
 pub fn join_hosted_project(
-    hosted_project_id: HostedProjectId,
+    hosted_project_id: ProjectId,
     app_state: Arc<AppState>,
     cx: &mut AppContext,
 ) -> Task<Result<()>> {