Add missing collab panel features (#3719)

Max Brunsfeld created

* channel modal
* channel invites in collab panel

Change summary

crates/collab_ui2/src/collab_panel.rs                | 718 +++----------
crates/collab_ui2/src/collab_panel/channel_modal.rs  | 572 ++++------
crates/collab_ui2/src/collab_panel/contact_finder.rs |   1 
crates/gpui2/src/window.rs                           |   7 
crates/picker2/src/picker2.rs                        |   2 
crates/ui2/src/components/context_menu.rs            |   5 
crates/ui2/src/components/icon.rs                    |   2 
7 files changed, 395 insertions(+), 912 deletions(-)

Detailed changes

crates/collab_ui2/src/collab_panel.rs 🔗

@@ -1,129 +1,45 @@
-#![allow(unused)]
 mod channel_modal;
 mod contact_finder;
 
-// use crate::{
-//     channel_view::{self, ChannelView},
-//     chat_panel::ChatPanel,
-//     face_pile::FacePile,
-//     panel_settings, CollaborationPanelSettings,
-// };
-// use anyhow::Result;
-// use call::ActiveCall;
-// use channel::{Channel, ChannelEvent, ChannelId, ChannelStore};
-// use channel_modal::ChannelModal;
-// use client::{
-//     proto::{self, PeerId},
-//     Client, Contact, User, UserStore,
-// };
+use self::channel_modal::ChannelModal;
+use crate::{
+    channel_view::ChannelView, chat_panel::ChatPanel, face_pile::FacePile,
+    CollaborationPanelSettings,
+};
+use call::ActiveCall;
+use channel::{Channel, ChannelEvent, ChannelId, ChannelStore};
+use client::{Client, Contact, User, UserStore};
 use contact_finder::ContactFinder;
+use db::kvp::KEY_VALUE_STORE;
+use editor::Editor;
+use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
+use fuzzy::{match_strings, StringMatchCandidate};
+use gpui::{
+    actions, canvas, div, fill, list, overlay, point, prelude::*, px, serde_json, AnyElement,
+    AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, EventEmitter,
+    FocusHandle, Focusable, FocusableView, InteractiveElement, IntoElement, ListOffset, ListState,
+    Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, RenderOnce,
+    SharedString, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView,
+};
 use menu::{Cancel, Confirm, SelectNext, SelectPrev};
+use project::{Fs, Project};
 use rpc::proto::{self, PeerId};
+use serde_derive::{Deserialize, Serialize};
+use settings::{Settings, SettingsStore};
 use smallvec::SmallVec;
+use std::{mem, sync::Arc};
 use theme::{ActiveTheme, ThemeSettings};
-// use context_menu::{ContextMenu, ContextMenuItem};
-// use db::kvp::KEY_VALUE_STORE;
-// use drag_and_drop::{DragAndDrop, Draggable};
-// use editor::{Cancel, Editor};
-// use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
-// use futures::StreamExt;
-// use fuzzy::{match_strings, StringMatchCandidate};
-// use gpui::{
-//     actions,
-//     elements::{
-//         Canvas, ChildView, Component, ContainerStyle, Empty, Flex, Image, Label, List, ListOffset,
-//         ListState, MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement,
-//         SafeStylable, Stack, Svg,
-//     },
-//     fonts::TextStyle,
-//     geometry::{
-//         rect::RectF,
-//         vector::{vec2f, Vector2F},
-//     },
-//     impl_actions,
-//     platform::{CursorStyle, MouseButton, PromptLevel},
-//     serde_json, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, FontCache,
-//     ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
-// };
-// use menu::{Confirm, SelectNext, SelectPrev};
-// use project::{Fs, Project};
-// use serde_derive::{Deserialize, Serialize};
-// use settings::SettingsStore;
-// use std::{borrow::Cow, hash::Hash, mem, sync::Arc};
-// use theme::{components::ComponentExt, IconButton, Interactive};
-// use util::{maybe, ResultExt, TryFutureExt};
-// use workspace::{
-//     dock::{DockPosition, Panel},
-//     item::ItemHandle,
-//     FollowNextCollaborator, Workspace,
-// };
-
-// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-// struct ToggleCollapse {
-//     location: ChannelId,
-// }
-
-// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-// struct NewChannel {
-//     location: ChannelId,
-// }
-
-// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-// struct RenameChannel {
-//     channel_id: ChannelId,
-// }
-
-// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-// struct ToggleSelectedIx {
-//     ix: usize,
-// }
-
-// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-// struct RemoveChannel {
-//     channel_id: ChannelId,
-// }
-
-// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-// struct InviteMembers {
-//     channel_id: ChannelId,
-// }
-
-// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-// struct ManageMembers {
-//     channel_id: ChannelId,
-// }
-
-#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]
-pub struct OpenChannelNotes {
-    pub channel_id: ChannelId,
-}
-
-// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-// pub struct JoinChannelCall {
-//     pub channel_id: u64,
-// }
-
-// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-// pub struct JoinChannelChat {
-//     pub channel_id: u64,
-// }
-
-// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-// pub struct CopyChannelLink {
-//     pub channel_id: u64,
-// }
-
-// #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-// struct StartMoveChannelFor {
-//     channel_id: ChannelId,
-// }
-
-// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-// struct MoveChannel {
-//     to: ChannelId,
-// }
-
-impl_actions!(collab_panel, [OpenChannelNotes]);
+use ui::prelude::*;
+use ui::{
+    h_stack, v_stack, Avatar, Button, Color, ContextMenu, Icon, IconButton, IconElement, IconSize,
+    Label, ListHeader, ListItem, Tooltip,
+};
+use util::{maybe, ResultExt, TryFutureExt};
+use workspace::{
+    dock::{DockPosition, Panel, PanelEvent},
+    notifications::NotifyResultExt,
+    Workspace,
+};
 
 actions!(
     collab_panel,
@@ -139,25 +55,6 @@ actions!(
     ]
 );
 
-// impl_actions!(
-//     collab_panel,
-//     [
-//         RemoveChannel,
-//         NewChannel,
-//         InviteMembers,
-//         ManageMembers,
-//         RenameChannel,
-//         ToggleCollapse,
-//         OpenChannelNotes,
-//         JoinChannelCall,
-//         JoinChannelChat,
-//         CopyChannelLink,
-//         StartMoveChannelFor,
-//         MoveChannel,
-//         ToggleSelectedIx
-//     ]
-// );
-
 #[derive(Debug, Copy, Clone, PartialEq, Eq)]
 struct ChannelMoveClipboard {
     channel_id: ChannelId,
@@ -165,44 +62,6 @@ struct ChannelMoveClipboard {
 
 const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
 
-use std::{mem, sync::Arc};
-
-use call::ActiveCall;
-use channel::{Channel, ChannelEvent, ChannelId, ChannelStore};
-use client::{Client, Contact, User, UserStore};
-use db::kvp::KEY_VALUE_STORE;
-use editor::Editor;
-use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
-use fuzzy::{match_strings, StringMatchCandidate};
-use gpui::{
-    actions, canvas, div, fill, impl_actions, list, overlay, point, prelude::*, px, serde_json,
-    AnyElement, AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div,
-    EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement, IntoElement,
-    ListOffset, ListState, Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel,
-    Render, RenderOnce, SharedString, Styled, Subscription, Task, View, ViewContext, VisualContext,
-    WeakView,
-};
-use project::{Fs, Project};
-use serde_derive::{Deserialize, Serialize};
-use settings::{Settings, SettingsStore};
-use ui::prelude::*;
-use ui::{
-    h_stack, v_stack, Avatar, Button, Color, ContextMenu, Icon, IconButton, IconElement, IconSize,
-    Label, ListHeader, ListItem, Tooltip,
-};
-use util::{maybe, ResultExt, TryFutureExt};
-use workspace::{
-    dock::{DockPosition, Panel, PanelEvent},
-    notifications::NotifyResultExt,
-    Workspace,
-};
-
-use crate::channel_view::ChannelView;
-use crate::chat_panel::ChatPanel;
-use crate::{face_pile::FacePile, CollaborationPanelSettings};
-
-use self::channel_modal::ChannelModal;
-
 pub fn init(cx: &mut AppContext) {
     cx.observe_new_views(|workspace: &mut Workspace, _| {
         workspace.register_action(|workspace, _: &ToggleFocus, cx| {
@@ -210,69 +69,6 @@ pub fn init(cx: &mut AppContext) {
         });
     })
     .detach();
-    //     contact_finder::init(cx);
-    //     channel_modal::init(cx);
-    //     channel_view::init(cx);
-
-    //     cx.add_action(CollabPanel::cancel);
-    //     cx.add_action(CollabPanel::select_next);
-    //     cx.add_action(CollabPanel::select_prev);
-    //     cx.add_action(CollabPanel::confirm);
-    //     cx.add_action(CollabPanel::insert_space);
-    //     cx.add_action(CollabPanel::remove);
-    //     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);
-    //     cx.add_action(CollabPanel::toggle_channel_collapsed_action);
-    //     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::join_channel_chat);
-    //     cx.add_action(CollabPanel::copy_channel_link);
-
-    //     cx.add_action(
-    //         |panel: &mut CollabPanel, action: &ToggleSelectedIx, cx: &mut ViewContext<CollabPanel>| {
-    //             if panel.selection.take() != Some(action.ix) {
-    //                 panel.selection = Some(action.ix)
-    //             }
-
-    //             cx.notify();
-    //         },
-    //     );
-
-    //     cx.add_action(
-    //         |panel: &mut CollabPanel, _: &MoveSelected, cx: &mut ViewContext<CollabPanel>| {
-    //             let Some(clipboard) = panel.channel_clipboard.take() else {
-    //                 return;
-    //             };
-    //             let Some(selected_channel) = panel.selected_channel() else {
-    //                 return;
-    //             };
-
-    //             panel
-    //                 .channel_store
-    //                 .update(cx, |channel_store, cx| {
-    //                     channel_store.move_channel(clipboard.channel_id, Some(selected_channel.id), cx)
-    //                 })
-    //                 .detach_and_log_err(cx)
-    //         },
-    //     );
-
-    //     cx.add_action(
-    //         |panel: &mut CollabPanel, action: &MoveChannel, cx: &mut ViewContext<CollabPanel>| {
-    //             if let Some(clipboard) = panel.channel_clipboard.take() {
-    //                 panel.channel_store.update(cx, |channel_store, cx| {
-    //                     channel_store
-    //                         .move_channel(clipboard.channel_id, Some(action.to), cx)
-    //                         .detach_and_log_err(cx)
-    //                 })
-    //             }
-    //         },
-    //     );
 }
 
 #[derive(Debug)]
@@ -317,16 +113,7 @@ pub struct CollabPanel {
     subscriptions: Vec<Subscription>,
     collapsed_sections: Vec<Section>,
     collapsed_channels: Vec<ChannelId>,
-    drag_target_channel: ChannelDragTarget,
     workspace: WeakView<Workspace>,
-    // context_menu_on_selected: bool,
-}
-
-#[derive(PartialEq, Eq)]
-enum ChannelDragTarget {
-    None,
-    Root,
-    Channel(ChannelId),
 }
 
 #[derive(Serialize, Deserialize)]
@@ -335,13 +122,6 @@ struct SerializedCollabPanel {
     collapsed_channels: Option<Vec<u64>>,
 }
 
-// #[derive(Debug)]
-// pub enum Event {
-//     DockPositionChanged,
-//     Focus,
-//     Dismissed,
-// }
-
 #[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
 enum Section {
     ActiveCall,
@@ -373,7 +153,7 @@ enum ListEntry {
     },
     IncomingRequest(Arc<User>),
     OutgoingRequest(Arc<User>),
-    //     ChannelInvite(Arc<Channel>),
+    ChannelInvite(Arc<Channel>),
     Channel {
         channel: Arc<Channel>,
         depth: usize,
@@ -472,8 +252,6 @@ impl CollabPanel {
                 collapsed_channels: Vec::default(),
                 workspace: workspace.weak_handle(),
                 client: workspace.app_state().client.clone(),
-                //                 context_menu_on_selected: true,
-                drag_target_channel: ChannelDragTarget::None,
             };
 
             this.update_entries(false, cx);
@@ -529,9 +307,6 @@ impl CollabPanel {
         })
     }
 
-    fn contacts(&self, cx: &AppContext) -> Option<Vec<Arc<Contact>>> {
-        Some(self.user_store.read(cx).contacts().to_owned())
-    }
     pub async fn load(
         workspace: WeakView<Workspace>,
         mut cx: AsyncWindowContext,
@@ -808,37 +583,37 @@ impl CollabPanel {
                 }
             }
 
-            //             let channel_invites = channel_store.channel_invitations();
-            //             if !channel_invites.is_empty() {
-            //                 self.match_candidates.clear();
-            //                 self.match_candidates
-            //                     .extend(channel_invites.iter().enumerate().map(|(ix, channel)| {
-            //                         StringMatchCandidate {
-            //                             id: ix,
-            //                             string: channel.name.clone(),
-            //                             char_bag: channel.name.chars().collect(),
-            //                         }
-            //                     }));
-            //                 let matches = executor.block(match_strings(
-            //                     &self.match_candidates,
-            //                     &query,
-            //                     true,
-            //                     usize::MAX,
-            //                     &Default::default(),
-            //                     executor.clone(),
-            //                 ));
-            //                 request_entries.extend(matches.iter().map(|mat| {
-            //                     ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone())
-            //                 }));
-
-            //                 if !request_entries.is_empty() {
-            //                     self.entries
-            //                         .push(ListEntry::Header(Section::ChannelInvites));
-            //                     if !self.collapsed_sections.contains(&Section::ChannelInvites) {
-            //                         self.entries.append(&mut request_entries);
-            //                     }
-            //                 }
-            //             }
+            let channel_invites = channel_store.channel_invitations();
+            if !channel_invites.is_empty() {
+                self.match_candidates.clear();
+                self.match_candidates
+                    .extend(channel_invites.iter().enumerate().map(|(ix, channel)| {
+                        StringMatchCandidate {
+                            id: ix,
+                            string: channel.name.clone().into(),
+                            char_bag: channel.name.chars().collect(),
+                        }
+                    }));
+                let matches = executor.block(match_strings(
+                    &self.match_candidates,
+                    &query,
+                    true,
+                    usize::MAX,
+                    &Default::default(),
+                    executor.clone(),
+                ));
+                request_entries.extend(matches.iter().map(|mat| {
+                    ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone())
+                }));
+
+                if !request_entries.is_empty() {
+                    self.entries
+                        .push(ListEntry::Header(Section::ChannelInvites));
+                    if !self.collapsed_sections.contains(&Section::ChannelInvites) {
+                        self.entries.append(&mut request_entries);
+                    }
+                }
+            }
         }
 
         self.entries.push(ListEntry::Header(Section::Contacts));
@@ -1049,9 +824,7 @@ impl CollabPanel {
             } else if is_current_user {
                 IconButton::new("leave-call", Icon::Exit)
                     .style(ButtonStyle::Subtle)
-                    .on_click(cx.listener(move |this, _, cx| {
-                        Self::leave_call(cx);
-                    }))
+                    .on_click(move |_, cx| Self::leave_call(cx))
                     .tooltip(|cx| Tooltip::text("Leave Call", cx))
                     .into_any_element()
             } else {
@@ -1061,7 +834,8 @@ impl CollabPanel {
                 this.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
                     .on_click(cx.listener(move |this, _, cx| {
                         this.workspace
-                            .update(cx, |workspace, cx| workspace.follow(peer_id, cx));
+                            .update(cx, |workspace, cx| workspace.follow(peer_id, cx))
+                            .ok();
                     }))
             })
     }
@@ -1073,7 +847,7 @@ impl CollabPanel {
         host_user_id: u64,
         //         is_current: bool,
         is_last: bool,
-        //         is_selected: bool,
+        is_selected: bool,
         //         theme: &theme::Theme,
         cx: &mut ViewContext<Self>,
     ) -> impl IntoElement {
@@ -1084,15 +858,16 @@ impl CollabPanel {
         }
         .into();
 
-        let theme = cx.theme();
-
         ListItem::new(project_id as usize)
+            .selected(is_selected)
             .on_click(cx.listener(move |this, _, cx| {
-                this.workspace.update(cx, |workspace, cx| {
-                    let app_state = workspace.app_state().clone();
-                    workspace::join_remote_project(project_id, host_user_id, app_state, cx)
-                        .detach_and_log_err(cx);
-                });
+                this.workspace
+                    .update(cx, |workspace, cx| {
+                        let app_state = workspace.app_state().clone();
+                        workspace::join_remote_project(project_id, host_user_id, app_state, cx)
+                            .detach_and_log_err(cx);
+                    })
+                    .ok();
             }))
             .start_slot(
                 h_stack()
@@ -1102,95 +877,19 @@ impl CollabPanel {
             )
             .child(Label::new(project_name.clone()))
             .tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx))
-
-        //         enum JoinProject {}
-        //         enum JoinProjectTooltip {}
-
-        //         let collab_theme = &theme.collab_panel;
-        //         let host_avatar_width = collab_theme
-        //             .contact_avatar
-        //             .width
-        //             .or(collab_theme.contact_avatar.height)
-        //             .unwrap_or(0.);
-        //         let tree_branch = collab_theme.tree_branch;
-
-        //         let content =
-        //             MouseEventHandler::new::<JoinProject, _>(project_id as usize, cx, |mouse_state, cx| {
-        //                 let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
-        //                 let row = if is_current {
-        //                     collab_theme
-        //                         .project_row
-        //                         .in_state(true)
-        //                         .style_for(&mut Default::default())
-        //                 } else {
-        //                     collab_theme
-        //                         .project_row
-        //                         .in_state(is_selected)
-        //                         .style_for(mouse_state)
-        //                 };
-
-        //                 Flex::row()
-        //                     .with_child(render_tree_branch(
-        //                         tree_branch,
-        //                         &row.name.text,
-        //                         is_last,
-        //                         vec2f(host_avatar_width, collab_theme.row_height),
-        //                         cx.font_cache(),
-        //                     ))
-        //                     .with_child(
-        //                         Svg::new("icons/file_icons/folder.svg")
-        //                             .with_color(collab_theme.channel_hash.color)
-        //                             .constrained()
-        //                             .with_width(collab_theme.channel_hash.width)
-        //                             .aligned()
-        //                             .left(),
-        //                     )
-        //                     .with_child(
-        //                         Label::new(project_name.clone(), row.name.text.clone())
-        //                             .aligned()
-        //                             .left()
-        //                             .contained()
-        //                             .with_style(row.name.container)
-        //                             .flex(1., false),
-        //                     )
-        //                     .constrained()
-        //                     .with_height(collab_theme.row_height)
-        //                     .contained()
-        //                     .with_style(row.container)
-        //             });
-
-        //         if is_current {
-        //             return content.into_any();
-        //         }
-
-        //         content
-        //             .with_cursor_style(CursorStyle::PointingHand)
-        //             .on_click(MouseButton::Left, move |_, this, cx| {
-        //                 if let Some(workspace) = this.workspace.upgrade(cx) {
-        //                     let app_state = workspace.read(cx).app_state().clone();
-        //                     workspace::join_remote_project(project_id, host_user_id, app_state, cx)
-        //                         .detach_and_log_err(cx);
-        //                 }
-        //             })
-        //             .with_tooltip::<JoinProjectTooltip>(
-        //                 project_id as usize,
-        //                 format!("Open {}", project_name),
-        //                 None,
-        //                 theme.tooltip.clone(),
-        //                 cx,
-        //             )
-        //             .into_any()
     }
 
     fn render_participant_screen(
         &self,
         peer_id: Option<PeerId>,
         is_last: bool,
+        is_selected: bool,
         cx: &mut ViewContext<Self>,
     ) -> impl IntoElement {
         let id = peer_id.map_or(usize::MAX, |id| id.as_u64() as usize);
 
         ListItem::new(("screen", id))
+            .selected(is_selected)
             .start_slot(
                 h_stack()
                     .gap_1()
@@ -1200,9 +899,11 @@ impl CollabPanel {
             .child(Label::new("Screen"))
             .when_some(peer_id, |this, _| {
                 this.on_click(cx.listener(move |this, _, cx| {
-                    this.workspace.update(cx, |workspace, cx| {
-                        workspace.open_shared_screen(peer_id.unwrap(), cx)
-                    });
+                    this.workspace
+                        .update(cx, |workspace, cx| {
+                            workspace.open_shared_screen(peer_id.unwrap(), cx)
+                        })
+                        .ok();
                 }))
                 .tooltip(move |cx| Tooltip::text(format!("Open shared screen"), cx))
             })
@@ -1219,40 +920,6 @@ impl CollabPanel {
         }
     }
 
-    //     fn render_contact_placeholder(
-    //         &self,
-    //         theme: &theme::CollabPanel,
-    //         is_selected: bool,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> AnyElement<Self> {
-    //         enum AddContacts {}
-    //         MouseEventHandler::new::<AddContacts, _>(0, cx, |state, _| {
-    //             let style = theme.list_empty_state.style_for(is_selected, state);
-    //             Flex::row()
-    //                 .with_child(
-    //                     Svg::new("icons/plus.svg")
-    //                         .with_color(theme.list_empty_icon.color)
-    //                         .constrained()
-    //                         .with_width(theme.list_empty_icon.width)
-    //                         .aligned()
-    //                         .left(),
-    //                 )
-    //                 .with_child(
-    //                     Label::new("Add a contact", style.text.clone())
-    //                         .contained()
-    //                         .with_style(theme.list_empty_label_container),
-    //                 )
-    //                 .align_children_center()
-    //                 .contained()
-    //                 .with_style(style.container)
-    //                 .into_any()
-    //         })
-    //         .on_click(MouseButton::Left, |_, this, cx| {
-    //             this.toggle_contact_finder(cx);
-    //         })
-    //         .into_any()
-    //     }
-
     fn render_channel_notes(
         &self,
         channel_id: ChannelId,
@@ -1291,86 +958,6 @@ impl CollabPanel {
             .tooltip(move |cx| Tooltip::text("Open Chat", cx))
     }
 
-    //     fn render_channel_invite(
-    //         channel: Arc<Channel>,
-    //         channel_store: ModelHandle<ChannelStore>,
-    //         theme: &theme::CollabPanel,
-    //         is_selected: bool,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> AnyElement<Self> {
-    //         enum Decline {}
-    //         enum Accept {}
-
-    //         let channel_id = channel.id;
-    //         let is_invite_pending = channel_store
-    //             .read(cx)
-    //             .has_pending_channel_invite_response(&channel);
-    //         let button_spacing = theme.contact_button_spacing;
-
-    //         Flex::row()
-    //             .with_child(
-    //                 Svg::new("icons/hash.svg")
-    //                     .with_color(theme.channel_hash.color)
-    //                     .constrained()
-    //                     .with_width(theme.channel_hash.width)
-    //                     .aligned()
-    //                     .left(),
-    //             )
-    //             .with_child(
-    //                 Label::new(channel.name.clone(), theme.contact_username.text.clone())
-    //                     .contained()
-    //                     .with_style(theme.contact_username.container)
-    //                     .aligned()
-    //                     .left()
-    //                     .flex(1., true),
-    //             )
-    //             .with_child(
-    //                 MouseEventHandler::new::<Decline, _>(channel.id as usize, cx, |mouse_state, _| {
-    //                     let button_style = if is_invite_pending {
-    //                         &theme.disabled_button
-    //                     } else {
-    //                         theme.contact_button.style_for(mouse_state)
-    //                     };
-    //                     render_icon_button(button_style, "icons/x.svg").aligned()
-    //                 })
-    //                 .with_cursor_style(CursorStyle::PointingHand)
-    //                 .on_click(MouseButton::Left, move |_, this, cx| {
-    //                     this.respond_to_channel_invite(channel_id, false, cx);
-    //                 })
-    //                 .contained()
-    //                 .with_margin_right(button_spacing),
-    //             )
-    //             .with_child(
-    //                 MouseEventHandler::new::<Accept, _>(channel.id as usize, cx, |mouse_state, _| {
-    //                     let button_style = if is_invite_pending {
-    //                         &theme.disabled_button
-    //                     } else {
-    //                         theme.contact_button.style_for(mouse_state)
-    //                     };
-    //                     render_icon_button(button_style, "icons/check.svg")
-    //                         .aligned()
-    //                         .flex_float()
-    //                 })
-    //                 .with_cursor_style(CursorStyle::PointingHand)
-    //                 .on_click(MouseButton::Left, move |_, this, cx| {
-    //                     this.respond_to_channel_invite(channel_id, true, cx);
-    //                 }),
-    //             )
-    //             .constrained()
-    //             .with_height(theme.row_height)
-    //             .contained()
-    //             .with_style(
-    //                 *theme
-    //                     .contact_row
-    //                     .in_state(is_selected)
-    //                     .style_for(&mut Default::default()),
-    //             )
-    //             .with_padding_left(
-    //                 theme.contact_row.default_style().padding.left + theme.channel_indent,
-    //             )
-    //             .into_any()
-    //     }
-
     fn has_subchannels(&self, ix: usize) -> bool {
         self.entries.get(ix).map_or(false, |entry| {
             if let ListEntry::Channel { has_children, .. } = entry {
@@ -1724,7 +1311,7 @@ impl CollabPanel {
         self.collapsed_channels.binary_search(&channel_id).is_ok()
     }
 
-    fn leave_call(cx: &mut ViewContext<Self>) {
+    fn leave_call(cx: &mut WindowContext) {
         ActiveCall::global(cx)
             .update(cx, |call, cx| call.hang_up(cx))
             .detach_and_log_err(cx);
@@ -1813,15 +1400,13 @@ impl CollabPanel {
         }
     }
 
-    fn start_move_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
+    fn start_move_channel(&mut self, channel_id: ChannelId, _cx: &mut ViewContext<Self>) {
         self.channel_clipboard = Some(ChannelMoveClipboard { channel_id });
     }
 
-    fn start_move_selected_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
+    fn start_move_selected_channel(&mut self, _: &StartMoveChannel, cx: &mut ViewContext<Self>) {
         if let Some(channel) = self.selected_channel() {
-            self.channel_clipboard = Some(ChannelMoveClipboard {
-                channel_id: channel.id,
-            })
+            self.start_move_channel(channel.id, cx);
         }
     }
 
@@ -1900,10 +1485,6 @@ impl CollabPanel {
         .detach();
     }
 
-    //     fn remove_selected_channel(&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) {
@@ -1911,9 +1492,7 @@ impl CollabPanel {
                 "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();
+            let answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
             cx.spawn(|this, mut cx| async move {
                 if answer.await? == 0 {
                     channel_store
@@ -1928,17 +1507,13 @@ 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!(
             "Are you sure you want to remove \"{}\" from your contacts?",
             github_login
         );
-        let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
-        let window = cx.window();
+        let answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
         cx.spawn(|_, mut cx| async move {
             if answer.await? == 0 {
                 user_store
@@ -1964,18 +1539,18 @@ impl CollabPanel {
             .detach_and_log_err(cx);
     }
 
-    //     fn respond_to_channel_invite(
-    //         &mut self,
-    //         channel_id: u64,
-    //         accept: bool,
-    //         cx: &mut ViewContext<Self>,
-    //     ) {
-    //         self.channel_store
-    //             .update(cx, |store, cx| {
-    //                 store.respond_to_channel_invite(channel_id, accept, cx)
-    //             })
-    //             .detach();
-    //     }
+    fn respond_to_channel_invite(
+        &mut self,
+        channel_id: u64,
+        accept: bool,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.channel_store
+            .update(cx, |store, cx| {
+                store.respond_to_channel_invite(channel_id, accept, cx)
+            })
+            .detach();
+    }
 
     fn call(&mut self, recipient_user_id: u64, cx: &mut ViewContext<Self>) {
         ActiveCall::global(cx)
@@ -2096,6 +1671,9 @@ impl CollabPanel {
             ListEntry::ChannelEditor { depth } => {
                 self.render_channel_editor(*depth, cx).into_any_element()
             }
+            ListEntry::ChannelInvite(channel) => self
+                .render_channel_invite(channel, is_selected, cx)
+                .into_any_element(),
             ListEntry::CallParticipant {
                 user,
                 peer_id,
@@ -2114,11 +1692,12 @@ impl CollabPanel {
                     &worktree_root_names,
                     *host_user_id,
                     *is_last,
+                    is_selected,
                     cx,
                 )
                 .into_any_element(),
             ListEntry::ParticipantScreen { peer_id, is_last } => self
-                .render_participant_screen(*peer_id, *is_last, cx)
+                .render_participant_screen(*peer_id, *is_last, is_selected, cx)
                 .into_any_element(),
             ListEntry::ChannelNotes { channel_id } => self
                 .render_channel_notes(*channel_id, cx)
@@ -2233,7 +1812,7 @@ impl CollabPanel {
                 ListHeader::new(text)
                     .when(can_collapse, |header| {
                         header.toggle(Some(!is_collapsed)).on_toggle(cx.listener(
-                            move |this, event, cx| {
+                            move |this, _, cx| {
                                 this.toggle_section_expanded(section, cx);
                             },
                         ))
@@ -2265,8 +1844,9 @@ impl CollabPanel {
         let busy = contact.busy || calling;
         let user_id = contact.user.id;
         let github_login = SharedString::from(contact.user.github_login.clone());
-        let mut item =
+        let item =
             ListItem::new(github_login.clone())
+                .selected(is_selected)
                 .on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
                 .child(
                     h_stack()
@@ -2330,8 +1910,8 @@ impl CollabPanel {
     ) -> impl IntoElement {
         let github_login = SharedString::from(user.github_login.clone());
         let user_id = user.id;
-        let is_contact_request_pending = self.user_store.read(cx).is_contact_request_pending(&user);
-        let color = if is_contact_request_pending {
+        let is_response_pending = self.user_store.read(cx).is_contact_request_pending(&user);
+        let color = if is_response_pending {
             Color::Muted
         } else {
             Color::Default
@@ -2339,13 +1919,13 @@ impl CollabPanel {
 
         let controls = if is_incoming {
             vec![
-                IconButton::new("remove_contact", Icon::Close)
+                IconButton::new("decline-contact", Icon::Close)
                     .on_click(cx.listener(move |this, _, cx| {
                         this.respond_to_contact_request(user_id, false, cx);
                     }))
                     .icon_color(color)
                     .tooltip(|cx| Tooltip::text("Decline invite", cx)),
-                IconButton::new("remove_contact", Icon::Check)
+                IconButton::new("accept-contact", Icon::Check)
                     .on_click(cx.listener(move |this, _, cx| {
                         this.respond_to_contact_request(user_id, true, cx);
                     }))
@@ -2363,6 +1943,7 @@ impl CollabPanel {
         };
 
         ListItem::new(github_login.clone())
+            .selected(is_selected)
             .child(
                 h_stack()
                     .w_full()
@@ -2373,6 +1954,54 @@ impl CollabPanel {
             .start_slot(Avatar::new(user.avatar_uri.clone()))
     }
 
+    fn render_channel_invite(
+        &self,
+        channel: &Arc<Channel>,
+        is_selected: bool,
+        cx: &mut ViewContext<Self>,
+    ) -> impl IntoElement {
+        let channel_id = channel.id;
+        let response_is_pending = self
+            .channel_store
+            .read(cx)
+            .has_pending_channel_invite_response(&channel);
+        let color = if response_is_pending {
+            Color::Muted
+        } else {
+            Color::Default
+        };
+
+        let controls = [
+            IconButton::new("reject-invite", Icon::Close)
+                .on_click(cx.listener(move |this, _, cx| {
+                    this.respond_to_channel_invite(channel_id, false, cx);
+                }))
+                .icon_color(color)
+                .tooltip(|cx| Tooltip::text("Decline invite", cx)),
+            IconButton::new("accept-invite", Icon::Check)
+                .on_click(cx.listener(move |this, _, cx| {
+                    this.respond_to_channel_invite(channel_id, true, cx);
+                }))
+                .icon_color(color)
+                .tooltip(|cx| Tooltip::text("Accept invite", cx)),
+        ];
+
+        ListItem::new(("channel-invite", channel.id as usize))
+            .selected(is_selected)
+            .child(
+                h_stack()
+                    .w_full()
+                    .justify_between()
+                    .child(Label::new(channel.name.clone()))
+                    .child(h_stack().children(controls)),
+            )
+            .start_slot(
+                IconElement::new(Icon::Hash)
+                    .size(IconSize::Small)
+                    .color(Color::Muted),
+            )
+    }
+
     fn render_contact_placeholder(
         &self,
         is_selected: bool,
@@ -2411,7 +2040,6 @@ impl CollabPanel {
             .channel_for_id(channel_id)
             .map(|channel| channel.visibility)
             == Some(proto::ChannelVisibility::Public);
-        let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id);
         let disclosed =
             has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok());
 
@@ -2423,14 +2051,10 @@ impl CollabPanel {
 
         let face_pile = if !participants.is_empty() {
             let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
-            let user = &participants[0];
-
             let result = FacePile {
                 faces: participants
                     .iter()
-                    .filter_map(|user| {
-                        Some(Avatar::new(user.avatar_uri.clone()).into_any_element())
-                    })
+                    .map(|user| Avatar::new(user.avatar_uri.clone()).into_any_element())
                     .take(FACEPILE_LIMIT)
                     .chain(if extra_count > 0 {
                         // todo!() @nate - this label looks wrong.
@@ -2454,7 +2078,7 @@ impl CollabPanel {
             .flex()
             .w_full()
             .on_drag(channel.clone(), move |channel, cx| {
-                cx.build_view(|cx| DraggedChannelView {
+                cx.build_view(|_| DraggedChannelView {
                     channel: channel.clone(),
                     width,
                 })
@@ -2480,12 +2104,10 @@ impl CollabPanel {
                         }),
                     )
                     .on_click(cx.listener(move |this, _, cx| {
-                        if this.drag_target_channel == ChannelDragTarget::None {
-                            if is_active {
-                                this.open_channel_notes(channel_id, cx)
-                            } else {
-                                this.join_channel(channel_id, cx)
-                            }
+                        if is_active {
+                            this.open_channel_notes(channel_id, cx)
+                        } else {
+                            this.join_channel(channel_id, cx)
                         }
                     }))
                     .on_secondary_mouse_down(cx.listener(

crates/collab_ui2/src/collab_panel/channel_modal.rs 🔗

@@ -5,13 +5,13 @@ use client::{
 };
 use fuzzy::{match_strings, StringMatchCandidate};
 use gpui::{
-    actions, div, AppContext, ClipboardItem, DismissEvent, Div, Entity, EventEmitter,
-    FocusableView, Model, ParentElement, Render, Styled, Task, View, ViewContext, VisualContext,
-    WeakView,
+    actions, div, overlay, AppContext, ClipboardItem, DismissEvent, Div, EventEmitter,
+    FocusableView, Model, ParentElement, Render, Styled, Subscription, Task, View, ViewContext,
+    VisualContext, WeakView,
 };
 use picker::{Picker, PickerDelegate};
 use std::sync::Arc;
-use ui::prelude::*;
+use ui::{prelude::*, Avatar, Checkbox, ContextMenu, ListItem};
 use util::TryFutureExt;
 use workspace::ModalView;
 
@@ -25,19 +25,10 @@ actions!(
     ]
 );
 
-// pub fn init(cx: &mut AppContext) {
-//     Picker::<ChannelModalDelegate>::init(cx);
-//     cx.add_action(ChannelModal::toggle_mode);
-//     cx.add_action(ChannelModal::toggle_member_admin);
-//     cx.add_action(ChannelModal::remove_member);
-//     cx.add_action(ChannelModal::dismiss);
-// }
-
 pub struct ChannelModal {
     picker: View<Picker<ChannelModalDelegate>>,
     channel_store: Model<ChannelStore>,
     channel_id: ChannelId,
-    has_focus: bool,
 }
 
 impl ChannelModal {
@@ -62,25 +53,19 @@ impl ChannelModal {
                     channel_store: channel_store.clone(),
                     channel_id,
                     match_candidates: Vec::new(),
+                    context_menu: None,
                     members,
                     mode,
-                    // context_menu: cx.add_view(|cx| {
-                    //     let mut menu = ContextMenu::new(cx.view_id(), cx);
-                    //     menu.set_position_mode(OverlayPositionMode::Local);
-                    //     menu
-                    // }),
                 },
                 cx,
             )
+            .modal(false)
         });
 
-        let has_focus = picker.focus_handle(cx).contains_focused(cx);
-
         Self {
             picker,
             channel_store,
             channel_id,
-            has_focus,
         }
     }
 
@@ -126,15 +111,19 @@ impl ChannelModal {
         .detach();
     }
 
-    fn toggle_member_admin(&mut self, _: &ToggleMemberAdmin, cx: &mut ViewContext<Self>) {
-        self.picker.update(cx, |picker, cx| {
-            picker.delegate.toggle_selected_member_admin(cx);
-        })
-    }
-
-    fn remove_member(&mut self, _: &RemoveMember, cx: &mut ViewContext<Self>) {
-        self.picker.update(cx, |picker, cx| {
-            picker.delegate.remove_selected_member(cx);
+    fn set_channel_visiblity(&mut self, selection: &Selection, cx: &mut ViewContext<Self>) {
+        self.channel_store.update(cx, |channel_store, cx| {
+            channel_store
+                .set_channel_visibility(
+                    self.channel_id,
+                    match selection {
+                        Selection::Unselected => ChannelVisibility::Members,
+                        Selection::Selected => ChannelVisibility::Public,
+                        Selection::Indeterminate => return,
+                    },
+                    cx,
+                )
+                .detach_and_log_err(cx)
         });
     }
 
@@ -156,167 +145,87 @@ impl Render for ChannelModal {
     type Element = Div;
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
-        v_stack().w(rems(34.)).child(self.picker.clone())
-        // let theme = &theme::current(cx).collab_panel.tabbed_modal;
-
-        // let mode = self.picker.read(cx).delegate().mode;
-        // let Some(channel) = self.channel_store.read(cx).channel_for_id(self.channel_id) else {
-        //     return Empty::new().into_any();
-        // };
-
-        // enum InviteMembers {}
-        // enum ManageMembers {}
-
-        // fn render_mode_button<T: 'static>(
-        //     mode: Mode,
-        //     text: &'static str,
-        //     current_mode: Mode,
-        //     theme: &theme::TabbedModal,
-        //     cx: &mut ViewContext<ChannelModal>,
-        // ) -> AnyElement<ChannelModal> {
-        //     let active = mode == current_mode;
-        //     MouseEventHandler::new::<T, _>(0, cx, move |state, _| {
-        //         let contained_text = theme.tab_button.style_for(active, state);
-        //         Label::new(text, contained_text.text.clone())
-        //             .contained()
-        //             .with_style(contained_text.container.clone())
-        //     })
-        //     .on_click(MouseButton::Left, move |_, this, cx| {
-        //         if !active {
-        //             this.set_mode(mode, cx);
-        //         }
-        //     })
-        //     .with_cursor_style(CursorStyle::PointingHand)
-        //     .into_any()
-        // }
-
-        // fn render_visibility(
-        //     channel_id: ChannelId,
-        //     visibility: ChannelVisibility,
-        //     theme: &theme::TabbedModal,
-        //     cx: &mut ViewContext<ChannelModal>,
-        // ) -> AnyElement<ChannelModal> {
-        //     enum TogglePublic {}
-
-        //     if visibility == ChannelVisibility::Members {
-        //         return Flex::row()
-        //             .with_child(
-        //                 MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
-        //                     let style = theme.visibility_toggle.style_for(state);
-        //                     Label::new(format!("{}", "Public access: OFF"), style.text.clone())
-        //                         .contained()
-        //                         .with_style(style.container.clone())
-        //                 })
-        //                 .on_click(MouseButton::Left, move |_, this, cx| {
-        //                     this.channel_store
-        //                         .update(cx, |channel_store, cx| {
-        //                             channel_store.set_channel_visibility(
-        //                                 channel_id,
-        //                                 ChannelVisibility::Public,
-        //                                 cx,
-        //                             )
-        //                         })
-        //                         .detach_and_log_err(cx);
-        //                 })
-        //                 .with_cursor_style(CursorStyle::PointingHand),
-        //             )
-        //             .into_any();
-        //     }
-
-        //     Flex::row()
-        //         .with_child(
-        //             MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
-        //                 let style = theme.visibility_toggle.style_for(state);
-        //                 Label::new(format!("{}", "Public access: ON"), style.text.clone())
-        //                     .contained()
-        //                     .with_style(style.container.clone())
-        //             })
-        //             .on_click(MouseButton::Left, move |_, this, cx| {
-        //                 this.channel_store
-        //                     .update(cx, |channel_store, cx| {
-        //                         channel_store.set_channel_visibility(
-        //                             channel_id,
-        //                             ChannelVisibility::Members,
-        //                             cx,
-        //                         )
-        //                     })
-        //                     .detach_and_log_err(cx);
-        //             })
-        //             .with_cursor_style(CursorStyle::PointingHand),
-        //         )
-        //         .with_spacing(14.0)
-        //         .with_child(
-        //             MouseEventHandler::new::<TogglePublic, _>(1, cx, move |state, _| {
-        //                 let style = theme.channel_link.style_for(state);
-        //                 Label::new(format!("{}", "copy link"), style.text.clone())
-        //                     .contained()
-        //                     .with_style(style.container.clone())
-        //             })
-        //             .on_click(MouseButton::Left, move |_, this, cx| {
-        //                 if let Some(channel) =
-        //                     this.channel_store.read(cx).channel_for_id(channel_id)
-        //                 {
-        //                     let item = ClipboardItem::new(channel.link());
-        //                     cx.write_to_clipboard(item);
-        //                 }
-        //             })
-        //             .with_cursor_style(CursorStyle::PointingHand),
-        //         )
-        //         .into_any()
-        // }
-
-        // Flex::column()
-        //     .with_child(
-        //         Flex::column()
-        //             .with_child(
-        //                 Label::new(format!("#{}", channel.name), theme.title.text.clone())
-        //                     .contained()
-        //                     .with_style(theme.title.container.clone()),
-        //             )
-        //             .with_child(render_visibility(channel.id, channel.visibility, theme, cx))
-        //             .with_child(Flex::row().with_children([
-        //                 render_mode_button::<InviteMembers>(
-        //                     Mode::InviteMembers,
-        //                     "Invite members",
-        //                     mode,
-        //                     theme,
-        //                     cx,
-        //                 ),
-        //                 render_mode_button::<ManageMembers>(
-        //                     Mode::ManageMembers,
-        //                     "Manage members",
-        //                     mode,
-        //                     theme,
-        //                     cx,
-        //                 ),
-        //             ]))
-        //             .expanded()
-        //             .contained()
-        //             .with_style(theme.header),
-        //     )
-        //     .with_child(
-        //         ChildView::new(&self.picker, cx)
-        //             .contained()
-        //             .with_style(theme.body),
-        //     )
-        //     .constrained()
-        //     .with_max_height(theme.max_height)
-        //     .with_max_width(theme.max_width)
-        //     .contained()
-        //     .with_style(theme.modal)
-        //     .into_any()
+        let channel_store = self.channel_store.read(cx);
+        let Some(channel) = channel_store.channel_for_id(self.channel_id) else {
+            return div();
+        };
+        let channel_name = channel.name.clone();
+        let channel_id = channel.id;
+        let visibility = channel.visibility;
+        let mode = self.picker.read(cx).delegate.mode;
+
+        v_stack()
+            .key_context("ChannelModal")
+            .on_action(cx.listener(Self::toggle_mode))
+            .on_action(cx.listener(Self::dismiss))
+            .elevation_3(cx)
+            .w(rems(34.))
+            .child(
+                v_stack()
+                    .px_2()
+                    .py_1()
+                    .rounded_t(px(8.))
+                    .bg(cx.theme().colors().element_background)
+                    .child(IconElement::new(Icon::Hash).size(IconSize::Medium))
+                    .child(Label::new(channel_name))
+                    .child(
+                        h_stack()
+                            .w_full()
+                            .justify_between()
+                            .child(
+                                h_stack()
+                                    .gap_2()
+                                    .child(
+                                        Checkbox::new(
+                                            "is-public",
+                                            if visibility == ChannelVisibility::Public {
+                                                ui::Selection::Selected
+                                            } else {
+                                                ui::Selection::Unselected
+                                            },
+                                        )
+                                        .on_click(cx.listener(Self::set_channel_visiblity)),
+                                    )
+                                    .child(Label::new("Public")),
+                            )
+                            .children(if visibility == ChannelVisibility::Public {
+                                Some(Button::new("copy-link", "Copy Link").on_click(cx.listener(
+                                    move |this, _, cx| {
+                                        if let Some(channel) =
+                                            this.channel_store.read(cx).channel_for_id(channel_id)
+                                        {
+                                            let item = ClipboardItem::new(channel.link());
+                                            cx.write_to_clipboard(item);
+                                        }
+                                    },
+                                )))
+                            } else {
+                                None
+                            }),
+                    )
+                    .child(
+                        div()
+                            .w_full()
+                            .flex()
+                            .flex_row()
+                            .child(
+                                Button::new("manage-members", "Manage Members")
+                                    .selected(mode == Mode::ManageMembers)
+                                    .on_click(cx.listener(|this, _, cx| {
+                                        this.set_mode(Mode::ManageMembers, cx);
+                                    })),
+                            )
+                            .child(
+                                Button::new("invite-members", "Invite Members")
+                                    .selected(mode == Mode::InviteMembers)
+                                    .on_click(cx.listener(|this, _, cx| {
+                                        this.set_mode(Mode::InviteMembers, cx);
+                                    })),
+                            ),
+                    ),
+            )
+            .child(self.picker.clone())
     }
-
-    // fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
-    //     self.has_focus = true;
-    //     if cx.is_self_focused() {
-    //         cx.focus(&self.picker)
-    //     }
-    // }
-
-    // fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
-    //     self.has_focus = false;
-    // }
 }
 
 #[derive(Copy, Clone, PartialEq)]
@@ -336,11 +245,11 @@ pub struct ChannelModalDelegate {
     mode: Mode,
     match_candidates: Vec<StringMatchCandidate>,
     members: Vec<ChannelMembership>,
-    // context_menu: ViewHandle<ContextMenu>,
+    context_menu: Option<(View<ContextMenu>, Subscription)>,
 }
 
 impl PickerDelegate for ChannelModalDelegate {
-    type ListItem = Div;
+    type ListItem = ListItem;
 
     fn placeholder_text(&self) -> Arc<str> {
         "Search collaborator by username...".into()
@@ -420,11 +329,11 @@ impl PickerDelegate for ChannelModalDelegate {
         if let Some((selected_user, role)) = self.user_at_index(self.selected_index) {
             match self.mode {
                 Mode::ManageMembers => {
-                    self.show_context_menu(role.unwrap_or(ChannelRole::Member), cx)
+                    self.show_context_menu(selected_user, role.unwrap_or(ChannelRole::Member), cx)
                 }
                 Mode::InviteMembers => match self.member_status(selected_user.id, cx) {
                     Some(proto::channel_member::Kind::Invitee) => {
-                        self.remove_selected_member(cx);
+                        self.remove_member(selected_user.id, cx);
                     }
                     Some(proto::channel_member::Kind::AncestorMember) | None => {
                         self.invite_member(selected_user, cx)
@@ -436,11 +345,13 @@ impl PickerDelegate for ChannelModalDelegate {
     }
 
     fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
-        self.channel_modal
-            .update(cx, |_, cx| {
-                cx.emit(DismissEvent);
-            })
-            .ok();
+        if self.context_menu.is_none() {
+            self.channel_modal
+                .update(cx, |_, cx| {
+                    cx.emit(DismissEvent);
+                })
+                .ok();
+        }
     }
 
     fn render_match(
@@ -449,129 +360,54 @@ impl PickerDelegate for ChannelModalDelegate {
         selected: bool,
         cx: &mut ViewContext<Picker<Self>>,
     ) -> Option<Self::ListItem> {
-        None
-        //     let full_theme = &theme::current(cx);
-        //     let theme = &full_theme.collab_panel.channel_modal;
-        //     let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
-        //     let (user, role) = self.user_at_index(ix).unwrap();
-        //     let request_status = self.member_status(user.id, cx);
-
-        //     let style = tabbed_modal
-        //         .picker
-        //         .item
-        //         .in_state(selected)
-        //         .style_for(mouse_state);
-
-        //     let in_manage = matches!(self.mode, Mode::ManageMembers);
-
-        //     let mut result = Flex::row()
-        //         .with_children(user.avatar.clone().map(|avatar| {
-        //             Image::from_data(avatar)
-        //                 .with_style(theme.contact_avatar)
-        //                 .aligned()
-        //                 .left()
-        //         }))
-        //         .with_child(
-        //             Label::new(user.github_login.clone(), style.label.clone())
-        //                 .contained()
-        //                 .with_style(theme.contact_username)
-        //                 .aligned()
-        //                 .left(),
-        //         )
-        //         .with_children({
-        //             (in_manage && request_status == Some(proto::channel_member::Kind::Invitee)).then(
-        //                 || {
-        //                     Label::new("Invited", theme.member_tag.text.clone())
-        //                         .contained()
-        //                         .with_style(theme.member_tag.container)
-        //                         .aligned()
-        //                         .left()
-        //                 },
-        //             )
-        //         })
-        //         .with_children(if in_manage && role == Some(ChannelRole::Admin) {
-        //             Some(
-        //                 Label::new("Admin", theme.member_tag.text.clone())
-        //                     .contained()
-        //                     .with_style(theme.member_tag.container)
-        //                     .aligned()
-        //                     .left(),
-        //             )
-        //         } else if in_manage && role == Some(ChannelRole::Guest) {
-        //             Some(
-        //                 Label::new("Guest", theme.member_tag.text.clone())
-        //                     .contained()
-        //                     .with_style(theme.member_tag.container)
-        //                     .aligned()
-        //                     .left(),
-        //             )
-        //         } else {
-        //             None
-        //         })
-        //         .with_children({
-        //             let svg = match self.mode {
-        //                 Mode::ManageMembers => Some(
-        //                     Svg::new("icons/ellipsis.svg")
-        //                         .with_color(theme.member_icon.color)
-        //                         .constrained()
-        //                         .with_width(theme.member_icon.icon_width)
-        //                         .aligned()
-        //                         .constrained()
-        //                         .with_width(theme.member_icon.button_width)
-        //                         .with_height(theme.member_icon.button_width)
-        //                         .contained()
-        //                         .with_style(theme.member_icon.container),
-        //                 ),
-        //                 Mode::InviteMembers => match request_status {
-        //                     Some(proto::channel_member::Kind::Member) => Some(
-        //                         Svg::new("icons/check.svg")
-        //                             .with_color(theme.member_icon.color)
-        //                             .constrained()
-        //                             .with_width(theme.member_icon.icon_width)
-        //                             .aligned()
-        //                             .constrained()
-        //                             .with_width(theme.member_icon.button_width)
-        //                             .with_height(theme.member_icon.button_width)
-        //                             .contained()
-        //                             .with_style(theme.member_icon.container),
-        //                     ),
-        //                     Some(proto::channel_member::Kind::Invitee) => Some(
-        //                         Svg::new("icons/check.svg")
-        //                             .with_color(theme.invitee_icon.color)
-        //                             .constrained()
-        //                             .with_width(theme.invitee_icon.icon_width)
-        //                             .aligned()
-        //                             .constrained()
-        //                             .with_width(theme.invitee_icon.button_width)
-        //                             .with_height(theme.invitee_icon.button_width)
-        //                             .contained()
-        //                             .with_style(theme.invitee_icon.container),
-        //                     ),
-        //                     Some(proto::channel_member::Kind::AncestorMember) | None => None,
-        //                 },
-        //             };
-
-        //             svg.map(|svg| svg.aligned().flex_float().into_any())
-        //         })
-        //         .contained()
-        //         .with_style(style.container)
-        //         .constrained()
-        //         .with_height(tabbed_modal.row_height)
-        //         .into_any();
-
-        //     if selected {
-        //         result = Stack::new()
-        //             .with_child(result)
-        //             .with_child(
-        //                 ChildView::new(&self.context_menu, cx)
-        //                     .aligned()
-        //                     .top()
-        //                     .right(),
-        //             )
-        //             .into_any();
-        //     }
-
-        //     result
+        let (user, role) = self.user_at_index(ix)?;
+        let request_status = self.member_status(user.id, cx);
+
+        Some(
+            ListItem::new(ix)
+                .inset(true)
+                .selected(selected)
+                .start_slot(Avatar::new(user.avatar_uri.clone()))
+                .child(Label::new(user.github_login.clone()))
+                .end_slot(h_stack().gap_2().map(|slot| {
+                    match self.mode {
+                        Mode::ManageMembers => slot
+                            .children(
+                                if request_status == Some(proto::channel_member::Kind::Invitee) {
+                                    Some(Label::new("Invited"))
+                                } else {
+                                    None
+                                },
+                            )
+                            .children(match role {
+                                Some(ChannelRole::Admin) => Some(Label::new("Admin")),
+                                Some(ChannelRole::Guest) => Some(Label::new("Guest")),
+                                _ => None,
+                            })
+                            .child(IconButton::new("ellipsis", Icon::Ellipsis))
+                            .children(
+                                if let (Some((menu, _)), true) = (&self.context_menu, selected) {
+                                    Some(
+                                        overlay()
+                                            .anchor(gpui::AnchorCorner::TopLeft)
+                                            .child(menu.clone()),
+                                    )
+                                } else {
+                                    None
+                                },
+                            ),
+                        Mode::InviteMembers => match request_status {
+                            Some(proto::channel_member::Kind::Invitee) => {
+                                slot.children(Some(Label::new("Invited")))
+                            }
+                            Some(proto::channel_member::Kind::Member) => {
+                                slot.children(Some(Label::new("Member")))
+                            }
+                            _ => slot,
+                        },
+                    }
+                })),
+        )
     }
 }
 
@@ -605,21 +441,20 @@ impl ChannelModalDelegate {
         }
     }
 
-    fn toggle_selected_member_admin(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
-        let (user, role) = self.user_at_index(self.selected_index)?;
-        let new_role = if role == Some(ChannelRole::Admin) {
-            ChannelRole::Member
-        } else {
-            ChannelRole::Admin
-        };
+    fn set_user_role(
+        &mut self,
+        user_id: UserId,
+        new_role: ChannelRole,
+        cx: &mut ViewContext<Picker<Self>>,
+    ) -> Option<()> {
         let update = self.channel_store.update(cx, |store, cx| {
-            store.set_member_role(self.channel_id, user.id, new_role, cx)
+            store.set_member_role(self.channel_id, user_id, new_role, cx)
         });
         cx.spawn(|picker, mut cx| async move {
             update.await?;
             picker.update(&mut cx, |picker, cx| {
                 let this = &mut picker.delegate;
-                if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user.id) {
+                if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user_id) {
                     member.role = new_role;
                 }
                 cx.focus_self();
@@ -630,9 +465,7 @@ impl ChannelModalDelegate {
         Some(())
     }
 
-    fn remove_selected_member(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
-        let (user, _) = self.user_at_index(self.selected_index)?;
-        let user_id = user.id;
+    fn remove_member(&mut self, user_id: UserId, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
         let update = self.channel_store.update(cx, |store, cx| {
             store.remove_member(self.channel_id, user_id, cx)
         });
@@ -656,7 +489,7 @@ impl ChannelModalDelegate {
                     .selected_index
                     .min(this.matching_member_indices.len().saturating_sub(1));
 
-                cx.focus_self();
+                picker.focus(cx);
                 cx.notify();
             })
         })
@@ -689,24 +522,55 @@ impl ChannelModalDelegate {
         .detach_and_log_err(cx);
     }
 
-    fn show_context_menu(&mut self, role: ChannelRole, cx: &mut ViewContext<Picker<Self>>) {
-        // self.context_menu.update(cx, |context_menu, cx| {
-        //     context_menu.show(
-        //         Default::default(),
-        //         AnchorCorner::TopRight,
-        //         vec![
-        //             ContextMenuItem::action("Remove", RemoveMember),
-        //             ContextMenuItem::action(
-        //                 if role == ChannelRole::Admin {
-        //                     "Make non-admin"
-        //                 } else {
-        //                     "Make admin"
-        //                 },
-        //                 ToggleMemberAdmin,
-        //             ),
-        //         ],
-        //         cx,
-        //     )
-        // })
+    fn show_context_menu(
+        &mut self,
+        user: Arc<User>,
+        role: ChannelRole,
+        cx: &mut ViewContext<Picker<Self>>,
+    ) {
+        let user_id = user.id;
+        let picker = cx.view().clone();
+        let context_menu = ContextMenu::build(cx, |mut menu, _cx| {
+            menu = menu.entry("Remove Member", {
+                let picker = picker.clone();
+                move |cx| {
+                    picker.update(cx, |picker, cx| {
+                        picker.delegate.remove_member(user_id, cx);
+                    })
+                }
+            });
+
+            let picker = picker.clone();
+            match role {
+                ChannelRole::Admin => {
+                    menu = menu.entry("Revoke Admin", move |cx| {
+                        picker.update(cx, |picker, cx| {
+                            picker
+                                .delegate
+                                .set_user_role(user_id, ChannelRole::Member, cx);
+                        })
+                    });
+                }
+                ChannelRole::Member => {
+                    menu = menu.entry("Make Admin", move |cx| {
+                        picker.update(cx, |picker, cx| {
+                            picker
+                                .delegate
+                                .set_user_role(user_id, ChannelRole::Admin, cx);
+                        })
+                    });
+                }
+                _ => {}
+            };
+
+            menu
+        });
+        cx.focus_view(&context_menu);
+        let subscription = cx.subscribe(&context_menu, |picker, _, _: &DismissEvent, cx| {
+            picker.delegate.context_menu = None;
+            picker.focus(cx);
+            cx.notify();
+        });
+        self.context_menu = Some((context_menu, subscription));
     }
 }

crates/gpui2/src/window.rs 🔗

@@ -2662,13 +2662,6 @@ impl<'a, V: 'static> ViewContext<'a, V> {
         self.defer(|view, cx| view.focus_handle(cx).focus(cx))
     }
 
-    pub fn dismiss_self(&mut self)
-    where
-        V: ManagedView,
-    {
-        self.defer(|_, cx| cx.emit(DismissEvent))
-    }
-
     pub fn listener<E>(
         &self,
         f: impl Fn(&mut V, &E, &mut ViewContext<V>) + 'static,

crates/picker2/src/picker2.rs 🔗

@@ -239,7 +239,7 @@ impl<D: PickerDelegate> Render for Picker<D> {
         );
 
         div()
-            .key_context("picker")
+            .key_context("Picker")
             .size_full()
             .when_some(self.width, |el, width| {
                 el.w(width)

crates/ui2/src/components/context_menu.rs 🔗

@@ -72,11 +72,11 @@ impl ContextMenu {
     pub fn entry(
         mut self,
         label: impl Into<SharedString>,
-        on_click: impl Fn(&mut WindowContext) + 'static,
+        handler: impl Fn(&mut WindowContext) + 'static,
     ) -> Self {
         self.items.push(ContextMenuItem::Entry {
             label: label.into(),
-            handler: Rc::new(on_click),
+            handler: Rc::new(handler),
             icon: None,
             action: None,
         });
@@ -114,6 +114,7 @@ impl ContextMenu {
 
     pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
         cx.emit(DismissEvent);
+        cx.emit(DismissEvent);
     }
 
     fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {

crates/ui2/src/components/icon.rs 🔗

@@ -51,6 +51,7 @@ pub enum Icon {
     CopilotDisabled,
     Dash,
     Disconnected,
+    Ellipsis,
     Envelope,
     ExternalLink,
     ExclamationTriangle,
@@ -133,6 +134,7 @@ impl Icon {
             Icon::CopilotDisabled => "icons/copilot_disabled.svg",
             Icon::Dash => "icons/dash.svg",
             Icon::Disconnected => "icons/disconnected.svg",
+            Icon::Ellipsis => "icons/ellipsis.svg",
             Icon::Envelope => "icons/feedback.svg",
             Icon::ExclamationTriangle => "icons/warning.svg",
             Icon::ExternalLink => "icons/external_link.svg",