From e37e76fc0bdb1f690162d6f055d48c4585f7f9b5 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Mon, 7 Aug 2023 15:29:30 -0700 Subject: [PATCH] Add context menu controls to the channel member management co-authored-by: Max --- .../src/collab_panel/channel_modal.rs | 244 +++++++++++------- crates/theme/src/theme.rs | 2 +- styles/src/style_tree/channel_modal.ts | 8 +- 3 files changed, 161 insertions(+), 93 deletions(-) diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index fc1b86354f..8747d9a0af 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -1,4 +1,5 @@ use client::{proto, ChannelId, ChannelMembership, ChannelStore, User, UserId, UserStore}; +use context_menu::{ContextMenu, ContextMenuItem}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ actions, @@ -11,12 +12,21 @@ use std::sync::Arc; use util::TryFutureExt; use workspace::Modal; -actions!(channel_modal, [SelectNextControl, ToggleMode]); +actions!( + channel_modal, + [ + SelectNextControl, + ToggleMode, + ToggleMemberAdmin, + RemoveMember + ] +); pub fn init(cx: &mut AppContext) { Picker::::init(cx); cx.add_action(ChannelModal::toggle_mode); - // cx.add_action(ChannelModal::select_next_control); + cx.add_action(ChannelModal::toggle_member_admin); + cx.add_action(ChannelModal::remove_member); } pub struct ChannelModal { @@ -48,7 +58,11 @@ impl ChannelModal { match_candidates: Vec::new(), members, mode, - selected_column: None, + context_menu: cx.add_view(|cx| { + let mut menu = ContextMenu::new(cx.view_id(), cx); + menu.set_position_mode(OverlayPositionMode::Local); + menu + }), }, cx, ) @@ -95,6 +109,8 @@ impl ChannelModal { this.picker.update(cx, |picker, cx| { let delegate = picker.delegate_mut(); delegate.mode = mode; + delegate.selected_index = 0; + picker.set_query("", cx); picker.update_matches(picker.query(cx), cx); cx.notify() }); @@ -104,24 +120,17 @@ impl ChannelModal { .detach(); } - // fn select_next_control(&mut self, _: &SelectNextControl, cx: &mut ViewContext) { - // self.picker.update(cx, |picker, cx| { - // let delegate = picker.delegate_mut(); - // match delegate.mode { - // Mode::ManageMembers => match delegate.selected_column { - // Some(UserColumn::Remove) => { - // delegate.selected_column = Some(UserColumn::ToggleAdmin) - // } - // Some(UserColumn::ToggleAdmin) => { - // delegate.selected_column = Some(UserColumn::Remove) - // } - // None => todo!(), - // }, - // Mode::InviteMembers => {} - // } - // cx.notify() - // }); - // } + fn toggle_member_admin(&mut self, _: &ToggleMemberAdmin, cx: &mut ViewContext) { + self.picker.update(cx, |picker, cx| { + picker.delegate_mut().toggle_selected_member_admin(cx); + }) + } + + fn remove_member(&mut self, _: &RemoveMember, cx: &mut ViewContext) { + self.picker.update(cx, |picker, cx| { + picker.delegate_mut().remove_selected_member(cx); + }); + } } impl Entity for ChannelModal { @@ -233,12 +242,6 @@ pub enum Mode { InviteMembers, } -#[derive(Copy, Clone, PartialEq)] -pub enum UserColumn { - ToggleAdmin, - Remove, -} - pub struct ChannelModalDelegate { matching_users: Vec>, matching_member_indices: Vec, @@ -247,9 +250,9 @@ pub struct ChannelModalDelegate { channel_id: ChannelId, selected_index: usize, mode: Mode, - selected_column: Option, match_candidates: Vec, members: Vec, + context_menu: ViewHandle, } impl PickerDelegate for ChannelModalDelegate { @@ -270,10 +273,6 @@ impl PickerDelegate for ChannelModalDelegate { fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext>) { self.selected_index = ix; - self.selected_column = match self.mode { - Mode::ManageMembers => Some(UserColumn::ToggleAdmin), - Mode::InviteMembers => None, - }; } fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()> { @@ -334,18 +333,17 @@ impl PickerDelegate for ChannelModalDelegate { fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { if let Some((selected_user, admin)) = self.user_at_index(self.selected_index) { - match self.member_status(selected_user.id, cx) { - Some(proto::channel_member::Kind::Member) - | Some(proto::channel_member::Kind::Invitee) => { - if self.selected_column == Some(UserColumn::ToggleAdmin) { - self.set_member_admin(selected_user.id, !admin.unwrap_or(false), cx); - } else { + match self.mode { + Mode::ManageMembers => self.show_context_menu(admin.unwrap_or(false), cx), + Mode::InviteMembers => match self.member_status(selected_user.id, cx) { + Some(proto::channel_member::Kind::Invitee) => { self.remove_member(selected_user.id, cx); } - } - Some(proto::channel_member::Kind::AncestorMember) | None => { - self.invite_member(selected_user, cx) - } + Some(proto::channel_member::Kind::AncestorMember) | None => { + self.invite_member(selected_user, cx) + } + Some(proto::channel_member::Kind::Member) => {} + }, } } } @@ -366,7 +364,10 @@ impl PickerDelegate for ChannelModalDelegate { let request_status = self.member_status(user.id, cx); let style = theme.picker.item.in_state(selected).style_for(mouse_state); - Flex::row() + + 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) @@ -380,57 +381,81 @@ impl PickerDelegate for ChannelModalDelegate { .aligned() .left(), ) - .with_children(admin.map(|_| { - Label::new("admin", theme.admin_toggle.text.clone()) - .contained() - .with_style(theme.admin_toggle.container) - .aligned() + .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(admin.and_then(|admin| { + (in_manage && admin).then(|| { + Label::new("Admin", theme.member_tag.text.clone()) + .contained() + .with_style(theme.member_tag.container) + .aligned() + .left() + }) })) .with_children({ - match self.mode { - Mode::ManageMembers => match request_status { - Some(proto::channel_member::Kind::Invitee) => Some( - Label::new("cancel invite", theme.cancel_invite_button.text.clone()) + let svg = match self.mode { + Mode::ManageMembers => Some( + Svg::new("icons/ellipsis_14.svg") + .with_color(theme.member_icon.color) + .constrained() + .with_width(theme.member_icon.width) + .aligned() + .contained() + .with_style(theme.member_icon.container), + ), + Mode::InviteMembers => match request_status { + Some(proto::channel_member::Kind::Member) => Some( + Svg::new("icons/check_8.svg") + .with_color(theme.member_icon.color) + .constrained() + .with_width(theme.member_icon.width) + .aligned() .contained() - .with_style(theme.cancel_invite_button.container) - .into_any(), + .with_style(theme.member_icon.container), ), - Some(proto::channel_member::Kind::Member) - | Some(proto::channel_member::Kind::AncestorMember) - | None => None, + Some(proto::channel_member::Kind::Invitee) => Some( + Svg::new("icons/check_8.svg") + .with_color(theme.invitee_icon.color) + .constrained() + .with_width(theme.invitee_icon.width) + .aligned() + .contained() + .with_style(theme.invitee_icon.container), + ), + Some(proto::channel_member::Kind::AncestorMember) | None => None, }, - Mode::InviteMembers => { - let svg = match request_status { - Some(proto::channel_member::Kind::Member) => Some( - Svg::new("icons/check_8.svg") - .with_color(theme.member_icon.color) - .constrained() - .with_width(theme.member_icon.width) - .aligned() - .contained() - .with_style(theme.member_icon.container), - ), - Some(proto::channel_member::Kind::Invitee) => Some( - Svg::new("icons/check_8.svg") - .with_color(theme.invitee_icon.color) - .constrained() - .with_width(theme.invitee_icon.width) - .aligned() - .contained() - .with_style(theme.invitee_icon.container), - ), - Some(proto::channel_member::Kind::AncestorMember) | None => None, - }; + }; - svg.map(|svg| svg.aligned().flex_float().into_any()) - } - } + svg.map(|svg| svg.aligned().flex_float().into_any()) }) .contained() .with_style(style.container) .constrained() .with_height(theme.row_height) - .into_any() + .into_any(); + + if selected { + result = Stack::new() + .with_child(result) + .with_child( + ChildView::new(&self.context_menu, cx) + .aligned() + .top() + .right(), + ) + .into_any(); + } + + result } } @@ -464,20 +489,30 @@ impl ChannelModalDelegate { } } - fn set_member_admin(&mut self, user_id: u64, admin: bool, cx: &mut ViewContext>) { + fn toggle_selected_member_admin(&mut self, cx: &mut ViewContext>) -> Option<()> { + let (user, admin) = self.user_at_index(self.selected_index)?; + let admin = !admin.unwrap_or(false); let update = self.channel_store.update(cx, |store, cx| { - store.set_member_admin(self.channel_id, user_id, admin, cx) + store.set_member_admin(self.channel_id, user.id, admin, cx) }); cx.spawn(|picker, mut cx| async move { update.await?; - picker.update(&mut cx, |picker, _| { + picker.update(&mut cx, |picker, cx| { let this = picker.delegate_mut(); - 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.admin = admin; } + cx.notify(); }) }) .detach_and_log_err(cx); + Some(()) + } + + fn remove_selected_member(&mut self, cx: &mut ViewContext>) -> Option<()> { + let (user, _) = self.user_at_index(self.selected_index)?; + self.remove_member(user.id, cx); + Some(()) } fn remove_member(&mut self, user_id: u64, cx: &mut ViewContext>) { @@ -486,11 +521,20 @@ impl ChannelModalDelegate { }); cx.spawn(|picker, mut cx| async move { update.await?; - picker.update(&mut cx, |picker, _| { + picker.update(&mut cx, |picker, cx| { let this = picker.delegate_mut(); if let Some(ix) = this.members.iter_mut().position(|m| m.user.id == user_id) { this.members.remove(ix); + this.matching_member_indices.retain_mut(|member_ix| { + if *member_ix == ix { + return false; + } else if *member_ix > ix { + *member_ix -= 1; + } + true + }) } + cx.notify(); }) }) .detach_and_log_err(cx); @@ -505,8 +549,7 @@ impl ChannelModalDelegate { invite_member.await?; this.update(&mut cx, |this, cx| { - let delegate_mut = this.delegate_mut(); - delegate_mut.members.push(ChannelMembership { + this.delegate_mut().members.push(ChannelMembership { user, kind: proto::channel_member::Kind::Invitee, admin: false, @@ -516,4 +559,25 @@ impl ChannelModalDelegate { }) .detach_and_log_err(cx); } + + fn show_context_menu(&mut self, user_is_admin: bool, cx: &mut ViewContext>) { + self.context_menu.update(cx, |context_menu, cx| { + context_menu.show( + Default::default(), + AnchorCorner::TopRight, + vec![ + ContextMenuItem::action("Remove", RemoveMember), + ContextMenuItem::action( + if user_is_admin { + "Make non-admin" + } else { + "Make admin" + }, + ToggleMemberAdmin, + ), + ], + cx, + ) + }) + } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index c778b5fc88..b3640f538f 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -261,7 +261,7 @@ pub struct ChannelModal { pub cancel_invite_button: ContainedText, pub member_icon: Icon, pub invitee_icon: Icon, - pub admin_toggle: ContainedText, + pub member_tag: ContainedText, } #[derive(Deserialize, Default, JsonSchema)] diff --git a/styles/src/style_tree/channel_modal.ts b/styles/src/style_tree/channel_modal.ts index 40fd497458..447522070b 100644 --- a/styles/src/style_tree/channel_modal.ts +++ b/styles/src/style_tree/channel_modal.ts @@ -76,12 +76,16 @@ export default function channel_modal(): any { ...text(theme.middle, "sans", { size: "xs" }), background: background(theme.middle), }, - admin_toggle: { + member_tag: { ...text(theme.middle, "sans", { size: "xs" }), border: border(theme.middle, "active"), background: background(theme.middle), margin: { - right: 8, + left: 8, + }, + padding: { + left: 4, + right: 4, } }, container: {