Simplify Membership Management (#6747)
Simplify Zed's collaboration system by: - Only allowing member management on root channels. - Disallowing moving sub-channels between different roots. - Disallowing public channels nested under private channels. This should make the mental model easier to understand, and makes it clearer who has what access. It is also significantly simpler to implement, and so hopefully more performant and less buggy. Still TODO: - [x] Update collab_ui to match. - [x] Fix channel buffer tests. Release Notes: - Simplified channel membership management.
This commit is contained in:
commit
8bc105ca1d
31 changed files with 900 additions and 1441 deletions
|
@ -183,7 +183,7 @@ impl ChannelView {
|
|||
} else {
|
||||
self.channel_store.update(cx, |store, cx| {
|
||||
let channel_buffer = self.channel_buffer.read(cx);
|
||||
store.notes_changed(
|
||||
store.update_latest_notes_version(
|
||||
channel_buffer.channel_id,
|
||||
channel_buffer.epoch(),
|
||||
&channel_buffer.buffer().read(cx).version(),
|
||||
|
|
|
@ -266,7 +266,7 @@ impl ChatPanel {
|
|||
} => {
|
||||
if !self.active {
|
||||
self.channel_store.update(cx, |store, cx| {
|
||||
store.new_message(*channel_id, *message_id, cx)
|
||||
store.update_latest_message_id(*channel_id, *message_id, cx)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ use gpui::{
|
|||
use menu::{Cancel, Confirm, SecondaryConfirm, SelectNext, SelectPrev};
|
||||
use project::{Fs, Project};
|
||||
use rpc::{
|
||||
proto::{self, PeerId},
|
||||
proto::{self, ChannelVisibility, PeerId},
|
||||
ErrorCode, ErrorExt,
|
||||
};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
@ -1134,13 +1134,6 @@ impl CollabPanel {
|
|||
"Rename",
|
||||
Some(Box::new(SecondaryConfirm)),
|
||||
cx.handler_for(&this, move |this, cx| this.rename_channel(channel_id, cx)),
|
||||
)
|
||||
.entry(
|
||||
"Move this channel",
|
||||
None,
|
||||
cx.handler_for(&this, move |this, cx| {
|
||||
this.start_move_channel(channel_id, cx)
|
||||
}),
|
||||
);
|
||||
|
||||
if let Some(channel_name) = clipboard_channel_name {
|
||||
|
@ -1153,23 +1146,52 @@ impl CollabPanel {
|
|||
);
|
||||
}
|
||||
|
||||
context_menu = context_menu
|
||||
.separator()
|
||||
.entry(
|
||||
"Invite Members",
|
||||
None,
|
||||
cx.handler_for(&this, move |this, cx| this.invite_members(channel_id, cx)),
|
||||
)
|
||||
.entry(
|
||||
if self.channel_store.read(cx).is_root_channel(channel_id) {
|
||||
context_menu = context_menu.separator().entry(
|
||||
"Manage Members",
|
||||
None,
|
||||
cx.handler_for(&this, move |this, cx| this.manage_members(channel_id, cx)),
|
||||
)
|
||||
.entry(
|
||||
"Delete",
|
||||
} else {
|
||||
context_menu = context_menu.entry(
|
||||
"Move this channel",
|
||||
None,
|
||||
cx.handler_for(&this, move |this, cx| this.remove_channel(channel_id, cx)),
|
||||
cx.handler_for(&this, move |this, cx| {
|
||||
this.start_move_channel(channel_id, cx)
|
||||
}),
|
||||
);
|
||||
if self.channel_store.read(cx).is_public_channel(channel_id) {
|
||||
context_menu = context_menu.separator().entry(
|
||||
"Make Channel Private",
|
||||
None,
|
||||
cx.handler_for(&this, move |this, cx| {
|
||||
this.set_channel_visibility(
|
||||
channel_id,
|
||||
ChannelVisibility::Members,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
context_menu = context_menu.separator().entry(
|
||||
"Make Channel Public",
|
||||
None,
|
||||
cx.handler_for(&this, move |this, cx| {
|
||||
this.set_channel_visibility(
|
||||
channel_id,
|
||||
ChannelVisibility::Public,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
context_menu = context_menu.entry(
|
||||
"Delete",
|
||||
None,
|
||||
cx.handler_for(&this, move |this, cx| this.remove_channel(channel_id, cx)),
|
||||
);
|
||||
}
|
||||
|
||||
context_menu
|
||||
|
@ -1490,10 +1512,6 @@ impl CollabPanel {
|
|||
cx.notify();
|
||||
}
|
||||
|
||||
fn invite_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
|
||||
self.show_channel_modal(channel_id, channel_modal::Mode::InviteMembers, cx);
|
||||
}
|
||||
|
||||
fn manage_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
|
||||
self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, cx);
|
||||
}
|
||||
|
@ -1530,6 +1548,27 @@ impl CollabPanel {
|
|||
}
|
||||
}
|
||||
|
||||
fn set_channel_visibility(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
visibility: ChannelVisibility,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.channel_store
|
||||
.update(cx, |channel_store, cx| {
|
||||
channel_store.set_channel_visibility(channel_id, visibility, cx)
|
||||
})
|
||||
.detach_and_prompt_err("Failed to set channel visibility", cx, |e, _| match e.error_code() {
|
||||
ErrorCode::BadPublicNesting =>
|
||||
if e.error_tag("direction") == Some("parent") {
|
||||
Some("To make a channel public, its parent channel must be public.".to_string())
|
||||
} else {
|
||||
Some("To make a channel private, all of its subchannels must be private.".to_string())
|
||||
},
|
||||
_ => None
|
||||
});
|
||||
}
|
||||
|
||||
fn start_move_channel(&mut self, channel_id: ChannelId, _cx: &mut ViewContext<Self>) {
|
||||
self.channel_clipboard = Some(ChannelMoveClipboard { channel_id });
|
||||
}
|
||||
|
@ -1546,14 +1585,27 @@ impl CollabPanel {
|
|||
cx: &mut ViewContext<CollabPanel>,
|
||||
) {
|
||||
if let Some(clipboard) = self.channel_clipboard.take() {
|
||||
self.channel_store
|
||||
.update(cx, |channel_store, cx| {
|
||||
channel_store.move_channel(clipboard.channel_id, Some(to_channel_id), cx)
|
||||
})
|
||||
.detach_and_prompt_err("Failed to move channel", cx, |_, _| None)
|
||||
self.move_channel(clipboard.channel_id, to_channel_id, cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn move_channel(&self, channel_id: ChannelId, to: ChannelId, cx: &mut ViewContext<Self>) {
|
||||
self.channel_store
|
||||
.update(cx, |channel_store, cx| {
|
||||
channel_store.move_channel(channel_id, to, cx)
|
||||
})
|
||||
.detach_and_prompt_err("Failed to move channel", cx, |e, _| match e.error_code() {
|
||||
ErrorCode::BadPublicNesting => {
|
||||
Some("Public channels must have public parents".into())
|
||||
}
|
||||
ErrorCode::CircularNesting => Some("You cannot move a channel into itself".into()),
|
||||
ErrorCode::WrongMoveTarget => {
|
||||
Some("You cannot move a channel into a different root channel".into())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
fn open_channel_notes(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
|
||||
if let Some(workspace) = self.workspace.upgrade() {
|
||||
ChannelView::open(channel_id, workspace, cx).detach();
|
||||
|
@ -1980,32 +2032,19 @@ impl CollabPanel {
|
|||
| Section::Offline => true,
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.w_full()
|
||||
.group("section-header")
|
||||
.child(
|
||||
ListHeader::new(text)
|
||||
.when(can_collapse, |header| {
|
||||
header.toggle(Some(!is_collapsed)).on_toggle(cx.listener(
|
||||
move |this, _, cx| {
|
||||
this.toggle_section_expanded(section, cx);
|
||||
},
|
||||
))
|
||||
})
|
||||
.inset(true)
|
||||
.end_slot::<AnyElement>(button)
|
||||
.selected(is_selected),
|
||||
)
|
||||
.when(section == Section::Channels, |el| {
|
||||
el.drag_over::<Channel>(|style| style.bg(cx.theme().colors().ghost_element_hover))
|
||||
.on_drop(cx.listener(move |this, dragged_channel: &Channel, cx| {
|
||||
this.channel_store
|
||||
.update(cx, |channel_store, cx| {
|
||||
channel_store.move_channel(dragged_channel.id, None, cx)
|
||||
})
|
||||
.detach_and_prompt_err("Failed to move channel", cx, |_, _| None)
|
||||
}))
|
||||
})
|
||||
h_flex().w_full().group("section-header").child(
|
||||
ListHeader::new(text)
|
||||
.when(can_collapse, |header| {
|
||||
header
|
||||
.toggle(Some(!is_collapsed))
|
||||
.on_toggle(cx.listener(move |this, _, cx| {
|
||||
this.toggle_section_expanded(section, cx);
|
||||
}))
|
||||
})
|
||||
.inset(true)
|
||||
.end_slot::<AnyElement>(button)
|
||||
.selected(is_selected),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_contact(
|
||||
|
@ -2219,17 +2258,16 @@ impl CollabPanel {
|
|||
Some(call_channel == channel_id)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
let is_public = self
|
||||
.channel_store
|
||||
.read(cx)
|
||||
let channel_store = self.channel_store.read(cx);
|
||||
let is_public = channel_store
|
||||
.channel_for_id(channel_id)
|
||||
.map(|channel| channel.visibility)
|
||||
== Some(proto::ChannelVisibility::Public);
|
||||
let disclosed =
|
||||
has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok());
|
||||
|
||||
let has_messages_notification = channel.unseen_message_id.is_some();
|
||||
let has_notes_notification = channel.unseen_note_version.is_some();
|
||||
let has_messages_notification = channel_store.has_new_messages(channel_id);
|
||||
let has_notes_notification = channel_store.has_channel_buffer_changed(channel_id);
|
||||
|
||||
const FACEPILE_LIMIT: usize = 3;
|
||||
let participants = self.channel_store.read(cx).channel_participants(channel_id);
|
||||
|
@ -2260,6 +2298,7 @@ impl CollabPanel {
|
|||
};
|
||||
|
||||
let width = self.width.unwrap_or(px(240.));
|
||||
let root_id = channel.root_id();
|
||||
|
||||
div()
|
||||
.h_6()
|
||||
|
@ -2267,19 +2306,28 @@ impl CollabPanel {
|
|||
.group("")
|
||||
.flex()
|
||||
.w_full()
|
||||
.on_drag(channel.clone(), move |channel, cx| {
|
||||
cx.new_view(|_| DraggedChannelView {
|
||||
channel: channel.clone(),
|
||||
width,
|
||||
.when(!channel.is_root_channel(), |el| {
|
||||
el.on_drag(channel.clone(), move |channel, cx| {
|
||||
cx.new_view(|_| DraggedChannelView {
|
||||
channel: channel.clone(),
|
||||
width,
|
||||
})
|
||||
})
|
||||
})
|
||||
.drag_over::<Channel>(|style| style.bg(cx.theme().colors().ghost_element_hover))
|
||||
.drag_over::<Channel>({
|
||||
move |style, dragged_channel: &Channel, cx| {
|
||||
if dragged_channel.root_id() == root_id {
|
||||
style.bg(cx.theme().colors().ghost_element_hover)
|
||||
} else {
|
||||
style
|
||||
}
|
||||
}
|
||||
})
|
||||
.on_drop(cx.listener(move |this, dragged_channel: &Channel, cx| {
|
||||
this.channel_store
|
||||
.update(cx, |channel_store, cx| {
|
||||
channel_store.move_channel(dragged_channel.id, Some(channel_id), cx)
|
||||
})
|
||||
.detach_and_prompt_err("Failed to move channel", cx, |_, _| None)
|
||||
if dragged_channel.root_id() != root_id {
|
||||
return;
|
||||
}
|
||||
this.move_channel(dragged_channel.id, channel_id, cx);
|
||||
}))
|
||||
.child(
|
||||
ListItem::new(channel_id as usize)
|
||||
|
|
|
@ -10,7 +10,6 @@ use gpui::{
|
|||
WeakView,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use rpc::proto::channel_member;
|
||||
use std::sync::Arc;
|
||||
use ui::{prelude::*, Avatar, Checkbox, ContextMenu, ListItem, ListItemSpacing};
|
||||
use util::TryFutureExt;
|
||||
|
@ -359,10 +358,8 @@ impl PickerDelegate for ChannelModalDelegate {
|
|||
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::Member) => {}
|
||||
None => self.invite_member(selected_user, cx),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -402,10 +399,6 @@ impl PickerDelegate for ChannelModalDelegate {
|
|||
.children(
|
||||
if request_status == Some(proto::channel_member::Kind::Invitee) {
|
||||
Some(Label::new("Invited"))
|
||||
} else if membership.map(|m| m.kind)
|
||||
== Some(channel_member::Kind::AncestorMember)
|
||||
{
|
||||
Some(Label::new("Parent"))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
|
@ -563,16 +556,9 @@ impl ChannelModalDelegate {
|
|||
let Some(membership) = self.member_at_index(ix) else {
|
||||
return;
|
||||
};
|
||||
if membership.kind == proto::channel_member::Kind::AncestorMember {
|
||||
return;
|
||||
}
|
||||
let user_id = membership.user.id;
|
||||
let picker = cx.view().clone();
|
||||
let context_menu = ContextMenu::build(cx, |mut menu, _cx| {
|
||||
if membership.kind == channel_member::Kind::AncestorMember {
|
||||
return menu.entry("Inherited membership", None, |_| {});
|
||||
};
|
||||
|
||||
let role = membership.role;
|
||||
|
||||
if role == ChannelRole::Admin || role == ChannelRole::Member {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue