Position and style the channel editor correctly

Mikayla created

Fix a bug where some channel updates would be lost
Add channel name sanitization before storing in the database

Change summary

assets/keymaps/default.json          |  8 ++
crates/collab_ui/src/collab_panel.rs | 96 ++++++++++++++++++++++++++---
crates/menu/src/menu.rs              |  3 
3 files changed, 95 insertions(+), 12 deletions(-)

Detailed changes

assets/keymaps/default.json 🔗

@@ -13,6 +13,7 @@
       "cmd-up": "menu::SelectFirst",
       "cmd-down": "menu::SelectLast",
       "enter": "menu::Confirm",
+      "ctrl-enter": "menu::ShowContextMenu",
       "cmd-enter": "menu::SecondaryConfirm",
       "escape": "menu::Cancel",
       "ctrl-c": "menu::Cancel",
@@ -550,6 +551,13 @@
       "alt-shift-f": "project_panel::NewSearchInDirectory"
     }
   },
+  {
+    "context": "CollabPanel",
+    "bindings": {
+      "ctrl-backspace": "collab_panel::Remove",
+      "space": "menu::Confirm"
+    }
+  },
   {
     "context": "ChannelModal",
     "bindings": {

crates/collab_ui/src/collab_panel.rs 🔗

@@ -15,7 +15,7 @@ use gpui::{
     actions,
     elements::{
         Canvas, ChildView, Empty, Flex, Image, Label, List, ListOffset, ListState,
-        MouseEventHandler, Orientation, Padding, ParentElement, Stack, Svg,
+        MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, Stack, Svg,
     },
     geometry::{
         rect::RectF,
@@ -64,7 +64,7 @@ struct ManageMembers {
     channel_id: u64,
 }
 
-actions!(collab_panel, [ToggleFocus]);
+actions!(collab_panel, [ToggleFocus, Remove, Secondary]);
 
 impl_actions!(
     collab_panel,
@@ -82,7 +82,9 @@ pub fn init(_client: Arc<Client>, cx: &mut AppContext) {
     cx.add_action(CollabPanel::select_next);
     cx.add_action(CollabPanel::select_prev);
     cx.add_action(CollabPanel::confirm);
-    cx.add_action(CollabPanel::remove_channel);
+    cx.add_action(CollabPanel::remove);
+    cx.add_action(CollabPanel::remove_channel_action);
+    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);
@@ -113,6 +115,7 @@ pub struct CollabPanel {
     subscriptions: Vec<Subscription>,
     collapsed_sections: Vec<Section>,
     workspace: WeakViewHandle<Workspace>,
+    context_menu_on_selected: bool,
 }
 
 #[derive(Serialize, Deserialize)]
@@ -274,7 +277,26 @@ impl CollabPanel {
                             )
                         }
                         ListEntry::Channel(channel) => {
-                            this.render_channel(&*channel, &theme.collab_panel, is_selected, cx)
+                            let channel_row = this.render_channel(
+                                &*channel,
+                                &theme.collab_panel,
+                                is_selected,
+                                cx,
+                            );
+
+                            if is_selected && this.context_menu_on_selected {
+                                Stack::new()
+                                    .with_child(channel_row)
+                                    .with_child(
+                                        ChildView::new(&this.context_menu, cx)
+                                            .aligned()
+                                            .bottom()
+                                            .right(),
+                                    )
+                                    .into_any()
+                            } else {
+                                return channel_row;
+                            }
                         }
                         ListEntry::ChannelInvite(channel) => Self::render_channel_invite(
                             channel.clone(),
@@ -332,6 +354,7 @@ impl CollabPanel {
                 collapsed_sections: Vec::default(),
                 workspace: workspace.weak_handle(),
                 client: workspace.app_state().client.clone(),
+                context_menu_on_selected: true,
                 list_state,
             };
 
@@ -1321,6 +1344,7 @@ impl CollabPanel {
         cx: &mut ViewContext<Self>,
     ) -> AnyElement<Self> {
         let channel_id = channel.id;
+
         MouseEventHandler::<Channel, Self>::new(channel.id as usize, cx, |state, cx| {
             Flex::row()
                 .with_child(
@@ -1367,7 +1391,7 @@ impl CollabPanel {
             this.join_channel(channel_id, cx);
         })
         .on_click(MouseButton::Right, move |e, this, cx| {
-            this.deploy_channel_context_menu(e.position, channel_id, cx);
+            this.deploy_channel_context_menu(Some(e.position), channel_id, cx);
         })
         .into_any()
     }
@@ -1573,15 +1597,27 @@ impl CollabPanel {
 
     fn deploy_channel_context_menu(
         &mut self,
-        position: Vector2F,
+        position: Option<Vector2F>,
         channel_id: u64,
         cx: &mut ViewContext<Self>,
     ) {
         if self.channel_store.read(cx).is_user_admin(channel_id) {
+            self.context_menu_on_selected = position.is_none();
+
             self.context_menu.update(cx, |context_menu, cx| {
+                context_menu.set_position_mode(if self.context_menu_on_selected {
+                    OverlayPositionMode::Local
+                } else {
+                    OverlayPositionMode::Window
+                });
+
                 context_menu.show(
-                    position,
-                    gpui::elements::AnchorCorner::BottomLeft,
+                    position.unwrap_or_default(),
+                    if self.context_menu_on_selected {
+                        gpui::elements::AnchorCorner::TopRight
+                    } else {
+                        gpui::elements::AnchorCorner::BottomLeft
+                    },
                     vec![
                         ContextMenuItem::action("New Channel", NewChannel { channel_id }),
                         ContextMenuItem::action("Remove Channel", RemoveChannel { channel_id }),
@@ -1591,6 +1627,8 @@ impl CollabPanel {
                     cx,
                 );
             });
+
+            cx.notify();
         }
     }
 
@@ -1755,6 +1793,33 @@ impl CollabPanel {
         self.show_channel_modal(action.channel_id, channel_modal::Mode::ManageMembers, cx);
     }
 
+    // TODO: Make join into a toggle
+    // TODO: Make enter work on channel editor
+    fn remove(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
+        if let Some(channel) = self.selected_channel() {
+            self.remove_channel(channel.id, cx)
+        }
+    }
+
+    fn rename(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {}
+
+    fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext<Self>) {
+        let Some(channel) = self.selected_channel() else {
+            return;
+        };
+
+        self.deploy_channel_context_menu(None, channel.id, cx);
+    }
+
+    fn selected_channel(&self) -> Option<&Arc<Channel>> {
+        self.selection
+            .and_then(|ix| self.entries.get(ix))
+            .and_then(|entry| match entry {
+                ListEntry::Channel(channel) => Some(channel),
+                _ => None,
+            })
+    }
+
     fn show_channel_modal(
         &mut self,
         channel_id: ChannelId,
@@ -1788,8 +1853,11 @@ impl CollabPanel {
         .detach();
     }
 
-    fn remove_channel(&mut self, action: &RemoveChannel, cx: &mut ViewContext<Self>) {
-        let channel_id = action.channel_id;
+    fn remove_channel_action(&mut self, action: &RemoveChannel, cx: &mut ViewContext<Self>) {
+        self.remove_channel(action.channel_id, cx)
+    }
+
+    fn remove_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
         let channel_store = self.channel_store.clone();
         if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) {
             let prompt_message = format!(
@@ -1818,6 +1886,9 @@ impl CollabPanel {
         }
     }
 
+    // Should move to the filter editor if clicking on it
+    // Should move selection to the channel editor if activating it
+
     fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext<Self>) {
         let user_store = self.user_store.clone();
         let prompt_message = format!(
@@ -1969,7 +2040,10 @@ impl View for CollabPanel {
                         .with_width(self.size(cx))
                         .into_any(),
                 )
-                .with_child(ChildView::new(&self.context_menu, cx))
+                .with_children(
+                    (!self.context_menu_on_selected)
+                        .then(|| ChildView::new(&self.context_menu, cx)),
+                )
                 .into_any()
         })
         .on_click(MouseButton::Left, |_, _, cx| cx.focus_self())

crates/menu/src/menu.rs 🔗

@@ -7,6 +7,7 @@ gpui::actions!(
         SelectPrev,
         SelectNext,
         SelectFirst,
-        SelectLast
+        SelectLast,
+        ShowContextMenu
     ]
 );