Render the DAG

Mikayla created

Change summary

crates/channel/src/channel_store.rs               |  14 
crates/channel/src/channel_store/channel_index.rs |  85 ++---
crates/collab_ui/src/collab_panel.rs              | 250 ++++++++++++----
crates/rpc/src/proto.rs                           |   1 
4 files changed, 227 insertions(+), 123 deletions(-)

Detailed changes

crates/channel/src/channel_store.rs ๐Ÿ”—

@@ -11,6 +11,7 @@ use std::{mem, sync::Arc, time::Duration};
 use util::ResultExt;
 
 use self::channel_index::ChannelIndex;
+pub use self::channel_index::ChannelPath;
 
 pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
 
@@ -145,11 +146,13 @@ impl ChannelStore {
         })
     }
 
-    pub fn channel_at_index(&self, ix: usize) -> Option<(usize, &Arc<Channel>)> {
+    pub fn channel_at_index(&self, ix: usize) -> Option<(usize, &Arc<Channel>, &Arc<[ChannelId]>)> {
         let path = self.channel_index.get(ix)?;
         let id = path.last().unwrap();
         let channel = self.channel_for_id(*id).unwrap();
-        Some((path.len() - 1, channel))
+
+
+        Some((path.len() - 1, channel, path))
     }
 
     pub fn channel_invitations(&self) -> &[Arc<Channel>] {
@@ -734,12 +737,15 @@ impl ChannelStore {
                 }
             }
 
-            self.channel_index.insert_channels(payload.channels);
+            let mut channel_index = self.channel_index.start_upsert();
+            for channel in payload.channels {
+                channel_index.upsert(channel)
+            }
         }
 
         for edge in payload.delete_channel_edge {
             self.channel_index
-                .remove_edge(edge.parent_id, edge.channel_id);
+                .delete_edge(edge.parent_id, edge.channel_id);
         }
 
         for permission in payload.channel_permissions {

crates/channel/src/channel_store/channel_index.rs ๐Ÿ”—

@@ -1,11 +1,11 @@
-use std::{ops::{Deref, DerefMut}, sync::Arc};
+use std::{ops::Deref, sync::Arc};
 
 use collections::HashMap;
 use rpc::proto;
 
 use crate::{ChannelId, Channel};
 
-pub type ChannelPath = Vec<ChannelId>;
+pub type ChannelPath = Arc<[ChannelId]>;
 pub type ChannelsById = HashMap<ChannelId, Arc<Channel>>;
 
 #[derive(Default, Debug)]
@@ -20,33 +20,6 @@ impl ChannelIndex {
         &self.channels_by_id
     }
 
-    /// Insert or update all of the given channels into the index
-    pub fn insert_channels(&mut self, channels: Vec<proto::Channel>) {
-        let mut insert = self.insert();
-
-        for channel_proto in channels {
-            if let Some(existing_channel) = insert.channels_by_id.get_mut(&channel_proto.id) {
-                Arc::make_mut(existing_channel).name = channel_proto.name;
-
-                if let Some(parent_id) = channel_proto.parent_id {
-                    insert.insert_edge(parent_id, channel_proto.id)
-                }
-            } else {
-                let channel = Arc::new(Channel {
-                    id: channel_proto.id,
-                    name: channel_proto.name,
-                });
-                insert.channels_by_id.insert(channel.id, channel.clone());
-
-                if let Some(parent_id) = channel_proto.parent_id {
-                    insert.insert_edge(parent_id, channel.id);
-                } else {
-                    insert.insert_root(channel.id);
-                }
-            }
-        }
-    }
-
     pub fn clear(&mut self) {
         self.paths.clear();
         self.channels_by_id.clear();
@@ -54,7 +27,7 @@ impl ChannelIndex {
 
     /// Remove the given edge from this index. This will not remove the channel
     /// and may result in dangling channels.
-    pub fn remove_edge(&mut self, parent_id: ChannelId, channel_id: ChannelId) {
+    pub fn delete_edge(&mut self, parent_id: ChannelId, channel_id: ChannelId) {
         self.paths.retain(|path| {
             !path
                 .windows(2)
@@ -68,8 +41,9 @@ impl ChannelIndex {
         self.paths.retain(|channel_path| !channel_path.iter().any(|channel_id| {channels.contains(channel_id)}))
     }
 
-    fn insert(& mut self) -> ChannelPathsInsertGuard {
-        ChannelPathsInsertGuard {
+    /// Upsert one or more channels into this index.
+    pub fn start_upsert(& mut self) -> ChannelPathsUpsertGuard {
+        ChannelPathsUpsertGuard {
             paths: &mut self.paths,
             channels_by_id: &mut self.channels_by_id,
         }
@@ -86,47 +60,54 @@ impl Deref for ChannelIndex {
 
 /// A guard for ensuring that the paths index maintains its sort and uniqueness
 /// invariants after a series of insertions
-struct ChannelPathsInsertGuard<'a> {
+pub struct ChannelPathsUpsertGuard<'a> {
     paths:  &'a mut Vec<ChannelPath>,
     channels_by_id: &'a mut ChannelsById,
 }
 
-impl Deref for ChannelPathsInsertGuard<'_> {
-    type Target = ChannelsById;
-
-    fn deref(&self) -> &Self::Target {
-        &self.channels_by_id
-    }
-}
+impl<'a> ChannelPathsUpsertGuard<'a> {
+    pub fn upsert(&mut self, channel_proto: proto::Channel) {
+        if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) {
+            Arc::make_mut(existing_channel).name = channel_proto.name;
 
-impl DerefMut for ChannelPathsInsertGuard<'_> {
-    fn deref_mut(&mut self) -> &mut Self::Target {
-        &mut self.channels_by_id
+            if let Some(parent_id) = channel_proto.parent_id {
+                self.insert_edge(parent_id, channel_proto.id)
+            }
+        } else {
+            let channel = Arc::new(Channel {
+                id: channel_proto.id,
+                name: channel_proto.name,
+            });
+            self.channels_by_id.insert(channel.id, channel.clone());
+
+            if let Some(parent_id) = channel_proto.parent_id {
+                self.insert_edge(parent_id, channel.id);
+            } else {
+                self.insert_root(channel.id);
+            }
+        }
     }
-}
-
 
-impl<'a> ChannelPathsInsertGuard<'a> {
-    pub fn insert_edge(&mut self, parent_id: ChannelId, channel_id: ChannelId) {
+    fn insert_edge(&mut self, parent_id: ChannelId, channel_id: ChannelId) {
         let mut ix = 0;
         while ix < self.paths.len() {
             let path = &self.paths[ix];
             if path.ends_with(&[parent_id]) {
-                let mut new_path = path.clone();
+                let mut new_path = path.to_vec();
                 new_path.push(channel_id);
-                self.paths.insert(ix + 1, new_path);
+                self.paths.insert(ix + 1, new_path.into());
                 ix += 1;
             }
             ix += 1;
         }
     }
 
-    pub fn insert_root(&mut self, channel_id: ChannelId) {
-        self.paths.push(vec![channel_id]);
+    fn insert_root(&mut self, channel_id: ChannelId) {
+        self.paths.push(Arc::from([channel_id]));
     }
 }
 
-impl<'a> Drop for ChannelPathsInsertGuard<'a> {
+impl<'a> Drop for ChannelPathsUpsertGuard<'a> {
     fn drop(&mut self) {
         self.paths.sort_by(|a, b| {
             let a = channel_path_sorting_key(a, &self.channels_by_id);

crates/collab_ui/src/collab_panel.rs ๐Ÿ”—

@@ -9,7 +9,7 @@ use crate::{
 };
 use anyhow::Result;
 use call::ActiveCall;
-use channel::{Channel, ChannelEvent, ChannelId, ChannelStore};
+use channel::{Channel, ChannelEvent, ChannelId, ChannelStore, ChannelPath};
 use channel_modal::ChannelModal;
 use client::{proto::PeerId, Client, Contact, User, UserStore};
 use contact_finder::ContactFinder;
@@ -40,7 +40,7 @@ use menu::{Confirm, SelectNext, SelectPrev};
 use project::{Fs, Project};
 use serde_derive::{Deserialize, Serialize};
 use settings::SettingsStore;
-use std::{borrow::Cow, mem, sync::Arc};
+use std::{borrow::Cow, mem, sync::Arc, hash::Hash};
 use theme::{components::ComponentExt, IconButton};
 use util::{iife, ResultExt, TryFutureExt};
 use workspace::{
@@ -51,32 +51,32 @@ use workspace::{
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 struct RemoveChannel {
-    channel_id: u64,
+    channel_id: ChannelId,
 }
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 struct ToggleCollapse {
-    channel_id: u64,
+    location: ChannelLocation<'static>,
 }
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 struct NewChannel {
-    channel_id: u64,
+    location: ChannelLocation<'static>,
 }
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 struct InviteMembers {
-    channel_id: u64,
+    channel_id: ChannelId,
 }
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 struct ManageMembers {
-    channel_id: u64,
+    channel_id: ChannelId,
 }
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 struct RenameChannel {
-    channel_id: u64,
+    location: ChannelLocation<'static>,
 }
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
@@ -89,6 +89,26 @@ pub struct JoinChannelCall {
     pub channel_id: u64,
 }
 
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+struct OpenChannelBuffer {
+    channel_id: ChannelId,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+struct CopyChannel {
+    channel_id: ChannelId,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+struct CutChannel {
+    channel_id: ChannelId,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+struct PasteChannel {
+    channel_id: ChannelId,
+}
+
 actions!(
     collab_panel,
     [
@@ -111,12 +131,35 @@ impl_actions!(
         ToggleCollapse,
         OpenChannelNotes,
         JoinChannelCall,
+        OpenChannelBuffer,
+        CopyChannel,
+        CutChannel,
+        PasteChannel,
     ]
 );
 
 const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
 
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
+pub struct ChannelLocation<'a> {
+    channel: ChannelId,
+    parent: Cow<'a, ChannelPath>,
+}
+
+impl From<(ChannelId, ChannelPath)> for ChannelLocation<'static> {
+    fn from(value: (ChannelId, ChannelPath)) -> Self {
+        ChannelLocation { channel: value.0, parent: Cow::Owned(value.1) }
+    }
+}
+
+impl<'a> From<(ChannelId, &'a ChannelPath)> for ChannelLocation<'a> {
+    fn from(value: (ChannelId, &'a ChannelPath)) -> Self {
+        ChannelLocation { channel: value.0, parent: Cow::Borrowed(value.1) }
+    }
+}
+
 pub fn init(cx: &mut AppContext) {
+    settings::register::<panel_settings::CollaborationPanelSettings>(cx);
     contact_finder::init(cx);
     channel_modal::init(cx);
     channel_view::init(cx);
@@ -137,16 +180,37 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(CollabPanel::collapse_selected_channel);
     cx.add_action(CollabPanel::expand_selected_channel);
     cx.add_action(CollabPanel::open_channel_notes);
+    cx.add_action(CollabPanel::open_channel_buffer);
+
+    cx.add_action(|panel: &mut CollabPanel, action: &CopyChannel, _: &mut ViewContext<CollabPanel>| {
+        panel.copy = Some(ChannelCopy::Copy(action.channel_id));
+    });
+
+    cx.add_action(|panel: &mut CollabPanel, action: &CutChannel, _: &mut ViewContext<CollabPanel>| {
+        // panel.copy = Some(ChannelCopy::Cut(action.channel_id));
+    });
+
+    cx.add_action(|panel: &mut CollabPanel, action: &PasteChannel, cx: &mut ViewContext<CollabPanel>| {
+        if let Some(copy) = &panel.copy {
+            match copy {
+                ChannelCopy::Cut {..} => todo!(),
+                ChannelCopy::Copy(channel) => panel.channel_store.update(cx, |channel_store, cx| {
+                    channel_store.move_channel(*channel, None, Some(action.channel_id), cx).detach_and_log_err(cx)
+                }),
+            }
+        }
+    });
+
 }
 
 #[derive(Debug)]
 pub enum ChannelEditingState {
     Create {
-        parent_id: Option<u64>,
+        location: Option<ChannelLocation<'static>>,
         pending_name: Option<String>,
     },
     Rename {
-        channel_id: u64,
+        location: ChannelLocation<'static>,
         pending_name: Option<String>,
     },
 }
@@ -160,10 +224,19 @@ impl ChannelEditingState {
     }
 }
 
+enum ChannelCopy {
+    Cut {
+        channel_id: u64,
+        parent_id: Option<u64>,
+    },
+    Copy(u64),
+}
+
 pub struct CollabPanel {
     width: Option<f32>,
     fs: Arc<dyn Fs>,
     has_focus: bool,
+    copy: Option<ChannelCopy>,
     pending_serialization: Task<Option<()>>,
     context_menu: ViewHandle<ContextMenu>,
     filter_editor: ViewHandle<Editor>,
@@ -179,7 +252,7 @@ pub struct CollabPanel {
     list_state: ListState<Self>,
     subscriptions: Vec<Subscription>,
     collapsed_sections: Vec<Section>,
-    collapsed_channels: Vec<ChannelId>,
+    collapsed_channels: Vec<ChannelLocation<'static>>,
     workspace: WeakViewHandle<Workspace>,
     context_menu_on_selected: bool,
 }
@@ -187,7 +260,7 @@ pub struct CollabPanel {
 #[derive(Serialize, Deserialize)]
 struct SerializedCollabPanel {
     width: Option<f32>,
-    collapsed_channels: Option<Vec<ChannelId>>,
+    collapsed_channels: Option<Vec<ChannelLocation<'static>>>,
 }
 
 #[derive(Debug)]
@@ -231,6 +304,7 @@ enum ListEntry {
     Channel {
         channel: Arc<Channel>,
         depth: usize,
+        path: Arc<[ChannelId]>,
     },
     ChannelNotes {
         channel_id: ChannelId,
@@ -348,10 +422,11 @@ impl CollabPanel {
                                 cx,
                             )
                         }
-                        ListEntry::Channel { channel, depth } => {
+                        ListEntry::Channel { channel, depth, path } => {
                             let channel_row = this.render_channel(
                                 &*channel,
                                 *depth,
+                                path.to_owned(),
                                 &theme.collab_panel,
                                 is_selected,
                                 cx,
@@ -420,6 +495,7 @@ impl CollabPanel {
             let mut this = Self {
                 width: None,
                 has_focus: false,
+                copy: None,
                 fs: workspace.app_state().fs.clone(),
                 pending_serialization: Task::ready(None),
                 context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
@@ -700,7 +776,7 @@ impl CollabPanel {
                     if matches!(
                         state,
                         ChannelEditingState::Create {
-                            parent_id: None,
+                            location: None,
                             ..
                         }
                     ) {
@@ -709,16 +785,18 @@ impl CollabPanel {
                 }
                 let mut collapse_depth = None;
                 for mat in matches {
-                    let (depth, channel) =
+                    let (depth, channel, path) =
                         channel_store.channel_at_index(mat.candidate_id).unwrap();
 
-                    if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
+                    let location: ChannelLocation<'_> = (channel.id, path).into();
+
+                    if collapse_depth.is_none() && self.is_channel_collapsed(&location) {
                         collapse_depth = Some(depth);
                     } else if let Some(collapsed_depth) = collapse_depth {
                         if depth > collapsed_depth {
                             continue;
                         }
-                        if self.is_channel_collapsed(channel.id) {
+                        if self.is_channel_collapsed(&location) {
                             collapse_depth = Some(depth);
                         } else {
                             collapse_depth = None;
@@ -726,18 +804,19 @@ impl CollabPanel {
                     }
 
                     match &self.channel_editing_state {
-                        Some(ChannelEditingState::Create { parent_id, .. })
-                            if *parent_id == Some(channel.id) =>
+                        Some(ChannelEditingState::Create { location: parent_id, .. })
+                            if *parent_id == Some(location) =>
                         {
                             self.entries.push(ListEntry::Channel {
                                 channel: channel.clone(),
                                 depth,
+                                path: path.clone(),
                             });
                             self.entries
                                 .push(ListEntry::ChannelEditor { depth: depth + 1 });
                         }
-                        Some(ChannelEditingState::Rename { channel_id, .. })
-                            if *channel_id == channel.id =>
+                        Some(ChannelEditingState::Rename { location, .. })
+                            if location.channel == channel.id && location.parent == Cow::Borrowed(path) =>
                         {
                             self.entries.push(ListEntry::ChannelEditor { depth });
                         }
@@ -745,6 +824,7 @@ impl CollabPanel {
                             self.entries.push(ListEntry::Channel {
                                 channel: channel.clone(),
                                 depth,
+                                path: path.clone()
                             });
                         }
                     }
@@ -1546,14 +1626,21 @@ impl CollabPanel {
         &self,
         channel: &Channel,
         depth: usize,
+        path: ChannelPath,
         theme: &theme::CollabPanel,
         is_selected: bool,
         cx: &mut ViewContext<Self>,
     ) -> AnyElement<Self> {
         let channel_id = channel.id;
         let has_children = self.channel_store.read(cx).has_children(channel_id);
-        let disclosed =
-            has_children.then(|| !self.collapsed_channels.binary_search(&channel_id).is_ok());
+
+        let disclosed = {
+            let location = ChannelLocation {
+                channel: channel_id,
+                parent: Cow::Borrowed(&path),
+            };
+            has_children.then(|| !self.collapsed_channels.binary_search(&location).is_ok())
+        };
 
         let is_active = iife!({
             let call_channel = ActiveCall::global(cx)
@@ -1569,7 +1656,7 @@ impl CollabPanel {
 
         enum ChannelCall {}
 
-        MouseEventHandler::new::<Channel, _>(channel.id as usize, cx, |state, cx| {
+        MouseEventHandler::new::<Channel, _>(id(&path) as usize, cx, |state, cx| {
             let row_hovered = state.hovered();
 
             Flex::<Self>::row()
@@ -1637,8 +1724,8 @@ impl CollabPanel {
                 )
                 .align_children_center()
                 .styleable_component()
-                .disclosable(disclosed, Box::new(ToggleCollapse { channel_id }))
-                .with_id(channel_id as usize)
+                .disclosable(disclosed, Box::new(ToggleCollapse { location: (channel_id, path.clone()).into() }))
+                .with_id(id(&path) as usize)
                 .with_style(theme.disclosure.clone())
                 .element()
                 .constrained()
@@ -1654,7 +1741,7 @@ impl CollabPanel {
             this.join_channel_chat(channel_id, cx);
         })
         .on_click(MouseButton::Right, move |e, this, cx| {
-            this.deploy_channel_context_menu(Some(e.position), channel_id, cx);
+            this.deploy_channel_context_menu(Some(e.position), &(channel_id, path.clone()).into(), cx);
         })
         .with_cursor_style(CursorStyle::PointingHand)
         .into_any()
@@ -1901,7 +1988,7 @@ impl CollabPanel {
     fn deploy_channel_context_menu(
         &mut self,
         position: Option<Vector2F>,
-        channel_id: u64,
+        location: &ChannelLocation<'static>,
         cx: &mut ViewContext<Self>,
     ) {
         self.context_menu_on_selected = position.is_none();
@@ -1913,27 +2000,29 @@ impl CollabPanel {
                 OverlayPositionMode::Window
             });
 
-            let expand_action_name = if self.is_channel_collapsed(channel_id) {
+            let expand_action_name = if self.is_channel_collapsed(&location) {
                 "Expand Subchannels"
             } else {
                 "Collapse Subchannels"
             };
 
             let mut items = vec![
-                ContextMenuItem::action(expand_action_name, ToggleCollapse { channel_id }),
-                ContextMenuItem::action("Open Notes", OpenChannelNotes { channel_id }),
+                ContextMenuItem::action(expand_action_name, ToggleCollapse { location: location.clone() }),
+                ContextMenuItem::action("Open Notes", OpenChannelBuffer { channel_id: location.channel }),
             ];
 
-            if self.channel_store.read(cx).is_user_admin(channel_id) {
+            if self.channel_store.read(cx).is_user_admin(location.channel) {
                 items.extend([
                     ContextMenuItem::Separator,
-                    ContextMenuItem::action("New Subchannel", NewChannel { channel_id }),
-                    ContextMenuItem::action("Rename", RenameChannel { channel_id }),
+                    ContextMenuItem::action("New Subchannel", NewChannel { location: location.clone() }),
+                    ContextMenuItem::action("Rename", RenameChannel { location: location.clone() }),
+                    ContextMenuItem::action("Copy", CopyChannel { channel_id: location.channel }),
+                    ContextMenuItem::action("Paste", PasteChannel { channel_id: location.channel }),
                     ContextMenuItem::Separator,
-                    ContextMenuItem::action("Invite Members", InviteMembers { channel_id }),
-                    ContextMenuItem::action("Manage Members", ManageMembers { channel_id }),
+                    ContextMenuItem::action("Invite Members", InviteMembers { channel_id: location.channel }),
+                    ContextMenuItem::action("Manage Members", ManageMembers { channel_id: location.channel }),
                     ContextMenuItem::Separator,
-                    ContextMenuItem::action("Delete", RemoveChannel { channel_id }),
+                    ContextMenuItem::action("Delete", RemoveChannel { channel_id: location.channel }),
                 ]);
             }
 
@@ -2059,7 +2148,7 @@ impl CollabPanel {
         if let Some(editing_state) = &mut self.channel_editing_state {
             match editing_state {
                 ChannelEditingState::Create {
-                    parent_id,
+                    location,
                     pending_name,
                     ..
                 } => {
@@ -2072,13 +2161,13 @@ impl CollabPanel {
 
                     self.channel_store
                         .update(cx, |channel_store, cx| {
-                            channel_store.create_channel(&channel_name, *parent_id, cx)
+                            channel_store.create_channel(&channel_name, location.as_ref().map(|location| location.channel), cx)
                         })
                         .detach();
                     cx.notify();
                 }
                 ChannelEditingState::Rename {
-                    channel_id,
+                    location,
                     pending_name,
                 } => {
                     if pending_name.is_some() {
@@ -2089,7 +2178,7 @@ impl CollabPanel {
 
                     self.channel_store
                         .update(cx, |channel_store, cx| {
-                            channel_store.rename(*channel_id, &channel_name, cx)
+                            channel_store.rename(location.channel, &channel_name, cx)
                         })
                         .detach();
                     cx.notify();
@@ -2116,38 +2205,42 @@ impl CollabPanel {
         _: &CollapseSelectedChannel,
         cx: &mut ViewContext<Self>,
     ) {
-        let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
+        let Some((channel_id, path)) = self.selected_channel().map(|(channel, parent)| (channel.id, parent)) else {
             return;
         };
 
-        if self.is_channel_collapsed(channel_id) {
+        let path = path.to_owned();
+
+        if self.is_channel_collapsed(&(channel_id, path.clone()).into()) {
             return;
         }
 
-        self.toggle_channel_collapsed(&ToggleCollapse { channel_id }, cx)
+        self.toggle_channel_collapsed(&ToggleCollapse { location: (channel_id, path).into() }, cx)
     }
 
     fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext<Self>) {
-        let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
+        let Some((channel_id, path)) = self.selected_channel().map(|(channel, parent)| (channel.id, parent)) else {
             return;
         };
 
-        if !self.is_channel_collapsed(channel_id) {
+        let path = path.to_owned();
+
+        if !self.is_channel_collapsed(&(channel_id, path.clone()).into()) {
             return;
         }
 
-        self.toggle_channel_collapsed(&ToggleCollapse { channel_id }, cx)
+        self.toggle_channel_collapsed(&ToggleCollapse { location: (channel_id, path).into() }, cx)
     }
 
     fn toggle_channel_collapsed(&mut self, action: &ToggleCollapse, cx: &mut ViewContext<Self>) {
-        let channel_id = action.channel_id;
+        let location = action.location.clone();
 
-        match self.collapsed_channels.binary_search(&channel_id) {
+        match self.collapsed_channels.binary_search(&location) {
             Ok(ix) => {
                 self.collapsed_channels.remove(ix);
             }
             Err(ix) => {
-                self.collapsed_channels.insert(ix, channel_id);
+                self.collapsed_channels.insert(ix, location);
             }
         };
         self.serialize(cx);
@@ -2156,8 +2249,8 @@ impl CollabPanel {
         cx.focus_self();
     }
 
-    fn is_channel_collapsed(&self, channel: ChannelId) -> bool {
-        self.collapsed_channels.binary_search(&channel).is_ok()
+    fn is_channel_collapsed(&self, location: &ChannelLocation) -> bool {
+        self.collapsed_channels.binary_search(location).is_ok()
     }
 
     fn leave_call(cx: &mut ViewContext<Self>) {
@@ -2182,7 +2275,7 @@ impl CollabPanel {
 
     fn new_root_channel(&mut self, cx: &mut ViewContext<Self>) {
         self.channel_editing_state = Some(ChannelEditingState::Create {
-            parent_id: None,
+            location: None,
             pending_name: None,
         });
         self.update_entries(false, cx);
@@ -2200,9 +2293,9 @@ impl CollabPanel {
 
     fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext<Self>) {
         self.collapsed_channels
-            .retain(|&channel| channel != action.channel_id);
+            .retain(|channel| *channel != action.location);
         self.channel_editing_state = Some(ChannelEditingState::Create {
-            parent_id: Some(action.channel_id),
+            location: Some(action.location.to_owned()),
             pending_name: None,
         });
         self.update_entries(false, cx);
@@ -2220,16 +2313,16 @@ impl CollabPanel {
     }
 
     fn remove(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
-        if let Some(channel) = self.selected_channel() {
+        if let Some((channel, _)) = self.selected_channel() {
             self.remove_channel(channel.id, cx)
         }
     }
 
     fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
-        if let Some(channel) = self.selected_channel() {
+        if let Some((channel, parent)) = self.selected_channel() {
             self.rename_channel(
                 &RenameChannel {
-                    channel_id: channel.id,
+                    location: (channel.id, parent.to_owned()).into(),
                 },
                 cx,
             );
@@ -2238,12 +2331,12 @@ impl CollabPanel {
 
     fn rename_channel(&mut self, action: &RenameChannel, cx: &mut ViewContext<Self>) {
         let channel_store = self.channel_store.read(cx);
-        if !channel_store.is_user_admin(action.channel_id) {
+        if !channel_store.is_user_admin(action.location.channel) {
             return;
         }
-        if let Some(channel) = channel_store.channel_for_id(action.channel_id).cloned() {
+        if let Some(channel) = channel_store.channel_for_id(action.location.channel).cloned() {
             self.channel_editing_state = Some(ChannelEditingState::Rename {
-                channel_id: action.channel_id,
+                location: action.location.to_owned(),
                 pending_name: None,
             });
             self.channel_name_editor.update(cx, |editor, cx| {
@@ -2263,18 +2356,18 @@ impl CollabPanel {
     }
 
     fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext<Self>) {
-        let Some(channel) = self.selected_channel() else {
+        let Some((channel, path)) = self.selected_channel() else {
             return;
         };
 
-        self.deploy_channel_context_menu(None, channel.id, cx);
+        self.deploy_channel_context_menu(None, &(channel.id, path.to_owned()).into(), cx);
     }
 
-    fn selected_channel(&self) -> Option<&Arc<Channel>> {
+    fn selected_channel(&self) -> Option<(&Arc<Channel>, &ChannelPath)> {
         self.selection
             .and_then(|ix| self.entries.get(ix))
             .and_then(|entry| match entry {
-                ListEntry::Channel { channel, .. } => Some(channel),
+                ListEntry::Channel { channel, path: parent, .. } => Some((channel, parent)),
                 _ => None,
             })
     }
@@ -2657,13 +2750,15 @@ impl PartialEq for ListEntry {
             ListEntry::Channel {
                 channel: channel_1,
                 depth: depth_1,
+                path: parent_1,
             } => {
                 if let ListEntry::Channel {
                     channel: channel_2,
                     depth: depth_2,
+                    path: parent_2,
                 } = other
                 {
-                    return channel_1.id == channel_2.id && depth_1 == depth_2;
+                    return channel_1.id == channel_2.id && depth_1 == depth_2 && parent_1 == parent_2;
                 }
             }
             ListEntry::ChannelNotes { channel_id } => {
@@ -2726,3 +2821,26 @@ fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Elemen
         .contained()
         .with_style(style.container)
 }
+
+/// Hash a channel path to a u64, for use as a mouse id
+/// Based on the Fowlerโ€“Nollโ€“Vo hash:
+/// https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function
+fn id(path: &[ChannelId]) -> u64 {
+    // I probably should have done this, but I didn't
+    // let hasher = DefaultHasher::new();
+    // let path = path.hash(&mut hasher);
+    // let x = hasher.finish();
+
+    const OFFSET: u64 = 14695981039346656037;
+    const PRIME: u64 = 1099511628211;
+
+    let mut hash = OFFSET;
+    for id in path.iter() {
+        for id in id.to_ne_bytes() {
+            hash = hash ^ (id as u64);
+            hash = (hash as u128 * PRIME as u128) as u64;
+        }
+    }
+
+    hash
+}

crates/rpc/src/proto.rs ๐Ÿ”—

@@ -328,7 +328,6 @@ request_messages!(
     (GetChannelMessages, GetChannelMessagesResponse),
     (GetChannelMembers, GetChannelMembersResponse),
     (JoinChannel, JoinRoomResponse),
-    (RemoveChannel, Ack),
     (RemoveChannelMessage, Ack),
     (DeleteChannel, Ack),
     (RenameProjectEntry, ProjectEntryResponse),