Only allow Manage Members on root channels

This commit is contained in:
Conrad Irwin 2024-01-26 09:40:41 -07:00
parent abdf302367
commit c6d33d4bb9
6 changed files with 150 additions and 31 deletions

View file

@ -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)

View file

@ -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())?;
} }
} }

View file

@ -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();
} }
} }

View file

@ -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]

View file

@ -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 });
} }

View file

@ -213,6 +213,7 @@ enum ErrorCode {
WrongReleaseChannel = 6; WrongReleaseChannel = 6;
NeedsCla = 7; NeedsCla = 7;
NotARootChannel = 8; NotARootChannel = 8;
BadPublicNesting = 9;
} }
message Test { message Test {