Only allow Manage Members on root channels
This commit is contained in:
parent
abdf302367
commit
c6d33d4bb9
6 changed files with 150 additions and 31 deletions
|
@ -79,6 +79,17 @@ impl Channel {
|
||||||
+ &self.id.to_string()
|
+ &self.id.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_root_channel(&self) -> bool {
|
||||||
|
self.parent_path.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn root_id(&self) -> ChannelId {
|
||||||
|
self.parent_path
|
||||||
|
.first()
|
||||||
|
.map(|id| *id as ChannelId)
|
||||||
|
.unwrap_or(self.id)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn slug(&self) -> String {
|
pub fn slug(&self) -> String {
|
||||||
let slug: String = self
|
let slug: String = self
|
||||||
.name
|
.name
|
||||||
|
@ -473,6 +484,22 @@ impl ChannelStore {
|
||||||
self.channel_role(channel_id) == proto::ChannelRole::Admin
|
self.channel_role(channel_id) == proto::ChannelRole::Admin
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_root_channel(&self, channel_id: ChannelId) -> bool {
|
||||||
|
self.channel_index
|
||||||
|
.by_id()
|
||||||
|
.get(&channel_id)
|
||||||
|
.map_or(false, |channel| channel.is_root_channel())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_public_channel(&self, channel_id: ChannelId) -> bool {
|
||||||
|
self.channel_index
|
||||||
|
.by_id()
|
||||||
|
.get(&channel_id)
|
||||||
|
.map_or(false, |channel| {
|
||||||
|
channel.visibility == ChannelVisibility::Public
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn channel_capability(&self, channel_id: ChannelId) -> Capability {
|
pub fn channel_capability(&self, channel_id: ChannelId) -> Capability {
|
||||||
match self.channel_role(channel_id) {
|
match self.channel_role(channel_id) {
|
||||||
ChannelRole::Admin | ChannelRole::Member => Capability::ReadWrite,
|
ChannelRole::Admin | ChannelRole::Member => Capability::ReadWrite,
|
||||||
|
@ -482,10 +509,11 @@ impl ChannelStore {
|
||||||
|
|
||||||
pub fn channel_role(&self, channel_id: ChannelId) -> proto::ChannelRole {
|
pub fn channel_role(&self, channel_id: ChannelId) -> proto::ChannelRole {
|
||||||
maybe!({
|
maybe!({
|
||||||
let channel = self.channel_for_id(channel_id)?;
|
let mut channel = self.channel_for_id(channel_id)?;
|
||||||
let root_channel_id = channel.parent_path.first()?;
|
if !channel.is_root_channel() {
|
||||||
let root_channel_state = self.channel_states.get(&root_channel_id);
|
channel = self.channel_for_id(channel.root_id())?;
|
||||||
debug_assert!(root_channel_state.is_some());
|
}
|
||||||
|
let root_channel_state = self.channel_states.get(&channel.id);
|
||||||
root_channel_state?.role
|
root_channel_state?.role
|
||||||
})
|
})
|
||||||
.unwrap_or(proto::ChannelRole::Guest)
|
.unwrap_or(proto::ChannelRole::Guest)
|
||||||
|
|
|
@ -190,7 +190,9 @@ impl Database {
|
||||||
let parent = self.get_channel_internal(parent_id, &*tx).await?;
|
let parent = self.get_channel_internal(parent_id, &*tx).await?;
|
||||||
|
|
||||||
if parent.visibility != ChannelVisibility::Public {
|
if parent.visibility != ChannelVisibility::Public {
|
||||||
Err(anyhow!("public channels must descend from public channels"))?;
|
Err(ErrorCode::BadPublicNesting
|
||||||
|
.with_tag("direction", "parent")
|
||||||
|
.anyhow())?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if visibility == ChannelVisibility::Members {
|
} else if visibility == ChannelVisibility::Members {
|
||||||
|
@ -202,7 +204,9 @@ impl Database {
|
||||||
channel.id != channel_id && channel.visibility == ChannelVisibility::Public
|
channel.id != channel_id && channel.visibility == ChannelVisibility::Public
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
Err(anyhow!("cannot make a parent of a public channel private"))?;
|
Err(ErrorCode::BadPublicNesting
|
||||||
|
.with_tag("direction", "children")
|
||||||
|
.anyhow())?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3281,6 +3281,18 @@ fn notify_membership_updated(
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
peer: &Peer,
|
peer: &Peer,
|
||||||
) {
|
) {
|
||||||
|
let user_channels_update = proto::UpdateUserChannels {
|
||||||
|
channel_memberships: result
|
||||||
|
.new_channels
|
||||||
|
.channel_memberships
|
||||||
|
.iter()
|
||||||
|
.map(|cm| proto::ChannelMembership {
|
||||||
|
channel_id: cm.channel_id.to_proto(),
|
||||||
|
role: cm.role.into(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
let mut update = build_channels_update(result.new_channels, vec![]);
|
let mut update = build_channels_update(result.new_channels, vec![]);
|
||||||
update.delete_channels = result
|
update.delete_channels = result
|
||||||
.removed_channels
|
.removed_channels
|
||||||
|
@ -3290,6 +3302,8 @@ fn notify_membership_updated(
|
||||||
update.remove_channel_invitations = vec![result.channel_id.to_proto()];
|
update.remove_channel_invitations = vec![result.channel_id.to_proto()];
|
||||||
|
|
||||||
for connection_id in connection_pool.user_connection_ids(user_id) {
|
for connection_id in connection_pool.user_connection_ids(user_id) {
|
||||||
|
peer.send(connection_id, user_channels_update.clone())
|
||||||
|
.trace_err();
|
||||||
peer.send(connection_id, update.clone()).trace_err();
|
peer.send(connection_id, update.clone()).trace_err();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1100,16 +1100,21 @@ async fn test_channel_membership_notifications(
|
||||||
let user_b = client_b.user_id().unwrap();
|
let user_b = client_b.user_id().unwrap();
|
||||||
|
|
||||||
let channels = server
|
let channels = server
|
||||||
.make_channel_tree(&[("zed", None), ("vim", Some("zed"))], (&client_a, cx_a))
|
.make_channel_tree(
|
||||||
|
&[("zed", None), ("vim", Some("zed")), ("opensource", None)],
|
||||||
|
(&client_a, cx_a),
|
||||||
|
)
|
||||||
.await;
|
.await;
|
||||||
let zed_channel = channels[0];
|
let zed_channel = channels[0];
|
||||||
let vim_channel = channels[1];
|
let vim_channel = channels[1];
|
||||||
|
let opensource_channel = channels[2];
|
||||||
|
|
||||||
try_join_all(client_a.channel_store().update(cx_a, |channel_store, cx| {
|
try_join_all(client_a.channel_store().update(cx_a, |channel_store, cx| {
|
||||||
[
|
[
|
||||||
channel_store.set_channel_visibility(zed_channel, proto::ChannelVisibility::Public, cx),
|
channel_store.set_channel_visibility(zed_channel, proto::ChannelVisibility::Public, cx),
|
||||||
channel_store.set_channel_visibility(vim_channel, proto::ChannelVisibility::Public, cx),
|
channel_store.set_channel_visibility(vim_channel, proto::ChannelVisibility::Public, cx),
|
||||||
channel_store.invite_member(zed_channel, user_b, proto::ChannelRole::Guest, cx),
|
channel_store.invite_member(zed_channel, user_b, proto::ChannelRole::Admin, cx),
|
||||||
|
channel_store.invite_member(opensource_channel, user_b, proto::ChannelRole::Member, cx),
|
||||||
]
|
]
|
||||||
}))
|
}))
|
||||||
.await
|
.await
|
||||||
|
@ -1144,6 +1149,34 @@ async fn test_channel_membership_notifications(
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
client_b.channel_store().update(cx_b, |channel_store, _| {
|
||||||
|
channel_store.is_channel_admin(zed_channel)
|
||||||
|
});
|
||||||
|
|
||||||
|
client_b
|
||||||
|
.channel_store()
|
||||||
|
.update(cx_b, |channel_store, cx| {
|
||||||
|
channel_store.respond_to_channel_invite(opensource_channel, true, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
cx_a.run_until_parked();
|
||||||
|
|
||||||
|
client_a
|
||||||
|
.channel_store()
|
||||||
|
.update(cx_a, |channel_store, cx| {
|
||||||
|
channel_store.set_member_role(opensource_channel, user_b, ChannelRole::Admin, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
cx_a.run_until_parked();
|
||||||
|
|
||||||
|
client_b.channel_store().update(cx_b, |channel_store, _| {
|
||||||
|
channel_store.is_channel_admin(opensource_channel)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
|
|
|
@ -23,7 +23,7 @@ use gpui::{
|
||||||
use menu::{Cancel, Confirm, SecondaryConfirm, SelectNext, SelectPrev};
|
use menu::{Cancel, Confirm, SecondaryConfirm, SelectNext, SelectPrev};
|
||||||
use project::{Fs, Project};
|
use project::{Fs, Project};
|
||||||
use rpc::{
|
use rpc::{
|
||||||
proto::{self, PeerId},
|
proto::{self, ChannelVisibility, PeerId},
|
||||||
ErrorCode, ErrorExt,
|
ErrorCode, ErrorExt,
|
||||||
};
|
};
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
@ -1134,13 +1134,6 @@ impl CollabPanel {
|
||||||
"Rename",
|
"Rename",
|
||||||
Some(Box::new(SecondaryConfirm)),
|
Some(Box::new(SecondaryConfirm)),
|
||||||
cx.handler_for(&this, move |this, cx| this.rename_channel(channel_id, cx)),
|
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 {
|
if let Some(channel_name) = clipboard_channel_name {
|
||||||
|
@ -1153,23 +1146,52 @@ impl CollabPanel {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
context_menu = context_menu
|
if self.channel_store.read(cx).is_root_channel(channel_id) {
|
||||||
.separator()
|
context_menu = context_menu.separator().entry(
|
||||||
.entry(
|
|
||||||
"Invite Members",
|
|
||||||
None,
|
|
||||||
cx.handler_for(&this, move |this, cx| this.invite_members(channel_id, cx)),
|
|
||||||
)
|
|
||||||
.entry(
|
|
||||||
"Manage Members",
|
"Manage Members",
|
||||||
None,
|
None,
|
||||||
cx.handler_for(&this, move |this, cx| this.manage_members(channel_id, cx)),
|
cx.handler_for(&this, move |this, cx| this.manage_members(channel_id, cx)),
|
||||||
)
|
)
|
||||||
.entry(
|
} else {
|
||||||
"Delete",
|
context_menu = context_menu.entry(
|
||||||
|
"Move this channel",
|
||||||
None,
|
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
|
context_menu
|
||||||
|
@ -1490,10 +1512,6 @@ impl CollabPanel {
|
||||||
cx.notify();
|
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>) {
|
fn manage_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
|
||||||
self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, cx);
|
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>) {
|
fn start_move_channel(&mut self, channel_id: ChannelId, _cx: &mut ViewContext<Self>) {
|
||||||
self.channel_clipboard = Some(ChannelMoveClipboard { channel_id });
|
self.channel_clipboard = Some(ChannelMoveClipboard { channel_id });
|
||||||
}
|
}
|
||||||
|
|
|
@ -213,6 +213,7 @@ enum ErrorCode {
|
||||||
WrongReleaseChannel = 6;
|
WrongReleaseChannel = 6;
|
||||||
NeedsCla = 7;
|
NeedsCla = 7;
|
||||||
NotARootChannel = 8;
|
NotARootChannel = 8;
|
||||||
|
BadPublicNesting = 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
message Test {
|
message Test {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue