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:
Conrad Irwin 2024-01-26 11:17:16 -07:00 committed by GitHub
commit 8bc105ca1d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 900 additions and 1441 deletions

View file

@ -61,11 +61,12 @@ impl ChannelBuffer {
.map(language::proto::deserialize_operation) .map(language::proto::deserialize_operation)
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
let buffer = cx.new_model(|_| { let buffer = cx.new_model(|cx| {
let capability = channel_store.read(cx).channel_capability(channel.id);
language::Buffer::remote( language::Buffer::remote(
response.buffer_id, response.buffer_id,
response.replica_id as u16, response.replica_id as u16,
channel.channel_buffer_capability(), capability,
base_text, base_text,
) )
})?; })?;

View file

@ -13,11 +13,11 @@ use gpui::{
}; };
use language::Capability; use language::Capability;
use rpc::{ use rpc::{
proto::{self, ChannelVisibility}, proto::{self, ChannelRole, ChannelVisibility},
TypedEnvelope, TypedEnvelope,
}; };
use std::{mem, sync::Arc, time::Duration}; use std::{mem, sync::Arc, time::Duration};
use util::{async_maybe, ResultExt}; use util::{async_maybe, maybe, ResultExt};
pub fn init(client: &Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) { pub fn init(client: &Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
let channel_store = let channel_store =
@ -29,33 +29,47 @@ pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
pub type ChannelId = u64; pub type ChannelId = u64;
#[derive(Debug, Clone, Default)]
struct NotesVersion {
epoch: u64,
version: clock::Global,
}
pub struct ChannelStore { pub struct ChannelStore {
pub channel_index: ChannelIndex, pub channel_index: ChannelIndex,
channel_invitations: Vec<Arc<Channel>>, channel_invitations: Vec<Arc<Channel>>,
channel_participants: HashMap<ChannelId, Vec<Arc<User>>>, channel_participants: HashMap<ChannelId, Vec<Arc<User>>>,
channel_states: HashMap<ChannelId, ChannelState>,
outgoing_invites: HashSet<(ChannelId, UserId)>, outgoing_invites: HashSet<(ChannelId, UserId)>,
update_channels_tx: mpsc::UnboundedSender<proto::UpdateChannels>, update_channels_tx: mpsc::UnboundedSender<proto::UpdateChannels>,
opened_buffers: HashMap<ChannelId, OpenedModelHandle<ChannelBuffer>>, opened_buffers: HashMap<ChannelId, OpenedModelHandle<ChannelBuffer>>,
opened_chats: HashMap<ChannelId, OpenedModelHandle<ChannelChat>>, opened_chats: HashMap<ChannelId, OpenedModelHandle<ChannelChat>>,
client: Arc<Client>, client: Arc<Client>,
user_store: Model<UserStore>, user_store: Model<UserStore>,
_rpc_subscription: Subscription, _rpc_subscriptions: [Subscription; 2],
_watch_connection_status: Task<Option<()>>, _watch_connection_status: Task<Option<()>>,
disconnect_channel_buffers_task: Option<Task<()>>, disconnect_channel_buffers_task: Option<Task<()>>,
_update_channels: Task<()>, _update_channels: Task<()>,
} }
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug)]
pub struct Channel { pub struct Channel {
pub id: ChannelId, pub id: ChannelId,
pub name: SharedString, pub name: SharedString,
pub visibility: proto::ChannelVisibility, pub visibility: proto::ChannelVisibility,
pub role: proto::ChannelRole,
pub unseen_note_version: Option<(u64, clock::Global)>,
pub unseen_message_id: Option<u64>,
pub parent_path: Vec<u64>, pub parent_path: Vec<u64>,
} }
#[derive(Default)]
pub struct ChannelState {
latest_chat_message: Option<u64>,
latest_notes_versions: Option<NotesVersion>,
observed_chat_message: Option<u64>,
observed_notes_versions: Option<NotesVersion>,
role: Option<ChannelRole>,
}
impl Channel { impl Channel {
pub fn link(&self) -> String { pub fn link(&self) -> String {
RELEASE_CHANNEL.link_prefix().to_owned() RELEASE_CHANNEL.link_prefix().to_owned()
@ -65,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
@ -74,14 +99,6 @@ impl Channel {
slug.trim_matches(|c| c == '-').to_string() slug.trim_matches(|c| c == '-').to_string()
} }
pub fn channel_buffer_capability(&self) -> Capability {
if self.role == proto::ChannelRole::Member || self.role == proto::ChannelRole::Admin {
Capability::ReadWrite
} else {
Capability::ReadOnly
}
}
} }
pub struct ChannelMembership { pub struct ChannelMembership {
@ -100,8 +117,7 @@ impl ChannelMembership {
}, },
kind_order: match self.kind { kind_order: match self.kind {
proto::channel_member::Kind::Member => 0, proto::channel_member::Kind::Member => 0,
proto::channel_member::Kind::AncestorMember => 1, proto::channel_member::Kind::Invitee => 1,
proto::channel_member::Kind::Invitee => 2,
}, },
username_order: self.user.github_login.as_str(), username_order: self.user.github_login.as_str(),
} }
@ -137,8 +153,10 @@ impl ChannelStore {
user_store: Model<UserStore>, user_store: Model<UserStore>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Self { ) -> Self {
let rpc_subscription = let rpc_subscriptions = [
client.add_message_handler(cx.weak_model(), Self::handle_update_channels); client.add_message_handler(cx.weak_model(), Self::handle_update_channels),
client.add_message_handler(cx.weak_model(), Self::handle_update_user_channels),
];
let mut connection_status = client.status(); let mut connection_status = client.status();
let (update_channels_tx, mut update_channels_rx) = mpsc::unbounded(); let (update_channels_tx, mut update_channels_rx) = mpsc::unbounded();
@ -175,7 +193,7 @@ impl ChannelStore {
update_channels_tx, update_channels_tx,
client, client,
user_store, user_store,
_rpc_subscription: rpc_subscription, _rpc_subscriptions: rpc_subscriptions,
_watch_connection_status: watch_connection_status, _watch_connection_status: watch_connection_status,
disconnect_channel_buffers_task: None, disconnect_channel_buffers_task: None,
_update_channels: cx.spawn(|this, mut cx| async move { _update_channels: cx.spawn(|this, mut cx| async move {
@ -195,6 +213,7 @@ impl ChannelStore {
.await .await
.log_err(); .log_err();
}), }),
channel_states: Default::default(),
} }
} }
@ -306,39 +325,16 @@ impl ChannelStore {
}) })
} }
pub fn has_channel_buffer_changed(&self, channel_id: ChannelId) -> Option<bool> { pub fn has_channel_buffer_changed(&self, channel_id: ChannelId) -> bool {
self.channel_index self.channel_states
.by_id()
.get(&channel_id) .get(&channel_id)
.map(|channel| channel.unseen_note_version.is_some()) .is_some_and(|state| state.has_channel_buffer_changed())
} }
pub fn has_new_messages(&self, channel_id: ChannelId) -> Option<bool> { pub fn has_new_messages(&self, channel_id: ChannelId) -> bool {
self.channel_index self.channel_states
.by_id()
.get(&channel_id) .get(&channel_id)
.map(|channel| channel.unseen_message_id.is_some()) .is_some_and(|state| state.has_new_messages())
}
pub fn notes_changed(
&mut self,
channel_id: ChannelId,
epoch: u64,
version: &clock::Global,
cx: &mut ModelContext<Self>,
) {
self.channel_index.note_changed(channel_id, epoch, version);
cx.notify();
}
pub fn new_message(
&mut self,
channel_id: ChannelId,
message_id: u64,
cx: &mut ModelContext<Self>,
) {
self.channel_index.new_message(channel_id, message_id);
cx.notify();
} }
pub fn acknowledge_message_id( pub fn acknowledge_message_id(
@ -347,8 +343,23 @@ impl ChannelStore {
message_id: u64, message_id: u64,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) { ) {
self.channel_index self.channel_states
.acknowledge_message_id(channel_id, message_id); .entry(channel_id)
.or_insert_with(|| Default::default())
.acknowledge_message_id(message_id);
cx.notify();
}
pub fn update_latest_message_id(
&mut self,
channel_id: ChannelId,
message_id: u64,
cx: &mut ModelContext<Self>,
) {
self.channel_states
.entry(channel_id)
.or_insert_with(|| Default::default())
.update_latest_message_id(message_id);
cx.notify(); cx.notify();
} }
@ -359,9 +370,25 @@ impl ChannelStore {
version: &clock::Global, version: &clock::Global,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) { ) {
self.channel_index self.channel_states
.acknowledge_note_version(channel_id, epoch, version); .entry(channel_id)
cx.notify(); .or_insert_with(|| Default::default())
.acknowledge_notes_version(epoch, version);
cx.notify()
}
pub fn update_latest_notes_version(
&mut self,
channel_id: ChannelId,
epoch: u64,
version: &clock::Global,
cx: &mut ModelContext<Self>,
) {
self.channel_states
.entry(channel_id)
.or_insert_with(|| Default::default())
.update_latest_notes_version(epoch, version);
cx.notify()
} }
pub fn open_channel_chat( pub fn open_channel_chat(
@ -454,10 +481,42 @@ impl ChannelStore {
} }
pub fn is_channel_admin(&self, channel_id: ChannelId) -> bool { pub fn is_channel_admin(&self, channel_id: ChannelId) -> bool {
let Some(channel) = self.channel_for_id(channel_id) else { self.channel_role(channel_id) == proto::ChannelRole::Admin
return false; }
};
channel.role == 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 {
match self.channel_role(channel_id) {
ChannelRole::Admin | ChannelRole::Member => Capability::ReadWrite,
_ => Capability::ReadOnly,
}
}
pub fn channel_role(&self, channel_id: ChannelId) -> proto::ChannelRole {
maybe!({
let mut channel = self.channel_for_id(channel_id)?;
if !channel.is_root_channel() {
channel = self.channel_for_id(channel.root_id())?;
}
let root_channel_state = self.channel_states.get(&channel.id);
root_channel_state?.role
})
.unwrap_or(proto::ChannelRole::Guest)
} }
pub fn channel_participants(&self, channel_id: ChannelId) -> &[Arc<User>] { pub fn channel_participants(&self, channel_id: ChannelId) -> &[Arc<User>] {
@ -508,7 +567,7 @@ impl ChannelStore {
pub fn move_channel( pub fn move_channel(
&mut self, &mut self,
channel_id: ChannelId, channel_id: ChannelId,
to: Option<ChannelId>, to: ChannelId,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Task<Result<()>> { ) -> Task<Result<()>> {
let client = self.client.clone(); let client = self.client.clone();
@ -747,6 +806,36 @@ impl ChannelStore {
Ok(()) Ok(())
} }
async fn handle_update_user_channels(
this: Model<Self>,
message: TypedEnvelope<proto::UpdateUserChannels>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
for buffer_version in message.payload.observed_channel_buffer_version {
let version = language::proto::deserialize_version(&buffer_version.version);
this.acknowledge_notes_version(
buffer_version.channel_id,
buffer_version.epoch,
&version,
cx,
);
}
for message_id in message.payload.observed_channel_message_id {
this.acknowledge_message_id(message_id.channel_id, message_id.message_id, cx);
}
for membership in message.payload.channel_memberships {
if let Some(role) = ChannelRole::from_i32(membership.role) {
this.channel_states
.entry(membership.channel_id)
.or_insert_with(|| ChannelState::default())
.set_role(role)
}
}
})
}
fn handle_connect(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> { fn handle_connect(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
self.channel_index.clear(); self.channel_index.clear();
self.channel_invitations.clear(); self.channel_invitations.clear();
@ -909,10 +998,7 @@ impl ChannelStore {
Arc::new(Channel { Arc::new(Channel {
id: channel.id, id: channel.id,
visibility: channel.visibility(), visibility: channel.visibility(),
role: channel.role(),
name: channel.name.into(), name: channel.name.into(),
unseen_note_version: None,
unseen_message_id: None,
parent_path: channel.parent_path, parent_path: channel.parent_path,
}), }),
), ),
@ -921,8 +1007,8 @@ impl ChannelStore {
let channels_changed = !payload.channels.is_empty() let channels_changed = !payload.channels.is_empty()
|| !payload.delete_channels.is_empty() || !payload.delete_channels.is_empty()
|| !payload.unseen_channel_messages.is_empty() || !payload.latest_channel_message_ids.is_empty()
|| !payload.unseen_channel_buffer_changes.is_empty(); || !payload.latest_channel_buffer_versions.is_empty();
if channels_changed { if channels_changed {
if !payload.delete_channels.is_empty() { if !payload.delete_channels.is_empty() {
@ -963,20 +1049,19 @@ impl ChannelStore {
} }
} }
for unseen_buffer_change in payload.unseen_channel_buffer_changes { for latest_buffer_version in payload.latest_channel_buffer_versions {
let version = language::proto::deserialize_version(&unseen_buffer_change.version); let version = language::proto::deserialize_version(&latest_buffer_version.version);
index.note_changed( self.channel_states
unseen_buffer_change.channel_id, .entry(latest_buffer_version.channel_id)
unseen_buffer_change.epoch, .or_default()
&version, .update_latest_notes_version(latest_buffer_version.epoch, &version)
);
} }
for unseen_channel_message in payload.unseen_channel_messages { for latest_channel_message in payload.latest_channel_message_ids {
index.new_messages( self.channel_states
unseen_channel_message.channel_id, .entry(latest_channel_message.channel_id)
unseen_channel_message.message_id, .or_default()
); .update_latest_message_id(latest_channel_message.message_id);
} }
} }
@ -1025,3 +1110,69 @@ impl ChannelStore {
})) }))
} }
} }
impl ChannelState {
fn set_role(&mut self, role: ChannelRole) {
self.role = Some(role);
}
fn has_channel_buffer_changed(&self) -> bool {
if let Some(latest_version) = &self.latest_notes_versions {
if let Some(observed_version) = &self.observed_notes_versions {
latest_version.epoch > observed_version.epoch
|| latest_version
.version
.changed_since(&observed_version.version)
} else {
true
}
} else {
false
}
}
fn has_new_messages(&self) -> bool {
let latest_message_id = self.latest_chat_message;
let observed_message_id = self.observed_chat_message;
latest_message_id.is_some_and(|latest_message_id| {
latest_message_id > observed_message_id.unwrap_or_default()
})
}
fn acknowledge_message_id(&mut self, message_id: u64) {
let observed = self.observed_chat_message.get_or_insert(message_id);
*observed = (*observed).max(message_id);
}
fn update_latest_message_id(&mut self, message_id: u64) {
self.latest_chat_message =
Some(message_id.max(self.latest_chat_message.unwrap_or_default()));
}
fn acknowledge_notes_version(&mut self, epoch: u64, version: &clock::Global) {
if let Some(existing) = &mut self.observed_notes_versions {
if existing.epoch == epoch {
existing.version.join(version);
return;
}
}
self.observed_notes_versions = Some(NotesVersion {
epoch,
version: version.clone(),
});
}
fn update_latest_notes_version(&mut self, epoch: u64, version: &clock::Global) {
if let Some(existing) = &mut self.latest_notes_versions {
if existing.epoch == epoch {
existing.version.join(version);
return;
}
}
self.latest_notes_versions = Some(NotesVersion {
epoch,
version: version.clone(),
});
}
}

View file

