@@ -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::*, Checkbox};
+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)
});
}
@@ -160,49 +149,79 @@ impl Render for ChannelModal {
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()
- .bg(cx.theme().colors().elevated_surface_background)
+ .key_context("ChannelModal")
+ .on_action(cx.listener(Self::toggle_mode))
+ .on_action(cx.listener(Self::dismiss))
+ .elevation_3(cx)
.w(rems(34.))
- .child(Label::new(channel.name.clone()))
- .child(
- div()
- .w_full()
- .flex_row()
- .child(Checkbox::new(
- "is-public",
- if channel.visibility == ChannelVisibility::Public {
- ui::Selection::Selected
- } else {
- ui::Selection::Unselected
- },
- ))
- .child(Label::new("Public")),
- )
.child(
- div()
- .w_full()
- .flex_row()
+ 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(
- Button::new("manage-members", "Manage Members")
- .selected(mode == Mode::ManageMembers)
- .on_click(cx.listener(|this, _, cx| {
- this.picker.update(cx, |picker, _| {
- picker.delegate.mode = Mode::ManageMembers
- });
- cx.notify();
- })),
+ 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(
- Button::new("invite-members", "Invite Members")
- .selected(mode == Mode::InviteMembers)
- .on_click(cx.listener(|this, _, cx| {
- this.picker.update(cx, |picker, _| {
- picker.delegate.mode = Mode::InviteMembers
- });
- cx.notify();
- })),
+ 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())
@@ -226,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()
@@ -310,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)
@@ -326,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(
@@ -339,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,
+ },
+ }
+ })),
+ )
}
}
@@ -495,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();
@@ -520,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)
});
@@ -546,7 +489,7 @@ impl ChannelModalDelegate {
.selected_index
.min(this.matching_member_indices.len().saturating_sub(1));
- cx.focus_self();
+ picker.focus(cx);
cx.notify();
})
})
@@ -579,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));
}
}