Track room participant role

Conrad Irwin created

(Also wire that through to project collaboration rules for now)

Change summary

crates/call/src/participant.rs               |  1 
crates/call/src/room.rs                      | 35 +++++++++++++++++++--
crates/collab/src/db/queries/channels.rs     |  5 +-
crates/collab/src/db/queries/rooms.rs        | 23 ++++++++++++-
crates/collab/src/tests/integration_tests.rs |  2 +
crates/project/src/project.rs                | 16 ++++++++++
6 files changed, 73 insertions(+), 9 deletions(-)

Detailed changes

crates/call/src/participant.rs 🔗

@@ -36,6 +36,7 @@ impl ParticipantLocation {
 pub struct LocalParticipant {
     pub projects: Vec<proto::ParticipantProject>,
     pub active_project: Option<WeakModel<Project>>,
+    pub role: proto::ChannelRole,
 }
 
 #[derive(Clone, Debug)]

crates/call/src/room.rs 🔗

@@ -247,14 +247,18 @@ impl Room {
             let response = client.request(proto::CreateRoom {}).await?;
             let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
             let room = cx.new_model(|cx| {
-                Self::new(
+                let mut room = Self::new(
                     room_proto.id,
                     None,
                     response.live_kit_connection_info,
                     client,
                     user_store,
                     cx,
-                )
+                );
+                if let Some(participant) = room_proto.participants.first() {
+                    room.local_participant.role = participant.role()
+                }
+                room
             })?;
 
             let initial_project_id = if let Some(initial_project) = initial_project {
@@ -710,7 +714,21 @@ impl Room {
                 this.participant_user_ids.clear();
 
                 if let Some(participant) = local_participant {
+                    let role = participant.role();
                     this.local_participant.projects = participant.projects;
+                    if this.local_participant.role != role {
+                        this.local_participant.role = role;
+                        // TODO!() this may be better done using optional replica ids instead.
+                        // (though need to figure out how to handle promotion? join and leave the project?)
+                        this.joined_projects.retain(|project| {
+                            if let Some(project) = project.upgrade() {
+                                project.update(cx, |project, _| project.set_role(role));
+                                true
+                            } else {
+                                false
+                            }
+                        });
+                    }
                 } else {
                     this.local_participant.projects.clear();
                 }
@@ -1091,10 +1109,19 @@ impl Room {
     ) -> Task<Result<Model<Project>>> {
         let client = self.client.clone();
         let user_store = self.user_store.clone();
+        let role = self.local_participant.role;
         cx.emit(Event::RemoteProjectJoined { project_id: id });
         cx.spawn(move |this, mut cx| async move {
-            let project =
-                Project::remote(id, client, user_store, language_registry, fs, cx.clone()).await?;
+            let project = Project::remote(
+                id,
+                client,
+                user_store,
+                language_registry,
+                fs,
+                role,
+                cx.clone(),
+            )
+            .await?;
 
             this.update(&mut cx, |this, cx| {
                 this.joined_projects.retain(|project| {

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

@@ -165,15 +165,16 @@ impl Database {
             if role.is_none() || role == Some(ChannelRole::Banned) {
                 Err(anyhow!("not allowed"))?
             }
+            let role = role.unwrap();
 
             let live_kit_room = format!("channel-{}", nanoid::nanoid!(30));
             let room_id = self
                 .get_or_create_channel_room(channel_id, &live_kit_room, environment, &*tx)
                 .await?;
 
-            self.join_channel_room_internal(room_id, user_id, connection, &*tx)
+            self.join_channel_room_internal(room_id, user_id, connection, role, &*tx)
                 .await
-                .map(|jr| (jr, accept_invite_result, role.unwrap()))
+                .map(|jr| (jr, accept_invite_result, role))
         })
         .await
     }

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

@@ -131,7 +131,12 @@ impl Database {
                     connection.owner_id as i32,
                 ))),
                 participant_index: ActiveValue::set(Some(0)),
-                ..Default::default()
+                role: ActiveValue::set(Some(ChannelRole::Admin)),
+
+                id: ActiveValue::NotSet,
+                location_kind: ActiveValue::NotSet,
+                location_project_id: ActiveValue::NotSet,
+                initial_project_id: ActiveValue::NotSet,
             }
             .insert(&*tx)
             .await?;
@@ -162,7 +167,13 @@ impl Database {
                     calling_connection.owner_id as i32,
                 ))),
                 initial_project_id: ActiveValue::set(initial_project_id),
-                ..Default::default()
+                role: ActiveValue::set(Some(ChannelRole::Member)),
+
+                id: ActiveValue::NotSet,
+                answering_connection_id: ActiveValue::NotSet,
+                answering_connection_server_id: ActiveValue::NotSet,
+                location_kind: ActiveValue::NotSet,
+                location_project_id: ActiveValue::NotSet,
             }
             .insert(&*tx)
             .await?;
@@ -384,6 +395,7 @@ impl Database {
         room_id: RoomId,
         user_id: UserId,
         connection: ConnectionId,
+        role: ChannelRole,
         tx: &DatabaseTransaction,
     ) -> Result<JoinRoom> {
         let participant_index = self
@@ -404,7 +416,11 @@ impl Database {
                 connection.owner_id as i32,
             ))),
             participant_index: ActiveValue::Set(Some(participant_index)),
-            ..Default::default()
+            role: ActiveValue::set(Some(role)),
+            id: ActiveValue::NotSet,
+            location_kind: ActiveValue::NotSet,
+            location_project_id: ActiveValue::NotSet,
+            initial_project_id: ActiveValue::NotSet,
         }])
         .on_conflict(
             OnConflict::columns([room_participant::Column::UserId])
@@ -413,6 +429,7 @@ impl Database {
                     room_participant::Column::AnsweringConnectionServerId,
                     room_participant::Column::AnsweringConnectionLost,
                     room_participant::Column::ParticipantIndex,
+                    room_participant::Column::Role,
                 ])
                 .to_owned(),
         )

crates/collab/src/tests/integration_tests.rs 🔗

@@ -19,6 +19,7 @@ use project::{
     search::SearchQuery, DiagnosticSummary, FormatTrigger, HoverBlockKind, Project, ProjectPath,
 };
 use rand::prelude::*;
+use rpc::proto::ChannelRole;
 use serde_json::json;
 use settings::SettingsStore;
 use std::{
@@ -3550,6 +3551,7 @@ async fn test_leaving_project(
             client_b.user_store().clone(),
             client_b.language_registry().clone(),
             FakeFs::new(cx.background_executor().clone()),
+            ChannelRole::Member,
             cx,
         )
     })

crates/project/src/project.rs 🔗

@@ -262,6 +262,8 @@ enum ProjectClientState {
     },
     Remote {
         sharing_has_stopped: bool,
+        // todo!() this should be represented differently!
+        is_read_only: bool,
         remote_id: u64,
         replica_id: ReplicaId,
     },
@@ -702,6 +704,7 @@ impl Project {
         user_store: Model<UserStore>,
         languages: Arc<LanguageRegistry>,
         fs: Arc<dyn Fs>,
+        role: proto::ChannelRole,
         mut cx: AsyncAppContext,
     ) -> Result<Model<Self>> {
         client.authenticate_and_connect(true, &cx).await?;
@@ -757,6 +760,7 @@ impl Project {
                 client: client.clone(),
                 client_state: Some(ProjectClientState::Remote {
                     sharing_has_stopped: false,
+                    is_read_only: false,
                     remote_id,
                     replica_id,
                 }),
@@ -797,6 +801,7 @@ impl Project {
                 prettiers_per_worktree: HashMap::default(),
                 prettier_instances: HashMap::default(),
             };
+            this.set_role(role);
             for worktree in worktrees {
                 let _ = this.add_worktree(&worktree, cx);
             }
@@ -1619,6 +1624,13 @@ impl Project {
         cx.notify();
     }
 
+    pub fn set_role(&mut self, role: proto::ChannelRole) {
+        if let Some(ProjectClientState::Remote { is_read_only, .. }) = &mut self.client_state {
+            *is_read_only =
+                !(role == proto::ChannelRole::Member || role == proto::ChannelRole::Admin)
+        }
+    }
+
     fn disconnected_from_host_internal(&mut self, cx: &mut AppContext) {
         if let Some(ProjectClientState::Remote {
             sharing_has_stopped,
@@ -1672,6 +1684,10 @@ impl Project {
 
     pub fn is_read_only(&self) -> bool {
         self.is_disconnected()
+            || match &self.client_state {
+                Some(ProjectClientState::Remote { is_read_only, .. }) => *is_read_only,
+                _ => false,
+            }
     }
 
     pub fn is_local(&self) -> bool {