Add renames

Mikayla and max created

co-authored-by: max <max@zed.dev>

Change summary

crates/client/src/channel_store.rs       |  51 ++++++--
crates/client/src/channel_store_tests.rs |   6 
crates/collab/src/db.rs                  |  49 ++++++--
crates/collab/src/db/tests.rs            |  75 ++++++++++--
crates/collab/src/rpc.rs                 |  58 +++++++---
crates/collab/src/tests/channel_tests.rs |  78 +++++++++++---
crates/collab_ui/src/collab_panel.rs     | 142 +++++++++++++++++++------
crates/rpc/proto/zed.proto               |  15 ++
crates/rpc/src/proto.rs                  |   2 
9 files changed, 356 insertions(+), 120 deletions(-)

Detailed changes

crates/client/src/channel_store.rs 🔗

@@ -16,6 +16,7 @@ pub struct ChannelStore {
     channels: Vec<Arc<Channel>>,
     channel_invitations: Vec<Arc<Channel>>,
     channel_participants: HashMap<ChannelId, Vec<Arc<User>>>,
+    channels_with_admin_privileges: HashSet<ChannelId>,
     outgoing_invites: HashSet<(ChannelId, UserId)>,
     client: Arc<Client>,
     user_store: ModelHandle<UserStore>,
@@ -28,7 +29,6 @@ pub struct Channel {
     pub id: ChannelId,
     pub name: String,
     pub parent_id: Option<ChannelId>,
-    pub user_is_admin: bool,
     pub depth: usize,
 }
 
@@ -79,6 +79,7 @@ impl ChannelStore {
             channels: vec![],
             channel_invitations: vec![],
             channel_participants: Default::default(),
+            channels_with_admin_privileges: Default::default(),
             outgoing_invites: Default::default(),
             client,
             user_store,
@@ -100,17 +101,18 @@ impl ChannelStore {
     }
 
     pub fn is_user_admin(&self, mut channel_id: ChannelId) -> bool {
-        while let Some(channel) = self.channel_for_id(channel_id) {
-            if channel.user_is_admin {
+        loop {
+            if self.channels_with_admin_privileges.contains(&channel_id) {
                 return true;
             }
-            if let Some(parent_id) = channel.parent_id {
-                channel_id = parent_id;
-            } else {
-                break;
+            if let Some(channel) = self.channel_for_id(channel_id) {
+                if let Some(parent_id) = channel.parent_id {
+                    channel_id = parent_id;
+                    continue;
+                }
             }
+            return false;
         }
-        false
     }
 
     pub fn channel_participants(&self, channel_id: ChannelId) -> &[Arc<User>] {
@@ -228,6 +230,22 @@ impl ChannelStore {
         })
     }
 
+    pub fn rename(
+        &mut self,
+        channel_id: ChannelId,
+        new_name: &str,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        let client = self.client.clone();
+        let name = new_name.to_string();
+        cx.spawn(|_this, _cx| async move {
+            client
+                .request(proto::RenameChannel { channel_id, name })
+                .await?;
+            Ok(())
+        })
+    }
+
     pub fn respond_to_channel_invite(
         &mut self,
         channel_id: ChannelId,
@@ -315,6 +333,8 @@ impl ChannelStore {
             .retain(|channel| !payload.remove_channel_invitations.contains(&channel.id));
         self.channel_participants
             .retain(|channel_id, _| !payload.remove_channels.contains(channel_id));
+        self.channels_with_admin_privileges
+            .retain(|channel_id| !payload.remove_channels.contains(channel_id));
 
         for channel in payload.channel_invitations {
             if let Some(existing_channel) = self
@@ -324,7 +344,6 @@ impl ChannelStore {
             {
                 let existing_channel = Arc::make_mut(existing_channel);
                 existing_channel.name = channel.name;
-                existing_channel.user_is_admin = channel.user_is_admin;
                 continue;
             }
 
@@ -333,7 +352,6 @@ impl ChannelStore {
                 Arc::new(Channel {
                     id: channel.id,
                     name: channel.name,
-                    user_is_admin: false,
                     parent_id: None,
                     depth: 0,
                 }),
@@ -344,7 +362,6 @@ impl ChannelStore {
             if let Some(existing_channel) = self.channels.iter_mut().find(|c| c.id == channel.id) {
                 let existing_channel = Arc::make_mut(existing_channel);
                 existing_channel.name = channel.name;
-                existing_channel.user_is_admin = channel.user_is_admin;
                 continue;
             }
 
@@ -357,7 +374,6 @@ impl ChannelStore {
                         Arc::new(Channel {
                             id: channel.id,
                             name: channel.name,
-                            user_is_admin: channel.user_is_admin,
                             parent_id: Some(parent_id),
                             depth,
                         }),
@@ -369,7 +385,6 @@ impl ChannelStore {
                     Arc::new(Channel {
                         id: channel.id,
                         name: channel.name,
-                        user_is_admin: channel.user_is_admin,
                         parent_id: None,
                         depth: 0,
                     }),
@@ -377,6 +392,16 @@ impl ChannelStore {
             }
         }
 
+        for permission in payload.channel_permissions {
+            if permission.is_admin {
+                self.channels_with_admin_privileges
+                    .insert(permission.channel_id);
+            } else {
+                self.channels_with_admin_privileges
+                    .remove(&permission.channel_id);
+            }
+        }
+
         let mut all_user_ids = Vec::new();
         let channel_participants = payload.channel_participants;
         for entry in &channel_participants {

crates/client/src/channel_store_tests.rs 🔗

@@ -18,13 +18,11 @@ 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()
@@ -49,13 +47,11 @@ 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()
@@ -92,7 +88,7 @@ fn assert_channels(
         let actual = store
             .channels()
             .iter()
-            .map(|c| (c.depth, c.name.as_str(), c.user_is_admin))
+            .map(|c| (c.depth, c.name.as_str(), store.is_user_admin(c.id)))
             .collect::<Vec<_>>();
         assert_eq!(actual, expected_channels);
     });

crates/collab/src/db.rs 🔗

@@ -3155,7 +3155,7 @@ impl Database {
         live_kit_room: &str,
         creator_id: UserId,
     ) -> Result<ChannelId> {
-        let name = name.trim().trim_start_matches('#');
+        let name = Self::sanitize_channel_name(name)?;
         self.transaction(move |tx| async move {
             if let Some(parent) = parent {
                 self.check_user_is_channel_admin(parent, creator_id, &*tx)
@@ -3303,6 +3303,39 @@ impl Database {
         .await
     }
 
+    fn sanitize_channel_name(name: &str) -> Result<&str> {
+        let new_name = name.trim().trim_start_matches('#');
+        if new_name == "" {
+            Err(anyhow!("channel name can't be blank"))?;
+        }
+        Ok(new_name)
+    }
+
+    pub async fn rename_channel(
+        &self,
+        channel_id: ChannelId,
+        user_id: UserId,
+        new_name: &str,
+    ) -> Result<String> {
+        self.transaction(move |tx| async move {
+            let new_name = Self::sanitize_channel_name(new_name)?.to_string();
+
+            self.check_user_is_channel_admin(channel_id, user_id, &*tx)
+                .await?;
+
+            channel::ActiveModel {
+                id: ActiveValue::Unchanged(channel_id),
+                name: ActiveValue::Set(new_name.clone()),
+                ..Default::default()
+            }
+            .update(&*tx)
+            .await?;
+
+            Ok(new_name)
+        })
+        .await
+    }
+
     pub async fn respond_to_channel_invite(
         &self,
         channel_id: ChannelId,
@@ -3400,7 +3433,6 @@ impl Database {
                 .map(|channel| Channel {
                     id: channel.id,
                     name: channel.name,
-                    user_is_admin: false,
                     parent_id: None,
                 })
                 .collect();
@@ -3426,10 +3458,6 @@ impl Database {
                 .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(channel_memberships.iter().map(|m| m.channel_id), &*tx)
                 .await?;
@@ -3445,7 +3473,6 @@ 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(),
                     });
                 }
@@ -3758,15 +3785,14 @@ impl Database {
                     .one(&*tx)
                     .await?;
 
-                let (user_is_admin, is_accepted) = channel_membership
-                    .map(|membership| (membership.admin, membership.accepted))
-                    .unwrap_or((false, false));
+                let is_accepted = channel_membership
+                    .map(|membership| membership.accepted)
+                    .unwrap_or(false);
 
                 Ok(Some((
                     Channel {
                         id: channel.id,
                         name: channel.name,
-                        user_is_admin,
                         parent_id: None,
                     },
                     is_accepted,
@@ -4043,7 +4069,6 @@ pub struct NewUserResult {
 pub struct Channel {
     pub id: ChannelId,
     pub name: String,
-    pub user_is_admin: bool,
     pub parent_id: Option<ChannelId>,
 }
 

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

@@ -962,43 +962,36 @@ 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,
             }
         ]
     );
@@ -1011,25 +1004,21 @@ test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, {
                 id: zed_id,
                 name: "zed".to_string(),
                 parent_id: None,
-                user_is_admin: false,
             },
             Channel {
                 id: crdb_id,
                 name: "crdb".to_string(),
                 parent_id: Some(zed_id),
-                user_is_admin: false,
             },
             Channel {
                 id: livestreaming_id,
                 name: "livestreaming".to_string(),
                 parent_id: Some(zed_id),
-                user_is_admin: false,
             },
             Channel {
                 id: replace_id,
                 name: "replace".to_string(),
                 parent_id: Some(zed_id),
-                user_is_admin: false,
             },
         ]
     );
@@ -1048,25 +1037,21 @@ 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: false,
             },
             Channel {
                 id: livestreaming_id,
                 name: "livestreaming".to_string(),
                 parent_id: Some(zed_id),
-                user_is_admin: false,
             },
             Channel {
                 id: replace_id,
                 name: "replace".to_string(),
                 parent_id: Some(zed_id),
-                user_is_admin: false,
             },
         ]
     );
@@ -1296,6 +1281,66 @@ test_both_dbs!(
     }
 );
 
+test_both_dbs!(
+    test_channel_renames_postgres,
+    test_channel_renames_sqlite,
+    db,
+    {
+        db.create_server("test").await.unwrap();
+
+        let user_1 = db
+            .create_user(
+                "user1@example.com",
+                false,
+                NewUserParams {
+                    github_login: "user1".into(),
+                    github_user_id: 5,
+                    invite_count: 0,
+                },
+            )
+            .await
+            .unwrap()
+            .user_id;
+
+        let user_2 = db
+            .create_user(
+                "user2@example.com",
+                false,
+                NewUserParams {
+                    github_login: "user2".into(),
+                    github_user_id: 6,
+                    invite_count: 0,
+                },
+            )
+            .await
+            .unwrap()
+            .user_id;
+
+        let zed_id = db.create_root_channel("zed", "1", user_1).await.unwrap();
+
+        db.rename_channel(zed_id, user_1, "#zed-archive")
+            .await
+            .unwrap();
+
+        let zed_archive_id = zed_id;
+
+        let (channel, _) = db
+            .get_channel(zed_archive_id, user_1)
+            .await
+            .unwrap()
+            .unwrap();
+        assert_eq!(channel.name, "zed-archive");
+
+        let non_permissioned_rename = db
+            .rename_channel(zed_archive_id, user_2, "hacked-lol")
+            .await;
+        assert!(non_permissioned_rename.is_err());
+
+        let bad_name_rename = db.rename_channel(zed_id, user_1, "#").await;
+        assert!(bad_name_rename.is_err())
+    }
+);
+
 #[gpui::test]
 async fn test_multiple_signup_overwrite() {
     let test_db = TestDb::postgres(build_background_executor());

crates/collab/src/rpc.rs 🔗

@@ -247,6 +247,7 @@ impl Server {
             .add_request_handler(invite_channel_member)
             .add_request_handler(remove_channel_member)
             .add_request_handler(set_channel_member_admin)
+            .add_request_handler(rename_channel)
             .add_request_handler(get_channel_members)
             .add_request_handler(respond_to_channel_invite)
             .add_request_handler(join_channel)
@@ -2151,7 +2152,6 @@ async fn create_channel(
         id: id.to_proto(),
         name: request.name,
         parent_id: request.parent_id,
-        user_is_admin: false,
     });
 
     let user_ids_to_notify = if let Some(parent_id) = parent_id {
@@ -2165,7 +2165,10 @@ async fn create_channel(
         for connection_id in connection_pool.user_connection_ids(user_id) {
             let mut update = update.clone();
             if user_id == session.user_id {
-                update.channels[0].user_is_admin = true;
+                update.channel_permissions.push(proto::ChannelPermission {
+                    channel_id: id.to_proto(),
+                    is_admin: true,
+                });
             }
             session.peer.send(connection_id, update)?;
         }
@@ -2224,7 +2227,6 @@ 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()
@@ -2283,18 +2285,9 @@ async fn set_channel_member_admin(
 
     let mut update = proto::UpdateChannels::default();
     if has_accepted {
-        update.channels.push(proto::Channel {
-            id: channel.id.to_proto(),
-            name: channel.name,
-            parent_id: None,
-            user_is_admin: request.admin,
-        });
-    } else {
-        update.channel_invitations.push(proto::Channel {
-            id: channel.id.to_proto(),
-            name: channel.name,
-            parent_id: None,
-            user_is_admin: request.admin,
+        update.channel_permissions.push(proto::ChannelPermission {
+            channel_id: channel.id.to_proto(),
+            is_admin: request.admin,
         });
     }
 
@@ -2310,6 +2303,38 @@ async fn set_channel_member_admin(
     Ok(())
 }
 
+async fn rename_channel(
+    request: proto::RenameChannel,
+    response: Response<proto::RenameChannel>,
+    session: Session,
+) -> Result<()> {
+    let db = session.db().await;
+    let channel_id = ChannelId::from_proto(request.channel_id);
+    let new_name = db
+        .rename_channel(channel_id, session.user_id, &request.name)
+        .await?;
+
+    response.send(proto::Ack {})?;
+
+    let mut update = proto::UpdateChannels::default();
+    update.channels.push(proto::Channel {
+        id: request.channel_id,
+        name: new_name,
+        parent_id: None,
+    });
+
+    let member_ids = db.get_channel_members(channel_id).await?;
+
+    let connection_pool = session.connection_pool().await;
+    for member_id in member_ids {
+        for connection_id in connection_pool.user_connection_ids(member_id) {
+            session.peer.send(connection_id, update.clone())?;
+        }
+    }
+
+    Ok(())
+}
+
 async fn get_channel_members(
     request: proto::GetChannelMembers,
     response: Response<proto::GetChannelMembers>,
@@ -2345,7 +2370,6 @@ async fn respond_to_channel_invite(
             .extend(channels.into_iter().map(|channel| proto::Channel {
                 id: channel.id.to_proto(),
                 name: channel.name,
-                user_is_admin: channel.user_is_admin,
                 parent_id: channel.parent_id.map(ChannelId::to_proto),
             }));
         update
@@ -2505,7 +2529,6 @@ 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()),
         });
     }
@@ -2523,7 +2546,6 @@ 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 🔗

@@ -40,14 +40,12 @@ async fn test_core_channels(
                     id: channel_a_id,
                     name: "channel-a".to_string(),
                     parent_id: None,
-                    user_is_admin: true,
                     depth: 0,
                 }),
                 Arc::new(Channel {
                     id: channel_b_id,
                     name: "channel-b".to_string(),
                     parent_id: Some(channel_a_id),
-                    user_is_admin: true,
                     depth: 1,
                 })
             ]
@@ -82,7 +80,6 @@ async fn test_core_channels(
                 id: channel_a_id,
                 name: "channel-a".to_string(),
                 parent_id: None,
-                user_is_admin: false,
                 depth: 0,
             })]
         )
@@ -131,14 +128,13 @@ async fn test_core_channels(
                     id: channel_a_id,
                     name: "channel-a".to_string(),
                     parent_id: None,
-                    user_is_admin: false,
+
                     depth: 0,
                 }),
                 Arc::new(Channel {
                     id: channel_b_id,
                     name: "channel-b".to_string(),
                     parent_id: Some(channel_a_id),
-                    user_is_admin: false,
                     depth: 1,
                 })
             ]
@@ -162,21 +158,18 @@ async fn test_core_channels(
                     id: channel_a_id,
                     name: "channel-a".to_string(),
                     parent_id: None,
-                    user_is_admin: false,
                     depth: 0,
                 }),
                 Arc::new(Channel {
                     id: channel_b_id,
                     name: "channel-b".to_string(),
                     parent_id: Some(channel_a_id),
-                    user_is_admin: false,
                     depth: 1,
                 }),
                 Arc::new(Channel {
                     id: channel_c_id,
                     name: "channel-c".to_string(),
                     parent_id: Some(channel_b_id),
-                    user_is_admin: false,
                     depth: 2,
                 }),
             ]
@@ -204,21 +197,18 @@ async fn test_core_channels(
                     id: channel_a_id,
                     name: "channel-a".to_string(),
                     parent_id: None,
-                    user_is_admin: true,
                     depth: 0,
                 }),
                 Arc::new(Channel {
                     id: channel_b_id,
                     name: "channel-b".to_string(),
                     parent_id: Some(channel_a_id),
-                    user_is_admin: false,
                     depth: 1,
                 }),
                 Arc::new(Channel {
                     id: channel_c_id,
                     name: "channel-c".to_string(),
                     parent_id: Some(channel_b_id),
-                    user_is_admin: false,
                     depth: 2,
                 }),
             ]
@@ -244,7 +234,7 @@ async fn test_core_channels(
                 id: channel_a_id,
                 name: "channel-a".to_string(),
                 parent_id: None,
-                user_is_admin: true,
+
                 depth: 0,
             })]
         )
@@ -256,7 +246,7 @@ async fn test_core_channels(
                 id: channel_a_id,
                 name: "channel-a".to_string(),
                 parent_id: None,
-                user_is_admin: true,
+
                 depth: 0,
             })]
         )
@@ -281,7 +271,6 @@ async fn test_core_channels(
                 id: channel_a_id,
                 name: "channel-a".to_string(),
                 parent_id: None,
-                user_is_admin: true,
                 depth: 0,
             })]
         )
@@ -395,7 +384,6 @@ async fn test_channel_room(
                 id: zed_id,
                 name: "zed".to_string(),
                 parent_id: None,
-                user_is_admin: false,
                 depth: 0,
             })]
         )
@@ -617,7 +605,7 @@ async fn test_permissions_update_while_invited(
                 id: rust_id,
                 name: "rust".to_string(),
                 parent_id: None,
-                user_is_admin: false,
+
                 depth: 0,
             })],
         );
@@ -643,7 +631,7 @@ async fn test_permissions_update_while_invited(
                 id: rust_id,
                 name: "rust".to_string(),
                 parent_id: None,
-                user_is_admin: true,
+
                 depth: 0,
             })],
         );
@@ -651,3 +639,59 @@ async fn test_permissions_update_while_invited(
         assert_eq!(channels.channels(), &[],);
     });
 }
+
+#[gpui::test]
+async fn test_channel_rename(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+) {
+    deterministic.forbid_parking();
+    let mut server = TestServer::start(&deterministic).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+
+    let rust_id = server
+        .make_channel("rust", (&client_a, cx_a), &mut [(&client_b, cx_b)])
+        .await;
+
+    // Rename the channel
+    client_a
+        .channel_store()
+        .update(cx_a, |channel_store, cx| {
+            channel_store.rename(rust_id, "#rust-archive", cx)
+        })
+        .await
+        .unwrap();
+
+    let rust_archive_id = rust_id;
+    deterministic.run_until_parked();
+
+    // Client A sees the channel with its new name.
+    client_a.channel_store().read_with(cx_a, |channels, _| {
+        assert_eq!(
+            channels.channels(),
+            &[Arc::new(Channel {
+                id: rust_archive_id,
+                name: "rust-archive".to_string(),
+                parent_id: None,
+
+                depth: 0,
+            })],
+        );
+    });
+
+    // Client B sees the channel with its new name.
+    client_b.channel_store().read_with(cx_b, |channels, _| {
+        assert_eq!(
+            channels.channels(),
+            &[Arc::new(Channel {
+                id: rust_archive_id,
+                name: "rust-archive".to_string(),
+                parent_id: None,
+
+                depth: 0,
+            })],
+        );
+    });
+}

crates/collab_ui/src/collab_panel.rs 🔗

@@ -64,11 +64,22 @@ struct ManageMembers {
     channel_id: u64,
 }
 
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+struct RenameChannel {
+    channel_id: u64,
+}
+
 actions!(collab_panel, [ToggleFocus, Remove, Secondary]);
 
 impl_actions!(
     collab_panel,
-    [RemoveChannel, NewChannel, InviteMembers, ManageMembers]
+    [
+        RemoveChannel,
+        NewChannel,
+        InviteMembers,
+        ManageMembers,
+        RenameChannel
+    ]
 );
 
 const CHANNELS_PANEL_KEY: &'static str = "ChannelsPanel";
@@ -83,16 +94,19 @@ pub fn init(_client: Arc<Client>, cx: &mut AppContext) {
     cx.add_action(CollabPanel::select_prev);
     cx.add_action(CollabPanel::confirm);
     cx.add_action(CollabPanel::remove);
-    cx.add_action(CollabPanel::remove_channel_action);
+    cx.add_action(CollabPanel::remove_selected_channel);
     cx.add_action(CollabPanel::show_inline_context_menu);
     cx.add_action(CollabPanel::new_subchannel);
     cx.add_action(CollabPanel::invite_members);
     cx.add_action(CollabPanel::manage_members);
+    cx.add_action(CollabPanel::rename_selected_channel);
+    cx.add_action(CollabPanel::rename_channel);
 }
 
-#[derive(Debug, Default)]
-pub struct ChannelEditingState {
-    parent_id: Option<u64>,
+#[derive(Debug)]
+pub enum ChannelEditingState {
+    Create { parent_id: Option<u64> },
+    Rename { channel_id: u64 },
 }
 
 pub struct CollabPanel {
@@ -581,19 +595,32 @@ impl CollabPanel {
                     executor.clone(),
                 ));
                 if let Some(state) = &self.channel_editing_state {
-                    if state.parent_id.is_none() {
+                    if matches!(state, ChannelEditingState::Create { parent_id: None }) {
                         self.entries.push(ListEntry::ChannelEditor { depth: 0 });
                     }
                 }
                 for mat in matches {
                     let channel = &channels[mat.candidate_id];
-                    self.entries.push(ListEntry::Channel(channel.clone()));
-                    if let Some(state) = &self.channel_editing_state {
-                        if state.parent_id == Some(channel.id) {
+
+                    match &self.channel_editing_state {
+                        Some(ChannelEditingState::Create { parent_id })
+                            if *parent_id == Some(channel.id) =>
+                        {
+                            self.entries.push(ListEntry::Channel(channel.clone()));
                             self.entries.push(ListEntry::ChannelEditor {
                                 depth: channel.depth + 1,
                             });
                         }
+                        Some(ChannelEditingState::Rename { channel_id })
+                            if *channel_id == channel.id =>
+                        {
+                            self.entries.push(ListEntry::ChannelEditor {
+                                depth: channel.depth + 1,
+                            });
+                        }
+                        _ => {
+                            self.entries.push(ListEntry::Channel(channel.clone()));
+                        }
                     }
                 }
             }
@@ -1065,15 +1092,15 @@ impl CollabPanel {
         &mut self,
         cx: &mut ViewContext<Self>,
     ) -> Option<(ChannelEditingState, String)> {
-        let result = self
-            .channel_editing_state
-            .take()
-            .map(|state| (state, self.channel_name_editor.read(cx).text(cx)));
-
-        self.channel_name_editor
-            .update(cx, |editor, cx| editor.set_text("", cx));
-
-        result
+        if let Some(state) = self.channel_editing_state.take() {
+            self.channel_name_editor.update(cx, |editor, cx| {
+                let name = editor.text(cx);
+                editor.set_text("", cx);
+                Some((state, name))
+            })
+        } else {
+            None
+        }
     }
 
     fn render_header(
@@ -1646,6 +1673,7 @@ impl CollabPanel {
                         ContextMenuItem::action("Remove Channel", RemoveChannel { channel_id }),
                         ContextMenuItem::action("Manage members", ManageMembers { channel_id }),
                         ContextMenuItem::action("Invite members", InviteMembers { channel_id }),
+                        ContextMenuItem::action("Rename Channel", RenameChannel { channel_id }),
                     ],
                     cx,
                 );
@@ -1702,6 +1730,10 @@ impl CollabPanel {
     }
 
     fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
+        if self.confirm_channel_edit(cx) {
+            return;
+        }
+
         if let Some(selection) = self.selection {
             if let Some(entry) = self.entries.get(selection) {
                 match entry {
@@ -1747,30 +1779,38 @@ impl CollabPanel {
                     ListEntry::Channel(channel) => {
                         self.join_channel(channel.id, cx);
                     }
-                    ListEntry::ChannelEditor { .. } => {
-                        self.confirm_channel_edit(cx);
-                    }
                     _ => {}
                 }
             }
-        } else {
-            self.confirm_channel_edit(cx);
         }
     }
 
-    fn confirm_channel_edit(&mut self, cx: &mut ViewContext<'_, '_, CollabPanel>) {
+    fn confirm_channel_edit(&mut self, cx: &mut ViewContext<'_, '_, CollabPanel>) -> bool {
         if let Some((editing_state, channel_name)) = self.take_editing_state(cx) {
-            let create_channel = self.channel_store.update(cx, |channel_store, _| {
-                channel_store.create_channel(&channel_name, editing_state.parent_id)
-            });
-
+            match editing_state {
+                ChannelEditingState::Create { parent_id } => {
+                    let request = self.channel_store.update(cx, |channel_store, _| {
+                        channel_store.create_channel(&channel_name, parent_id)
+                    });
+                    cx.foreground()
+                        .spawn(async move {
+                            request.await?;
+                            anyhow::Ok(())
+                        })
+                        .detach();
+                }
+                ChannelEditingState::Rename { channel_id } => {
+                    self.channel_store
+                        .update(cx, |channel_store, cx| {
+                            channel_store.rename(channel_id, &channel_name, cx)
+                        })
+                        .detach();
+                }
+            }
             self.update_entries(false, cx);
-
-            cx.foreground()
-                .spawn(async move {
-                    create_channel.await.log_err();
-                })
-                .detach();
+            true
+        } else {
+            false
         }
     }
 
@@ -1804,14 +1844,14 @@ impl CollabPanel {
     }
 
     fn new_root_channel(&mut self, cx: &mut ViewContext<Self>) {
-        self.channel_editing_state = Some(ChannelEditingState { parent_id: None });
+        self.channel_editing_state = Some(ChannelEditingState::Create { parent_id: None });
         self.update_entries(true, cx);
         cx.focus(self.channel_name_editor.as_any());
         cx.notify();
     }
 
     fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext<Self>) {
-        self.channel_editing_state = Some(ChannelEditingState {
+        self.channel_editing_state = Some(ChannelEditingState::Create {
             parent_id: Some(action.channel_id),
         });
         self.update_entries(true, cx);
@@ -1835,7 +1875,33 @@ impl CollabPanel {
         }
     }
 
-    fn rename(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {}
+    fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
+        if let Some(channel) = self.selected_channel() {
+            self.rename_channel(
+                &RenameChannel {
+                    channel_id: channel.id,
+                },
+                cx,
+            );
+        }
+    }
+
+    fn rename_channel(&mut self, action: &RenameChannel, cx: &mut ViewContext<Self>) {
+        if let Some(channel) = self
+            .channel_store
+            .read(cx)
+            .channel_for_id(action.channel_id)
+        {
+            self.channel_editing_state = Some(ChannelEditingState::Rename {
+                channel_id: action.channel_id,
+            });
+            self.channel_name_editor.update(cx, |editor, cx| {
+                editor.set_text(channel.name.clone(), cx);
+                editor.select_all(&Default::default(), cx);
+            });
+            self.update_entries(true, cx);
+        }
+    }
 
     fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext<Self>) {
         let Some(channel) = self.selected_channel() else {
@@ -1887,7 +1953,7 @@ impl CollabPanel {
         .detach();
     }
 
-    fn remove_channel_action(&mut self, action: &RemoveChannel, cx: &mut ViewContext<Self>) {
+    fn remove_selected_channel(&mut self, action: &RemoveChannel, cx: &mut ViewContext<Self>) {
         self.remove_channel(action.channel_id, cx)
     }
 

crates/rpc/proto/zed.proto 🔗

@@ -141,6 +141,7 @@ message Envelope {
         GetChannelMembers get_channel_members = 128;
         GetChannelMembersResponse get_channel_members_response = 129;
         SetChannelMemberAdmin set_channel_member_admin = 130;
+        RenameChannel rename_channel = 131;
     }
 }
 
@@ -874,6 +875,12 @@ message UpdateChannels {
     repeated Channel channel_invitations = 3;
     repeated uint64 remove_channel_invitations = 4;
     repeated ChannelParticipants channel_participants = 5;
+    repeated ChannelPermission channel_permissions = 6;
+}
+
+message ChannelPermission {
+    uint64 channel_id = 1;
+    bool is_admin = 2;
 }
 
 message ChannelParticipants {
@@ -935,6 +942,11 @@ message SetChannelMemberAdmin {
     bool admin = 3;
 }
 
+message RenameChannel {
+    uint64 channel_id = 1;
+    string name = 2;
+}
+
 message RespondToChannelInvite {
     uint64 channel_id = 1;
     bool accept = 2;
@@ -1303,8 +1315,7 @@ message Nonce {
 message Channel {
     uint64 id = 1;
     string name = 2;
-    bool user_is_admin = 3;
-    optional uint64 parent_id = 4;
+    optional uint64 parent_id = 3;
 }
 
 message Contact {

crates/rpc/src/proto.rs 🔗

@@ -217,6 +217,7 @@ messages!(
     (JoinChannel, Foreground),
     (RoomUpdated, Foreground),
     (SaveBuffer, Foreground),
+    (RenameChannel, Foreground),
     (SetChannelMemberAdmin, Foreground),
     (SearchProject, Background),
     (SearchProjectResponse, Background),
@@ -304,6 +305,7 @@ request_messages!(
     (JoinChannel, JoinRoomResponse),
     (RemoveChannel, Ack),
     (RenameProjectEntry, ProjectEntryResponse),
+    (RenameChannel, Ack),
     (SaveBuffer, BufferSaved),
     (SearchProject, SearchProjectResponse),
     (ShareProject, ShareProjectResponse),