Start work on exposing which channels the user has admin rights to

Max Brunsfeld created

Change summary

crates/client/src/channel_store.rs       |  4 ++
crates/client/src/channel_store_tests.rs | 21 ++++++++------
crates/collab/src/db.rs                  | 39 +++++++++++++++++--------
crates/collab/src/db/tests.rs            | 15 +++++++--
crates/collab/src/rpc.rs                 | 13 ++++++--
crates/collab/src/tests/channel_tests.rs |  9 +++--
crates/rpc/proto/zed.proto               |  3 +
7 files changed, 69 insertions(+), 35 deletions(-)

Detailed changes

crates/client/src/channel_store.rs 🔗

@@ -26,6 +26,7 @@ pub struct Channel {
     pub id: ChannelId,
     pub name: String,
     pub parent_id: Option<ChannelId>,
+    pub user_is_admin: bool,
     pub depth: usize,
 }
 
@@ -247,6 +248,7 @@ impl ChannelStore {
                 Arc::new(Channel {
                     id: channel.id,
                     name: channel.name,
+                    user_is_admin: false,
                     parent_id: None,
                     depth: 0,
                 }),
@@ -267,6 +269,7 @@ impl ChannelStore {
                         Arc::new(Channel {
                             id: channel.id,
                             name: channel.name,
+                            user_is_admin: channel.user_is_admin,
                             parent_id: Some(parent_id),
                             depth,
                         }),
@@ -278,6 +281,7 @@ impl ChannelStore {
                     Arc::new(Channel {
                         id: channel.id,
                         name: channel.name,
+                        user_is_admin: channel.user_is_admin,
                         parent_id: None,
                         depth: 0,
                     }),

crates/client/src/channel_store_tests.rs 🔗

@@ -18,11 +18,13 @@ fn test_update_channels(cx: &mut AppContext) {
                     id: 1,
                     name: "b".to_string(),
                     parent_id: None,
+                    user_is_admin: true,
                 },
                 proto::Channel {
                     id: 2,
                     name: "a".to_string(),
                     parent_id: None,
+                    user_is_admin: false,
                 },
             ],
             ..Default::default()
@@ -33,8 +35,8 @@ fn test_update_channels(cx: &mut AppContext) {
         &channel_store,
         &[
             //
-            (0, "a"),
-            (0, "b"),
+            (0, "a", true),
+            (0, "b", false),
         ],
         cx,
     );
@@ -47,11 +49,13 @@ fn test_update_channels(cx: &mut AppContext) {
                     id: 3,
                     name: "x".to_string(),
                     parent_id: Some(1),
+                    user_is_admin: false,
                 },
                 proto::Channel {
                     id: 4,
                     name: "y".to_string(),
                     parent_id: Some(2),
+                    user_is_admin: false,
                 },
             ],
             ..Default::default()
@@ -61,11 +65,10 @@ fn test_update_channels(cx: &mut AppContext) {
     assert_channels(
         &channel_store,
         &[
-            //
-            (0, "a"),
-            (1, "y"),
-            (0, "b"),
-            (1, "x"),
+            (0, "a", true),
+            (1, "y", true),
+            (0, "b", false),
+            (1, "x", false),
         ],
         cx,
     );
@@ -81,14 +84,14 @@ fn update_channels(
 
 fn assert_channels(
     channel_store: &ModelHandle<ChannelStore>,
-    expected_channels: &[(usize, &str)],
+    expected_channels: &[(usize, &str, bool)],
     cx: &AppContext,
 ) {
     channel_store.read_with(cx, |store, _| {
         let actual = store
             .channels()
             .iter()
-            .map(|c| (c.depth, c.name.as_str()))
+            .map(|c| (c.depth, c.name.as_str(), c.user_is_admin))
             .collect::<Vec<_>>();
         assert_eq!(actual, expected_channels);
     });

crates/collab/src/db.rs 🔗

@@ -3385,6 +3385,7 @@ impl Database {
                 .map(|channel| Channel {
                     id: channel.id,
                     name: channel.name,
+                    user_is_admin: false,
                     parent_id: None,
                 })
                 .collect();
@@ -3401,20 +3402,21 @@ impl Database {
         self.transaction(|tx| async move {
             let tx = tx;
 
-            let starting_channel_ids: Vec<ChannelId> = channel_member::Entity::find()
+            let channel_memberships = channel_member::Entity::find()
                 .filter(
                     channel_member::Column::UserId
                         .eq(user_id)
                         .and(channel_member::Column::Accepted.eq(true)),
                 )
-                .select_only()
-                .column(channel_member::Column::ChannelId)
-                .into_values::<_, QueryChannelIds>()
                 .all(&*tx)
                 .await?;
 
+            let admin_channel_ids = channel_memberships
+                .iter()
+                .filter_map(|m| m.admin.then_some(m.channel_id))
+                .collect::<HashSet<_>>();
             let parents_by_child_id = self
-                .get_channel_descendants(starting_channel_ids, &*tx)
+                .get_channel_descendants(channel_memberships.iter().map(|m| m.channel_id), &*tx)
                 .await?;
 
             let mut channels = Vec::with_capacity(parents_by_child_id.len());
@@ -3428,6 +3430,7 @@ impl Database {
                     channels.push(Channel {
                         id: row.id,
                         name: row.name,
+                        user_is_admin: admin_channel_ids.contains(&row.id),
                         parent_id: parents_by_child_id.get(&row.id).copied().flatten(),
                     });
                 }
@@ -3627,7 +3630,7 @@ impl Database {
             r#"
             WITH RECURSIVE channel_tree(child_id, parent_id) AS (
                     SELECT root_ids.column1 as child_id, CAST(NULL as INTEGER) as parent_id
-                    FROM (VALUES {}) as root_ids
+                    FROM (VALUES {values}) as root_ids
                 UNION
                     SELECT channel_parents.child_id, channel_parents.parent_id
                     FROM channel_parents, channel_tree
@@ -3637,7 +3640,6 @@ impl Database {
             FROM channel_tree
             ORDER BY child_id, parent_id IS NOT NULL
             "#,
-            values
         );
 
         #[derive(FromQueryResult, Debug, PartialEq)]
@@ -3663,14 +3665,29 @@ impl Database {
         Ok(parents_by_child_id)
     }
 
-    pub async fn get_channel(&self, channel_id: ChannelId) -> Result<Option<Channel>> {
+    pub async fn get_channel(
+        &self,
+        channel_id: ChannelId,
+        user_id: UserId,
+    ) -> Result<Option<Channel>> {
         self.transaction(|tx| async move {
             let tx = tx;
             let channel = channel::Entity::find_by_id(channel_id).one(&*tx).await?;
+            let user_is_admin = channel_member::Entity::find()
+                .filter(
+                    channel_member::Column::ChannelId
+                        .eq(channel_id)
+                        .and(channel_member::Column::UserId.eq(user_id))
+                        .and(channel_member::Column::Admin.eq(true)),
+                )
+                .count(&*tx)
+                .await?
+                > 0;
 
             Ok(channel.map(|channel| Channel {
                 id: channel.id,
                 name: channel.name,
+                user_is_admin,
                 parent_id: None,
             }))
         })
@@ -3942,6 +3959,7 @@ pub struct NewUserResult {
 pub struct Channel {
     pub id: ChannelId,
     pub name: String,
+    pub user_is_admin: bool,
     pub parent_id: Option<ChannelId>,
 }
 
@@ -4199,11 +4217,6 @@ pub struct WorktreeSettingsFile {
     pub content: String,
 }
 
-#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
-enum QueryChannelIds {
-    ChannelId,
-}
-
 #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
 enum QueryUserIds {
     UserId,

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

@@ -960,43 +960,50 @@ test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, {
                 id: zed_id,
                 name: "zed".to_string(),
                 parent_id: None,
+                user_is_admin: true,
             },
             Channel {
                 id: crdb_id,
                 name: "crdb".to_string(),
                 parent_id: Some(zed_id),
+                user_is_admin: true,
             },
             Channel {
                 id: livestreaming_id,
                 name: "livestreaming".to_string(),
                 parent_id: Some(zed_id),
+                user_is_admin: true,
             },
             Channel {
                 id: replace_id,
                 name: "replace".to_string(),
                 parent_id: Some(zed_id),
+                user_is_admin: true,
             },
             Channel {
                 id: rust_id,
                 name: "rust".to_string(),
                 parent_id: None,
+                user_is_admin: true,
             },
             Channel {
                 id: cargo_id,
                 name: "cargo".to_string(),
                 parent_id: Some(rust_id),
+                user_is_admin: true,
             },
             Channel {
                 id: cargo_ra_id,
                 name: "cargo-ra".to_string(),
                 parent_id: Some(cargo_id),
+                user_is_admin: true,
             }
         ]
     );
 
     // Remove a single channel
     db.remove_channel(crdb_id, a_id).await.unwrap();
-    assert!(db.get_channel(crdb_id).await.unwrap().is_none());
+    assert!(db.get_channel(crdb_id, a_id).await.unwrap().is_none());
 
     // Remove a channel tree
     let (mut channel_ids, user_ids) = db.remove_channel(rust_id, a_id).await.unwrap();
@@ -1004,9 +1011,9 @@ test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, {
     assert_eq!(channel_ids, &[rust_id, cargo_id, cargo_ra_id]);
     assert_eq!(user_ids, &[a_id]);
 
-    assert!(db.get_channel(rust_id).await.unwrap().is_none());
-    assert!(db.get_channel(cargo_id).await.unwrap().is_none());
-    assert!(db.get_channel(cargo_ra_id).await.unwrap().is_none());
+    assert!(db.get_channel(rust_id, a_id).await.unwrap().is_none());
+    assert!(db.get_channel(cargo_id, a_id).await.unwrap().is_none());
+    assert!(db.get_channel(cargo_ra_id, a_id).await.unwrap().is_none());
 });
 
 test_both_dbs!(

crates/collab/src/rpc.rs 🔗

@@ -2150,6 +2150,7 @@ async fn create_channel(
         id: id.to_proto(),
         name: request.name,
         parent_id: request.parent_id,
+        user_is_admin: true,
     });
 
     if let Some(parent_id) = parent_id {
@@ -2204,7 +2205,7 @@ async fn invite_channel_member(
     let db = session.db().await;
     let channel_id = ChannelId::from_proto(request.channel_id);
     let channel = db
-        .get_channel(channel_id)
+        .get_channel(channel_id, session.user_id)
         .await?
         .ok_or_else(|| anyhow!("channel not found"))?;
     let invitee_id = UserId::from_proto(request.user_id);
@@ -2216,6 +2217,7 @@ async fn invite_channel_member(
         id: channel.id.to_proto(),
         name: channel.name,
         parent_id: None,
+        user_is_admin: false,
     });
     for connection_id in session
         .connection_pool()
@@ -2264,12 +2266,12 @@ async fn respond_to_channel_invite(
 ) -> Result<()> {
     let db = session.db().await;
     let channel_id = ChannelId::from_proto(request.channel_id);
+    db.respond_to_channel_invite(channel_id, session.user_id, request.accept)
+        .await?;
     let channel = db
-        .get_channel(channel_id)
+        .get_channel(channel_id, session.user_id)
         .await?
         .ok_or_else(|| anyhow!("no such channel"))?;
-    db.respond_to_channel_invite(channel_id, session.user_id, request.accept)
-        .await?;
 
     let mut update = proto::UpdateChannels::default();
     update
@@ -2279,6 +2281,7 @@ async fn respond_to_channel_invite(
         update.channels.push(proto::Channel {
             id: channel.id.to_proto(),
             name: channel.name,
+            user_is_admin: channel.user_is_admin,
             parent_id: None,
         });
     }
@@ -2430,6 +2433,7 @@ fn build_initial_channels_update(
         update.channels.push(proto::Channel {
             id: channel.id.to_proto(),
             name: channel.name,
+            user_is_admin: channel.user_is_admin,
             parent_id: channel.parent_id.map(|id| id.to_proto()),
         });
     }
@@ -2447,6 +2451,7 @@ fn build_initial_channels_update(
         update.channel_invitations.push(proto::Channel {
             id: channel.id.to_proto(),
             name: channel.name,
+            user_is_admin: false,
             parent_id: None,
         });
     }

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

@@ -1,13 +1,10 @@
+use crate::tests::{room_participants, RoomParticipants, TestServer};
 use call::ActiveCall;
 use client::{Channel, User};
 use gpui::{executor::Deterministic, TestAppContext};
 use rpc::proto;
 use std::sync::Arc;
 
-use crate::tests::{room_participants, RoomParticipants};
-
-use super::TestServer;
-
 #[gpui::test]
 async fn test_basic_channels(
     deterministic: Arc<Deterministic>,
@@ -35,6 +32,7 @@ async fn test_basic_channels(
                 id: channel_a_id,
                 name: "channel-a".to_string(),
                 parent_id: None,
+                user_is_admin: true,
                 depth: 0,
             })]
         )
@@ -69,6 +67,7 @@ async fn test_basic_channels(
                 id: channel_a_id,
                 name: "channel-a".to_string(),
                 parent_id: None,
+                user_is_admin: false,
                 depth: 0,
             })]
         )
@@ -111,6 +110,7 @@ async fn test_basic_channels(
                 id: channel_a_id,
                 name: "channel-a".to_string(),
                 parent_id: None,
+                user_is_admin: false,
                 depth: 0,
             })]
         )
@@ -204,6 +204,7 @@ async fn test_channel_room(
                 id: zed_id,
                 name: "zed".to_string(),
                 parent_id: None,
+                user_is_admin: false,
                 depth: 0,
             })]
         )

crates/rpc/proto/zed.proto 🔗

@@ -1295,7 +1295,8 @@ message Nonce {
 message Channel {
     uint64 id = 1;
     string name = 2;
-    optional uint64 parent_id = 3;
+    bool user_is_admin = 3;
+    optional uint64 parent_id = 4;
 }
 
 message Contact {