Channel Context Menu

Conrad Irwin created

Change summary

crates/collab_ui2/src/collab_panel.rs      | 378 ++++++++++++-----------
crates/project_panel2/src/project_panel.rs |   2 
2 files changed, 196 insertions(+), 184 deletions(-)

Detailed changes

crates/collab_ui2/src/collab_panel.rs 🔗

@@ -151,10 +151,10 @@ actions!(
 //     ]
 // );
 
-// #[derive(Debug, Copy, Clone, PartialEq, Eq)]
-// struct ChannelMoveClipboard {
-//     channel_id: ChannelId,
-// }
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+struct ChannelMoveClipboard {
+    channel_id: ChannelId,
+}
 
 const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
 
@@ -168,10 +168,11 @@ use editor::Editor;
 use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
 use fuzzy::{match_strings, StringMatchCandidate};
 use gpui::{
-    actions, div, img, prelude::*, serde_json, Action, AppContext, AsyncWindowContext,
-    ClipboardItem, Div, EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement,
-    IntoElement, Model, ParentElement, Pixels, Point, PromptLevel, Render, RenderOnce,
-    SharedString, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView,
+    actions, div, img, overlay, prelude::*, px, rems, serde_json, Action, AppContext,
+    AsyncWindowContext, ClipboardItem, DismissEvent, Div, EventEmitter, FocusHandle, Focusable,
+    FocusableView, InteractiveElement, IntoElement, Model, MouseDownEvent, ParentElement, Pixels,
+    Point, PromptLevel, Render, RenderOnce, SharedString, Styled, Subscription, Task, View,
+    ViewContext, VisualContext, WeakView,
 };
 use project::Fs;
 use serde_derive::{Deserialize, Serialize};
@@ -286,7 +287,7 @@ pub struct CollabPanel {
     width: Option<f32>,
     fs: Arc<dyn Fs>,
     focus_handle: FocusHandle,
-    // channel_clipboard: Option<ChannelMoveClipboard>,
+    channel_clipboard: Option<ChannelMoveClipboard>,
     pending_serialization: Task<Option<()>>,
     context_menu: Option<(View<ContextMenu>, Point<Pixels>, Subscription)>,
     filter_editor: View<Editor>,
@@ -569,7 +570,7 @@ impl CollabPanel {
             let mut this = Self {
                 width: None,
                 focus_handle: cx.focus_handle(),
-                //                 channel_clipboard: None,
+                channel_clipboard: None,
                 fs: workspace.app_state().fs.clone(),
                 pending_serialization: Task::ready(None),
                 context_menu: None,
@@ -1665,46 +1666,44 @@ impl CollabPanel {
     //             .into_any()
     //     }
 
-    //     fn has_subchannels(&self, ix: usize) -> bool {
-    //         self.entries.get(ix).map_or(false, |entry| {
-    //             if let ListEntry::Channel { has_children, .. } = entry {
-    //                 *has_children
-    //             } else {
-    //                 false
-    //             }
-    //         })
-    //     }
+    fn has_subchannels(&self, ix: usize) -> bool {
+        self.entries.get(ix).map_or(false, |entry| {
+            if let ListEntry::Channel { has_children, .. } = entry {
+                *has_children
+            } else {
+                false
+            }
+        })
+    }
 
     fn deploy_channel_context_menu(
         &mut self,
         position: Point<Pixels>,
-        channel: &Channel,
+        channel_id: ChannelId,
         ix: usize,
         cx: &mut ViewContext<Self>,
     ) {
         // self.context_menu_on_selected = position.is_none();
 
-        // let clipboard_channel_name = self.channel_clipboard.as_ref().and_then(|clipboard| {
-        //     self.channel_store
-        //         .read(cx)
-        //         .channel_for_id(clipboard.channel_id)
-        //         .map(|channel| channel.name.clone())
-        // });
-        let this = cx.view();
-        let has_subchannels = self.has_subchannels(ix);
-        let is_channel_collapsed = self.is_channel_collapsed(channel_id);
-
-        let menu = ContextMenu::build(cx, |context_menu, cx| {
-            if has_subchannels {
-                let expand_action_name = if is_channel_collapsed {
+        let clipboard_channel_name = self.channel_clipboard.as_ref().and_then(|clipboard| {
+            self.channel_store
+                .read(cx)
+                .channel_for_id(clipboard.channel_id)
+                .map(|channel| channel.name.clone())
+        });
+        let this = cx.view().clone();
+
+        let context_menu = ContextMenu::build(cx, |mut context_menu, cx| {
+            if self.has_subchannels(ix) {
+                let expand_action_name = if self.is_channel_collapsed(channel_id) {
                     "Expand Subchannels"
                 } else {
                     "Collapse Subchannels"
                 };
                 context_menu = context_menu.entry(
                     expand_action_name,
-                    cx.handler_for(&this, |this, cx| {
-                        this.toggle_channel_collapsed(channel.id, cx)
+                    cx.handler_for(&this, move |this, cx| {
+                        this.toggle_channel_collapsed(channel_id, cx)
                     }),
                 );
             }
@@ -1712,80 +1711,75 @@ impl CollabPanel {
             context_menu = context_menu
                 .entry(
                     "Open Notes",
-                    cx.handler_for(&this, |this, cx| this.open_channel_notes(channel.id, cx)),
+                    cx.handler_for(&this, move |this, cx| {
+                        this.open_channel_notes(channel_id, cx)
+                    }),
                 )
                 .entry(
                     "Open Chat",
-                    cx.handler_for(&this, |this, cx| this.join_channel_chat(channel.id, cx)),
+                    cx.handler_for(&this, move |this, cx| {
+                        this.join_channel_chat(channel_id, cx)
+                    }),
                 )
                 .entry(
                     "Copy Channel Link",
-                    cx.handler_for(&this, |this, cx| this.copy_channel_link(channel.id, cx)),
+                    cx.handler_for(&this, move |this, cx| {
+                        this.copy_channel_link(channel_id, cx)
+                    }),
                 );
 
-            if self.channel_store.read(cx).is_channel_admin(channel.id) {
+            if self.channel_store.read(cx).is_channel_admin(channel_id) {
                 context_menu = context_menu
                     .separator()
                     .entry(
                         "New Subchannel",
-                        cx.handler_for(&this, |this, cx| this.new_subchannel(channel.id, cx)),
+                        cx.handler_for(&this, move |this, cx| this.new_subchannel(channel_id, cx)),
                     )
                     .entry(
                         "Rename",
-                        cx.handler_for(&this, |this, cx| this.rename_channel(channel.id, cx)),
+                        cx.handler_for(&this, move |this, cx| this.rename_channel(channel_id, cx)),
                     )
                     .entry(
                         "Move this channel",
-                        cx.handler_for(&this, |this, cx| this.start_move_channel(channel.id, cx)),
+                        cx.handler_for(&this, move |this, cx| {
+                            this.start_move_channel(channel_id, cx)
+                        }),
                     );
 
-                // if let Some(channel_name) = clipboard_channel_name {
-                //     items.push(ContextMenuItem::Separator);
-                //     items.push(ContextMenuItem::action(
-                //         format!("Move '#{}' here", channel_name),
-                //         MoveChannel { to: channel.id },
-                //     ));
-                // }
+                if let Some(channel_name) = clipboard_channel_name {
+                    context_menu = context_menu.separator().entry(
+                        format!("Move '#{}' here", channel_name),
+                        cx.handler_for(&this, move |this, cx| {
+                            this.move_channel_on_clipboard(channel_id, cx)
+                        }),
+                    );
+                }
 
-                // items.extend([
-                //     ContextMenuItem::Separator,
-                //     ContextMenuItem::action(
-                //         "Invite Members",
-                //         InviteMembers {
-                //             channel_id: channel.id,
-                //         },
-                //     ),
-                //     ContextMenuItem::action(
-                //         "Manage Members",
-                //         ManageMembers {
-                //             channel_id: channel.id,
-                //         },
-                //     ),
-                //     ContextMenuItem::Separator,
-                //     ContextMenuItem::action(
-                //         "Delete",
-                //         RemoveChannel {
-                //             channel_id: channel.id,
-                //         },
-                //     ),
-                // ]);
+                context_menu = context_menu
+                    .separator()
+                    .entry(
+                        "Invite Members",
+                        cx.handler_for(&this, move |this, cx| this.invite_members(channel_id, cx)),
+                    )
+                    .entry(
+                        "Manage Members",
+                        cx.handler_for(&this, move |this, cx| this.manage_members(channel_id, cx)),
+                    )
+                    .entry(
+                        "Delete",
+                        cx.handler_for(&this, move |this, cx| this.remove_channel(channel_id, cx)),
+                    );
             }
 
-            // context_menu.show(
-            //     position.unwrap_or_default(),
-            //     if self.context_menu_on_selected {
-            //         gpui::elements::AnchorCorner::TopRight
-            //     } else {
-            //         gpui::elements::AnchorCorner::BottomLeft
-            //     },
-            //     items,
-            //     cx,
-            // );
-
             context_menu
         });
 
-        self.context_menu = Some((menu, (), ()));
+        cx.focus_view(&context_menu);
+        let subscription = cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
+            this.context_menu.take();
+            cx.notify();
+        });
+        self.context_menu = Some((context_menu, position, subscription));
 
         cx.notify();
     }
@@ -2074,55 +2068,52 @@ impl CollabPanel {
         self.collapsed_channels
             .retain(|channel| *channel != channel_id);
         self.channel_editing_state = Some(ChannelEditingState::Create {
-            location: Some(action.location.to_owned()),
+            location: Some(channel_id),
             pending_name: None,
         });
         self.update_entries(false, cx);
         self.select_channel_editor();
-        cx.focus(self.channel_name_editor.as_any());
+        cx.focus_view(&self.channel_name_editor);
         cx.notify();
     }
 
-    //     fn invite_members(&mut self, action: &InviteMembers, cx: &mut ViewContext<Self>) {
-    //         self.show_channel_modal(action.channel_id, channel_modal::Mode::InviteMembers, cx);
-    //     }
+    fn invite_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
+        todo!();
+        // self.show_channel_modal(channel_id, channel_modal::Mode::InviteMembers, cx);
+    }
 
-    //     fn manage_members(&mut self, action: &ManageMembers, cx: &mut ViewContext<Self>) {
-    //         self.show_channel_modal(action.channel_id, channel_modal::Mode::ManageMembers, cx);
-    //     }
+    fn manage_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
+        todo!();
+        // self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, cx);
+    }
 
-    //     fn remove(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
-    //         if let Some(channel) = self.selected_channel() {
-    //             self.remove_channel(channel.id, cx)
-    //         }
-    //     }
+    fn remove_selected_channel(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
+        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() {
-    //             self.rename_channel(
-    //                 &RenameChannel {
-    //                     channel_id: channel.id,
-    //                 },
-    //                 cx,
-    //             );
-    //         }
-    //     }
+    fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
+        if let Some(channel) = self.selected_channel() {
+            self.rename_channel(channel.id, cx);
+        }
+    }
 
-    fn rename_channel(&mut self, action: &RenameChannel, cx: &mut ViewContext<Self>) {
+    fn rename_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
         let channel_store = self.channel_store.read(cx);
-        if !channel_store.is_channel_admin(action.channel_id) {
+        if !channel_store.is_channel_admin(channel_id) {
             return;
         }
-        if let Some(channel) = channel_store.channel_for_id(action.channel_id).cloned() {
+        if let Some(channel) = channel_store.channel_for_id(channel_id).cloned() {
             self.channel_editing_state = Some(ChannelEditingState::Rename {
-                location: action.channel_id.to_owned(),
+                location: channel_id,
                 pending_name: None,
             });
             self.channel_name_editor.update(cx, |editor, cx| {
                 editor.set_text(channel.name.clone(), cx);
                 editor.select_all(&Default::default(), cx);
             });
-            cx.focus(self.channel_name_editor.as_any());
+            cx.focus_view(&self.channel_name_editor);
             self.update_entries(false, cx);
             self.select_channel_editor();
         }
@@ -2140,7 +2131,21 @@ impl CollabPanel {
         }
     }
 
-    fn open_channel_notes(&mut self, action: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
+    fn move_channel_on_clipboard(
+        &mut self,
+        to_channel_id: ChannelId,
+        cx: &mut ViewContext<CollabPanel>,
+    ) {
+        if let Some(clipboard) = self.channel_clipboard.take() {
+            self.channel_store.update(cx, |channel_store, cx| {
+                channel_store
+                    .move_channel(clipboard.channel_id, Some(to_channel_id), cx)
+                    .detach_and_log_err(cx)
+            })
+        }
+    }
+
+    fn open_channel_notes(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
         if let Some(workspace) = self.workspace.upgrade() {
             todo!();
             // ChannelView::open(action.channel_id, workspace, cx).detach();
@@ -2201,35 +2206,29 @@ impl CollabPanel {
     //         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!(
-    //                 "Are you sure you want to remove the channel \"{}\"?",
-    //                 channel.name
-    //             );
-    //             let mut answer =
-    //                 cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
-    //             let window = cx.window();
-    //             cx.spawn(|this, mut cx| async move {
-    //                 if answer.next().await == Some(0) {
-    //                     if let Err(e) = channel_store
-    //                         .update(&mut cx, |channels, _| channels.remove_channel(channel_id))
-    //                         .await
-    //                     {
-    //                         window.prompt(
-    //                             PromptLevel::Info,
-    //                             &format!("Failed to remove channel: {}", e),
-    //                             &["Ok"],
-    //                             &mut cx,
-    //                         );
-    //                     }
-    //                     this.update(&mut cx, |_, cx| cx.focus_self()).ok();
-    //                 }
-    //             })
-    //             .detach();
-    //         }
-    //     }
+    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!(
+                "Are you sure you want to remove the channel \"{}\"?",
+                channel.name
+            );
+            let mut answer =
+                cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
+            let window = cx.window();
+            cx.spawn(|this, mut cx| async move {
+                if answer.await? == 0 {
+                    channel_store
+                        .update(&mut cx, |channels, _| channels.remove_channel(channel_id))?
+                        .await
+                        .notify_async_err(&mut cx);
+                    this.update(&mut cx, |_, cx| cx.focus_self()).ok();
+                }
+                anyhow::Ok(())
+            })
+            .detach();
+        }
+    }
 
     //     // Should move to the filter editor if clicking on it
     //     // Should move selection to the channel editor if activating it
@@ -2314,15 +2313,16 @@ impl CollabPanel {
         let Some(workspace) = self.workspace.upgrade() else {
             return;
         };
-        cx.defer(move |cx| {
+        cx.window_context().defer(move |cx| {
             workspace.update(cx, |workspace, cx| {
-                if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
-                    panel.update(cx, |panel, cx| {
-                        panel
-                            .select_channel(channel_id, None, cx)
-                            .detach_and_log_err(cx);
-                    });
-                }
+                todo!();
+                // if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
+                //     panel.update(cx, |panel, cx| {
+                //         panel
+                //             .select_channel(channel_id, None, cx)
+                //             .detach_and_log_err(cx);
+                //     });
+                // }
             });
         });
     }
@@ -2354,37 +2354,41 @@ impl CollabPanel {
     fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> List {
         let is_selected = false; // todo!() this.selection == Some(ix);
 
-        List::new().children(self.entries.clone().into_iter().map(|entry| {
-            match entry {
-                ListEntry::Header(section) => {
-                    let is_collapsed = self.collapsed_sections.contains(&section);
-                    self.render_header(section, is_selected, is_collapsed, cx)
-                        .into_any_element()
-                }
-                ListEntry::Contact { contact, calling } => self
-                    .render_contact(&*contact, calling, is_selected, cx)
-                    .into_any_element(),
-                ListEntry::ContactPlaceholder => self
-                    .render_contact_placeholder(is_selected, cx)
-                    .into_any_element(),
-                ListEntry::IncomingRequest(user) => self
-                    .render_contact_request(user, true, is_selected, cx)
-                    .into_any_element(),
-                ListEntry::OutgoingRequest(user) => self
-                    .render_contact_request(user, false, is_selected, cx)
-                    .into_any_element(),
-                ListEntry::Channel {
-                    channel,
-                    depth,
-                    has_children,
-                } => self
-                    .render_channel(&*channel, depth, has_children, is_selected, cx)
-                    .into_any_element(),
-                ListEntry::ChannelEditor { depth } => {
-                    self.render_channel_editor(depth, cx).into_any_element()
-                }
-            }
-        }))
+        List::new().children(
+            self.entries
+                .clone()
+                .into_iter()
+                .enumerate()
+                .map(|(ix, entry)| match entry {
+                    ListEntry::Header(section) => {
+                        let is_collapsed = self.collapsed_sections.contains(&section);
+                        self.render_header(section, is_selected, is_collapsed, cx)
+                            .into_any_element()
+                    }
+                    ListEntry::Contact { contact, calling } => self
+                        .render_contact(&*contact, calling, is_selected, cx)
+                        .into_any_element(),
+                    ListEntry::ContactPlaceholder => self
+                        .render_contact_placeholder(is_selected, cx)
+                        .into_any_element(),
+                    ListEntry::IncomingRequest(user) => self
+                        .render_contact_request(user, true, is_selected, cx)
+                        .into_any_element(),
+                    ListEntry::OutgoingRequest(user) => self
+                        .render_contact_request(user, false, is_selected, cx)
+                        .into_any_element(),
+                    ListEntry::Channel {
+                        channel,
+                        depth,
+                        has_children,
+                    } => self
+                        .render_channel(&*channel, depth, has_children, is_selected, ix, cx)
+                        .into_any_element(),
+                    ListEntry::ChannelEditor { depth } => {
+                        self.render_channel_editor(depth, cx).into_any_element()
+                    }
+                }),
+        )
     }
 
     fn render_header(
@@ -2713,6 +2717,7 @@ impl CollabPanel {
         depth: usize,
         has_children: bool,
         is_selected: bool,
+        ix: usize,
         cx: &mut ViewContext<Self>,
     ) -> impl IntoElement {
         let channel_id = channel.id;
@@ -2769,6 +2774,7 @@ impl CollabPanel {
         div().group("").child(
             ListItem::new(channel_id as usize)
                 .indent_level(depth)
+                .indent_step_size(cx.rem_size() * 14.0 / 16.0) // @todo()! @nate this is to  step over the disclosure toggle
                 .left_icon(if is_public { Icon::Public } else { Icon::Hash })
                 .selected(is_selected || is_active)
                 .child(
@@ -2829,14 +2835,14 @@ impl CollabPanel {
                 .on_click(cx.listener(move |this, _, cx| {
                     if this.drag_target_channel == ChannelDragTarget::None {
                         if is_active {
-                            this.open_channel_notes(&OpenChannelNotes { channel_id }, cx)
+                            this.open_channel_notes(channel_id, cx)
                         } else {
                             this.join_channel(channel_id, cx)
                         }
                     }
                 }))
-                .on_secondary_mouse_down(cx.listener(|this, _, cx| {
-                    todo!() // open context menu
+                .on_secondary_mouse_down(cx.listener(move |this, event: &MouseDownEvent, cx| {
+                    this.deploy_channel_context_menu(event.position, channel_id, ix, cx)
                 })),
         )
 
@@ -3235,6 +3241,12 @@ impl Render for CollabPanel {
                     el.child(self.render_signed_in(cx))
                 }
             })
+            .children(self.context_menu.as_ref().map(|(menu, position, _)| {
+                overlay()
+                    .position(*position)
+                    .anchor(gpui::AnchorCorner::TopLeft)
+                    .child(menu.clone())
+            }))
     }
 }
 

crates/project_panel2/src/project_panel.rs 🔗

@@ -1480,7 +1480,7 @@ impl Render for ProjectPanel {
                 .children(self.context_menu.as_ref().map(|(menu, position, _)| {
                     overlay()
                         .position(*position)
-                        .anchor(gpui::AnchorCorner::BottomLeft)
+                        .anchor(gpui::AnchorCorner::TopLeft)
                         .child(menu.clone())
                 }))
         } else {