@ -37,43 +37,6 @@ impl ChannelIndex {
channels_by_id: &mut self.channels_by_id, channels_by_id: &mut self.channels_by_id,
} }
} }
pub fn acknowledge_note_version(
&mut self,
channel_id: ChannelId,
epoch: u64,
version: &clock::Global,
) {
if let Some(channel) = self.channels_by_id.get_mut(&channel_id) {
let channel = Arc::make_mut(channel);
if let Some((unseen_epoch, unseen_version)) = &channel.unseen_note_version {
if epoch > *unseen_epoch
|| epoch == *unseen_epoch && version.observed_all(unseen_version)
{
channel.unseen_note_version = None;
}
}
}
}
pub fn acknowledge_message_id(&mut self, channel_id: ChannelId, message_id: u64) {
if let Some(channel) = self.channels_by_id.get_mut(&channel_id) {
let channel = Arc::make_mut(channel);
if let Some(unseen_message_id) = channel.unseen_message_id {
if message_id >= unseen_message_id {
channel.unseen_message_id = None;
}
}
}
}
pub fn note_changed(&mut self, channel_id: ChannelId, epoch: u64, version: &clock::Global) {
insert_note_changed(&mut self.channels_by_id, channel_id, epoch, version);
}
pub fn new_message(&mut self, channel_id: ChannelId, message_id: u64) {
insert_new_message(&mut self.channels_by_id, channel_id, message_id)
}
} }
/// A guard for ensuring that the paths index maintains its sort and uniqueness /// A guard for ensuring that the paths index maintains its sort and uniqueness
@ -85,36 +48,25 @@ pub struct ChannelPathsInsertGuard<'a> {
} }
impl<'a> ChannelPathsInsertGuard<'a> { impl<'a> ChannelPathsInsertGuard<'a> {
pub fn note_changed(&mut self, channel_id: ChannelId, epoch: u64, version: &clock::Global) {
insert_note_changed(self.channels_by_id, channel_id, epoch, version);
}
pub fn new_messages(&mut self, channel_id: ChannelId, message_id: u64) {
insert_new_message(self.channels_by_id, channel_id, message_id)
}
pub fn insert(&mut self, channel_proto: proto::Channel) -> bool { pub fn insert(&mut self, channel_proto: proto::Channel) -> bool {
let mut ret = false; let mut ret = false;
if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) { if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) {
let existing_channel = Arc::make_mut(existing_channel); let existing_channel = Arc::make_mut(existing_channel);
ret = existing_channel.visibility != channel_proto.visibility() ret = existing_channel.visibility != channel_proto.visibility()
|| existing_channel.role != channel_proto.role() || existing_channel.name != channel_proto.name
|| existing_channel.name != channel_proto.name; || existing_channel.parent_path != channel_proto.parent_path;
existing_channel.visibility = channel_proto.visibility(); existing_channel.visibility = channel_proto.visibility();
existing_channel.role = channel_proto.role();
existing_channel.name = channel_proto.name.into(); existing_channel.name = channel_proto.name.into();
existing_channel.parent_path = channel_proto.parent_path.into();
} else { } else {
self.channels_by_id.insert( self.channels_by_id.insert(
channel_proto.id, channel_proto.id,
Arc::new(Channel { Arc::new(Channel {
id: channel_proto.id, id: channel_proto.id,
visibility: channel_proto.visibility(), visibility: channel_proto.visibility(),
role: channel_proto.role(),
name: channel_proto.name.into(), name: channel_proto.name.into(),
unseen_note_version: None,
unseen_message_id: None,
parent_path: channel_proto.parent_path, parent_path: channel_proto.parent_path,
}), }),
); );
@ -153,32 +105,3 @@ fn channel_path_sorting_key<'a>(
.filter_map(|id| Some(channels_by_id.get(id)?.name.as_ref())) .filter_map(|id| Some(channels_by_id.get(id)?.name.as_ref()))
.chain(name) .chain(name)
} }
fn insert_note_changed(
channels_by_id: &mut BTreeMap<ChannelId, Arc<Channel>>,
channel_id: u64,
epoch: u64,
version: &clock::Global,
) {
if let Some(channel) = channels_by_id.get_mut(&channel_id) {
let unseen_version = Arc::make_mut(channel)
.unseen_note_version
.get_or_insert((0, clock::Global::new()));
if epoch > unseen_version.0 {
*unseen_version = (epoch, version.clone());
} else {
unseen_version.1.join(version);
}
}
}
fn insert_new_message(
channels_by_id: &mut BTreeMap<ChannelId, Arc<Channel>>,
channel_id: u64,
message_id: u64,
) {
if let Some(channel) = channels_by_id.get_mut(&channel_id) {
let unseen_message_id = Arc::make_mut(channel).unseen_message_id.get_or_insert(0);
*unseen_message_id = message_id.max(*unseen_message_id);
}
}

View file

@ -19,14 +19,12 @@ fn test_update_channels(cx: &mut AppContext) {
id: 1, id: 1,
name: "b".to_string(), name: "b".to_string(),
visibility: proto::ChannelVisibility::Members as i32, visibility: proto::ChannelVisibility::Members as i32,
role: proto::ChannelRole::Admin.into(),
parent_path: Vec::new(), parent_path: Vec::new(),
}, },
proto::Channel { proto::Channel {
id: 2, id: 2,
name: "a".to_string(), name: "a".to_string(),
visibility: proto::ChannelVisibility::Members as i32, visibility: proto::ChannelVisibility::Members as i32,
role: proto::ChannelRole::Member.into(),
parent_path: Vec::new(), parent_path: Vec::new(),
}, },
], ],
@ -38,8 +36,8 @@ fn test_update_channels(cx: &mut AppContext) {
&channel_store, &channel_store,
&[ &[
// //
(0, "a".to_string(), proto::ChannelRole::Member), (0, "a".to_string()),
(0, "b".to_string(), proto::ChannelRole::Admin), (0, "b".to_string()),
], ],
cx, cx,
); );
@ -52,14 +50,12 @@ fn test_update_channels(cx: &mut AppContext) {
id: 3, id: 3,
name: "x".to_string(), name: "x".to_string(),
visibility: proto::ChannelVisibility::Members as i32, visibility: proto::ChannelVisibility::Members as i32,
role: proto::ChannelRole::Admin.into(),
parent_path: vec![1], parent_path: vec![1],
}, },
proto::Channel { proto::Channel {
id: 4, id: 4,
name: "y".to_string(), name: "y".to_string(),
visibility: proto::ChannelVisibility::Members as i32, visibility: proto::ChannelVisibility::Members as i32,
role: proto::ChannelRole::Member.into(),
parent_path: vec![2], parent_path: vec![2],
}, },
], ],
@ -70,10 +66,10 @@ fn test_update_channels(cx: &mut AppContext) {
assert_channels( assert_channels(
&channel_store, &channel_store,
&[ &[
(0, "a".to_string(), proto::ChannelRole::Member), (0, "a".to_string()),
(1, "y".to_string(), proto::ChannelRole::Member), (1, "y".to_string()),
(0, "b".to_string(), proto::ChannelRole::Admin), (0, "b".to_string()),
(1, "x".to_string(), proto::ChannelRole::Admin), (1, "x".to_string()),
], ],
cx, cx,
); );
@ -91,21 +87,18 @@ fn test_dangling_channel_paths(cx: &mut AppContext) {
id: 0, id: 0,
name: "a".to_string(), name: "a".to_string(),
visibility: proto::ChannelVisibility::Members as i32, visibility: proto::ChannelVisibility::Members as i32,
role: proto::ChannelRole::Admin.into(),
parent_path: vec![], parent_path: vec![],
}, },
proto::Channel { proto::Channel {
id: 1, id: 1,
name: "b".to_string(), name: "b".to_string(),
visibility: proto::ChannelVisibility::Members as i32, visibility: proto::ChannelVisibility::Members as i32,
role: proto::ChannelRole::Admin.into(),
parent_path: vec![0], parent_path: vec![0],
}, },
proto::Channel { proto::Channel {
id: 2, id: 2,
name: "c".to_string(), name: "c".to_string(),
visibility: proto::ChannelVisibility::Members as i32, visibility: proto::ChannelVisibility::Members as i32,
role: proto::ChannelRole::Admin.into(),
parent_path: vec![0, 1], parent_path: vec![0, 1],
}, },
], ],
@ -118,9 +111,9 @@ fn test_dangling_channel_paths(cx: &mut AppContext) {
&channel_store, &channel_store,
&[ &[
// //
(0, "a".to_string(), proto::ChannelRole::Admin), (0, "a".to_string()),
(1, "b".to_string(), proto::ChannelRole::Admin), (1, "b".to_string()),
(2, "c".to_string(), proto::ChannelRole::Admin), (2, "c".to_string()),
], ],
cx, cx,
); );
@ -135,11 +128,7 @@ fn test_dangling_channel_paths(cx: &mut AppContext) {
); );
// Make sure that the 1/2/3 path is gone // Make sure that the 1/2/3 path is gone
assert_channels( assert_channels(&channel_store, &[(0, "a".to_string())], cx);
&channel_store,
&[(0, "a".to_string(), proto::ChannelRole::Admin)],
cx,
);
} }
#[gpui::test] #[gpui::test]
@ -156,18 +145,13 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
id: channel_id, id: channel_id,
name: "the-channel".to_string(), name: "the-channel".to_string(),
visibility: proto::ChannelVisibility::Members as i32, visibility: proto::ChannelVisibility::Members as i32,
role: proto::ChannelRole::Member.into(),
parent_path: vec![], parent_path: vec![],
}], }],
..Default::default() ..Default::default()
}); });
cx.executor().run_until_parked(); cx.executor().run_until_parked();
cx.update(|cx| { cx.update(|cx| {
assert_channels( assert_channels(&channel_store, &[(0, "the-channel".to_string())], cx);
&channel_store,
&[(0, "the-channel".to_string(), proto::ChannelRole::Member)],
cx,
);
}); });
let get_users = server.receive::<proto::GetUsers>().await.unwrap(); let get_users = server.receive::<proto::GetUsers>().await.unwrap();
@ -368,13 +352,13 @@ fn update_channels(
#[track_caller] #[track_caller]
fn assert_channels( fn assert_channels(
channel_store: &Model<ChannelStore>, channel_store: &Model<ChannelStore>,
expected_channels: &[(usize, String, proto::ChannelRole)], expected_channels: &[(usize, String)],
cx: &mut AppContext, cx: &mut AppContext,
) { ) {
let actual = channel_store.update(cx, |store, _| { let actual = channel_store.update(cx, |store, _| {
store store
.ordered_channels() .ordered_channels()
.map(|(depth, channel)| (depth, channel.name.to_string(), channel.role)) .map(|(depth, channel)| (depth, channel.name.to_string()))
.collect::<Vec<_>>() .collect::<Vec<_>>()
}); });
assert_eq!(actual, expected_channels); assert_eq!(actual, expected_channels);

View file

@ -40,7 +40,7 @@ use std::{
sync::Arc, sync::Arc,
time::Duration, time::Duration,
}; };
use tables::*; pub use tables::*;
use tokio::sync::{Mutex, OwnedMutexGuard}; use tokio::sync::{Mutex, OwnedMutexGuard};
pub use ids::*; pub use ids::*;
@ -502,35 +502,6 @@ pub struct NewUserResult {
pub signup_device_id: Option<String>, pub signup_device_id: Option<String>,
} }
/// The result of moving a channel.
#[derive(Debug)]
pub struct MoveChannelResult {
pub previous_participants: Vec<ChannelMember>,
pub descendent_ids: Vec<ChannelId>,
}
/// The result of renaming a channel.
#[derive(Debug)]
pub struct RenameChannelResult {
pub channel: Channel,
pub participants_to_update: HashMap<UserId, Channel>,
}
/// The result of creating a channel.
#[derive(Debug)]
pub struct CreateChannelResult {
pub channel: Channel,
pub participants_to_update: Vec<(UserId, ChannelsForUser)>,
}
/// The result of setting a channel's visibility.
#[derive(Debug)]
pub struct SetChannelVisibilityResult {
pub participants_to_update: HashMap<UserId, ChannelsForUser>,
pub participants_to_remove: HashSet<UserId>,
pub channels_to_remove: Vec<ChannelId>,
}
/// The result of updating a channel membership. /// The result of updating a channel membership.
#[derive(Debug)] #[derive(Debug)]
pub struct MembershipUpdated { pub struct MembershipUpdated {
@ -570,18 +541,16 @@ pub struct Channel {
pub id: ChannelId, pub id: ChannelId,
pub name: String, pub name: String,
pub visibility: ChannelVisibility, pub visibility: ChannelVisibility,
pub role: ChannelRole,
/// parent_path is the channel ids from the root to this one (not including this one) /// parent_path is the channel ids from the root to this one (not including this one)
pub parent_path: Vec<ChannelId>, pub parent_path: Vec<ChannelId>,
} }
impl Channel { impl Channel {
fn from_model(value: channel::Model, role: ChannelRole) -> Self { fn from_model(value: channel::Model) -> Self {
Channel { Channel {
id: value.id, id: value.id,
visibility: value.visibility, visibility: value.visibility,
name: value.clone().name, name: value.clone().name,
role,
parent_path: value.ancestors().collect(), parent_path: value.ancestors().collect(),
} }
} }
@ -591,7 +560,6 @@ impl Channel {
id: self.id.to_proto(), id: self.id.to_proto(),
name: self.name.clone(), name: self.name.clone(),
visibility: self.visibility.into(), visibility: self.visibility.into(),
role: self.role.into(),
parent_path: self.parent_path.iter().map(|c| c.to_proto()).collect(), parent_path: self.parent_path.iter().map(|c| c.to_proto()).collect(),
} }
} }
@ -617,9 +585,10 @@ impl ChannelMember {
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub struct ChannelsForUser { pub struct ChannelsForUser {
pub channels: Vec<Channel>, pub channels: Vec<Channel>,
pub channel_memberships: Vec<channel_member::Model>,
pub channel_participants: HashMap<ChannelId, Vec<UserId>>, pub channel_participants: HashMap<ChannelId, Vec<UserId>>,
pub unseen_buffer_changes: Vec<proto::UnseenChannelBufferChange>, pub latest_buffer_versions: Vec<proto::ChannelBufferVersion>,
pub channel_messages: Vec<proto::UnseenChannelMessage>, pub latest_channel_messages: Vec<proto::ChannelMessageId>,
} }
#[derive(Debug)] #[derive(Debug)]

View file

@ -129,6 +129,15 @@ impl ChannelRole {
} }
} }
pub fn can_see_channel(&self, visibility: ChannelVisibility) -> bool {
use ChannelRole::*;
match self {
Admin | Member => true,
Guest => visibility == ChannelVisibility::Public,
Banned => false,
}
}
/// True if the role allows access to all descendant channels /// True if the role allows access to all descendant channels
pub fn can_see_all_descendants(&self) -> bool { pub fn can_see_all_descendants(&self) -> bool {
use ChannelRole::*; use ChannelRole::*;

View file

@ -748,18 +748,11 @@ impl Database {
.await .await
} }
pub async fn unseen_channel_buffer_changes( pub async fn latest_channel_buffer_changes(
&self, &self,
user_id: UserId,
channel_ids: &[ChannelId], channel_ids: &[ChannelId],
tx: &DatabaseTransaction, tx: &DatabaseTransaction,
) -> Result<Vec<proto::UnseenChannelBufferChange>> { ) -> Result<Vec<proto::ChannelBufferVersion>> {
#[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
enum QueryIds {
ChannelId,
Id,
}
let mut channel_ids_by_buffer_id = HashMap::default(); let mut channel_ids_by_buffer_id = HashMap::default();
let mut rows = buffer::Entity::find() let mut rows = buffer::Entity::find()
.filter(buffer::Column::ChannelId.is_in(channel_ids.iter().copied())) .filter(buffer::Column::ChannelId.is_in(channel_ids.iter().copied()))
@ -771,51 +764,23 @@ impl Database {
} }
drop(rows); drop(rows);
let mut observed_edits_by_buffer_id = HashMap::default();
let mut rows = observed_buffer_edits::Entity::find()
.filter(observed_buffer_edits::Column::UserId.eq(user_id))
.filter(
observed_buffer_edits::Column::BufferId
.is_in(channel_ids_by_buffer_id.keys().copied()),
)
.stream(&*tx)
.await?;
while let Some(row) = rows.next().await {
let row = row?;
observed_edits_by_buffer_id.insert(row.buffer_id, row);
}
drop(rows);
let latest_operations = self let latest_operations = self
.get_latest_operations_for_buffers(channel_ids_by_buffer_id.keys().copied(), &*tx) .get_latest_operations_for_buffers(channel_ids_by_buffer_id.keys().copied(), &*tx)
.await?; .await?;
let mut changes = Vec::default(); Ok(latest_operations
for latest in latest_operations { .iter()
if let Some(observed) = observed_edits_by_buffer_id.get(&latest.buffer_id) { .flat_map(|op| {
if ( Some(proto::ChannelBufferVersion {
observed.epoch, channel_id: channel_ids_by_buffer_id.get(&op.buffer_id)?.to_proto(),
observed.lamport_timestamp, epoch: op.epoch as u64,
observed.replica_id,
) >= (latest.epoch, latest.lamport_timestamp, latest.replica_id)
{
continue;
}
}
if let Some(channel_id) = channel_ids_by_buffer_id.get(&latest.buffer_id) {
changes.push(proto::UnseenChannelBufferChange {
channel_id: channel_id.to_proto(),
epoch: latest.epoch as u64,
version: vec![proto::VectorClockEntry { version: vec![proto::VectorClockEntry {
replica_id: latest.replica_id as u32, replica_id: op.replica_id as u32,
timestamp: latest.lamport_timestamp as u32, timestamp: op.lamport_timestamp as u32,
}], }],
}); })
} })
} .collect())
Ok(changes)
} }
/// Returns the latest operations for the buffers with the specified IDs. /// Returns the latest operations for the buffers with the specified IDs.

View file

@ -19,7 +19,7 @@ impl Database {
#[cfg(test)] #[cfg(test)]
pub async fn create_root_channel(&self, name: &str, creator_id: UserId) -> Result<ChannelId> { pub async fn create_root_channel(&self, name: &str, creator_id: UserId) -> Result<ChannelId> {
Ok(self.create_channel(name, None, creator_id).await?.id) Ok(self.create_channel(name, None, creator_id).await?.0.id)
} }
#[cfg(test)] #[cfg(test)]
@ -32,6 +32,7 @@ impl Database {
Ok(self Ok(self
.create_channel(name, Some(parent), creator_id) .create_channel(name, Some(parent), creator_id)
.await? .await?
.0
.id) .id)
} }
@ -41,10 +42,15 @@ impl Database {
name: &str, name: &str,
parent_channel_id: Option<ChannelId>, parent_channel_id: Option<ChannelId>,
admin_id: UserId, admin_id: UserId,
) -> Result<Channel> { ) -> Result<(
Channel,
Option<channel_member::Model>,
Vec<channel_member::Model>,
)> {
let name = Self::sanitize_channel_name(name)?; let name = Self::sanitize_channel_name(name)?;
self.transaction(move |tx| async move { self.transaction(move |tx| async move {
let mut parent = None; let mut parent = None;
let mut membership = None;
if let Some(parent_channel_id) = parent_channel_id { if let Some(parent_channel_id) = parent_channel_id {
let parent_channel = self.get_channel_internal(parent_channel_id, &*tx).await?; let parent_channel = self.get_channel_internal(parent_channel_id, &*tx).await?;
@ -68,18 +74,25 @@ impl Database {
.await?; .await?;
if parent.is_none() { if parent.is_none() {
channel_member::ActiveModel { membership = Some(
id: ActiveValue::NotSet, channel_member::ActiveModel {
channel_id: ActiveValue::Set(channel.id), id: ActiveValue::NotSet,
user_id: ActiveValue::Set(admin_id), channel_id: ActiveValue::Set(channel.id),
accepted: ActiveValue::Set(true), user_id: ActiveValue::Set(admin_id),
role: ActiveValue::Set(ChannelRole::Admin), accepted: ActiveValue::Set(true),
} role: ActiveValue::Set(ChannelRole::Admin),
.insert(&*tx) }
.await?; .insert(&*tx)
.await?,
);
} }
Ok(Channel::from_model(channel, ChannelRole::Admin)) let channel_members = channel_member::Entity::find()
.filter(channel_member::Column::ChannelId.eq(channel.root_id()))
.all(&*tx)
.await?;
Ok((Channel::from_model(channel), membership, channel_members))
}) })
.await .await
} }
@ -122,16 +135,9 @@ impl Database {
); );
} else if channel.visibility == ChannelVisibility::Public { } else if channel.visibility == ChannelVisibility::Public {
role = Some(ChannelRole::Guest); role = Some(ChannelRole::Guest);
let channel_to_join = self
.public_ancestors_including_self(&channel, &*tx)
.await?
.first()
.cloned()
.unwrap_or(channel.clone());
channel_member::Entity::insert(channel_member::ActiveModel { channel_member::Entity::insert(channel_member::ActiveModel {
id: ActiveValue::NotSet, id: ActiveValue::NotSet,
channel_id: ActiveValue::Set(channel_to_join.id), channel_id: ActiveValue::Set(channel.root_id()),
user_id: ActiveValue::Set(user_id), user_id: ActiveValue::Set(user_id),
accepted: ActiveValue::Set(true), accepted: ActiveValue::Set(true),
role: ActiveValue::Set(ChannelRole::Guest), role: ActiveValue::Set(ChannelRole::Guest),
@ -140,7 +146,7 @@ impl Database {
.await?; .await?;
accept_invite_result = Some( accept_invite_result = Some(
self.calculate_membership_updated(&channel_to_join, user_id, &*tx) self.calculate_membership_updated(&channel, user_id, &*tx)
.await?, .await?,
); );
@ -173,76 +179,47 @@ impl Database {
channel_id: ChannelId, channel_id: ChannelId,
visibility: ChannelVisibility, visibility: ChannelVisibility,
admin_id: UserId, admin_id: UserId,
) -> Result<SetChannelVisibilityResult> { ) -> Result<(Channel, Vec<channel_member::Model>)> {
self.transaction(move |tx| async move { self.transaction(move |tx| async move {
let channel = self.get_channel_internal(channel_id, &*tx).await?; let channel = self.get_channel_internal(channel_id, &*tx).await?;
self.check_user_is_channel_admin(&channel, admin_id, &*tx) self.check_user_is_channel_admin(&channel, admin_id, &*tx)
.await?; .await?;
let previous_members = self if visibility == ChannelVisibility::Public {
.get_channel_participant_details_internal(&channel, &*tx) if let Some(parent_id) = channel.parent_id() {
.await?; let parent = self.get_channel_internal(parent_id, &*tx).await?;
if parent.visibility != ChannelVisibility::Public {
Err(ErrorCode::BadPublicNesting
.with_tag("direction", "parent")
.anyhow())?;
}
}
} else if visibility == ChannelVisibility::Members {
if self
.get_channel_descendants_including_self(vec![channel_id], &*tx)
.await?
.into_iter()
.any(|channel| {
channel.id != channel_id && channel.visibility == ChannelVisibility::Public
})
{
Err(ErrorCode::BadPublicNesting
.with_tag("direction", "children")
.anyhow())?;
}
}
let mut model = channel.into_active_model(); let mut model = channel.into_active_model();
model.visibility = ActiveValue::Set(visibility); model.visibility = ActiveValue::Set(visibility);
let channel = model.update(&*tx).await?; let channel = model.update(&*tx).await?;
let mut participants_to_update: HashMap<UserId, ChannelsForUser> = self let channel_members = channel_member::Entity::find()
.participants_to_notify_for_channel_change(&channel, &*tx) .filter(channel_member::Column::ChannelId.eq(channel.root_id()))
.await? .all(&*tx)
.into_iter() .await?;
.collect();
let mut channels_to_remove: Vec<ChannelId> = vec![]; Ok((Channel::from_model(channel), channel_members))
let mut participants_to_remove: HashSet<UserId> = HashSet::default();
match visibility {
ChannelVisibility::Members => {
let all_descendents: Vec<ChannelId> = self
.get_channel_descendants_including_self(vec![channel_id], &*tx)
.await?
.into_iter()
.map(|channel| channel.id)
.collect();
channels_to_remove = channel::Entity::find()
.filter(
channel::Column::Id
.is_in(all_descendents)
.and(channel::Column::Visibility.eq(ChannelVisibility::Public)),
)
.all(&*tx)
.await?
.into_iter()
.map(|channel| channel.id)
.collect();
channels_to_remove.push(channel_id);
for member in previous_members {
if member.role.can_only_see_public_descendants() {
participants_to_remove.insert(member.user_id);
}
}
}
ChannelVisibility::Public => {
if let Some(public_parent) = self.public_parent_channel(&channel, &*tx).await? {
let parent_updates = self
.participants_to_notify_for_channel_change(&public_parent, &*tx)
.await?;
for (user_id, channels) in parent_updates {
participants_to_update.insert(user_id, channels);
}
}
}
}
Ok(SetChannelVisibilityResult {
participants_to_update,
participants_to_remove,
channels_to_remove,
})
}) })
.await .await
} }
@ -275,7 +252,7 @@ impl Database {
.await?; .await?;
let members_to_notify: Vec<UserId> = channel_member::Entity::find() let members_to_notify: Vec<UserId> = channel_member::Entity::find()
.filter(channel_member::Column::ChannelId.is_in(channel.ancestors_including_self())) .filter(channel_member::Column::ChannelId.eq(channel.root_id()))
.select_only() .select_only()
.column(channel_member::Column::UserId) .column(channel_member::Column::UserId)
.distinct() .distinct()
@ -312,6 +289,9 @@ impl Database {
let channel = self.get_channel_internal(channel_id, &*tx).await?; let channel = self.get_channel_internal(channel_id, &*tx).await?;
self.check_user_is_channel_admin(&channel, inviter_id, &*tx) self.check_user_is_channel_admin(&channel, inviter_id, &*tx)
.await?; .await?;
if !channel.is_root() {
Err(ErrorCode::NotARootChannel.anyhow())?
}
channel_member::ActiveModel { channel_member::ActiveModel {
id: ActiveValue::NotSet, id: ActiveValue::NotSet,
@ -323,7 +303,7 @@ impl Database {
.insert(&*tx) .insert(&*tx)
.await?; .await?;
let channel = Channel::from_model(channel, role); let channel = Channel::from_model(channel);
let notifications = self let notifications = self
.create_notification( .create_notification(
@ -362,35 +342,24 @@ impl Database {
channel_id: ChannelId, channel_id: ChannelId,
admin_id: UserId, admin_id: UserId,
new_name: &str, new_name: &str,
) -> Result<RenameChannelResult> { ) -> Result<(Channel, Vec<channel_member::Model>)> {
self.transaction(move |tx| async move { self.transaction(move |tx| async move {
let new_name = Self::sanitize_channel_name(new_name)?.to_string(); let new_name = Self::sanitize_channel_name(new_name)?.to_string();
let channel = self.get_channel_internal(channel_id, &*tx).await?; let channel = self.get_channel_internal(channel_id, &*tx).await?;
let role = self self.check_user_is_channel_admin(&channel, admin_id, &*tx)
.check_user_is_channel_admin(&channel, admin_id, &*tx)
.await?; .await?;
let mut model = channel.into_active_model(); let mut model = channel.into_active_model();
model.name = ActiveValue::Set(new_name.clone()); model.name = ActiveValue::Set(new_name.clone());
let channel = model.update(&*tx).await?; let channel = model.update(&*tx).await?;
let participants = self let channel_members = channel_member::Entity::find()
.get_channel_participant_details_internal(&channel, &*tx) .filter(channel_member::Column::ChannelId.eq(channel.root_id()))
.all(&*tx)
.await?; .await?;
Ok(RenameChannelResult { Ok((Channel::from_model(channel), channel_members))
channel: Channel::from_model(channel.clone(), role),
participants_to_update: participants
.iter()
.map(|participant| {
(
participant.user_id,
Channel::from_model(channel.clone(), participant.role),
)
})
.collect(),
})
}) })
.await .await
} }
@ -565,10 +534,7 @@ impl Database {
let channels = channels let channels = channels
.into_iter() .into_iter()
.filter_map(|channel| { .filter_map(|channel| Some(Channel::from_model(channel)))
let role = *role_for_channel.get(&channel.id)?;
Some(Channel::from_model(channel, role))
})
.collect(); .collect();
Ok(channels) Ok(channels)
@ -576,6 +542,26 @@ impl Database {
.await .await
} }
pub async fn get_channel_memberships(
&self,
user_id: UserId,
) -> Result<(Vec<channel_member::Model>, Vec<channel::Model>)> {
self.transaction(|tx| async move {
let memberships = channel_member::Entity::find()
.filter(channel_member::Column::UserId.eq(user_id))
.all(&*tx)
.await?;
let channels = self
.get_channel_descendants_including_self(
memberships.iter().map(|m| m.channel_id),
&*tx,
)
.await?;
Ok((memberships, channels))
})
.await
}
/// Returns all channels for the user with the given ID. /// Returns all channels for the user with the given ID.
pub async fn get_channels_for_user(&self, user_id: UserId) -> Result<ChannelsForUser> { pub async fn get_channels_for_user(&self, user_id: UserId) -> Result<ChannelsForUser> {
self.transaction(|tx| async move { self.transaction(|tx| async move {
@ -594,12 +580,16 @@ impl Database {
ancestor_channel: Option<&channel::Model>, ancestor_channel: Option<&channel::Model>,
tx: &DatabaseTransaction, tx: &DatabaseTransaction,
) -> Result<ChannelsForUser> { ) -> Result<ChannelsForUser> {
let mut filter = channel_member::Column::UserId
.eq(user_id)
.and(channel_member::Column::Accepted.eq(true));
if let Some(ancestor) = ancestor_channel {
filter = filter.and(channel_member::Column::ChannelId.eq(ancestor.root_id()));
}
let channel_memberships = channel_member::Entity::find() let channel_memberships = channel_member::Entity::find()
.filter( .filter(filter)
channel_member::Column::UserId
.eq(user_id)
.and(channel_member::Column::Accepted.eq(true)),
)
.all(&*tx) .all(&*tx)
.await?; .await?;
@ -610,56 +600,20 @@ impl Database {
) )
.await?; .await?;
let mut roles_by_channel_id: HashMap<ChannelId, ChannelRole> = HashMap::default(); let roles_by_channel_id = channel_memberships
for membership in channel_memberships.iter() { .iter()
roles_by_channel_id.insert(membership.channel_id, membership.role); .map(|membership| (membership.channel_id, membership.role))
} .collect::<HashMap<_, _>>();
let mut visible_channel_ids: HashSet<ChannelId> = HashSet::default();
let channels: Vec<Channel> = descendants let channels: Vec<Channel> = descendants
.into_iter() .into_iter()
.filter_map(|channel| { .filter_map(|channel| {
let parent_role = channel let parent_role = roles_by_channel_id.get(&channel.root_id())?;
.parent_id() if parent_role.can_see_channel(channel.visibility) {
.and_then(|parent_id| roles_by_channel_id.get(&parent_id)); Some(Channel::from_model(channel))
let role = if let Some(parent_role) = parent_role {
let role = if let Some(existing_role) = roles_by_channel_id.get(&channel.id) {
existing_role.max(*parent_role)
} else {
*parent_role
};
roles_by_channel_id.insert(channel.id, role);
role
} else { } else {
*roles_by_channel_id.get(&channel.id)? None
};
let can_see_parent_paths = role.can_see_all_descendants()
|| role.can_only_see_public_descendants()
&& channel.visibility == ChannelVisibility::Public;
if !can_see_parent_paths {
return None;
} }
visible_channel_ids.insert(channel.id);
if let Some(ancestor) = ancestor_channel {
if !channel
.ancestors_including_self()
.any(|id| id == ancestor.id)
{
return None;
}
}
let mut channel = Channel::from_model(channel, role);
channel
.parent_path
.retain(|id| visible_channel_ids.contains(&id));
Some(channel)
}) })
.collect(); .collect();
@ -687,89 +641,21 @@ impl Database {
} }
let channel_ids = channels.iter().map(|c| c.id).collect::<Vec<_>>(); let channel_ids = channels.iter().map(|c| c.id).collect::<Vec<_>>();
let channel_buffer_changes = self let latest_buffer_versions = self
.unseen_channel_buffer_changes(user_id, &channel_ids, &*tx) .latest_channel_buffer_changes(&channel_ids, &*tx)
.await?; .await?;
let unseen_messages = self let latest_messages = self.latest_channel_messages(&channel_ids, &*tx).await?;
.unseen_channel_messages(user_id, &channel_ids, &*tx)
.await?;
Ok(ChannelsForUser { Ok(ChannelsForUser {
channel_memberships,
channels, channels,
channel_participants, channel_participants,
unseen_buffer_changes: channel_buffer_changes, latest_buffer_versions,
channel_messages: unseen_messages, latest_channel_messages: latest_messages,
}) })
} }
pub async fn new_participants_to_notify(
&self,
parent_channel_id: ChannelId,
) -> Result<Vec<(UserId, ChannelsForUser)>> {
self.weak_transaction(|tx| async move {
let parent_channel = self.get_channel_internal(parent_channel_id, &*tx).await?;
self.participants_to_notify_for_channel_change(&parent_channel, &*tx)
.await
})
.await
}
// TODO: this is very expensive, and we should rethink
async fn participants_to_notify_for_channel_change(
&self,
new_parent: &channel::Model,
tx: &DatabaseTransaction,
) -> Result<Vec<(UserId, ChannelsForUser)>> {
let mut results: Vec<(UserId, ChannelsForUser)> = Vec::new();
let members = self
.get_channel_participant_details_internal(new_parent, &*tx)
.await?;
for member in members.iter() {
if !member.role.can_see_all_descendants() {
continue;
}
results.push((
member.user_id,
self.get_user_channels(member.user_id, Some(new_parent), &*tx)
.await?,
))
}
let public_parents = self
.public_ancestors_including_self(new_parent, &*tx)
.await?;
let public_parent = public_parents.last();
let Some(public_parent) = public_parent else {
return Ok(results);
};
// could save some time in the common case by skipping this if the
// new channel is not public and has no public descendants.
let public_members = if public_parent == new_parent {
members
} else {
self.get_channel_participant_details_internal(public_parent, &*tx)
.await?
};
for member in public_members {
if !member.role.can_only_see_public_descendants() {
continue;
};
results.push((
member.user_id,
self.get_user_channels(member.user_id, Some(public_parent), &*tx)
.await?,
))
}
Ok(results)
}
/// Sets the role for the specified channel member. /// Sets the role for the specified channel member.
pub async fn set_channel_member_role( pub async fn set_channel_member_role(
&self, &self,
@ -807,7 +693,7 @@ impl Database {
)) ))
} else { } else {
Ok(SetMemberRoleResult::InviteUpdated(Channel::from_model( Ok(SetMemberRoleResult::InviteUpdated(Channel::from_model(
channel, role, channel,
))) )))
} }
}) })
@ -837,22 +723,30 @@ impl Database {
if role == ChannelRole::Admin { if role == ChannelRole::Admin {
Ok(members Ok(members
.into_iter() .into_iter()
.map(|channel_member| channel_member.to_proto()) .map(|channel_member| proto::ChannelMember {
role: channel_member.role.into(),
user_id: channel_member.user_id.to_proto(),
kind: if channel_member.accepted {
Kind::Member
} else {
Kind::Invitee
}
.into(),
})
.collect()) .collect())
} else { } else {
return Ok(members return Ok(members
.into_iter() .into_iter()
.filter_map(|member| { .filter_map(|member| {
if member.kind == proto::channel_member::Kind::Invitee { if !member.accepted {
return None; return None;
} }
Some(ChannelMember { Some(proto::ChannelMember {
role: member.role, role: member.role.into(),
user_id: member.user_id, user_id: member.user_id.to_proto(),
kind: proto::channel_member::Kind::Member, kind: Kind::Member.into(),
}) })
}) })
.map(|channel_member| channel_member.to_proto())
.collect()); .collect());
} }
} }
@ -861,83 +755,11 @@ impl Database {
&self, &self,
channel: &channel::Model, channel: &channel::Model,
tx: &DatabaseTransaction, tx: &DatabaseTransaction,
) -> Result<Vec<ChannelMember>> { ) -> Result<Vec<channel_member::Model>> {
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] Ok(channel_member::Entity::find()
enum QueryMemberDetails { .filter(channel_member::Column::ChannelId.eq(channel.root_id()))
UserId, .all(tx)
Role, .await?)
IsDirectMember,
Accepted,
Visibility,
}
let mut stream = channel_member::Entity::find()
.left_join(channel::Entity)
.filter(channel_member::Column::ChannelId.is_in(channel.ancestors_including_self()))
.select_only()
.column(channel_member::Column::UserId)
.column(channel_member::Column::Role)
.column_as(
channel_member::Column::ChannelId.eq(channel.id),
QueryMemberDetails::IsDirectMember,
)
.column(channel_member::Column::Accepted)
.column(channel::Column::Visibility)
.into_values::<_, QueryMemberDetails>()
.stream(&*tx)
.await?;
let mut user_details: HashMap<UserId, ChannelMember> = HashMap::default();
while let Some(user_membership) = stream.next().await {
let (user_id, channel_role, is_direct_member, is_invite_accepted, visibility): (
UserId,
ChannelRole,
bool,
bool,
ChannelVisibility,
) = user_membership?;
let kind = match (is_direct_member, is_invite_accepted) {
(true, true) => proto::channel_member::Kind::Member,
(true, false) => proto::channel_member::Kind::Invitee,
(false, true) => proto::channel_member::Kind::AncestorMember,
(false, false) => continue,
};
if channel_role == ChannelRole::Guest
&& visibility != ChannelVisibility::Public
&& channel.visibility != ChannelVisibility::Public
{
continue;
}
if let Some(details_mut) = user_details.get_mut(&user_id) {
if channel_role.should_override(details_mut.role) {
details_mut.role = channel_role;
}
if kind == Kind::Member {
details_mut.kind = kind;
// the UI is going to be a bit confusing if you already have permissions
// that are greater than or equal to the ones you're being invited to.
} else if kind == Kind::Invitee && details_mut.kind == Kind::AncestorMember {
details_mut.kind = kind;
}
} else {
user_details.insert(
user_id,
ChannelMember {
user_id,
kind,
role: channel_role,
},
);
}
}
Ok(user_details
.into_iter()
.map(|(_, details)| details)
.collect())
} }
/// Returns the participants in the given channel. /// Returns the participants in the given channel.
@ -1016,7 +838,7 @@ impl Database {
tx: &DatabaseTransaction, tx: &DatabaseTransaction,
) -> Result<Option<channel_member::Model>> { ) -> Result<Option<channel_member::Model>> {
let row = channel_member::Entity::find() let row = channel_member::Entity::find()
.filter(channel_member::Column::ChannelId.is_in(channel.ancestors_including_self())) .filter(channel_member::Column::ChannelId.eq(channel.root_id()))
.filter(channel_member::Column::UserId.eq(user_id)) .filter(channel_member::Column::UserId.eq(user_id))
.filter(channel_member::Column::Accepted.eq(false)) .filter(channel_member::Column::Accepted.eq(false))
.one(&*tx) .one(&*tx)
@ -1025,33 +847,6 @@ impl Database {
Ok(row) Ok(row)
} }
async fn public_parent_channel(
&self,
channel: &channel::Model,
tx: &DatabaseTransaction,
) -> Result<Option<channel::Model>> {
let mut path = self.public_ancestors_including_self(channel, &*tx).await?;
if path.last().unwrap().id == channel.id {
path.pop();
}
Ok(path.pop())
}
pub(crate) async fn public_ancestors_including_self(
&self,
channel: &channel::Model,
tx: &DatabaseTransaction,
) -> Result<Vec<channel::Model>> {
let visible_channels = channel::Entity::find()
.filter(channel::Column::Id.is_in(channel.ancestors_including_self()))
.filter(channel::Column::Visibility.eq(ChannelVisibility::Public))
.order_by_asc(channel::Column::ParentPath)
.all(&*tx)
.await?;
Ok(visible_channels)
}
/// Returns the role for a user in the given channel. /// Returns the role for a user in the given channel.
pub async fn channel_role_for_user( pub async fn channel_role_for_user(
&self, &self,
@ -1059,77 +854,25 @@ impl Database {
user_id: UserId, user_id: UserId,
tx: &DatabaseTransaction, tx: &DatabaseTransaction,
) -> Result<Option<ChannelRole>> { ) -> Result<Option<ChannelRole>> {
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] let membership = channel_member::Entity::find()
enum QueryChannelMembership {
ChannelId,
Role,
Visibility,
}
let mut rows = channel_member::Entity::find()
.left_join(channel::Entity)
.filter( .filter(
channel_member::Column::ChannelId channel_member::Column::ChannelId
.is_in(channel.ancestors_including_self()) .eq(channel.root_id())
.and(channel_member::Column::UserId.eq(user_id)) .and(channel_member::Column::UserId.eq(user_id))
.and(channel_member::Column::Accepted.eq(true)), .and(channel_member::Column::Accepted.eq(true)),
) )
.select_only() .one(&*tx)
.column(channel_member::Column::ChannelId)
.column(channel_member::Column::Role)
.column(channel::Column::Visibility)
.into_values::<_, QueryChannelMembership>()
.stream(&*tx)
.await?; .await?;
let mut user_role: Option<ChannelRole> = None; let Some(membership) = membership else {
return Ok(None);
};
let mut is_participant = false; if !membership.role.can_see_channel(channel.visibility) {
let mut current_channel_visibility = None; return Ok(None);
// note these channels are not iterated in any particular order,
// our current logic takes the highest permission available.
while let Some(row) = rows.next().await {
let (membership_channel, role, visibility): (
ChannelId,
ChannelRole,
ChannelVisibility,
) = row?;
match role {
ChannelRole::Admin | ChannelRole::Member | ChannelRole::Banned => {
if let Some(users_role) = user_role {
user_role = Some(users_role.max(role));
} else {
user_role = Some(role)
}
}
ChannelRole::Guest if visibility == ChannelVisibility::Public => {
is_participant = true
}
ChannelRole::Guest => {}
}
if channel.id == membership_channel {
current_channel_visibility = Some(visibility);
}
}
// free up database connection
drop(rows);
if is_participant && user_role.is_none() {
if current_channel_visibility.is_none() {
current_channel_visibility = channel::Entity::find()
.filter(channel::Column::Id.eq(channel.id))
.one(&*tx)
.await?
.map(|channel| channel.visibility);
}
if current_channel_visibility == Some(ChannelVisibility::Public) {
user_role = Some(ChannelRole::Guest);
}
} }
Ok(user_role) Ok(Some(membership.role))
} }
// Get the descendants of the given set if channels, ordered by their // Get the descendants of the given set if channels, ordered by their
@ -1182,11 +925,10 @@ impl Database {
pub async fn get_channel(&self, channel_id: ChannelId, user_id: UserId) -> Result<Channel> { pub async fn get_channel(&self, channel_id: ChannelId, user_id: UserId) -> Result<Channel> {
self.transaction(|tx| async move { self.transaction(|tx| async move {
let channel = self.get_channel_internal(channel_id, &*tx).await?; let channel = self.get_channel_internal(channel_id, &*tx).await?;
let role = self self.check_user_is_channel_participant(&channel, user_id, &*tx)
.check_user_is_channel_participant(&channel, user_id, &*tx)
.await?; .await?;
Ok(Channel::from_model(channel, role)) Ok(Channel::from_model(channel))
}) })
.await .await
} }
@ -1243,61 +985,39 @@ impl Database {
pub async fn move_channel( pub async fn move_channel(
&self, &self,
channel_id: ChannelId, channel_id: ChannelId,
new_parent_id: Option<ChannelId>, new_parent_id: ChannelId,
admin_id: UserId, admin_id: UserId,
) -> Result<Option<MoveChannelResult>> { ) -> Result<(Vec<Channel>, Vec<channel_member::Model>)> {
self.transaction(|tx| async move { self.transaction(|tx| async move {
let channel = self.get_channel_internal(channel_id, &*tx).await?; let channel = self.get_channel_internal(channel_id, &*tx).await?;
self.check_user_is_channel_admin(&channel, admin_id, &*tx) self.check_user_is_channel_admin(&channel, admin_id, &*tx)
.await?; .await?;
let new_parent = self.get_channel_internal(new_parent_id, &*tx).await?;
let new_parent_path; if new_parent.root_id() != channel.root_id() {
let new_parent_channel; Err(anyhow!(ErrorCode::WrongMoveTarget))?;
if let Some(new_parent_id) = new_parent_id {
let new_parent = self.get_channel_internal(new_parent_id, &*tx).await?;
self.check_user_is_channel_admin(&new_parent, admin_id, &*tx)
.await?;
if new_parent
.ancestors_including_self()
.any(|id| id == channel.id)
{
Err(anyhow!("cannot move a channel into one of its descendants"))?;
}
new_parent_path = new_parent.path();
new_parent_channel = Some(new_parent);
} else {
new_parent_path = String::new();
new_parent_channel = None;
};
let previous_participants = self
.get_channel_participant_details_internal(&channel, &*tx)
.await?;
let old_path = format!("{}{}/", channel.parent_path, channel.id);
let new_path = format!("{}{}/", new_parent_path, channel.id);
if old_path == new_path {
return Ok(None);
} }
if new_parent
.ancestors_including_self()
.any(|id| id == channel.id)
{
Err(anyhow!(ErrorCode::CircularNesting))?;
}
if channel.visibility == ChannelVisibility::Public
&& new_parent.visibility != ChannelVisibility::Public
{
Err(anyhow!(ErrorCode::BadPublicNesting))?;
}
let root_id = channel.root_id();
let old_path = format!("{}{}/", channel.parent_path, channel.id);
let new_path = format!("{}{}/", new_parent.path(), channel.id);
let mut model = channel.into_active_model(); let mut model = channel.into_active_model();
model.parent_path = ActiveValue::Set(new_parent_path); model.parent_path = ActiveValue::Set(new_parent.path());
model.update(&*tx).await?; let channel = model.update(&*tx).await?;
if new_parent_channel.is_none() {
channel_member::ActiveModel {
id: ActiveValue::NotSet,
channel_id: ActiveValue::Set(channel_id),
user_id: ActiveValue::Set(admin_id),
accepted: ActiveValue::Set(true),
role: ActiveValue::Set(ChannelRole::Admin),
}
.insert(&*tx)
.await?;
}
let descendent_ids = let descendent_ids =
ChannelId::find_by_statement::<QueryIds>(Statement::from_sql_and_values( ChannelId::find_by_statement::<QueryIds>(Statement::from_sql_and_values(
@ -1312,10 +1032,22 @@ impl Database {
.all(&*tx) .all(&*tx)
.await?; .await?;
Ok(Some(MoveChannelResult { let all_moved_ids = Some(channel.id).into_iter().chain(descendent_ids);
previous_participants,
descendent_ids, let channels = channel::Entity::find()
})) .filter(channel::Column::Id.is_in(all_moved_ids))
.all(&*tx)
.await?
.into_iter()
.map(|c| Channel::from_model(c))
.collect::<Vec<_>>();
let channel_members = channel_member::Entity::find()
.filter(channel_member::Column::ChannelId.eq(root_id))
.all(&*tx)
.await?;
Ok((channels, channel_members))
}) })
.await .await
} }

View file

@ -385,25 +385,11 @@ impl Database {
Ok(()) Ok(())
} }
/// Returns the unseen messages for the given user in the specified channels. pub async fn latest_channel_messages(
pub async fn unseen_channel_messages(
&self, &self,
user_id: UserId,
channel_ids: &[ChannelId], channel_ids: &[ChannelId],
tx: &DatabaseTransaction, tx: &DatabaseTransaction,
) -> Result<Vec<proto::UnseenChannelMessage>> { ) -> Result<Vec<proto::ChannelMessageId>> {
let mut observed_messages_by_channel_id = HashMap::default();
let mut rows = observed_channel_messages::Entity::find()
.filter(observed_channel_messages::Column::UserId.eq(user_id))
.filter(observed_channel_messages::Column::ChannelId.is_in(channel_ids.iter().copied()))
.stream(&*tx)
.await?;
while let Some(row) = rows.next().await {
let row = row?;
observed_messages_by_channel_id.insert(row.channel_id, row);
}
drop(rows);
let mut values = String::new(); let mut values = String::new();
for id in channel_ids { for id in channel_ids {
if !values.is_empty() { if !values.is_empty() {
@ -413,7 +399,7 @@ impl Database {
} }
if values.is_empty() { if values.is_empty() {
return Ok(Default::default()); return Ok(Vec::default());
} }
let sql = format!( let sql = format!(
@ -437,26 +423,20 @@ impl Database {
); );
let stmt = Statement::from_string(self.pool.get_database_backend(), sql); let stmt = Statement::from_string(self.pool.get_database_backend(), sql);
let last_messages = channel_message::Model::find_by_statement(stmt) let mut last_messages = channel_message::Model::find_by_statement(stmt)
.all(&*tx) .stream(&*tx)
.await?; .await?;
let mut changes = Vec::new(); let mut results = Vec::new();
for last_message in last_messages { while let Some(result) = last_messages.next().await {
if let Some(observed_message) = let message = result?;
observed_messages_by_channel_id.get(&last_message.channel_id) results.push(proto::ChannelMessageId {
{ channel_id: message.channel_id.to_proto(),
if observed_message.channel_message_id == last_message.id { message_id: message.id.to_proto(),
continue;
}
}
changes.push(proto::UnseenChannelMessage {
channel_id: last_message.channel_id.to_proto(),
message_id: last_message.id.to_proto(),
}); });
} }
Ok(changes) Ok(results)
} }
/// Removes the channel message with the given ID. /// Removes the channel message with the given ID.

View file

@ -17,6 +17,14 @@ impl Model {
self.ancestors().last() self.ancestors().last()
} }
pub fn is_root(&self) -> bool {
self.parent_path.is_empty()
}
pub fn root_id(&self) -> ChannelId {
self.ancestors().next().unwrap_or(self.id)
}
pub fn ancestors(&self) -> impl Iterator<Item = ChannelId> + '_ { pub fn ancestors(&self) -> impl Iterator<Item = ChannelId> + '_ {
self.parent_path self.parent_path
.trim_end_matches('/') .trim_end_matches('/')

View file

@ -150,14 +150,13 @@ impl Drop for TestDb {
} }
} }
fn channel_tree(channels: &[(ChannelId, &[ChannelId], &'static str, ChannelRole)]) -> Vec<Channel> { fn channel_tree(channels: &[(ChannelId, &[ChannelId], &'static str)]) -> Vec<Channel> {
channels channels
.iter() .iter()
.map(|(id, parent_path, name, role)| Channel { .map(|(id, parent_path, name)| Channel {
id: *id, id: *id,
name: name.to_string(), name: name.to_string(),
visibility: ChannelVisibility::Members, visibility: ChannelVisibility::Members,
role: *role,
parent_path: parent_path.to_vec(), parent_path: parent_path.to_vec(),
}) })
.collect() .collect()

View file

@ -330,8 +330,7 @@ async fn test_channel_buffers_last_operations(db: &Database) {
.transaction(|tx| { .transaction(|tx| {
let buffers = &buffers; let buffers = &buffers;
async move { async move {
db.unseen_channel_buffer_changes( db.latest_channel_buffer_changes(
observer_id,
&[ &[
buffers[0].channel_id, buffers[0].channel_id,
buffers[1].channel_id, buffers[1].channel_id,
@ -348,12 +347,12 @@ async fn test_channel_buffers_last_operations(db: &Database) {
pretty_assertions::assert_eq!( pretty_assertions::assert_eq!(
buffer_changes, buffer_changes,
[ [
rpc::proto::UnseenChannelBufferChange { rpc::proto::ChannelBufferVersion {
channel_id: buffers[0].channel_id.to_proto(), channel_id: buffers[0].channel_id.to_proto(),
epoch: 0, epoch: 0,
version: serialize_version(&text_buffers[0].version()), version: serialize_version(&text_buffers[0].version()),
}, },
rpc::proto::UnseenChannelBufferChange { rpc::proto::ChannelBufferVersion {
channel_id: buffers[1].channel_id.to_proto(), channel_id: buffers[1].channel_id.to_proto(),
epoch: 1, epoch: 1,
version: serialize_version(&text_buffers[1].version()) version: serialize_version(&text_buffers[1].version())
@ -362,99 +361,7 @@ async fn test_channel_buffers_last_operations(db: &Database) {
== buffer_changes[1].version.first().unwrap().replica_id) == buffer_changes[1].version.first().unwrap().replica_id)
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
}, },
rpc::proto::UnseenChannelBufferChange { rpc::proto::ChannelBufferVersion {
channel_id: buffers[2].channel_id.to_proto(),
epoch: 0,
version: serialize_version(&text_buffers[2].version()),
},
]
);
db.observe_buffer_version(
buffers[1].id,
observer_id,
1,
serialize_version(&text_buffers[1].version()).as_slice(),
)
.await
.unwrap();
let buffer_changes = db
.transaction(|tx| {
let buffers = &buffers;
async move {
db.unseen_channel_buffer_changes(
observer_id,
&[
buffers[0].channel_id,
buffers[1].channel_id,
buffers[2].channel_id,
],
&*tx,
)
.await
}
})
.await
.unwrap();
assert_eq!(
buffer_changes,
[
rpc::proto::UnseenChannelBufferChange {
channel_id: buffers[0].channel_id.to_proto(),
epoch: 0,
version: serialize_version(&text_buffers[0].version()),
},
rpc::proto::UnseenChannelBufferChange {
channel_id: buffers[2].channel_id.to_proto(),
epoch: 0,
version: serialize_version(&text_buffers[2].version()),
},
]
);
// Observe an earlier version of the buffer.
db.observe_buffer_version(
buffers[1].id,
observer_id,
1,
&[rpc::proto::VectorClockEntry {
replica_id: 0,
timestamp: 0,
}],
)
.await
.unwrap();
let buffer_changes = db
.transaction(|tx| {
let buffers = &buffers;
async move {
db.unseen_channel_buffer_changes(
observer_id,
&[
buffers[0].channel_id,
buffers[1].channel_id,
buffers[2].channel_id,
],
&*tx,
)
.await
}
})
.await
.unwrap();
assert_eq!(
buffer_changes,
[
rpc::proto::UnseenChannelBufferChange {
channel_id: buffers[0].channel_id.to_proto(),
epoch: 0,
version: serialize_version(&text_buffers[0].version()),
},
rpc::proto::UnseenChannelBufferChange {
channel_id: buffers[2].channel_id.to_proto(), channel_id: buffers[2].channel_id.to_proto(),
epoch: 0, epoch: 0,
version: serialize_version(&text_buffers[2].version()), version: serialize_version(&text_buffers[2].version()),

View file

@ -62,23 +62,13 @@ async fn test_channels(db: &Arc<Database>) {
assert_eq!( assert_eq!(
result.channels, result.channels,
channel_tree(&[ channel_tree(&[
(zed_id, &[], "zed", ChannelRole::Admin), (zed_id, &[], "zed"),
(crdb_id, &[zed_id], "crdb", ChannelRole::Admin), (crdb_id, &[zed_id], "crdb"),
( (livestreaming_id, &[zed_id], "livestreaming",),
livestreaming_id, (replace_id, &[zed_id], "replace"),
&[zed_id], (rust_id, &[], "rust"),
"livestreaming", (cargo_id, &[rust_id], "cargo"),
ChannelRole::Admin (cargo_ra_id, &[rust_id, cargo_id], "cargo-ra",)
),
(replace_id, &[zed_id], "replace", ChannelRole::Admin),
(rust_id, &[], "rust", ChannelRole::Admin),
(cargo_id, &[rust_id], "cargo", ChannelRole::Admin),
(
cargo_ra_id,
&[rust_id, cargo_id],
"cargo-ra",
ChannelRole::Admin
)
],) ],)
); );
@ -86,15 +76,10 @@ async fn test_channels(db: &Arc<Database>) {
assert_eq!( assert_eq!(
result.channels, result.channels,
channel_tree(&[ channel_tree(&[
(zed_id, &[], "zed", ChannelRole::Member), (zed_id, &[], "zed"),
(crdb_id, &[zed_id], "crdb", ChannelRole::Member), (crdb_id, &[zed_id], "crdb"),
( (livestreaming_id, &[zed_id], "livestreaming",),
livestreaming_id, (replace_id, &[zed_id], "replace")
&[zed_id],
"livestreaming",
ChannelRole::Member
),
(replace_id, &[zed_id], "replace", ChannelRole::Member)
],) ],)
); );
@ -112,15 +97,10 @@ async fn test_channels(db: &Arc<Database>) {
assert_eq!( assert_eq!(
result.channels, result.channels,
channel_tree(&[ channel_tree(&[
(zed_id, &[], "zed", ChannelRole::Admin), (zed_id, &[], "zed"),
(crdb_id, &[zed_id], "crdb", ChannelRole::Admin), (crdb_id, &[zed_id], "crdb"),
( (livestreaming_id, &[zed_id], "livestreaming",),
livestreaming_id, (replace_id, &[zed_id], "replace")
&[zed_id],
"livestreaming",
ChannelRole::Admin
),
(replace_id, &[zed_id], "replace", ChannelRole::Admin)
],) ],)
); );
@ -271,14 +251,19 @@ async fn test_channel_invites(db: &Arc<Database>) {
&[ &[
proto::ChannelMember { proto::ChannelMember {
user_id: user_1.to_proto(), user_id: user_1.to_proto(),
kind: proto::channel_member::Kind::AncestorMember.into(), kind: proto::channel_member::Kind::Member.into(),
role: proto::ChannelRole::Admin.into(), role: proto::ChannelRole::Admin.into(),
}, },
proto::ChannelMember { proto::ChannelMember {
user_id: user_2.to_proto(), user_id: user_2.to_proto(),
kind: proto::channel_member::Kind::AncestorMember.into(), kind: proto::channel_member::Kind::Member.into(),
role: proto::ChannelRole::Member.into(), role: proto::ChannelRole::Member.into(),
}, },
proto::ChannelMember {
user_id: user_3.to_proto(),
kind: proto::channel_member::Kind::Invitee.into(),
role: proto::ChannelRole::Admin.into(),
},
] ]
); );
} }
@ -420,13 +405,6 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
.await .await
.unwrap(); .unwrap();
// Move to same parent should be a no-op
assert!(db
.move_channel(projects_id, Some(zed_id), user_id)
.await
.unwrap()
.is_none());
let result = db.get_channels_for_user(user_id).await.unwrap(); let result = db.get_channels_for_user(user_id).await.unwrap();
assert_channel_tree( assert_channel_tree(
result.channels, result.channels,
@ -437,20 +415,8 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
], ],
); );
// Move the project channel to the root
db.move_channel(projects_id, None, user_id).await.unwrap();
let result = db.get_channels_for_user(user_id).await.unwrap();
assert_channel_tree(
result.channels,
&[
(zed_id, &[]),
(projects_id, &[]),
(livestreaming_id, &[projects_id]),
],
);
// Can't move a channel into its ancestor // Can't move a channel into its ancestor
db.move_channel(projects_id, Some(livestreaming_id), user_id) db.move_channel(projects_id, livestreaming_id, user_id)
.await .await
.unwrap_err(); .unwrap_err();
let result = db.get_channels_for_user(user_id).await.unwrap(); let result = db.get_channels_for_user(user_id).await.unwrap();
@ -458,8 +424,8 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
result.channels, result.channels,
&[ &[
(zed_id, &[]), (zed_id, &[]),
(projects_id, &[]), (projects_id, &[zed_id]),
(livestreaming_id, &[projects_id]), (livestreaming_id, &[zed_id, projects_id]),
], ],
); );
} }
@ -476,32 +442,39 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
let guest = new_test_user(db, "guest@example.com").await; let guest = new_test_user(db, "guest@example.com").await;
let zed_channel = db.create_root_channel("zed", admin).await.unwrap(); let zed_channel = db.create_root_channel("zed", admin).await.unwrap();
let active_channel_id = db let internal_channel_id = db
.create_sub_channel("active", zed_channel, admin) .create_sub_channel("active", zed_channel, admin)
.await .await
.unwrap(); .unwrap();
let vim_channel_id = db let public_channel_id = db
.create_sub_channel("vim", active_channel_id, admin) .create_sub_channel("vim", zed_channel, admin)
.await .await
.unwrap(); .unwrap();
db.set_channel_visibility(vim_channel_id, crate::db::ChannelVisibility::Public, admin) db.set_channel_visibility(zed_channel, crate::db::ChannelVisibility::Public, admin)
.await .await
.unwrap(); .unwrap();
db.invite_channel_member(active_channel_id, member, admin, ChannelRole::Member) db.set_channel_visibility(
public_channel_id,
crate::db::ChannelVisibility::Public,
admin,
)
.await
.unwrap();
db.invite_channel_member(zed_channel, member, admin, ChannelRole::Member)
.await .await
.unwrap(); .unwrap();
db.invite_channel_member(vim_channel_id, guest, admin, ChannelRole::Guest) db.invite_channel_member(zed_channel, guest, admin, ChannelRole::Guest)
.await .await
.unwrap(); .unwrap();
db.respond_to_channel_invite(active_channel_id, member, true) db.respond_to_channel_invite(zed_channel, member, true)
.await .await
.unwrap(); .unwrap();
db.transaction(|tx| async move { db.transaction(|tx| async move {
db.check_user_is_channel_participant( db.check_user_is_channel_participant(
&db.get_channel_internal(vim_channel_id, &*tx).await?, &db.get_channel_internal(public_channel_id, &*tx).await?,
admin, admin,
&*tx, &*tx,
) )
@ -511,7 +484,7 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
.unwrap(); .unwrap();
db.transaction(|tx| async move { db.transaction(|tx| async move {
db.check_user_is_channel_participant( db.check_user_is_channel_participant(
&db.get_channel_internal(vim_channel_id, &*tx).await?, &db.get_channel_internal(public_channel_id, &*tx).await?,
member, member,
&*tx, &*tx,
) )
@ -521,7 +494,7 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
.unwrap(); .unwrap();
let mut members = db let mut members = db
.get_channel_participant_details(vim_channel_id, admin) .get_channel_participant_details(public_channel_id, admin)
.await .await
.unwrap(); .unwrap();
@ -532,12 +505,12 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
&[ &[
proto::ChannelMember { proto::ChannelMember {
user_id: admin.to_proto(), user_id: admin.to_proto(),
kind: proto::channel_member::Kind::AncestorMember.into(), kind: proto::channel_member::Kind::Member.into(),
role: proto::ChannelRole::Admin.into(), role: proto::ChannelRole::Admin.into(),
}, },
proto::ChannelMember { proto::ChannelMember {
user_id: member.to_proto(), user_id: member.to_proto(),
kind: proto::channel_member::Kind::AncestorMember.into(), kind: proto::channel_member::Kind::Member.into(),
role: proto::ChannelRole::Member.into(), role: proto::ChannelRole::Member.into(),
}, },
proto::ChannelMember { proto::ChannelMember {
@ -548,13 +521,13 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
] ]
); );
db.respond_to_channel_invite(vim_channel_id, guest, true) db.respond_to_channel_invite(zed_channel, guest, true)
.await .await
.unwrap(); .unwrap();
db.transaction(|tx| async move { db.transaction(|tx| async move {
db.check_user_is_channel_participant( db.check_user_is_channel_participant(
&db.get_channel_internal(vim_channel_id, &*tx).await?, &db.get_channel_internal(public_channel_id, &*tx).await?,
guest, guest,
&*tx, &*tx,
) )
@ -564,23 +537,29 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
.unwrap(); .unwrap();
let channels = db.get_channels_for_user(guest).await.unwrap().channels; let channels = db.get_channels_for_user(guest).await.unwrap().channels;
assert_channel_tree(channels, &[(vim_channel_id, &[])]); assert_channel_tree(
channels,
&[(zed_channel, &[]), (public_channel_id, &[zed_channel])],
);
let channels = db.get_channels_for_user(member).await.unwrap().channels; let channels = db.get_channels_for_user(member).await.unwrap().channels;
assert_channel_tree( assert_channel_tree(
channels, channels,
&[ &[
(active_channel_id, &[]), (zed_channel, &[]),
(vim_channel_id, &[active_channel_id]), (internal_channel_id, &[zed_channel]),
(public_channel_id, &[zed_channel]),
], ],
); );
db.set_channel_member_role(vim_channel_id, admin, guest, ChannelRole::Banned) db.set_channel_member_role(zed_channel, admin, guest, ChannelRole::Banned)
.await .await
.unwrap(); .unwrap();
assert!(db assert!(db
.transaction(|tx| async move { .transaction(|tx| async move {
db.check_user_is_channel_participant( db.check_user_is_channel_participant(
&db.get_channel_internal(vim_channel_id, &*tx).await.unwrap(), &db.get_channel_internal(public_channel_id, &*tx)
.await
.unwrap(),
guest, guest,
&*tx, &*tx,
) )
@ -590,7 +569,7 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
.is_err()); .is_err());
let mut members = db let mut members = db
.get_channel_participant_details(vim_channel_id, admin) .get_channel_participant_details(public_channel_id, admin)
.await .await
.unwrap(); .unwrap();
@ -601,12 +580,12 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
&[ &[
proto::ChannelMember { proto::ChannelMember {
user_id: admin.to_proto(), user_id: admin.to_proto(),
kind: proto::channel_member::Kind::AncestorMember.into(), kind: proto::channel_member::Kind::Member.into(),
role: proto::ChannelRole::Admin.into(), role: proto::ChannelRole::Admin.into(),
}, },
proto::ChannelMember { proto::ChannelMember {
user_id: member.to_proto(), user_id: member.to_proto(),
kind: proto::channel_member::Kind::AncestorMember.into(), kind: proto::channel_member::Kind::Member.into(),
role: proto::ChannelRole::Member.into(), role: proto::ChannelRole::Member.into(),
}, },
proto::ChannelMember { proto::ChannelMember {
@ -617,11 +596,7 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
] ]
); );
db.remove_channel_member(vim_channel_id, guest, admin) db.remove_channel_member(zed_channel, guest, admin)
.await
.unwrap();
db.set_channel_visibility(zed_channel, crate::db::ChannelVisibility::Public, admin)
.await .await
.unwrap(); .unwrap();
@ -631,7 +606,7 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
// currently people invited to parent channels are not shown here // currently people invited to parent channels are not shown here
let mut members = db let mut members = db
.get_channel_participant_details(vim_channel_id, admin) .get_channel_participant_details(public_channel_id, admin)
.await .await
.unwrap(); .unwrap();
@ -642,14 +617,19 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
&[ &[
proto::ChannelMember { proto::ChannelMember {
user_id: admin.to_proto(), user_id: admin.to_proto(),
kind: proto::channel_member::Kind::AncestorMember.into(), kind: proto::channel_member::Kind::Member.into(),
role: proto::ChannelRole::Admin.into(), role: proto::ChannelRole::Admin.into(),
}, },
proto::ChannelMember { proto::ChannelMember {
user_id: member.to_proto(), user_id: member.to_proto(),
kind: proto::channel_member::Kind::AncestorMember.into(), kind: proto::channel_member::Kind::Member.into(),
role: proto::ChannelRole::Member.into(), role: proto::ChannelRole::Member.into(),
}, },
proto::ChannelMember {
user_id: guest.to_proto(),
kind: proto::channel_member::Kind::Invitee.into(),
role: proto::ChannelRole::Guest.into(),
},
] ]
); );
@ -670,7 +650,7 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
assert!(db assert!(db
.transaction(|tx| async move { .transaction(|tx| async move {
db.check_user_is_channel_participant( db.check_user_is_channel_participant(
&db.get_channel_internal(active_channel_id, &*tx) &db.get_channel_internal(internal_channel_id, &*tx)
.await .await
.unwrap(), .unwrap(),
guest, guest,
@ -683,7 +663,9 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
db.transaction(|tx| async move { db.transaction(|tx| async move {
db.check_user_is_channel_participant( db.check_user_is_channel_participant(
&db.get_channel_internal(vim_channel_id, &*tx).await.unwrap(), &db.get_channel_internal(public_channel_id, &*tx)
.await
.unwrap(),
guest, guest,
&*tx, &*tx,
) )
@ -693,7 +675,7 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
.unwrap(); .unwrap();
let mut members = db let mut members = db
.get_channel_participant_details(vim_channel_id, admin) .get_channel_participant_details(public_channel_id, admin)
.await .await
.unwrap(); .unwrap();
@ -704,17 +686,17 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
&[ &[
proto::ChannelMember { proto::ChannelMember {
user_id: admin.to_proto(), user_id: admin.to_proto(),
kind: proto::channel_member::Kind::AncestorMember.into(), kind: proto::channel_member::Kind::Member.into(),
role: proto::ChannelRole::Admin.into(), role: proto::ChannelRole::Admin.into(),
}, },
proto::ChannelMember { proto::ChannelMember {
user_id: member.to_proto(), user_id: member.to_proto(),
kind: proto::channel_member::Kind::AncestorMember.into(), kind: proto::channel_member::Kind::Member.into(),
role: proto::ChannelRole::Member.into(), role: proto::ChannelRole::Member.into(),
}, },
proto::ChannelMember { proto::ChannelMember {
user_id: guest.to_proto(), user_id: guest.to_proto(),
kind: proto::channel_member::Kind::AncestorMember.into(), kind: proto::channel_member::Kind::Member.into(),
role: proto::ChannelRole::Guest.into(), role: proto::ChannelRole::Guest.into(),
}, },
] ]
@ -723,67 +705,10 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
let channels = db.get_channels_for_user(guest).await.unwrap().channels; let channels = db.get_channels_for_user(guest).await.unwrap().channels;
assert_channel_tree( assert_channel_tree(
channels, channels,
&[(zed_channel, &[]), (vim_channel_id, &[zed_channel])], &[(zed_channel, &[]), (public_channel_id, &[zed_channel])],
) )
} }
test_both_dbs!(
test_user_joins_correct_channel,
test_user_joins_correct_channel_postgres,
test_user_joins_correct_channel_sqlite
);
async fn test_user_joins_correct_channel(db: &Arc<Database>) {
let admin = new_test_user(db, "admin@example.com").await;
let zed_channel = db.create_root_channel("zed", admin).await.unwrap();
let active_channel = db
.create_sub_channel("active", zed_channel, admin)
.await
.unwrap();
let vim_channel = db
.create_sub_channel("vim", active_channel, admin)
.await
.unwrap();
let vim2_channel = db
.create_sub_channel("vim2", vim_channel, admin)
.await
.unwrap();
db.set_channel_visibility(zed_channel, crate::db::ChannelVisibility::Public, admin)
.await
.unwrap();
db.set_channel_visibility(vim_channel, crate::db::ChannelVisibility::Public, admin)
.await
.unwrap();
db.set_channel_visibility(vim2_channel, crate::db::ChannelVisibility::Public, admin)
.await
.unwrap();
let most_public = db
.transaction(|tx| async move {
Ok(db
.public_ancestors_including_self(
&db.get_channel_internal(vim_channel, &*tx).await.unwrap(),
&tx,
)
.await?
.first()
.cloned())
})
.await
.unwrap()
.unwrap()
.id;
assert_eq!(most_public, zed_channel)
}
test_both_dbs!( test_both_dbs!(
test_guest_access, test_guest_access,
test_guest_access_postgres, test_guest_access_postgres,

View file

@ -15,7 +15,7 @@ test_both_dbs!(
async fn test_channel_message_retrieval(db: &Arc<Database>) { async fn test_channel_message_retrieval(db: &Arc<Database>) {
let user = new_test_user(db, "user@example.com").await; let user = new_test_user(db, "user@example.com").await;
let channel = db.create_channel("channel", None, user).await.unwrap(); let channel = db.create_channel("channel", None, user).await.unwrap().0;
let owner_id = db.create_server("test").await.unwrap().0 as u32; let owner_id = db.create_server("test").await.unwrap().0 as u32;
db.join_channel_chat(channel.id, rpc::ConnectionId { owner_id, id: 0 }, user) db.join_channel_chat(channel.id, rpc::ConnectionId { owner_id, id: 0 }, user)
@ -235,11 +235,10 @@ async fn test_unseen_channel_messages(db: &Arc<Database>) {
.await .await
.unwrap(); .unwrap();
let second_message = db let _ = db
.create_channel_message(channel_1, user, "1_2", &[], OffsetDateTime::now_utc(), 2) .create_channel_message(channel_1, user, "1_2", &[], OffsetDateTime::now_utc(), 2)
.await .await
.unwrap() .unwrap();
.message_id;
let third_message = db let third_message = db
.create_channel_message(channel_1, user, "1_3", &[], OffsetDateTime::now_utc(), 3) .create_channel_message(channel_1, user, "1_3", &[], OffsetDateTime::now_utc(), 3)
@ -258,97 +257,27 @@ async fn test_unseen_channel_messages(db: &Arc<Database>) {
.message_id; .message_id;
// Check that observer has new messages // Check that observer has new messages
let unseen_messages = db let latest_messages = db
.transaction(|tx| async move { .transaction(|tx| async move {
db.unseen_channel_messages(observer, &[channel_1, channel_2], &*tx) db.latest_channel_messages(&[channel_1, channel_2], &*tx)
.await .await
}) })
.await .await
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
unseen_messages, latest_messages,
[ [
rpc::proto::UnseenChannelMessage { rpc::proto::ChannelMessageId {
channel_id: channel_1.to_proto(), channel_id: channel_1.to_proto(),
message_id: third_message.to_proto(), message_id: third_message.to_proto(),
}, },
rpc::proto::UnseenChannelMessage { rpc::proto::ChannelMessageId {
channel_id: channel_2.to_proto(), channel_id: channel_2.to_proto(),
message_id: fourth_message.to_proto(), message_id: fourth_message.to_proto(),
}, },
] ]
); );
// Observe the second message
db.observe_channel_message(channel_1, observer, second_message)
.await
.unwrap();
// Make sure the observer still has a new message
let unseen_messages = db
.transaction(|tx| async move {
db.unseen_channel_messages(observer, &[channel_1, channel_2], &*tx)
.await
})
.await
.unwrap();
assert_eq!(
unseen_messages,
[
rpc::proto::UnseenChannelMessage {
channel_id: channel_1.to_proto(),
message_id: third_message.to_proto(),
},
rpc::proto::UnseenChannelMessage {
channel_id: channel_2.to_proto(),
message_id: fourth_message.to_proto(),
},
]
);
// Observe the third message,
db.observe_channel_message(channel_1, observer, third_message)
.await
.unwrap();
// Make sure the observer does not have a new method
let unseen_messages = db
.transaction(|tx| async move {
db.unseen_channel_messages(observer, &[channel_1, channel_2], &*tx)
.await
})
.await
.unwrap();
assert_eq!(
unseen_messages,
[rpc::proto::UnseenChannelMessage {
channel_id: channel_2.to_proto(),
message_id: fourth_message.to_proto(),
}]
);
// Observe the second message again, should not regress our observed state
db.observe_channel_message(channel_1, observer, second_message)
.await
.unwrap();
// Make sure the observer does not have a new message
let unseen_messages = db
.transaction(|tx| async move {
db.unseen_channel_messages(observer, &[channel_1, channel_2], &*tx)
.await
})
.await
.unwrap();
assert_eq!(
unseen_messages,
[rpc::proto::UnseenChannelMessage {
channel_id: channel_2.to_proto(),
message_id: fourth_message.to_proto(),
}]
);
} }
test_both_dbs!( test_both_dbs!(
@ -362,7 +291,12 @@ async fn test_channel_message_mentions(db: &Arc<Database>) {
let user_b = new_test_user(db, "user_b@example.com").await; let user_b = new_test_user(db, "user_b@example.com").await;
let user_c = new_test_user(db, "user_c@example.com").await; let user_c = new_test_user(db, "user_c@example.com").await;
let channel = db.create_channel("channel", None, user_a).await.unwrap().id; let channel = db
.create_channel("channel", None, user_a)
.await
.unwrap()
.0
.id;
db.invite_channel_member(channel, user_b, user_a, ChannelRole::Member) db.invite_channel_member(channel, user_b, user_a, ChannelRole::Member)
.await .await
.unwrap(); .unwrap();

View file

@ -5,8 +5,7 @@ use crate::{
db::{ db::{
self, BufferId, ChannelId, ChannelRole, ChannelsForUser, CreatedChannelMessage, Database, self, BufferId, ChannelId, ChannelRole, ChannelsForUser, CreatedChannelMessage, Database,
InviteMemberResult, MembershipUpdated, MessageId, NotificationId, ProjectId, InviteMemberResult, MembershipUpdated, MessageId, NotificationId, ProjectId,
RemoveChannelMemberResult, RenameChannelResult, RespondToChannelInvite, RoomId, ServerId, RemoveChannelMemberResult, RespondToChannelInvite, RoomId, ServerId, User, UserId,
SetChannelVisibilityResult, User, UserId,
}, },
executor::Executor, executor::Executor,
AppState, Error, Result, AppState, Error, Result,
@ -602,6 +601,7 @@ impl Server {
let mut pool = this.connection_pool.lock(); let mut pool = this.connection_pool.lock();
pool.add_connection(connection_id, user_id, user.admin); pool.add_connection(connection_id, user_id, user.admin);
this.peer.send(connection_id, build_initial_contacts_update(contacts, &pool))?; this.peer.send(connection_id, build_initial_contacts_update(contacts, &pool))?;
this.peer.send(connection_id, build_update_user_channels(&channels_for_user.channel_memberships))?;
this.peer.send(connection_id, build_channels_update( this.peer.send(connection_id, build_channels_update(
channels_for_user, channels_for_user,
channel_invites channel_invites
@ -2300,7 +2300,7 @@ async fn create_channel(
let db = session.db().await; let db = session.db().await;
let parent_id = request.parent_id.map(|id| ChannelId::from_proto(id)); let parent_id = request.parent_id.map(|id| ChannelId::from_proto(id));
let channel = db let (channel, owner, channel_members) = db
.create_channel(&request.name, parent_id, session.user_id) .create_channel(&request.name, parent_id, session.user_id)
.await?; .await?;
@ -2309,20 +2309,30 @@ async fn create_channel(
parent_id: request.parent_id, parent_id: request.parent_id,
})?; })?;
let participants_to_update; let connection_pool = session.connection_pool().await;
if let Some(parent) = parent_id { if let Some(owner) = owner {
participants_to_update = db.new_participants_to_notify(parent).await?; let update = proto::UpdateUserChannels {
} else { channel_memberships: vec![proto::ChannelMembership {
participants_to_update = vec![]; channel_id: owner.channel_id.to_proto(),
role: owner.role.into(),
}],
..Default::default()
};
for connection_id in connection_pool.user_connection_ids(owner.user_id) {
session.peer.send(connection_id, update.clone())?;
}
} }
let connection_pool = session.connection_pool().await; for channel_member in channel_members {
for (user_id, channels) in participants_to_update { if !channel_member.role.can_see_channel(channel.visibility) {
let update = build_channels_update(channels, vec![]); continue;
for connection_id in connection_pool.user_connection_ids(user_id) { }
if user_id == session.user_id {
continue; let update = proto::UpdateChannels {
} channels: vec![channel.to_proto()],
..Default::default()
};
for connection_id in connection_pool.user_connection_ids(channel_member.user_id) {
session.peer.send(connection_id, update.clone())?; session.peer.send(connection_id, update.clone())?;
} }
} }
@ -2439,7 +2449,9 @@ async fn remove_channel_member(
Ok(()) Ok(())
} }
/// Toggle the channel between public and private /// Toggle the channel between public and private.
/// Care is taken to maintain the invariant that public channels only descend from public channels,
/// (though members-only channels can appear at any point in the hierarchy).
async fn set_channel_visibility( async fn set_channel_visibility(
request: proto::SetChannelVisibility, request: proto::SetChannelVisibility,
response: Response<proto::SetChannelVisibility>, response: Response<proto::SetChannelVisibility>,
@ -2449,27 +2461,25 @@ async fn set_channel_visibility(
let channel_id = ChannelId::from_proto(request.channel_id); let channel_id = ChannelId::from_proto(request.channel_id);
let visibility = request.visibility().into(); let visibility = request.visibility().into();
let SetChannelVisibilityResult { let (channel, channel_members) = db
participants_to_update,
participants_to_remove,
channels_to_remove,
} = db
.set_channel_visibility(channel_id, visibility, session.user_id) .set_channel_visibility(channel_id, visibility, session.user_id)
.await?; .await?;
let connection_pool = session.connection_pool().await; let connection_pool = session.connection_pool().await;
for (user_id, channels) in participants_to_update { for member in channel_members {
let update = build_channels_update(channels, vec![]); let update = if member.role.can_see_channel(channel.visibility) {
for connection_id in connection_pool.user_connection_ids(user_id) { proto::UpdateChannels {
session.peer.send(connection_id, update.clone())?; channels: vec![channel.to_proto()],
} ..Default::default()
} }
for user_id in participants_to_remove { } else {
let update = proto::UpdateChannels { proto::UpdateChannels {
delete_channels: channels_to_remove.iter().map(|id| id.to_proto()).collect(), delete_channels: vec![channel.id.to_proto()],
..Default::default() ..Default::default()
}
}; };
for connection_id in connection_pool.user_connection_ids(user_id) {
for connection_id in connection_pool.user_connection_ids(member.user_id) {
session.peer.send(connection_id, update.clone())?; session.peer.send(connection_id, update.clone())?;
} }
} }
@ -2478,7 +2488,7 @@ async fn set_channel_visibility(
Ok(()) Ok(())
} }
/// Alter the role for a user in the channel /// Alter the role for a user in the channel.
async fn set_channel_member_role( async fn set_channel_member_role(
request: proto::SetChannelMemberRole, request: proto::SetChannelMemberRole,
response: Response<proto::SetChannelMemberRole>, response: Response<proto::SetChannelMemberRole>,
@ -2534,10 +2544,7 @@ async fn rename_channel(
) -> Result<()> { ) -> Result<()> {
let db = session.db().await; let db = session.db().await;
let channel_id = ChannelId::from_proto(request.channel_id); let channel_id = ChannelId::from_proto(request.channel_id);
let RenameChannelResult { let (channel, channel_members) = db
channel,
participants_to_update,
} = db
.rename_channel(channel_id, session.user_id, &request.name) .rename_channel(channel_id, session.user_id, &request.name)
.await?; .await?;
@ -2546,13 +2553,15 @@ async fn rename_channel(
})?; })?;
let connection_pool = session.connection_pool().await; let connection_pool = session.connection_pool().await;
for (user_id, channel) in participants_to_update { for channel_member in channel_members {
for connection_id in connection_pool.user_connection_ids(user_id) { if !channel_member.role.can_see_channel(channel.visibility) {
let update = proto::UpdateChannels { continue;
channels: vec![channel.to_proto()], }
..Default::default() let update = proto::UpdateChannels {
}; channels: vec![channel.to_proto()],
..Default::default()
};
for connection_id in connection_pool.user_connection_ids(channel_member.user_id) {
session.peer.send(connection_id, update.clone())?; session.peer.send(connection_id, update.clone())?;
} }
} }
@ -2567,57 +2576,37 @@ async fn move_channel(
session: Session, session: Session,
) -> Result<()> { ) -> Result<()> {
let channel_id = ChannelId::from_proto(request.channel_id); let channel_id = ChannelId::from_proto(request.channel_id);
let to = request.to.map(ChannelId::from_proto); let to = ChannelId::from_proto(request.to);
let result = session let (channels, channel_members) = session
.db() .db()
.await .await
.move_channel(channel_id, to, session.user_id) .move_channel(channel_id, to, session.user_id)
.await?; .await?;
if let Some(result) = result { let connection_pool = session.connection_pool().await;
let participants_to_update: HashMap<_, _> = session for member in channel_members {
.db() let channels = channels
.await .iter()
.new_participants_to_notify(to.unwrap_or(channel_id)) .filter_map(|channel| {
.await? if member.role.can_see_channel(channel.visibility) {
.into_iter() Some(channel.to_proto())
.collect(); } else {
None
let mut moved_channels: HashSet<ChannelId> = HashSet::default();
for id in result.descendent_ids {
moved_channels.insert(id);
}
moved_channels.insert(channel_id);
let mut participants_to_remove: HashSet<UserId> = HashSet::default();
for participant in result.previous_participants {
if participant.kind == proto::channel_member::Kind::AncestorMember {
if !participants_to_update.contains_key(&participant.user_id) {
participants_to_remove.insert(participant.user_id);
} }
} })
.collect::<Vec<_>>();
if channels.is_empty() {
continue;
} }
let moved_channels: Vec<u64> = moved_channels.iter().map(|id| id.to_proto()).collect(); let update = proto::UpdateChannels {
channels,
..Default::default()
};
let connection_pool = session.connection_pool().await; for connection_id in connection_pool.user_connection_ids(member.user_id) {
for (user_id, channels) in participants_to_update { session.peer.send(connection_id, update.clone())?;
let mut update = build_channels_update(channels, vec![]);
update.delete_channels = moved_channels.clone();
for connection_id in connection_pool.user_connection_ids(user_id) {
session.peer.send(connection_id, update.clone())?;
}
}
for user_id in participants_to_remove {
let update = proto::UpdateChannels {
delete_channels: moved_channels.clone(),
..Default::default()
};
for connection_id in connection_pool.user_connection_ids(user_id) {
session.peer.send(connection_id, update.clone())?;
}
} }
} }
@ -2851,7 +2840,7 @@ async fn update_channel_buffer(
session.peer.send( session.peer.send(
peer_id.into(), peer_id.into(),
proto::UpdateChannels { proto::UpdateChannels {
unseen_channel_buffer_changes: vec![proto::UnseenChannelBufferChange { latest_channel_buffer_versions: vec![proto::ChannelBufferVersion {
channel_id: channel_id.to_proto(), channel_id: channel_id.to_proto(),
epoch: epoch as u64, epoch: epoch as u64,
version: version.clone(), version: version.clone(),
@ -3037,7 +3026,7 @@ async fn send_channel_message(
session.peer.send( session.peer.send(
peer_id.into(), peer_id.into(),
proto::UpdateChannels { proto::UpdateChannels {
unseen_channel_messages: vec![proto::UnseenChannelMessage { latest_channel_message_ids: vec![proto::ChannelMessageId {
channel_id: channel_id.to_proto(), channel_id: channel_id.to_proto(),
message_id: message_id.to_proto(), message_id: message_id.to_proto(),
}], }],
@ -3292,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
@ -3301,10 +3302,27 @@ 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();
} }
} }
fn build_update_user_channels(
memberships: &Vec<db::channel_member::Model>,
) -> proto::UpdateUserChannels {
proto::UpdateUserChannels {
channel_memberships: memberships
.iter()
.map(|m| proto::ChannelMembership {
channel_id: m.channel_id.to_proto(),
role: m.role.into(),
})
.collect(),
..Default::default()
}
}
fn build_channels_update( fn build_channels_update(
channels: ChannelsForUser, channels: ChannelsForUser,
channel_invites: Vec<db::Channel>, channel_invites: Vec<db::Channel>,
@ -3315,8 +3333,8 @@ fn build_channels_update(
update.channels.push(channel.to_proto()); update.channels.push(channel.to_proto());
} }
update.unseen_channel_buffer_changes = channels.unseen_buffer_changes; update.latest_channel_buffer_versions = channels.latest_buffer_versions;
update.unseen_channel_messages = channels.channel_messages; update.latest_channel_message_ids = channels.latest_channel_messages;
for (channel_id, participants) in channels.channel_participants { for (channel_id, participants) in channels.channel_participants {
update update

View file

@ -637,7 +637,6 @@ async fn test_channel_buffer_changes(
.channel_store() .channel_store()
.read(cx) .read(cx)
.has_channel_buffer_changed(channel_id) .has_channel_buffer_changed(channel_id)
.unwrap()
}); });
assert!(has_buffer_changed); assert!(has_buffer_changed);
@ -655,7 +654,6 @@ async fn test_channel_buffer_changes(
.channel_store() .channel_store()
.read(cx) .read(cx)
.has_channel_buffer_changed(channel_id) .has_channel_buffer_changed(channel_id)
.unwrap()
}); });
assert!(!has_buffer_changed); assert!(!has_buffer_changed);
@ -672,7 +670,6 @@ async fn test_channel_buffer_changes(
.channel_store() .channel_store()
.read(cx) .read(cx)
.has_channel_buffer_changed(channel_id) .has_channel_buffer_changed(channel_id)
.unwrap()
}); });
assert!(!has_buffer_changed); assert!(!has_buffer_changed);
@ -687,7 +684,6 @@ async fn test_channel_buffer_changes(
.channel_store() .channel_store()
.read(cx) .read(cx)
.has_channel_buffer_changed(channel_id) .has_channel_buffer_changed(channel_id)
.unwrap()
}); });
assert!(!has_buffer_changed); assert!(!has_buffer_changed);
@ -714,7 +710,6 @@ async fn test_channel_buffer_changes(
.channel_store() .channel_store()
.read(cx) .read(cx)
.has_channel_buffer_changed(channel_id) .has_channel_buffer_changed(channel_id)
.unwrap()
}); });
assert!(has_buffer_changed); assert!(has_buffer_changed);
} }

View file

@ -195,6 +195,13 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
}) })
.await .await
.unwrap(); .unwrap();
client_a
.channel_store()
.update(cx_a, |store, cx| {
store.set_channel_visibility(parent_channel_id, proto::ChannelVisibility::Public, cx)
})
.await
.unwrap();
client_a client_a
.channel_store() .channel_store()
.update(cx_a, |store, cx| { .update(cx_a, |store, cx| {

View file

@ -313,7 +313,6 @@ async fn test_channel_message_changes(
.channel_store() .channel_store()
.read(cx) .read(cx)
.has_new_messages(channel_id) .has_new_messages(channel_id)
.unwrap()
}); });
assert!(b_has_messages); assert!(b_has_messages);
@ -341,7 +340,6 @@ async fn test_channel_message_changes(
.channel_store() .channel_store()
.read(cx) .read(cx)
.has_new_messages(channel_id) .has_new_messages(channel_id)
.unwrap()
}); });
assert!(!b_has_messages); assert!(!b_has_messages);
@ -359,7 +357,6 @@ async fn test_channel_message_changes(
.channel_store() .channel_store()
.read(cx) .read(cx)
.has_new_messages(channel_id) .has_new_messages(channel_id)
.unwrap()
}); });
assert!(!b_has_messages); assert!(!b_has_messages);
@ -382,7 +379,6 @@ async fn test_channel_message_changes(
.channel_store() .channel_store()
.read(cx) .read(cx)
.has_new_messages(channel_id) .has_new_messages(channel_id)
.unwrap()
}); });
assert!(b_has_messages); assert!(b_has_messages);
@ -402,7 +398,6 @@ async fn test_channel_message_changes(
.channel_store() .channel_store()
.read(cx) .read(cx)
.has_new_messages(channel_id) .has_new_messages(channel_id)
.unwrap()
}); });
assert!(b_has_messages); assert!(b_has_messages);

View file

@ -48,13 +48,11 @@ async fn test_core_channels(
id: channel_a_id, id: channel_a_id,
name: "channel-a".into(), name: "channel-a".into(),
depth: 0, depth: 0,
role: ChannelRole::Admin,
}, },
ExpectedChannel { ExpectedChannel {
id: channel_b_id, id: channel_b_id,
name: "channel-b".into(), name: "channel-b".into(),
depth: 1, depth: 1,
role: ChannelRole::Admin,
}, },
], ],
); );
@ -94,7 +92,6 @@ async fn test_core_channels(
id: channel_a_id, id: channel_a_id,
name: "channel-a".into(), name: "channel-a".into(),
depth: 0, depth: 0,
role: ChannelRole::Member,
}], }],
); );
@ -141,13 +138,11 @@ async fn test_core_channels(
ExpectedChannel { ExpectedChannel {
id: channel_a_id, id: channel_a_id,
name: "channel-a".into(), name: "channel-a".into(),
role: ChannelRole::Member,
depth: 0, depth: 0,
}, },
ExpectedChannel { ExpectedChannel {
id: channel_b_id, id: channel_b_id,
name: "channel-b".into(), name: "channel-b".into(),
role: ChannelRole::Member,
depth: 1, depth: 1,
}, },
], ],
@ -169,19 +164,16 @@ async fn test_core_channels(
ExpectedChannel { ExpectedChannel {
id: channel_a_id, id: channel_a_id,
name: "channel-a".into(), name: "channel-a".into(),
role: ChannelRole::Member,
depth: 0, depth: 0,
}, },
ExpectedChannel { ExpectedChannel {
id: channel_b_id, id: channel_b_id,
name: "channel-b".into(), name: "channel-b".into(),
role: ChannelRole::Member,
depth: 1, depth: 1,
}, },
ExpectedChannel { ExpectedChannel {
id: channel_c_id, id: channel_c_id,
name: "channel-c".into(), name: "channel-c".into(),
role: ChannelRole::Member,
depth: 2, depth: 2,
}, },
], ],
@ -213,19 +205,16 @@ async fn test_core_channels(
id: channel_a_id, id: channel_a_id,
name: "channel-a".into(), name: "channel-a".into(),
depth: 0, depth: 0,
role: ChannelRole::Admin,
}, },
ExpectedChannel { ExpectedChannel {
id: channel_b_id, id: channel_b_id,
name: "channel-b".into(), name: "channel-b".into(),
depth: 1, depth: 1,
role: ChannelRole::Admin,
}, },
ExpectedChannel { ExpectedChannel {
id: channel_c_id, id: channel_c_id,
name: "channel-c".into(), name: "channel-c".into(),
depth: 2, depth: 2,
role: ChannelRole::Admin,
}, },
], ],
); );
@ -247,7 +236,6 @@ async fn test_core_channels(
id: channel_a_id, id: channel_a_id,
name: "channel-a".into(), name: "channel-a".into(),
depth: 0, depth: 0,
role: ChannelRole::Admin,
}], }],
); );
assert_channels( assert_channels(
@ -257,7 +245,6 @@ async fn test_core_channels(
id: channel_a_id, id: channel_a_id,
name: "channel-a".into(), name: "channel-a".into(),
depth: 0, depth: 0,
role: ChannelRole::Admin,
}], }],
); );
@ -280,7 +267,6 @@ async fn test_core_channels(
id: channel_a_id, id: channel_a_id,
name: "channel-a".into(), name: "channel-a".into(),
depth: 0, depth: 0,
role: ChannelRole::Admin,
}], }],
); );
@ -311,7 +297,6 @@ async fn test_core_channels(
id: channel_a_id, id: channel_a_id,
name: "channel-a-renamed".into(), name: "channel-a-renamed".into(),
depth: 0, depth: 0,
role: ChannelRole::Admin,
}], }],
); );
} }
@ -420,7 +405,6 @@ async fn test_channel_room(
id: zed_id, id: zed_id,
name: "zed".into(), name: "zed".into(),
depth: 0, depth: 0,
role: ChannelRole::Member,
}], }],
); );
cx_b.read(|cx| { cx_b.read(|cx| {
@ -681,7 +665,6 @@ async fn test_permissions_update_while_invited(
depth: 0, depth: 0,
id: rust_id, id: rust_id,
name: "rust".into(), name: "rust".into(),
role: ChannelRole::Member,
}], }],
); );
assert_channels(client_b.channel_store(), cx_b, &[]); assert_channels(client_b.channel_store(), cx_b, &[]);
@ -709,7 +692,6 @@ async fn test_permissions_update_while_invited(
depth: 0, depth: 0,
id: rust_id, id: rust_id,
name: "rust".into(), name: "rust".into(),
role: ChannelRole::Member,
}], }],
); );
assert_channels(client_b.channel_store(), cx_b, &[]); assert_channels(client_b.channel_store(), cx_b, &[]);
@ -748,7 +730,6 @@ async fn test_channel_rename(
depth: 0, depth: 0,
id: rust_id, id: rust_id,
name: "rust-archive".into(), name: "rust-archive".into(),
role: ChannelRole::Admin,
}], }],
); );
@ -760,7 +741,6 @@ async fn test_channel_rename(
depth: 0, depth: 0,
id: rust_id, id: rust_id,
name: "rust-archive".into(), name: "rust-archive".into(),
role: ChannelRole::Member,
}], }],
); );
} }
@ -889,7 +869,6 @@ async fn test_lost_channel_creation(
depth: 0, depth: 0,
id: channel_id, id: channel_id,
name: "x".into(), name: "x".into(),
role: ChannelRole::Member,
}], }],
); );
@ -913,13 +892,11 @@ async fn test_lost_channel_creation(
depth: 0, depth: 0,
id: channel_id, id: channel_id,
name: "x".into(), name: "x".into(),
role: ChannelRole::Admin,
}, },
ExpectedChannel { ExpectedChannel {
depth: 1, depth: 1,
id: subchannel_id, id: subchannel_id,
name: "subchannel".into(), name: "subchannel".into(),
role: ChannelRole::Admin,
}, },
], ],
); );
@ -944,13 +921,11 @@ async fn test_lost_channel_creation(
depth: 0, depth: 0,
id: channel_id, id: channel_id,
name: "x".into(), name: "x".into(),
role: ChannelRole::Member,
}, },
ExpectedChannel { ExpectedChannel {
depth: 1, depth: 1,
id: subchannel_id, id: subchannel_id,
name: "subchannel".into(), name: "subchannel".into(),
role: ChannelRole::Member,
}, },
], ],
); );
@ -1035,7 +1010,7 @@ async fn test_channel_link_notifications(
let vim_channel = client_a let vim_channel = client_a
.channel_store() .channel_store()
.update(cx_a, |channel_store, cx| { .update(cx_a, |channel_store, cx| {
channel_store.create_channel("vim", None, cx) channel_store.create_channel("vim", Some(zed_channel), cx)
}) })
.await .await
.unwrap(); .unwrap();
@ -1048,26 +1023,16 @@ async fn test_channel_link_notifications(
.await .await
.unwrap(); .unwrap();
client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.move_channel(vim_channel, Some(active_channel), cx)
})
.await
.unwrap();
executor.run_until_parked();
// the new channel shows for b and c // the new channel shows for b and c
assert_channels_list_shape( assert_channels_list_shape(
client_a.channel_store(), client_a.channel_store(),
cx_a, cx_a,
&[(zed_channel, 0), (active_channel, 1), (vim_channel, 2)], &[(zed_channel, 0), (active_channel, 1), (vim_channel, 1)],
); );
assert_channels_list_shape( assert_channels_list_shape(
client_b.channel_store(), client_b.channel_store(),
cx_b, cx_b,
&[(zed_channel, 0), (active_channel, 1), (vim_channel, 2)], &[(zed_channel, 0), (active_channel, 1), (vim_channel, 1)],
); );
assert_channels_list_shape( assert_channels_list_shape(
client_c.channel_store(), client_c.channel_store(),
@ -1078,7 +1043,7 @@ async fn test_channel_link_notifications(
let helix_channel = client_a let helix_channel = client_a
.channel_store() .channel_store()
.update(cx_a, |channel_store, cx| { .update(cx_a, |channel_store, cx| {
channel_store.create_channel("helix", None, cx) channel_store.create_channel("helix", Some(zed_channel), cx)
}) })
.await .await
.unwrap(); .unwrap();
@ -1086,7 +1051,7 @@ async fn test_channel_link_notifications(
client_a client_a
.channel_store() .channel_store()
.update(cx_a, |channel_store, cx| { .update(cx_a, |channel_store, cx| {
channel_store.move_channel(helix_channel, Some(vim_channel), cx) channel_store.move_channel(helix_channel, vim_channel, cx)
}) })
.await .await
.unwrap(); .unwrap();
@ -1102,6 +1067,7 @@ async fn test_channel_link_notifications(
}) })
.await .await
.unwrap(); .unwrap();
cx_a.run_until_parked();
// the new channel shows for b and c // the new channel shows for b and c
assert_channels_list_shape( assert_channels_list_shape(
@ -1110,8 +1076,8 @@ async fn test_channel_link_notifications(
&[ &[
(zed_channel, 0), (zed_channel, 0),
(active_channel, 1), (active_channel, 1),
(vim_channel, 2), (vim_channel, 1),
(helix_channel, 3), (helix_channel, 2),
], ],
); );
assert_channels_list_shape( assert_channels_list_shape(
@ -1119,41 +1085,6 @@ async fn test_channel_link_notifications(
cx_c, cx_c,
&[(zed_channel, 0), (vim_channel, 1), (helix_channel, 2)], &[(zed_channel, 0), (vim_channel, 1), (helix_channel, 2)],
); );
client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.set_channel_visibility(vim_channel, proto::ChannelVisibility::Members, cx)
})
.await
.unwrap();
executor.run_until_parked();
// the members-only channel is still shown for c, but hidden for b
assert_channels_list_shape(
client_b.channel_store(),
cx_b,
&[
(zed_channel, 0),
(active_channel, 1),
(vim_channel, 2),
(helix_channel, 3),
],
);
cx_b.read(|cx| {
client_b.channel_store().read_with(cx, |channel_store, _| {
assert_eq!(
channel_store
.channel_for_id(vim_channel)
.unwrap()
.visibility,
proto::ChannelVisibility::Members
)
})
});
assert_channels_list_shape(client_c.channel_store(), cx_c, &[(zed_channel, 0)]);
} }
#[gpui::test] #[gpui::test]
@ -1170,24 +1101,20 @@ async fn test_channel_membership_notifications(
let channels = server let channels = server
.make_channel_tree( .make_channel_tree(
&[ &[("zed", None), ("vim", Some("zed")), ("opensource", None)],
("zed", None),
("active", Some("zed")),
("vim", Some("active")),
],
(&client_a, cx_a), (&client_a, cx_a),
) )
.await; .await;
let zed_channel = channels[0]; let zed_channel = channels[0];
let _active_channel = channels[1]; let vim_channel = channels[1];
let vim_channel = channels[2]; 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(vim_channel, user_b, proto::ChannelRole::Member, cx), channel_store.invite_member(zed_channel, user_b, proto::ChannelRole::Admin, cx),
channel_store.invite_member(zed_channel, user_b, proto::ChannelRole::Guest, cx), channel_store.invite_member(opensource_channel, user_b, proto::ChannelRole::Member, cx),
] ]
})) }))
.await .await
@ -1203,14 +1130,6 @@ async fn test_channel_membership_notifications(
.await .await
.unwrap(); .unwrap();
client_b
.channel_store()
.update(cx_b, |channel_store, cx| {
channel_store.respond_to_channel_invite(vim_channel, true, cx)
})
.await
.unwrap();
executor.run_until_parked(); executor.run_until_parked();
// we have an admin (a), and a guest (b) with access to all of zed, and membership in vim. // we have an admin (a), and a guest (b) with access to all of zed, and membership in vim.
@ -1222,45 +1141,42 @@ async fn test_channel_membership_notifications(
depth: 0, depth: 0,
id: zed_channel, id: zed_channel,
name: "zed".into(), name: "zed".into(),
role: ChannelRole::Guest,
}, },
ExpectedChannel { ExpectedChannel {
depth: 1, depth: 1,
id: vim_channel, id: vim_channel,
name: "vim".into(), name: "vim".into(),
role: ChannelRole::Member,
}, },
], ],
); );
client_a client_b.channel_store().update(cx_b, |channel_store, _| {
channel_store.is_channel_admin(zed_channel)
});
client_b
.channel_store() .channel_store()
.update(cx_a, |channel_store, cx| { .update(cx_b, |channel_store, cx| {
channel_store.remove_member(vim_channel, user_b, cx) channel_store.respond_to_channel_invite(opensource_channel, true, cx)
}) })
.await .await
.unwrap(); .unwrap();
executor.run_until_parked(); cx_a.run_until_parked();
assert_channels( client_a
client_b.channel_store(), .channel_store()
cx_b, .update(cx_a, |channel_store, cx| {
&[ channel_store.set_member_role(opensource_channel, user_b, ChannelRole::Admin, cx)
ExpectedChannel { })
depth: 0, .await
id: zed_channel, .unwrap();
name: "zed".into(),
role: ChannelRole::Guest, cx_a.run_until_parked();
},
ExpectedChannel { client_b.channel_store().update(cx_b, |channel_store, _| {
depth: 1, channel_store.is_channel_admin(opensource_channel)
id: vim_channel, });
name: "vim".into(),
role: ChannelRole::Guest,
},
],
)
} }
#[gpui::test] #[gpui::test]
@ -1329,25 +1245,6 @@ async fn test_guest_access(
assert_eq!(participants.len(), 1); assert_eq!(participants.len(), 1);
assert_eq!(participants[0].id, client_b.user_id().unwrap()); assert_eq!(participants[0].id, client_b.user_id().unwrap());
}); });
client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.set_channel_visibility(channel_a, proto::ChannelVisibility::Members, cx)
})
.await
.unwrap();
executor.run_until_parked();
assert_channels_list_shape(client_b.channel_store(), cx_b, &[]);
active_call_b
.update(cx_b, |call, cx| call.join_channel(channel_b, cx))
.await
.unwrap();
executor.run_until_parked();
assert_channels_list_shape(client_b.channel_store(), cx_b, &[(channel_b, 0)]);
} }
#[gpui::test] #[gpui::test]
@ -1451,7 +1348,7 @@ async fn test_channel_moving(
client_a client_a
.channel_store() .channel_store()
.update(cx_a, |channel_store, cx| { .update(cx_a, |channel_store, cx| {
channel_store.move_channel(channel_d_id, Some(channel_b_id), cx) channel_store.move_channel(channel_d_id, channel_b_id, cx)
}) })
.await .await
.unwrap(); .unwrap();
@ -1476,7 +1373,6 @@ struct ExpectedChannel {
depth: usize, depth: usize,
id: ChannelId, id: ChannelId,
name: SharedString, name: SharedString,
role: ChannelRole,
} }
#[track_caller] #[track_caller]
@ -1494,7 +1390,6 @@ fn assert_channel_invitations(
depth: 0, depth: 0,
name: channel.name.clone(), name: channel.name.clone(),
id: channel.id, id: channel.id,
role: channel.role,
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
}) })
@ -1516,7 +1411,6 @@ fn assert_channels(
depth, depth,
name: channel.name.clone().into(), name: channel.name.clone().into(),
id: channel.id, id: channel.id,
role: channel.role,
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
}) })

View file

@ -3884,6 +3884,7 @@ async fn test_collaborating_with_diagnostics(
// Join project as client C and observe the diagnostics. // Join project as client C and observe the diagnostics.
let project_c = client_c.build_remote_project(project_id, cx_c).await; let project_c = client_c.build_remote_project(project_id, cx_c).await;
executor.run_until_parked();
let project_c_diagnostic_summaries = let project_c_diagnostic_summaries =
Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| { Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| {
project.diagnostic_summaries(false, cx).collect::<Vec<_>>() project.diagnostic_summaries(false, cx).collect::<Vec<_>>()

View file

@ -125,6 +125,7 @@ impl TestServer {
let channel_id = server let channel_id = server
.make_channel("a", None, (&client_a, cx_a), &mut [(&client_b, cx_b)]) .make_channel("a", None, (&client_a, cx_a), &mut [(&client_b, cx_b)])
.await; .await;
cx_a.run_until_parked();
(client_a, client_b, channel_id) (client_a, client_b, channel_id)
} }

View file

@ -183,7 +183,7 @@ impl ChannelView {
} else { } else {
self.channel_store.update(cx, |store, cx| { self.channel_store.update(cx, |store, cx| {
let channel_buffer = self.channel_buffer.read(cx); let channel_buffer = self.channel_buffer.read(cx);
store.notes_changed( store.update_latest_notes_version(
channel_buffer.channel_id, channel_buffer.channel_id,
channel_buffer.epoch(), channel_buffer.epoch(),
&channel_buffer.buffer().read(cx).version(), &channel_buffer.buffer().read(cx).version(),

View file

@ -266,7 +266,7 @@ impl ChatPanel {
} => { } => {
if !self.active { if !self.active {
self.channel_store.update(cx, |store, cx| { 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)
}) })
} }
} }

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 });
} }
@ -1546,14 +1585,27 @@ impl CollabPanel {
cx: &mut ViewContext<CollabPanel>, cx: &mut ViewContext<CollabPanel>,
) { ) {
if let Some(clipboard) = self.channel_clipboard.take() { if let Some(clipboard) = self.channel_clipboard.take() {
self.channel_store self.move_channel(clipboard.channel_id, to_channel_id, cx)
.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)
} }
} }
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>) { fn open_channel_notes(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
if let Some(workspace) = self.workspace.upgrade() { if let Some(workspace) = self.workspace.upgrade() {
ChannelView::open(channel_id, workspace, cx).detach(); ChannelView::open(channel_id, workspace, cx).detach();
@ -1980,32 +2032,19 @@ impl CollabPanel {
| Section::Offline => true, | Section::Offline => true,
}; };
h_flex() h_flex().w_full().group("section-header").child(
.w_full() ListHeader::new(text)
.group("section-header") .when(can_collapse, |header| {
.child( header
ListHeader::new(text) .toggle(Some(!is_collapsed))
.when(can_collapse, |header| { .on_toggle(cx.listener(move |this, _, cx| {
header.toggle(Some(!is_collapsed)).on_toggle(cx.listener( this.toggle_section_expanded(section, cx);
move |this, _, cx| { }))
this.toggle_section_expanded(section, cx); })
}, .inset(true)
)) .end_slot::<AnyElement>(button)
}) .selected(is_selected),
.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)
}))
})
} }
fn render_contact( fn render_contact(
@ -2219,17 +2258,16 @@ impl CollabPanel {
Some(call_channel == channel_id) Some(call_channel == channel_id)
}) })
.unwrap_or(false); .unwrap_or(false);
let is_public = self let channel_store = self.channel_store.read(cx);
.channel_store let is_public = channel_store
.read(cx)
.channel_for_id(channel_id) .channel_for_id(channel_id)
.map(|channel| channel.visibility) .map(|channel| channel.visibility)
== Some(proto::ChannelVisibility::Public); == Some(proto::ChannelVisibility::Public);
let disclosed = let disclosed =
has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok()); has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok());
let has_messages_notification = channel.unseen_message_id.is_some(); let has_messages_notification = channel_store.has_new_messages(channel_id);
let has_notes_notification = channel.unseen_note_version.is_some(); let has_notes_notification = channel_store.has_channel_buffer_changed(channel_id);
const FACEPILE_LIMIT: usize = 3; const FACEPILE_LIMIT: usize = 3;
let participants = self.channel_store.read(cx).channel_participants(channel_id); 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 width = self.width.unwrap_or(px(240.));
let root_id = channel.root_id();
div() div()
.h_6() .h_6()
@ -2267,19 +2306,28 @@ impl CollabPanel {
.group("") .group("")
.flex() .flex()
.w_full() .w_full()
.on_drag(channel.clone(), move |channel, cx| { .when(!channel.is_root_channel(), |el| {
cx.new_view(|_| DraggedChannelView { el.on_drag(channel.clone(), move |channel, cx| {
channel: channel.clone(), cx.new_view(|_| DraggedChannelView {
width, 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| { .on_drop(cx.listener(move |this, dragged_channel: &Channel, cx| {
this.channel_store if dragged_channel.root_id() != root_id {
.update(cx, |channel_store, cx| { return;
channel_store.move_channel(dragged_channel.id, Some(channel_id), cx) }
}) this.move_channel(dragged_channel.id, channel_id, cx);
.detach_and_prompt_err("Failed to move channel", cx, |_, _| None)
})) }))
.child( .child(
ListItem::new(channel_id as usize) ListItem::new(channel_id as usize)

View file

@ -10,7 +10,6 @@ use gpui::{
WeakView, WeakView,
}; };
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
use rpc::proto::channel_member;
use std::sync::Arc; use std::sync::Arc;
use ui::{prelude::*, Avatar, Checkbox, ContextMenu, ListItem, ListItemSpacing}; use ui::{prelude::*, Avatar, Checkbox, ContextMenu, ListItem, ListItemSpacing};
use util::TryFutureExt; use util::TryFutureExt;
@ -359,10 +358,8 @@ impl PickerDelegate for ChannelModalDelegate {
Some(proto::channel_member::Kind::Invitee) => { Some(proto::channel_member::Kind::Invitee) => {
self.remove_member(selected_user.id, cx); 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) => {} Some(proto::channel_member::Kind::Member) => {}
None => self.invite_member(selected_user, cx),
}, },
} }
} }
@ -402,10 +399,6 @@ impl PickerDelegate for ChannelModalDelegate {
.children( .children(
if request_status == Some(proto::channel_member::Kind::Invitee) { if request_status == Some(proto::channel_member::Kind::Invitee) {
Some(Label::new("Invited")) Some(Label::new("Invited"))
} else if membership.map(|m| m.kind)
== Some(channel_member::Kind::AncestorMember)
{
Some(Label::new("Parent"))
} else { } else {
None None
}, },
@ -563,16 +556,9 @@ impl ChannelModalDelegate {
let Some(membership) = self.member_at_index(ix) else { let Some(membership) = self.member_at_index(ix) else {
return; return;
}; };
if membership.kind == proto::channel_member::Kind::AncestorMember {
return;
}
let user_id = membership.user.id; let user_id = membership.user.id;
let picker = cx.view().clone(); let picker = cx.view().clone();
let context_menu = ContextMenu::build(cx, |mut menu, _cx| { 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; let role = membership.role;
if role == ChannelRole::Admin || role == ChannelRole::Member { if role == ChannelRole::Admin || role == ChannelRole::Member {

View file

@ -782,10 +782,20 @@ pub trait InteractiveElement: Sized {
} }
/// Apply the given style when the given data type is dragged over this element /// Apply the given style when the given data type is dragged over this element
fn drag_over<S: 'static>(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self { fn drag_over<S: 'static>(
self.interactivity() mut self,
.drag_over_styles f: impl 'static + Fn(StyleRefinement, &S, &WindowContext) -> StyleRefinement,
.push((TypeId::of::<S>(), f(StyleRefinement::default()))); ) -> Self {
self.interactivity().drag_over_styles.push((
TypeId::of::<S>(),
Box::new(move |currently_dragged: &dyn Any, cx| {
f(
StyleRefinement::default(),
currently_dragged.downcast_ref::<S>().unwrap(),
cx,
)
}),
));
self self
} }
@ -1174,7 +1184,10 @@ pub struct Interactivity {
pub(crate) group_hover_style: Option<GroupStyle>, pub(crate) group_hover_style: Option<GroupStyle>,
pub(crate) active_style: Option<Box<StyleRefinement>>, pub(crate) active_style: Option<Box<StyleRefinement>>,
pub(crate) group_active_style: Option<GroupStyle>, pub(crate) group_active_style: Option<GroupStyle>,
pub(crate) drag_over_styles: Vec<(TypeId, StyleRefinement)>, pub(crate) drag_over_styles: Vec<(
TypeId,
Box<dyn Fn(&dyn Any, &mut WindowContext) -> StyleRefinement>,
)>,
pub(crate) group_drag_over_styles: Vec<(TypeId, GroupStyle)>, pub(crate) group_drag_over_styles: Vec<(TypeId, GroupStyle)>,
pub(crate) mouse_down_listeners: Vec<MouseDownListener>, pub(crate) mouse_down_listeners: Vec<MouseDownListener>,
pub(crate) mouse_up_listeners: Vec<MouseUpListener>, pub(crate) mouse_up_listeners: Vec<MouseUpListener>,
@ -1980,7 +1993,7 @@ impl Interactivity {
} }
} }
for (state_type, drag_over_style) in &self.drag_over_styles { for (state_type, build_drag_over_style) in &self.drag_over_styles {
if *state_type == drag.value.as_ref().type_id() if *state_type == drag.value.as_ref().type_id()
&& bounds && bounds
.intersect(&cx.content_mask().bounds) .intersect(&cx.content_mask().bounds)
@ -1990,7 +2003,7 @@ impl Interactivity {
cx.stacking_order(), cx.stacking_order(),
) )
{ {
style.refine(drag_over_style); style.refine(&build_drag_over_style(drag.value.as_ref(), cx));
} }
} }
} }

View file

@ -1368,7 +1368,7 @@ impl ProjectPanel {
entry_id: *entry_id, entry_id: *entry_id,
}) })
}) })
.drag_over::<ProjectEntryId>(|style| { .drag_over::<ProjectEntryId>(|style, _, cx| {
style.bg(cx.theme().colors().drop_target_background) style.bg(cx.theme().colors().drop_target_background)
}) })
.on_drop(cx.listener(move |this, dragged_id: &ProjectEntryId, cx| { .on_drop(cx.listener(move |this, dragged_id: &ProjectEntryId, cx| {

View file

@ -181,7 +181,9 @@ message Envelope {
MarkNotificationRead mark_notification_read = 153; MarkNotificationRead mark_notification_read = 153;
LspExtExpandMacro lsp_ext_expand_macro = 154; LspExtExpandMacro lsp_ext_expand_macro = 154;
LspExtExpandMacroResponse lsp_ext_expand_macro_response = 155; LspExtExpandMacroResponse lsp_ext_expand_macro_response = 155;
SetRoomParticipantRole set_room_participant_role = 156; // Current max SetRoomParticipantRole set_room_participant_role = 156;
UpdateUserChannels update_user_channels = 157; // current max
} }
} }
@ -210,6 +212,10 @@ enum ErrorCode {
Forbidden = 5; Forbidden = 5;
WrongReleaseChannel = 6; WrongReleaseChannel = 6;
NeedsCla = 7; NeedsCla = 7;
NotARootChannel = 8;
BadPublicNesting = 9;
CircularNesting = 10;
WrongMoveTarget = 11;
} }
message Test { message Test {
@ -992,21 +998,26 @@ message UpdateChannels {
repeated Channel channel_invitations = 5; repeated Channel channel_invitations = 5;
repeated uint64 remove_channel_invitations = 6; repeated uint64 remove_channel_invitations = 6;
repeated ChannelParticipants channel_participants = 7; repeated ChannelParticipants channel_participants = 7;
repeated UnseenChannelMessage unseen_channel_messages = 9; repeated ChannelMessageId latest_channel_message_ids = 8;
repeated UnseenChannelBufferChange unseen_channel_buffer_changes = 10; repeated ChannelBufferVersion latest_channel_buffer_versions = 9;
} }
message UnseenChannelMessage { message UpdateUserChannels {
repeated ChannelMessageId observed_channel_message_id = 1;
repeated ChannelBufferVersion observed_channel_buffer_version = 2;
repeated ChannelMembership channel_memberships = 3;
}
message ChannelMembership {
uint64 channel_id = 1;
ChannelRole role = 2;
}
message ChannelMessageId {
uint64 channel_id = 1; uint64 channel_id = 1;
uint64 message_id = 2; uint64 message_id = 2;
} }
message UnseenChannelBufferChange {
uint64 channel_id = 1;
uint64 epoch = 2;
repeated VectorClockEntry version = 3;
}
message ChannelPermission { message ChannelPermission {
uint64 channel_id = 1; uint64 channel_id = 1;
ChannelRole role = 3; ChannelRole role = 3;
@ -1041,7 +1052,6 @@ message ChannelMember {
enum Kind { enum Kind {
Member = 0; Member = 0;
Invitee = 1; Invitee = 1;
AncestorMember = 2;
} }
} }
@ -1148,7 +1158,7 @@ message GetChannelMessagesById {
message MoveChannel { message MoveChannel {
uint64 channel_id = 1; uint64 channel_id = 1;
optional uint64 to = 2; uint64 to = 2;
} }
message JoinChannelBuffer { message JoinChannelBuffer {
@ -1586,7 +1596,6 @@ message Channel {
uint64 id = 1; uint64 id = 1;
string name = 2; string name = 2;
ChannelVisibility visibility = 3; ChannelVisibility visibility = 3;
ChannelRole role = 4;
repeated uint64 parent_path = 5; repeated uint64 parent_path = 5;
} }

View file

@ -269,6 +269,7 @@ messages!(
(UpdateChannelBuffer, Foreground), (UpdateChannelBuffer, Foreground),
(UpdateChannelBufferCollaborators, Foreground), (UpdateChannelBufferCollaborators, Foreground),
(UpdateChannels, Foreground), (UpdateChannels, Foreground),
(UpdateUserChannels, Foreground),
(UpdateContacts, Foreground), (UpdateContacts, Foreground),
(UpdateDiagnosticSummary, Foreground), (UpdateDiagnosticSummary, Foreground),
(UpdateDiffBase, Foreground), (UpdateDiffBase, Foreground),

View file

@ -11,4 +11,4 @@ pub use notification::*;
pub use peer::*; pub use peer::*;
mod macros; mod macros;
pub const PROTOCOL_VERSION: u32 = 67; pub const PROTOCOL_VERSION: u32 = 68;

View file

@ -1334,8 +1334,12 @@ impl Pane {
}, },
|tab, cx| cx.new_view(|_| tab.clone()), |tab, cx| cx.new_view(|_| tab.clone()),
) )
.drag_over::<DraggedTab>(|tab| tab.bg(cx.theme().colors().drop_target_background)) .drag_over::<DraggedTab>(|tab, _, cx| {
.drag_over::<ProjectEntryId>(|tab| tab.bg(cx.theme().colors().drop_target_background)) tab.bg(cx.theme().colors().drop_target_background)
})
.drag_over::<ProjectEntryId>(|tab, _, cx| {
tab.bg(cx.theme().colors().drop_target_background)
})
.when_some(self.can_drop_predicate.clone(), |this, p| { .when_some(self.can_drop_predicate.clone(), |this, p| {
this.can_drop(move |a, cx| p(a, cx)) this.can_drop(move |a, cx| p(a, cx))
}) })
@ -1505,10 +1509,10 @@ impl Pane {
.child("") .child("")
.h_full() .h_full()
.flex_grow() .flex_grow()
.drag_over::<DraggedTab>(|bar| { .drag_over::<DraggedTab>(|bar, _, cx| {
bar.bg(cx.theme().colors().drop_target_background) bar.bg(cx.theme().colors().drop_target_background)
}) })
.drag_over::<ProjectEntryId>(|bar| { .drag_over::<ProjectEntryId>(|bar, _, cx| {
bar.bg(cx.theme().colors().drop_target_background) bar.bg(cx.theme().colors().drop_target_background)
}) })
.on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| { .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {