Add context menu controls to the channel member management
co-authored-by: Max <max@zed.dev>
This commit is contained in:
parent
9913067e51
commit
e37e76fc0b
3 changed files with 161 additions and 93 deletions
|
@ -1,4 +1,5 @@
|
||||||
use client::{proto, ChannelId, ChannelMembership, ChannelStore, User, UserId, UserStore};
|
use client::{proto, ChannelId, ChannelMembership, ChannelStore, User, UserId, UserStore};
|
||||||
|
use context_menu::{ContextMenu, ContextMenuItem};
|
||||||
use fuzzy::{match_strings, StringMatchCandidate};
|
use fuzzy::{match_strings, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions,
|
actions,
|
||||||
|
@ -11,12 +12,21 @@ use std::sync::Arc;
|
||||||
use util::TryFutureExt;
|
use util::TryFutureExt;
|
||||||
use workspace::Modal;
|
use workspace::Modal;
|
||||||
|
|
||||||
actions!(channel_modal, [SelectNextControl, ToggleMode]);
|
actions!(
|
||||||
|
channel_modal,
|
||||||
|
[
|
||||||
|
SelectNextControl,
|
||||||
|
ToggleMode,
|
||||||
|
ToggleMemberAdmin,
|
||||||
|
RemoveMember
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
Picker::<ChannelModalDelegate>::init(cx);
|
Picker::<ChannelModalDelegate>::init(cx);
|
||||||
cx.add_action(ChannelModal::toggle_mode);
|
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 {
|
pub struct ChannelModal {
|
||||||
|
@ -48,7 +58,11 @@ impl ChannelModal {
|
||||||
match_candidates: Vec::new(),
|
match_candidates: Vec::new(),
|
||||||
members,
|
members,
|
||||||
mode,
|
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,
|
cx,
|
||||||
)
|
)
|
||||||
|
@ -95,6 +109,8 @@ impl ChannelModal {
|
||||||
this.picker.update(cx, |picker, cx| {
|
this.picker.update(cx, |picker, cx| {
|
||||||
let delegate = picker.delegate_mut();
|
let delegate = picker.delegate_mut();
|
||||||
delegate.mode = mode;
|
delegate.mode = mode;
|
||||||
|
delegate.selected_index = 0;
|
||||||
|
picker.set_query("", cx);
|
||||||
picker.update_matches(picker.query(cx), cx);
|
picker.update_matches(picker.query(cx), cx);
|
||||||
cx.notify()
|
cx.notify()
|
||||||
});
|
});
|
||||||
|
@ -104,24 +120,17 @@ impl ChannelModal {
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
// fn select_next_control(&mut self, _: &SelectNextControl, cx: &mut ViewContext<Self>) {
|
fn toggle_member_admin(&mut self, _: &ToggleMemberAdmin, cx: &mut ViewContext<Self>) {
|
||||||
// self.picker.update(cx, |picker, cx| {
|
self.picker.update(cx, |picker, cx| {
|
||||||
// let delegate = picker.delegate_mut();
|
picker.delegate_mut().toggle_selected_member_admin(cx);
|
||||||
// match delegate.mode {
|
})
|
||||||
// Mode::ManageMembers => match delegate.selected_column {
|
}
|
||||||
// Some(UserColumn::Remove) => {
|
|
||||||
// delegate.selected_column = Some(UserColumn::ToggleAdmin)
|
fn remove_member(&mut self, _: &RemoveMember, cx: &mut ViewContext<Self>) {
|
||||||
// }
|
self.picker.update(cx, |picker, cx| {
|
||||||
// Some(UserColumn::ToggleAdmin) => {
|
picker.delegate_mut().remove_selected_member(cx);
|
||||||
// delegate.selected_column = Some(UserColumn::Remove)
|
});
|
||||||
// }
|
}
|
||||||
// None => todo!(),
|
|
||||||
// },
|
|
||||||
// Mode::InviteMembers => {}
|
|
||||||
// }
|
|
||||||
// cx.notify()
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Entity for ChannelModal {
|
impl Entity for ChannelModal {
|
||||||
|
@ -233,12 +242,6 @@ pub enum Mode {
|
||||||
InviteMembers,
|
InviteMembers,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq)]
|
|
||||||
pub enum UserColumn {
|
|
||||||
ToggleAdmin,
|
|
||||||
Remove,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ChannelModalDelegate {
|
pub struct ChannelModalDelegate {
|
||||||
matching_users: Vec<Arc<User>>,
|
matching_users: Vec<Arc<User>>,
|
||||||
matching_member_indices: Vec<usize>,
|
matching_member_indices: Vec<usize>,
|
||||||
|
@ -247,9 +250,9 @@ pub struct ChannelModalDelegate {
|
||||||
channel_id: ChannelId,
|
channel_id: ChannelId,
|
||||||
selected_index: usize,
|
selected_index: usize,
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
selected_column: Option<UserColumn>,
|
|
||||||
match_candidates: Vec<StringMatchCandidate>,
|
match_candidates: Vec<StringMatchCandidate>,
|
||||||
members: Vec<ChannelMembership>,
|
members: Vec<ChannelMembership>,
|
||||||
|
context_menu: ViewHandle<ContextMenu>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PickerDelegate for ChannelModalDelegate {
|
impl PickerDelegate for ChannelModalDelegate {
|
||||||
|
@ -270,10 +273,6 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||||
|
|
||||||
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
|
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
|
||||||
self.selected_index = ix;
|
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<Picker<Self>>) -> Task<()> {
|
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||||
|
@ -334,18 +333,17 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||||
|
|
||||||
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
|
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||||
if let Some((selected_user, admin)) = self.user_at_index(self.selected_index) {
|
if let Some((selected_user, admin)) = self.user_at_index(self.selected_index) {
|
||||||
match self.member_status(selected_user.id, cx) {
|
match self.mode {
|
||||||
Some(proto::channel_member::Kind::Member)
|
Mode::ManageMembers => self.show_context_menu(admin.unwrap_or(false), cx),
|
||||||
| Some(proto::channel_member::Kind::Invitee) => {
|
Mode::InviteMembers => match self.member_status(selected_user.id, cx) {
|
||||||
if self.selected_column == Some(UserColumn::ToggleAdmin) {
|
Some(proto::channel_member::Kind::Invitee) => {
|
||||||
self.set_member_admin(selected_user.id, !admin.unwrap_or(false), cx);
|
|
||||||
} else {
|
|
||||||
self.remove_member(selected_user.id, cx);
|
self.remove_member(selected_user.id, cx);
|
||||||
}
|
}
|
||||||
}
|
Some(proto::channel_member::Kind::AncestorMember) | None => {
|
||||||
Some(proto::channel_member::Kind::AncestorMember) | None => {
|
self.invite_member(selected_user, cx)
|
||||||
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 request_status = self.member_status(user.id, cx);
|
||||||
|
|
||||||
let style = theme.picker.item.in_state(selected).style_for(mouse_state);
|
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| {
|
.with_children(user.avatar.clone().map(|avatar| {
|
||||||
Image::from_data(avatar)
|
Image::from_data(avatar)
|
||||||
.with_style(theme.contact_avatar)
|
.with_style(theme.contact_avatar)
|
||||||
|
@ -380,57 +381,81 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||||
.aligned()
|
.aligned()
|
||||||
.left(),
|
.left(),
|
||||||
)
|
)
|
||||||
.with_children(admin.map(|_| {
|
.with_children({
|
||||||
Label::new("admin", theme.admin_toggle.text.clone())
|
(in_manage && request_status == Some(proto::channel_member::Kind::Invitee)).then(
|
||||||
.contained()
|
|| {
|
||||||
.with_style(theme.admin_toggle.container)
|
Label::new("Invited", theme.member_tag.text.clone())
|
||||||
.aligned()
|
.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({
|
.with_children({
|
||||||
match self.mode {
|
let svg = match self.mode {
|
||||||
Mode::ManageMembers => match request_status {
|
Mode::ManageMembers => Some(
|
||||||
Some(proto::channel_member::Kind::Invitee) => Some(
|
Svg::new("icons/ellipsis_14.svg")
|
||||||
Label::new("cancel invite", theme.cancel_invite_button.text.clone())
|
.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()
|
.contained()
|
||||||
.with_style(theme.cancel_invite_button.container)
|
.with_style(theme.member_icon.container),
|
||||||
.into_any(),
|
|
||||||
),
|
),
|
||||||
Some(proto::channel_member::Kind::Member)
|
Some(proto::channel_member::Kind::Invitee) => Some(
|
||||||
| Some(proto::channel_member::Kind::AncestorMember)
|
Svg::new("icons/check_8.svg")
|
||||||
| None => None,
|
.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()
|
.contained()
|
||||||
.with_style(style.container)
|
.with_style(style.container)
|
||||||
.constrained()
|
.constrained()
|
||||||
.with_height(theme.row_height)
|
.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<Picker<Self>>) {
|
fn toggle_selected_member_admin(&mut self, cx: &mut ViewContext<Picker<Self>>) -> 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| {
|
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 {
|
cx.spawn(|picker, mut cx| async move {
|
||||||
update.await?;
|
update.await?;
|
||||||
picker.update(&mut cx, |picker, _| {
|
picker.update(&mut cx, |picker, cx| {
|
||||||
let this = picker.delegate_mut();
|
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;
|
member.admin = admin;
|
||||||
}
|
}
|
||||||
|
cx.notify();
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_selected_member(&mut self, cx: &mut ViewContext<Picker<Self>>) -> 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<Picker<Self>>) {
|
fn remove_member(&mut self, user_id: u64, cx: &mut ViewContext<Picker<Self>>) {
|
||||||
|
@ -486,11 +521,20 @@ impl ChannelModalDelegate {
|
||||||
});
|
});
|
||||||
cx.spawn(|picker, mut cx| async move {
|
cx.spawn(|picker, mut cx| async move {
|
||||||
update.await?;
|
update.await?;
|
||||||
picker.update(&mut cx, |picker, _| {
|
picker.update(&mut cx, |picker, cx| {
|
||||||
let this = picker.delegate_mut();
|
let this = picker.delegate_mut();
|
||||||
if let Some(ix) = this.members.iter_mut().position(|m| m.user.id == user_id) {
|
if let Some(ix) = this.members.iter_mut().position(|m| m.user.id == user_id) {
|
||||||
this.members.remove(ix);
|
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);
|
.detach_and_log_err(cx);
|
||||||
|
@ -505,8 +549,7 @@ impl ChannelModalDelegate {
|
||||||
invite_member.await?;
|
invite_member.await?;
|
||||||
|
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
let delegate_mut = this.delegate_mut();
|
this.delegate_mut().members.push(ChannelMembership {
|
||||||
delegate_mut.members.push(ChannelMembership {
|
|
||||||
user,
|
user,
|
||||||
kind: proto::channel_member::Kind::Invitee,
|
kind: proto::channel_member::Kind::Invitee,
|
||||||
admin: false,
|
admin: false,
|
||||||
|
@ -516,4 +559,25 @@ impl ChannelModalDelegate {
|
||||||
})
|
})
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn show_context_menu(&mut self, user_is_admin: bool, 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 user_is_admin {
|
||||||
|
"Make non-admin"
|
||||||
|
} else {
|
||||||
|
"Make admin"
|
||||||
|
},
|
||||||
|
ToggleMemberAdmin,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -261,7 +261,7 @@ pub struct ChannelModal {
|
||||||
pub cancel_invite_button: ContainedText,
|
pub cancel_invite_button: ContainedText,
|
||||||
pub member_icon: Icon,
|
pub member_icon: Icon,
|
||||||
pub invitee_icon: Icon,
|
pub invitee_icon: Icon,
|
||||||
pub admin_toggle: ContainedText,
|
pub member_tag: ContainedText,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Default, JsonSchema)]
|
#[derive(Deserialize, Default, JsonSchema)]
|
||||||
|
|
|
@ -76,12 +76,16 @@ export default function channel_modal(): any {
|
||||||
...text(theme.middle, "sans", { size: "xs" }),
|
...text(theme.middle, "sans", { size: "xs" }),
|
||||||
background: background(theme.middle),
|
background: background(theme.middle),
|
||||||
},
|
},
|
||||||
admin_toggle: {
|
member_tag: {
|
||||||
...text(theme.middle, "sans", { size: "xs" }),
|
...text(theme.middle, "sans", { size: "xs" }),
|
||||||
border: border(theme.middle, "active"),
|
border: border(theme.middle, "active"),
|
||||||
background: background(theme.middle),
|
background: background(theme.middle),
|
||||||
margin: {
|
margin: {
|
||||||
right: 8,
|
left: 8,
|
||||||
|
},
|
||||||
|
padding: {
|
||||||
|
left: 4,
|
||||||
|
right: 4,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
container: {
|
container: {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue