Fix notifications on channel changes

Conrad Irwin created

Change summary

crates/channel/src/channel_store.rs      |   7 
crates/collab/src/db.rs                  |  17 +
crates/collab/src/db/ids.rs              |  16 +
crates/collab/src/db/queries/channels.rs | 360 +++++++++++++++++--------
crates/collab/src/rpc.rs                 | 225 ++++++---------
crates/collab/src/tests/channel_tests.rs | 205 ++++++++++++++
6 files changed, 583 insertions(+), 247 deletions(-)

Detailed changes

crates/channel/src/channel_store.rs 🔗

@@ -940,6 +940,13 @@ impl ChannelStore {
 
                 for channel_id in &payload.delete_channels {
                     let channel_id = *channel_id;
+                    if payload
+                        .channels
+                        .iter()
+                        .any(|channel| channel.id == channel_id)
+                    {
+                        continue;
+                    }
                     if let Some(OpenedModelHandle::Open(buffer)) =
                         self.opened_buffers.remove(&channel_id)
                     {

crates/collab/src/db.rs 🔗

@@ -436,6 +436,23 @@ pub struct Channel {
     pub role: ChannelRole,
 }
 
+#[derive(Debug, PartialEq, Eq, Hash)]
+pub struct ChannelMember {
+    pub role: ChannelRole,
+    pub user_id: UserId,
+    pub kind: proto::channel_member::Kind,
+}
+
+impl ChannelMember {
+    pub fn to_proto(&self) -> proto::ChannelMember {
+        proto::ChannelMember {
+            role: self.role.into(),
+            user_id: self.user_id.to_proto(),
+            kind: self.kind.into(),
+        }
+    }
+}
+
 #[derive(Debug, PartialEq)]
 pub struct ChannelsForUser {
     pub channels: ChannelGraph,

crates/collab/src/db/ids.rs 🔗

@@ -114,6 +114,22 @@ impl ChannelRole {
             other
         }
     }
+
+    pub fn can_see_all_descendants(&self) -> bool {
+        use ChannelRole::*;
+        match self {
+            Admin | Member => true,
+            Guest | Banned => false,
+        }
+    }
+
+    pub fn can_only_see_public_descendants(&self) -> bool {
+        use ChannelRole::*;
+        match self {
+            Guest => true,
+            Admin | Member | Banned => false,
+        }
+    }
 }
 
 impl From<proto::ChannelRole> for ChannelRole {

crates/collab/src/db/queries/channels.rs 🔗

@@ -633,32 +633,84 @@ impl Database {
             .await
     }
 
-    pub async fn get_channel_members_and_roles(
+    pub async fn participants_to_notify_for_channel_change(
         &self,
-        id: ChannelId,
-    ) -> Result<Vec<(UserId, ChannelRole)>> {
+        new_parent: ChannelId,
+        admin_id: UserId,
+    ) -> Result<Vec<(UserId, ChannelsForUser)>> {
         self.transaction(|tx| async move {
-            #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
-            enum QueryUserIdsAndRoles {
-                UserId,
-                Role,
-            }
+            let mut results: Vec<(UserId, ChannelsForUser)> = Vec::new();
 
-            let ancestor_ids = self.get_channel_ancestors(id, &*tx).await?;
-            let user_ids_and_roles = channel_member::Entity::find()
-                .distinct()
-                .filter(
-                    channel_member::Column::ChannelId
-                        .is_in(ancestor_ids.iter().copied())
-                        .and(channel_member::Column::Accepted.eq(true)),
-                )
-                .select_only()
-                .column(channel_member::Column::UserId)
-                .column(channel_member::Column::Role)
-                .into_values::<_, QueryUserIdsAndRoles>()
-                .all(&*tx)
+            let members = self
+                .get_channel_participant_details_internal(new_parent, admin_id, &*tx)
                 .await?;
-            Ok(user_ids_and_roles)
+
+            dbg!(&members);
+
+            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,
+                        vec![channel_member::Model {
+                            id: Default::default(),
+                            channel_id: new_parent,
+                            user_id: member.user_id,
+                            role: member.role,
+                            accepted: true,
+                        }],
+                        &*tx,
+                    )
+                    .await?,
+                ))
+            }
+
+            let public_parent = self
+                .public_path_to_channel_internal(new_parent, &*tx)
+                .await?
+                .last()
+                .copied();
+
+            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, admin_id, &*tx)
+                    .await?
+            };
+
+            dbg!(&public_members);
+
+            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,
+                        vec![channel_member::Model {
+                            id: Default::default(),
+                            channel_id: public_parent,
+                            user_id: member.user_id,
+                            role: member.role,
+                            accepted: true,
+                        }],
+                        &*tx,
+                    )
+                    .await?,
+                ))
+            }
+
+            Ok(results)
         })
         .await
     }
@@ -696,103 +748,119 @@ impl Database {
         .await
     }
 
-    pub async fn get_channel_participant_details(
+    pub async fn get_channel_participant_details_internal(
         &self,
         channel_id: ChannelId,
         admin_id: UserId,
-    ) -> Result<Vec<proto::ChannelMember>> {
-        self.transaction(|tx| async move {
-            self.check_user_is_channel_admin(channel_id, admin_id, &*tx)
-                .await?;
+        tx: &DatabaseTransaction,
+    ) -> Result<Vec<ChannelMember>> {
+        self.check_user_is_channel_admin(channel_id, admin_id, &*tx)
+            .await?;
 
-            let channel_visibility = channel::Entity::find()
-                .filter(channel::Column::Id.eq(channel_id))
-                .one(&*tx)
-                .await?
-                .map(|channel| channel.visibility)
-                .unwrap_or(ChannelVisibility::Members);
+        let channel_visibility = channel::Entity::find()
+            .filter(channel::Column::Id.eq(channel_id))
+            .one(&*tx)
+            .await?
+            .map(|channel| channel.visibility)
+            .unwrap_or(ChannelVisibility::Members);
 
-            #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
-            enum QueryMemberDetails {
-                UserId,
-                Role,
-                IsDirectMember,
-                Accepted,
-                Visibility,
-            }
+        #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
+        enum QueryMemberDetails {
+            UserId,
+            Role,
+            IsDirectMember,
+            Accepted,
+            Visibility,
+        }
 
-            let tx = tx;
-            let ancestor_ids = self.get_channel_ancestors(channel_id, &*tx).await?;
-            let mut stream = channel_member::Entity::find()
-                .left_join(channel::Entity)
-                .filter(channel_member::Column::ChannelId.is_in(ancestor_ids.iter().copied()))
-                .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 tx = tx;
+        let ancestor_ids = self.get_channel_ancestors(channel_id, &*tx).await?;
+        let mut stream = channel_member::Entity::find()
+            .left_join(channel::Entity)
+            .filter(channel_member::Column::ChannelId.is_in(ancestor_ids.iter().copied()))
+            .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?;
 
-            struct UserDetail {
-                kind: Kind,
-                channel_role: ChannelRole,
+        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;
             }
-            let mut user_details: HashMap<UserId, UserDetail> = 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 let Some(details_mut) = user_details.get_mut(&user_id) {
-                    if channel_role.should_override(details_mut.channel_role) {
-                        details_mut.channel_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, UserDetail { kind, 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(|(user_id, details)| proto::ChannelMember {
-                    user_id: user_id.to_proto(),
-                    kind: details.kind.into(),
-                    role: details.channel_role.into(),
-                })
-                .collect())
-        })
-        .await
+        Ok(user_details
+            .into_iter()
+            .map(|(_, details)| details)
+            .collect())
+    }
+
+    pub async fn get_channel_participant_details(
+        &self,
+        channel_id: ChannelId,
+        admin_id: UserId,
+    ) -> Result<Vec<proto::ChannelMember>> {
+        let members = self
+            .transaction(move |tx| async move {
+                Ok(self
+                    .get_channel_participant_details_internal(channel_id, admin_id, &*tx)
+                    .await?)
+            })
+            .await?;
+
+        Ok(members
+            .into_iter()
+            .map(|channel_member| channel_member.to_proto())
+            .collect())
     }
 
     pub async fn get_channel_participants_internal(
@@ -886,19 +954,35 @@ impl Database {
     // ordered from higher in tree to lower
     // only considers one path to a channel
     // includes the channel itself
-    pub async fn public_path_to_channel(&self, channel_id: ChannelId) -> Result<Vec<ChannelId>> {
+    pub async fn path_to_channel(&self, channel_id: ChannelId) -> Result<Vec<ChannelId>> {
         self.transaction(move |tx| async move {
-            Ok(self
-                .public_path_to_channel_internal(channel_id, &*tx)
-                .await?)
+            Ok(self.path_to_channel_internal(channel_id, &*tx).await?)
         })
         .await
     }
 
-    // ordered from higher in tree to lower
-    // only considers one path to a channel
-    // includes the channel itself
-    pub async fn public_path_to_channel_internal(
+    pub async fn parent_channel_id(&self, channel_id: ChannelId) -> Result<Option<ChannelId>> {
+        let path = self.path_to_channel(channel_id).await?;
+        if path.len() >= 2 {
+            Ok(Some(path[path.len() - 2]))
+        } else {
+            Ok(None)
+        }
+    }
+
+    pub async fn public_parent_channel_id(
+        &self,
+        channel_id: ChannelId,
+    ) -> Result<Option<ChannelId>> {
+        let path = self.path_to_channel(channel_id).await?;
+        if path.len() >= 2 && path.last().copied() == Some(channel_id) {
+            Ok(Some(path[path.len() - 2]))
+        } else {
+            Ok(path.last().copied())
+        }
+    }
+
+    pub async fn path_to_channel_internal(
         &self,
         channel_id: ChannelId,
         tx: &DatabaseTransaction,
@@ -913,12 +997,35 @@ impl Database {
             return Ok(vec![]);
         };
 
-        let ancestor_ids: Vec<ChannelId> = path
+        Ok(path
             .id_path
             .trim_matches('/')
             .split('/')
             .map(|id| ChannelId::from_proto(id.parse().unwrap()))
-            .collect();
+            .collect())
+    }
+
+    // ordered from higher in tree to lower
+    // only considers one path to a channel
+    // includes the channel itself
+    pub async fn public_path_to_channel(&self, channel_id: ChannelId) -> Result<Vec<ChannelId>> {
+        self.transaction(move |tx| async move {
+            Ok(self
+                .public_path_to_channel_internal(channel_id, &*tx)
+                .await?)
+        })
+        .await
+    }
+
+    // ordered from higher in tree to lower
+    // only considers one path to a channel
+    // includes the channel itself
+    pub async fn public_path_to_channel_internal(
+        &self,
+        channel_id: ChannelId,
+        tx: &DatabaseTransaction,
+    ) -> Result<Vec<ChannelId>> {
+        let ancestor_ids = self.path_to_channel_internal(channel_id, &*tx).await?;
 
         let rows = channel::Entity::find()
             .filter(channel::Column::Id.is_in(ancestor_ids.iter().copied()))
@@ -1044,6 +1151,27 @@ impl Database {
         Ok(channel_ids)
     }
 
+    // returns all ids of channels in the tree under this channel_id.
+    pub async fn get_channel_descendant_ids(
+        &self,
+        channel_id: ChannelId,
+    ) -> Result<HashSet<ChannelId>> {
+        self.transaction(|tx| async move {
+            let pairs = self.get_channel_descendants([channel_id], &*tx).await?;
+
+            let mut results: HashSet<ChannelId> = HashSet::default();
+            for ChannelEdge {
+                parent_id: _,
+                channel_id,
+            } in pairs
+            {
+                results.insert(ChannelId::from_proto(channel_id));
+            }
+            Ok(results)
+        })
+        .await
+    }
+
     // Returns the channel desendants as a sorted list of edges for further processing.
     // The edges are sorted such that you will see unknown channel ids as children
     // before you see them as parents.

crates/collab/src/rpc.rs 🔗

@@ -2220,26 +2220,13 @@ async fn create_channel(
         return Ok(());
     };
 
-    let members: Vec<proto::ChannelMember> = db
-        .get_channel_participant_details(parent_id, session.user_id)
-        .await?
-        .into_iter()
-        .filter(|member| {
-            member.role() == proto::ChannelRole::Admin
-                || member.role() == proto::ChannelRole::Member
-        })
-        .collect();
-
-    let mut updates: HashMap<UserId, proto::UpdateChannels> = HashMap::default();
-
-    for member in members {
-        let user_id = UserId::from_proto(member.user_id);
-        let channels = db.get_channel_for_user(parent_id, user_id).await?;
-        updates.insert(user_id, build_initial_channels_update(channels, vec![]));
-    }
+    let updates = db
+        .participants_to_notify_for_channel_change(parent_id, session.user_id)
+        .await?;
 
     let connection_pool = session.connection_pool().await;
-    for (user_id, update) in updates {
+    for (user_id, channels) in updates {
+        let update = build_initial_channels_update(channels, vec![]);
         for connection_id in connection_pool.user_connection_ids(user_id) {
             if user_id == session.user_id {
                 continue;
@@ -2353,31 +2340,55 @@ async fn set_channel_visibility(
     let channel_id = ChannelId::from_proto(request.channel_id);
     let visibility = request.visibility().into();
 
-    let channel = db
-        .set_channel_visibility(channel_id, visibility, session.user_id)
+    let previous_members = db
+        .get_channel_participant_details(channel_id, session.user_id)
         .await?;
 
-    let members = db
-        .get_channel_participant_details(channel_id, session.user_id)
+    db.set_channel_visibility(channel_id, visibility, session.user_id)
         .await?;
 
+    let mut updates: HashMap<UserId, ChannelsForUser> = db
+        .participants_to_notify_for_channel_change(channel_id, session.user_id)
+        .await?
+        .into_iter()
+        .collect();
+
+    let mut participants_who_lost_access: HashSet<UserId> = HashSet::default();
+    match visibility {
+        ChannelVisibility::Members => {
+            for member in previous_members {
+                if ChannelRole::from(member.role()).can_only_see_public_descendants() {
+                    participants_who_lost_access.insert(UserId::from_proto(member.user_id));
+                }
+            }
+        }
+        ChannelVisibility::Public => {
+            if let Some(public_parent_id) = db.public_parent_channel_id(channel_id).await? {
+                let parent_updates = db
+                    .participants_to_notify_for_channel_change(public_parent_id, session.user_id)
+                    .await?;
+
+                for (user_id, channels) in parent_updates {
+                    updates.insert(user_id, channels);
+                }
+            }
+        }
+    }
+
     let connection_pool = session.connection_pool().await;
-    // TODO: notify people who were guests and are now not allowed.
-    for member in members {
-        for connection_id in connection_pool.user_connection_ids(UserId::from_proto(member.user_id))
-        {
-            session.peer.send(
-                connection_id,
-                proto::UpdateChannels {
-                    channels: vec![proto::Channel {
-                        id: channel.id.to_proto(),
-                        name: channel.name.clone(),
-                        visibility: channel.visibility.into(),
-                        role: member.role.into(),
-                    }],
-                    ..Default::default()
-                },
-            )?;
+    for (user_id, channels) in updates {
+        let update = build_initial_channels_update(channels, vec![]);
+        for connection_id in connection_pool.user_connection_ids(user_id) {
+            session.peer.send(connection_id, update.clone())?;
+        }
+    }
+    for user_id in participants_who_lost_access {
+        let update = proto::UpdateChannels {
+            delete_channels: vec![channel_id.to_proto()],
+            ..Default::default()
+        };
+        for connection_id in connection_pool.user_connection_ids(user_id) {
+            session.peer.send(connection_id, update.clone())?;
         }
     }
 
@@ -2485,42 +2496,20 @@ async fn link_channel(
     // TODO: Remove this restriction once we have symlinks
     db.assert_root_channel(channel_id).await?;
 
-    let channels_to_send = db.link_channel(session.user_id, channel_id, to).await?;
-    let members = db.get_channel_members_and_roles(to).await?;
-    let connection_pool = session.connection_pool().await;
+    db.link_channel(session.user_id, channel_id, to).await?;
 
-    for (member_id, role) in members {
-        let build_channel_proto = |channel: &db::Channel| proto::Channel {
-            id: channel.id.to_proto(),
-            visibility: channel.visibility.into(),
-            name: channel.name.clone(),
-            role: role.into(),
-        };
+    let member_updates = db
+        .participants_to_notify_for_channel_change(to, session.user_id)
+        .await?;
 
-        for connection_id in connection_pool.user_connection_ids(member_id) {
-            let channels: Vec<_> = if role == ChannelRole::Guest {
-                channels_to_send
-                    .channels
-                    .iter()
-                    .filter(|channel| channel.visibility != ChannelVisibility::Public)
-                    .map(build_channel_proto)
-                    .collect()
-            } else {
-                channels_to_send
-                    .channels
-                    .iter()
-                    .map(build_channel_proto)
-                    .collect()
-            };
+    dbg!(&member_updates);
 
-            session.peer.send(
-                connection_id,
-                proto::UpdateChannels {
-                    channels,
-                    insert_edge: channels_to_send.edges.clone(),
-                    ..Default::default()
-                },
-            )?;
+    let connection_pool = session.connection_pool().await;
+
+    for (member_id, channels) in member_updates {
+        let update = build_initial_channels_update(channels, vec![]);
+        for connection_id in connection_pool.user_connection_ids(member_id) {
+            session.peer.send(connection_id, update.clone())?;
         }
     }
 
@@ -2548,11 +2537,11 @@ async fn move_channel(
     let from_parent = ChannelId::from_proto(request.from);
     let to = ChannelId::from_proto(request.to);
 
-    let from_public_parent = db
-        .public_path_to_channel(from_parent)
-        .await?
-        .last()
-        .copied();
+    let previous_participants = db
+        .get_channel_participant_details(channel_id, session.user_id)
+        .await?;
+
+    debug_assert_eq!(db.parent_channel_id(channel_id).await?, Some(from_parent));
 
     let channels_to_send = db
         .move_channel(session.user_id, channel_id, from_parent, to)
@@ -2563,68 +2552,42 @@ async fn move_channel(
         return Ok(());
     }
 
-    let to_public_parent = db.public_path_to_channel(to).await?.last().cloned();
-
-    let members_from = db
-        .get_channel_participant_details(from_parent, session.user_id)
-        .await?
-        .into_iter()
-        .filter(|member| {
-            member.role() == proto::ChannelRole::Admin || member.role() == proto::ChannelRole::Guest
-        });
-
-    let members_to = db
-        .get_channel_participant_details(to, session.user_id)
-        .await?
-        .into_iter()
-        .filter(|member| {
-            member.role() == proto::ChannelRole::Admin || member.role() == proto::ChannelRole::Guest
-        });
-
-    let mut updates: HashMap<UserId, proto::UpdateChannels> = HashMap::default();
-
-    for member in members_to {
-        let user_id = UserId::from_proto(member.user_id);
-        let channels = db.get_channel_for_user(to, user_id).await?;
-        updates.insert(user_id, build_initial_channels_update(channels, vec![]));
-    }
+    let updates = db
+        .participants_to_notify_for_channel_change(to, session.user_id)
+        .await?;
 
-    if let Some(to_public_parent) = to_public_parent {
-        // only notify guests of public channels (admins/members are notified by members_to above, and banned users don't care)
-        let public_members_to = db
-            .get_channel_participant_details(to, session.user_id)
-            .await?
-            .into_iter()
-            .filter(|member| member.role() == proto::ChannelRole::Guest);
+    let mut participants_who_lost_access: HashSet<UserId> = HashSet::default();
+    let mut channels_to_delete = db.get_channel_descendant_ids(channel_id).await?;
+    channels_to_delete.insert(channel_id);
 
-        for member in public_members_to {
-            let user_id = UserId::from_proto(member.user_id);
-            if updates.contains_key(&user_id) {
-                continue;
-            }
-            let channels = db.get_channel_for_user(to_public_parent, user_id).await?;
-            updates.insert(user_id, build_initial_channels_update(channels, vec![]));
+    for previous_participant in previous_participants.iter() {
+        let user_id = UserId::from_proto(previous_participant.user_id);
+        if previous_participant.kind() == proto::channel_member::Kind::AncestorMember {
+            participants_who_lost_access.insert(user_id);
         }
     }
 
-    for member in members_from {
-        let user_id = UserId::from_proto(member.user_id);
-        let update = updates
-            .entry(user_id)
-            .or_insert(proto::UpdateChannels::default());
-        update.delete_edge.push(proto::ChannelEdge {
-            channel_id: channel_id.to_proto(),
-            parent_id: from_parent.to_proto(),
-        })
-    }
-
-    if let Some(_from_public_parent) = from_public_parent {
-        // TODO: for each guest member of the old public parent
-        // delete the edge that they could see (from the from_public_parent down)
+    let connection_pool = session.connection_pool().await;
+    for (user_id, channels) in updates {
+        let mut update = build_initial_channels_update(channels, vec![]);
+        update.delete_channels = channels_to_delete
+            .iter()
+            .map(|channel_id| channel_id.to_proto())
+            .collect();
+        participants_who_lost_access.remove(&user_id);
+        for connection_id in connection_pool.user_connection_ids(user_id) {
+            session.peer.send(connection_id, update.clone())?;
+        }
     }
 
-    let connection_pool = session.connection_pool().await;
-    for (user_id, update) in updates {
+    for user_id in participants_who_lost_access {
+        let update = proto::UpdateChannels {
+            delete_channels: channels_to_delete
+                .iter()
+                .map(|channel_id| channel_id.to_proto())
+                .collect(),
+            ..Default::default()
+        };
         for connection_id in connection_pool.user_connection_ids(user_id) {
             session.peer.send(connection_id, update.clone())?;
         }

crates/collab/src/tests/channel_tests.rs 🔗

@@ -5,6 +5,7 @@ use crate::{
 use call::ActiveCall;
 use channel::{ChannelId, ChannelMembership, ChannelStore};
 use client::User;
+use futures::future::try_join_all;
 use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
 use rpc::{
     proto::{self, ChannelRole},
@@ -913,6 +914,210 @@ async fn test_lost_channel_creation(
         ],
     );
 }
+
+#[gpui::test]
+async fn test_channel_link_notifications(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+    cx_c: &mut TestAppContext,
+) {
+    deterministic.forbid_parking();
+
+    let mut server = TestServer::start(&deterministic).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+    let client_c = server.create_client(cx_c, "user_c").await;
+
+    let user_b = client_b.user_id().unwrap();
+    let user_c = client_c.user_id().unwrap();
+
+    let channels = server
+        .make_channel_tree(&[("zed", None)], (&client_a, cx_a))
+        .await;
+    let zed_channel = channels[0];
+
+    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.invite_member(zed_channel, user_b, proto::ChannelRole::Member, cx),
+            channel_store.invite_member(zed_channel, user_c, proto::ChannelRole::Guest, cx),
+        ]
+    }))
+    .await
+    .unwrap();
+
+    deterministic.run_until_parked();
+
+    client_b
+        .channel_store()
+        .update(cx_b, |channel_store, _| {
+            channel_store.respond_to_channel_invite(zed_channel, true)
+        })
+        .await
+        .unwrap();
+
+    client_c
+        .channel_store()
+        .update(cx_c, |channel_store, _| {
+            channel_store.respond_to_channel_invite(zed_channel, true)
+        })
+        .await
+        .unwrap();
+
+    deterministic.run_until_parked();
+
+    // we have an admin (a), member (b) and guest (c) all part of the zed channel.
+
+    // create a new private sub-channel
+    // create a new priate channel, make it public, and move it under the previous one, and verify it shows for b and c
+    let active_channel = client_a
+        .channel_store()
+        .update(cx_a, |channel_store, cx| {
+            channel_store.create_channel("active", Some(zed_channel), cx)
+        })
+        .await
+        .unwrap();
+
+    // the new channel shows for b and not c
+    assert_channels_list_shape(
+        client_a.channel_store(),
+        cx_a,
+        &[(zed_channel, 0), (active_channel, 1)],
+    );
+    assert_channels_list_shape(
+        client_b.channel_store(),
+        cx_b,
+        &[(zed_channel, 0), (active_channel, 1)],
+    );
+    assert_channels_list_shape(client_c.channel_store(), cx_c, &[(zed_channel, 0)]);
+
+    let vim_channel = client_a
+        .channel_store()
+        .update(cx_a, |channel_store, cx| {
+            channel_store.create_channel("vim", None, cx)
+        })
+        .await
+        .unwrap();
+
+    client_a
+        .channel_store()
+        .update(cx_a, |channel_store, cx| {
+            channel_store.set_channel_visibility(vim_channel, proto::ChannelVisibility::Public, cx)
+        })
+        .await
+        .unwrap();
+
+    client_a
+        .channel_store()
+        .update(cx_a, |channel_store, cx| {
+            channel_store.link_channel(vim_channel, active_channel, cx)
+        })
+        .await
+        .unwrap();
+
+    deterministic.run_until_parked();
+
+    // the new channel shows for b and c
+    assert_channels_list_shape(
+        client_a.channel_store(),
+        cx_a,
+        &[(zed_channel, 0), (active_channel, 1), (vim_channel, 2)],
+    );
+    assert_channels_list_shape(
+        client_b.channel_store(),
+        cx_b,
+        &[(zed_channel, 0), (active_channel, 1), (vim_channel, 2)],
+    );
+    assert_channels_list_shape(
+        client_c.channel_store(),
+        cx_c,
+        &[(zed_channel, 0), (vim_channel, 1)],
+    );
+
+    let helix_channel = client_a
+        .channel_store()
+        .update(cx_a, |channel_store, cx| {
+            channel_store.create_channel("helix", None, cx)
+        })
+        .await
+        .unwrap();
+
+    client_a
+        .channel_store()
+        .update(cx_a, |channel_store, cx| {
+            channel_store.link_channel(helix_channel, vim_channel, cx)
+        })
+        .await
+        .unwrap();
+
+    client_a
+        .channel_store()
+        .update(cx_a, |channel_store, cx| {
+            channel_store.set_channel_visibility(
+                helix_channel,
+                proto::ChannelVisibility::Public,
+                cx,
+            )
+        })
+        .await
+        .unwrap();
+
+    // the new channel shows for b and c
+    assert_channels_list_shape(
+        client_b.channel_store(),
+        cx_b,
+        &[
+            (zed_channel, 0),
+            (active_channel, 1),
+            (vim_channel, 2),
+            (helix_channel, 3),
+        ],
+    );
+    assert_channels_list_shape(
+        client_c.channel_store(),
+        cx_c,
+        &[(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();
+
+    // 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),
+        ],
+    );
+    client_b
+        .channel_store()
+        .read_with(cx_b, |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), (helix_channel, 1)],
+    );
+}
+
 #[gpui::test]
 async fn test_guest_access(
     deterministic: Arc<Deterministic>,