Channel notifications from the server works

Mikayla created

Change summary

crates/channel/src/channel_store.rs                            |  16 
crates/channel/src/channel_store/channel_index.rs              |   7 
crates/collab/migrations.sqlite/20221109000000_test_schema.sql |   8 
crates/collab/migrations/20230925210437_add_observed_notes.sql |   8 
crates/collab/src/db.rs                                        |   1 
crates/collab/src/db/queries/buffers.rs                        | 155 ++-
crates/collab/src/db/queries/channels.rs                       |  15 
crates/collab/src/db/tables.rs                                 |   2 
crates/collab/src/db/tables/observed_buffer_edits.rs           |  18 
crates/collab/src/db/tests/buffer_tests.rs                     |  17 
crates/collab/src/rpc.rs                                       |  27 
crates/collab/src/tests/channel_buffer_tests.rs                |  63 +
crates/collab_ui/src/collab_panel.rs                           |  19 
crates/rpc/proto/zed.proto                                     |   1 
crates/theme/src/theme.rs                                      |   2 
styles/src/style_tree/collab_panel.ts                          |  14 
16 files changed, 266 insertions(+), 107 deletions(-)

Detailed changes

crates/channel/src/channel_store.rs 🔗

@@ -43,6 +43,7 @@ pub type ChannelData = (Channel, ChannelPath);
 pub struct Channel {
     pub id: ChannelId,
     pub name: String,
+    pub has_changed: bool,
 }
 
 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)]
@@ -207,6 +208,13 @@ impl ChannelStore {
         )
     }
 
+    pub fn has_channel_buffer_changed(&self, channel_id: ChannelId) -> Option<bool> {
+        self.channel_index
+            .by_id()
+            .get(&channel_id)
+            .map(|channel| channel.has_changed)
+    }
+
     pub fn open_channel_chat(
         &mut self,
         channel_id: ChannelId,
@@ -779,6 +787,7 @@ impl ChannelStore {
                     Arc::new(Channel {
                         id: channel.id,
                         name: channel.name,
+                        has_changed: false,
                     }),
                 ),
             }
@@ -787,7 +796,8 @@ impl ChannelStore {
         let channels_changed = !payload.channels.is_empty()
             || !payload.delete_channels.is_empty()
             || !payload.insert_edge.is_empty()
-            || !payload.delete_edge.is_empty();
+            || !payload.delete_edge.is_empty()
+            || !payload.notes_changed.is_empty();
 
         if channels_changed {
             if !payload.delete_channels.is_empty() {
@@ -814,6 +824,10 @@ impl ChannelStore {
                 index.insert(channel)
             }
 
+            for id_changed in payload.notes_changed {
+                index.has_changed(id_changed);
+            }
+
             for edge in payload.insert_edge {
                 index.insert_edge(edge.channel_id, edge.parent_id);
             }

crates/channel/src/channel_store/channel_index.rs 🔗

@@ -76,6 +76,12 @@ impl<'a> ChannelPathsInsertGuard<'a> {
         }
     }
 
+    pub fn has_changed(&mut self, channel_id: ChannelId) {
+        if let Some(channel) = self.channels_by_id.get_mut(&channel_id) {
+            Arc::make_mut(channel).has_changed = true;
+        }
+    }
+
     pub fn insert(&mut self, channel_proto: proto::Channel) {
         if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) {
             Arc::make_mut(existing_channel).name = channel_proto.name;
@@ -85,6 +91,7 @@ impl<'a> ChannelPathsInsertGuard<'a> {
                 Arc::new(Channel {
                     id: channel_proto.id,
                     name: channel_proto.name,
+                    has_changed: false,
                 }),
             );
             self.insert_root(channel_proto.id);

crates/collab/migrations.sqlite/20221109000000_test_schema.sql 🔗

@@ -291,12 +291,12 @@ CREATE INDEX "index_user_features_on_user_id" ON "user_features" ("user_id");
 CREATE INDEX "index_user_features_on_feature_id" ON "user_features" ("feature_id");
 
 
-CREATE TABLE "observed_channel_note_edits" (
+CREATE TABLE "observed_buffer_edits" (
     "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
-    "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
+    "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE,
     "epoch" INTEGER NOT NULL,
     "lamport_timestamp" INTEGER NOT NULL,
-    PRIMARY KEY (user_id, channel_id)
+    PRIMARY KEY (user_id, buffer_id)
 );
 
-CREATE UNIQUE INDEX "index_observed_notes_user_and_channel_id" ON "observed_channel_note_edits" ("user_id", "channel_id");
+CREATE UNIQUE INDEX "index_observed_buffers_user_and_buffer_id" ON "observed_buffer_edits" ("user_id", "buffer_id");

crates/collab/migrations/20230925210437_add_observed_notes.sql 🔗

@@ -1,9 +1,9 @@
-CREATE TABLE "observed_channel_note_edits" (
+CREATE TABLE "observed_buffer_edits" (
     "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
-    "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
+    "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE,
     "epoch" INTEGER NOT NULL,
     "lamport_timestamp" INTEGER NOT NULL,
-    PRIMARY KEY (user_id, channel_id)
+    PRIMARY KEY (user_id, buffer_id)
 );
 
-CREATE UNIQUE INDEX "index_observed_notes_user_and_channel_id" ON "observed_channel_note_edits" ("user_id", "channel_id");
+CREATE UNIQUE INDEX "index_observed_buffer_user_and_buffer_id" ON "observed_buffer_edits" ("user_id", "buffer_id");

crates/collab/src/db.rs 🔗

@@ -436,6 +436,7 @@ pub struct Channel {
 pub struct ChannelsForUser {
     pub channels: ChannelGraph,
     pub channel_participants: HashMap<ChannelId, Vec<UserId>>,
+    pub channels_with_changed_notes: HashSet<ChannelId>,
     pub channels_with_admin_privileges: HashSet<ChannelId>,
 }
 

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

@@ -80,20 +80,20 @@ impl Database {
 
             // Save the last observed operation
             if let Some(max_operation) = max_operation {
-                observed_note_edits::Entity::insert(observed_note_edits::ActiveModel {
+                observed_buffer_edits::Entity::insert(observed_buffer_edits::ActiveModel {
                     user_id: ActiveValue::Set(user_id),
-                    channel_id: ActiveValue::Set(channel_id),
+                    buffer_id: ActiveValue::Set(buffer.id),
                     epoch: ActiveValue::Set(max_operation.0),
                     lamport_timestamp: ActiveValue::Set(max_operation.1),
                 })
                 .on_conflict(
                     OnConflict::columns([
-                        observed_note_edits::Column::UserId,
-                        observed_note_edits::Column::ChannelId,
+                        observed_buffer_edits::Column::UserId,
+                        observed_buffer_edits::Column::BufferId,
                     ])
                     .update_columns([
-                        observed_note_edits::Column::Epoch,
-                        observed_note_edits::Column::LamportTimestamp,
+                        observed_buffer_edits::Column::Epoch,
+                        observed_buffer_edits::Column::LamportTimestamp,
                     ])
                     .to_owned(),
                 )
@@ -110,20 +110,20 @@ impl Database {
                     .map(|model| (model.epoch, model.lamport_timestamp));
 
                 if let Some(buffer_max) = buffer_max {
-                    observed_note_edits::Entity::insert(observed_note_edits::ActiveModel {
+                    observed_buffer_edits::Entity::insert(observed_buffer_edits::ActiveModel {
                         user_id: ActiveValue::Set(user_id),
-                        channel_id: ActiveValue::Set(channel_id),
+                        buffer_id: ActiveValue::Set(buffer.id),
                         epoch: ActiveValue::Set(buffer_max.0),
                         lamport_timestamp: ActiveValue::Set(buffer_max.1),
                     })
                     .on_conflict(
                         OnConflict::columns([
-                            observed_note_edits::Column::UserId,
-                            observed_note_edits::Column::ChannelId,
+                            observed_buffer_edits::Column::UserId,
+                            observed_buffer_edits::Column::BufferId,
                         ])
                         .update_columns([
-                            observed_note_edits::Column::Epoch,
-                            observed_note_edits::Column::LamportTimestamp,
+                            observed_buffer_edits::Column::Epoch,
+                            observed_buffer_edits::Column::LamportTimestamp,
                         ])
                         .to_owned(),
                     )
@@ -463,7 +463,7 @@ impl Database {
         channel_id: ChannelId,
         user: UserId,
         operations: &[proto::Operation],
-    ) -> Result<Vec<ConnectionId>> {
+    ) -> Result<(Vec<ConnectionId>, Vec<UserId>)> {
         self.transaction(move |tx| async move {
             self.check_user_is_channel_member(channel_id, user, &*tx)
                 .await?;
@@ -483,10 +483,23 @@ impl Database {
                 .filter_map(|op| operation_to_storage(op, &buffer, serialization_version))
                 .collect::<Vec<_>>();
 
+            let mut channel_members;
+
             if !operations.is_empty() {
                 // get current channel participants and save the max operation above
-                self.save_max_operation_for_collaborators(operations.as_slice(), channel_id, &*tx)
+                self.save_max_operation_for_collaborators(
+                    operations.as_slice(),
+                    channel_id,
+                    buffer.id,
+                    &*tx,
+                )
+                .await?;
+
+                channel_members = self.get_channel_members_internal(channel_id, &*tx).await?;
+                let collaborators = self
+                    .get_channel_buffer_collaborators_internal(channel_id, &*tx)
                     .await?;
+                channel_members.retain(|member| !collaborators.contains(member));
 
                 buffer_operation::Entity::insert_many(operations)
                     .on_conflict(
@@ -501,6 +514,8 @@ impl Database {
                     )
                     .exec(&*tx)
                     .await?;
+            } else {
+                channel_members = Vec::new();
             }
 
             let mut connections = Vec::new();
@@ -519,7 +534,7 @@ impl Database {
                 });
             }
 
-            Ok(connections)
+            Ok((connections, channel_members))
         })
         .await
     }
@@ -528,6 +543,7 @@ impl Database {
         &self,
         operations: &[buffer_operation::ActiveModel],
         channel_id: ChannelId,
+        buffer_id: BufferId,
         tx: &DatabaseTransaction,
     ) -> Result<()> {
         let max_operation = operations
@@ -553,22 +569,22 @@ impl Database {
             .get_channel_buffer_collaborators_internal(channel_id, tx)
             .await?;
 
-        observed_note_edits::Entity::insert_many(users.iter().map(|id| {
-            observed_note_edits::ActiveModel {
+        observed_buffer_edits::Entity::insert_many(users.iter().map(|id| {
+            observed_buffer_edits::ActiveModel {
                 user_id: ActiveValue::Set(*id),
-                channel_id: ActiveValue::Set(channel_id),
+                buffer_id: ActiveValue::Set(buffer_id),
                 epoch: max_operation.0.clone(),
                 lamport_timestamp: ActiveValue::Set(*max_operation.1.as_ref()),
             }
         }))
         .on_conflict(
             OnConflict::columns([
-                observed_note_edits::Column::UserId,
-                observed_note_edits::Column::ChannelId,
+                observed_buffer_edits::Column::UserId,
+                observed_buffer_edits::Column::BufferId,
             ])
             .update_columns([
-                observed_note_edits::Column::Epoch,
-                observed_note_edits::Column::LamportTimestamp,
+                observed_buffer_edits::Column::Epoch,
+                observed_buffer_edits::Column::LamportTimestamp,
             ])
             .to_owned(),
         )
@@ -699,54 +715,75 @@ impl Database {
         Ok(())
     }
 
-    pub async fn has_buffer_changed(&self, user_id: UserId, channel_id: ChannelId) -> Result<bool> {
-        self.transaction(|tx| async move {
-            let user_max = observed_note_edits::Entity::find()
-                .filter(observed_note_edits::Column::UserId.eq(user_id))
-                .filter(observed_note_edits::Column::ChannelId.eq(channel_id))
-                .one(&*tx)
-                .await?
-                .map(|model| (model.epoch, model.lamport_timestamp));
+    #[cfg(test)]
+    pub async fn test_has_note_changed(
+        &self,
+        user_id: UserId,
+        channel_id: ChannelId,
+    ) -> Result<bool> {
+        self.transaction(|tx| async move { self.has_note_changed(user_id, channel_id, &*tx).await })
+            .await
+    }
 
-            let channel_buffer = channel::Model {
-                id: channel_id,
-                ..Default::default()
-            }
-            .find_related(buffer::Entity)
+    pub async fn has_note_changed(
+        &self,
+        user_id: UserId,
+        channel_id: ChannelId,
+        tx: &DatabaseTransaction,
+    ) -> Result<bool> {
+        let Some(buffer_id) = channel::Model {
+            id: channel_id,
+            ..Default::default()
+        }
+        .find_related(buffer::Entity)
+        .one(&*tx)
+        .await?
+        .map(|buffer| buffer.id) else {
+            return Ok(false);
+        };
+
+        let user_max = observed_buffer_edits::Entity::find()
+            .filter(observed_buffer_edits::Column::UserId.eq(user_id))
+            .filter(observed_buffer_edits::Column::BufferId.eq(buffer_id))
             .one(&*tx)
-            .await?;
+            .await?
+            .map(|model| (model.epoch, model.lamport_timestamp));
 
-            let Some(channel_buffer) = channel_buffer else {
-                return Ok(false);
-            };
+        let channel_buffer = channel::Model {
+            id: channel_id,
+            ..Default::default()
+        }
+        .find_related(buffer::Entity)
+        .one(&*tx)
+        .await?;
 
-            let mut channel_max = buffer_operation::Entity::find()
+        let Some(channel_buffer) = channel_buffer else {
+            return Ok(false);
+        };
+
+        let mut channel_max = buffer_operation::Entity::find()
+            .filter(buffer_operation::Column::BufferId.eq(channel_buffer.id))
+            .filter(buffer_operation::Column::Epoch.eq(channel_buffer.epoch))
+            .order_by(buffer_operation::Column::Epoch, Desc)
+            .order_by(buffer_operation::Column::LamportTimestamp, Desc)
+            .one(&*tx)
+            .await?
+            .map(|model| (model.epoch, model.lamport_timestamp));
+
+        // If there are no edits in this epoch
+        if channel_max.is_none() {
+            // check if this user observed the last edit of the previous epoch
+            channel_max = buffer_operation::Entity::find()
                 .filter(buffer_operation::Column::BufferId.eq(channel_buffer.id))
-                .filter(buffer_operation::Column::Epoch.eq(channel_buffer.epoch))
+                .filter(buffer_operation::Column::Epoch.eq(channel_buffer.epoch.saturating_sub(1)))
                 .order_by(buffer_operation::Column::Epoch, Desc)
                 .order_by(buffer_operation::Column::LamportTimestamp, Desc)
                 .one(&*tx)
                 .await?
                 .map(|model| (model.epoch, model.lamport_timestamp));
+        }
 
-            // If there are no edits in this epoch
-            if channel_max.is_none() {
-                // check if this user observed the last edit of the previous epoch
-                channel_max = buffer_operation::Entity::find()
-                    .filter(buffer_operation::Column::BufferId.eq(channel_buffer.id))
-                    .filter(
-                        buffer_operation::Column::Epoch.eq(channel_buffer.epoch.saturating_sub(1)),
-                    )
-                    .order_by(buffer_operation::Column::Epoch, Desc)
-                    .order_by(buffer_operation::Column::LamportTimestamp, Desc)
-                    .one(&*tx)
-                    .await?
-                    .map(|model| (model.epoch, model.lamport_timestamp));
-            }
-
-            Ok(user_max != channel_max)
-        })
-        .await
+        Ok(user_max != channel_max)
     }
 }
 

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

@@ -391,7 +391,8 @@ impl Database {
                 .all(&*tx)
                 .await?;
 
-            self.get_user_channels(channel_memberships, &tx).await
+            self.get_user_channels(user_id, channel_memberships, &tx)
+                .await
         })
         .await
     }
@@ -414,13 +415,15 @@ impl Database {
                 .all(&*tx)
                 .await?;
 
-            self.get_user_channels(channel_membership, &tx).await
+            self.get_user_channels(user_id, channel_membership, &tx)
+                .await
         })
         .await
     }
 
     pub async fn get_user_channels(
         &self,
+        user_id: UserId,
         channel_memberships: Vec<channel_member::Model>,
         tx: &DatabaseTransaction,
     ) -> Result<ChannelsForUser> {
@@ -460,10 +463,18 @@ impl Database {
             }
         }
 
+        let mut channels_with_changed_notes = HashSet::default();
+        for channel in graph.channels.iter() {
+            if self.has_note_changed(user_id, channel.id, tx).await? {
+                channels_with_changed_notes.insert(channel.id);
+            }
+        }
+
         Ok(ChannelsForUser {
             channels: graph,
             channel_participants,
             channels_with_admin_privileges,
+            channels_with_changed_notes,
         })
     }
 

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

@@ -12,7 +12,7 @@ pub mod contact;
 pub mod feature_flag;
 pub mod follower;
 pub mod language_server;
-pub mod observed_note_edits;
+pub mod observed_buffer_edits;
 pub mod project;
 pub mod project_collaborator;
 pub mod room;

crates/collab/src/db/tables/observed_note_edits.rs → crates/collab/src/db/tables/observed_buffer_edits.rs 🔗

@@ -1,12 +1,12 @@
-use crate::db::{ChannelId, UserId};
+use crate::db::{BufferId, UserId};
 use sea_orm::entity::prelude::*;
 
 #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
-#[sea_orm(table_name = "observed_channel_note_edits")]
+#[sea_orm(table_name = "observed_buffer_edits")]
 pub struct Model {
     #[sea_orm(primary_key)]
     pub user_id: UserId,
-    pub channel_id: ChannelId,
+    pub buffer_id: BufferId,
     pub epoch: i32,
     pub lamport_timestamp: i32,
 }
@@ -14,11 +14,11 @@ pub struct Model {
 #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
 pub enum Relation {
     #[sea_orm(
-        belongs_to = "super::channel::Entity",
-        from = "Column::ChannelId",
-        to = "super::channel::Column::Id"
+        belongs_to = "super::buffer::Entity",
+        from = "Column::BufferId",
+        to = "super::buffer::Column::Id"
     )]
-    Channel,
+    Buffer,
     #[sea_orm(
         belongs_to = "super::user::Entity",
         from = "Column::UserId",
@@ -27,9 +27,9 @@ pub enum Relation {
     User,
 }
 
-impl Related<super::channel::Entity> for Entity {
+impl Related<super::buffer::Entity> for Entity {
     fn to() -> RelationDef {
-        Relation::Channel.def()
+        Relation::Buffer.def()
     }
 }
 

crates/collab/src/db/tests/buffer_tests.rs 🔗

@@ -220,7 +220,7 @@ async fn test_channel_buffers_diffs(db: &Database) {
     };
 
     // Zero test: A should not register as changed on an unitialized channel buffer
-    assert!(!db.has_buffer_changed(a_id, zed_id).await.unwrap());
+    assert!(!db.test_has_note_changed(a_id, zed_id).await.unwrap());
 
     let _ = db
         .join_channel_buffer(zed_id, a_id, connection_id_a)
@@ -228,7 +228,7 @@ async fn test_channel_buffers_diffs(db: &Database) {
         .unwrap();
 
     // Zero test: A should register as changed on an empty channel buffer
-    assert!(!db.has_buffer_changed(a_id, zed_id).await.unwrap());
+    assert!(!db.test_has_note_changed(a_id, zed_id).await.unwrap());
 
     let mut buffer_a = Buffer::new(0, 0, "".to_string());
     let mut operations = Vec::new();
@@ -245,15 +245,16 @@ async fn test_channel_buffers_diffs(db: &Database) {
         .unwrap();
 
     // Smoke test: Does B register as changed, A as unchanged?
-    assert!(db.has_buffer_changed(b_id, zed_id).await.unwrap());
-    assert!(!db.has_buffer_changed(a_id, zed_id).await.unwrap());
+    assert!(db.test_has_note_changed(b_id, zed_id).await.unwrap());
+
+    assert!(!db.test_has_note_changed(a_id, zed_id).await.unwrap());
 
     db.leave_channel_buffer(zed_id, connection_id_a)
         .await
         .unwrap();
 
     // Snapshotting from leaving the channel buffer should not have a diff
-    assert!(!db.has_buffer_changed(a_id, zed_id).await.unwrap());
+    assert!(!db.test_has_note_changed(a_id, zed_id).await.unwrap());
 
     let _ = db
         .join_channel_buffer(zed_id, b_id, connection_id_b)
@@ -261,13 +262,13 @@ async fn test_channel_buffers_diffs(db: &Database) {
         .unwrap();
 
     // B has opened the channel buffer, so we shouldn't have any diff
-    assert!(!db.has_buffer_changed(b_id, zed_id).await.unwrap());
+    assert!(!db.test_has_note_changed(b_id, zed_id).await.unwrap());
 
     db.leave_channel_buffer(zed_id, connection_id_b)
         .await
         .unwrap();
 
     // Since B just opened and closed the buffer without editing, neither should have a diff
-    assert!(!db.has_buffer_changed(a_id, zed_id).await.unwrap());
-    assert!(!db.has_buffer_changed(b_id, zed_id).await.unwrap());
+    assert!(!db.test_has_note_changed(a_id, zed_id).await.unwrap());
+    assert!(!db.test_has_note_changed(b_id, zed_id).await.unwrap());
 }

crates/collab/src/rpc.rs 🔗

@@ -2691,7 +2691,7 @@ async fn update_channel_buffer(
     let db = session.db().await;
     let channel_id = ChannelId::from_proto(request.channel_id);
 
-    let collaborators = db
+    let (collaborators, non_collaborators) = db
         .update_channel_buffer(channel_id, session.user_id, &request.operations)
         .await?;
 
@@ -2704,6 +2704,25 @@ async fn update_channel_buffer(
         },
         &session.peer,
     );
+
+    let pool = &*session.connection_pool().await;
+
+    broadcast(
+        None,
+        non_collaborators
+            .iter()
+            .flat_map(|user_id| pool.user_connection_ids(*user_id)),
+        |peer_id| {
+            session.peer.send(
+                peer_id.into(),
+                proto::UpdateChannels {
+                    notes_changed: vec![channel_id.to_proto()],
+                    ..Default::default()
+                },
+            )
+        },
+    );
+
     Ok(())
 }
 
@@ -2986,6 +3005,12 @@ fn build_initial_channels_update(
         });
     }
 
+    update.notes_changed = channels
+        .channels_with_changed_notes
+        .iter()
+        .map(|channel_id| channel_id.to_proto())
+        .collect();
+
     update.insert_edge = channels.channels.edges;
 
     for (channel_id, participants) in channels.channel_participants {

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

@@ -410,10 +410,7 @@ async fn test_channel_buffer_disconnect(
     channel_buffer_a.update(cx_a, |buffer, _| {
         assert_eq!(
             buffer.channel().as_ref(),
-            &Channel {
-                id: channel_id,
-                name: "the-channel".to_string()
-            }
+            &channel(channel_id, "the-channel")
         );
         assert!(!buffer.is_connected());
     });
@@ -438,15 +435,20 @@ async fn test_channel_buffer_disconnect(
     channel_buffer_b.update(cx_b, |buffer, _| {
         assert_eq!(
             buffer.channel().as_ref(),
-            &Channel {
-                id: channel_id,
-                name: "the-channel".to_string()
-            }
+            &channel(channel_id, "the-channel")
         );
         assert!(!buffer.is_connected());
     });
 }
 
+fn channel(id: u64, name: &'static str) -> Channel {
+    Channel {
+        id,
+        name: name.to_string(),
+        has_changed: false,
+    }
+}
+
 #[gpui::test]
 async fn test_rejoin_channel_buffer(
     deterministic: Arc<Deterministic>,
@@ -627,6 +629,7 @@ async fn test_following_to_channel_notes_without_a_shared_project(
     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;
 
     cx_a.update(editor::init);
@@ -757,6 +760,50 @@ async fn test_following_to_channel_notes_without_a_shared_project(
     });
 }
 
+#[gpui::test]
+async fn test_channel_buffer_changes(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &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 channel_id = server
+        .make_channel(
+            "the-channel",
+            None,
+            (&client_a, cx_a),
+            &mut [(&client_b, cx_b)],
+        )
+        .await;
+
+    let channel_buffer_a = client_a
+        .channel_store()
+        .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
+        .await
+        .unwrap();
+
+    channel_buffer_a.update(cx_a, |buffer, cx| {
+        buffer.buffer().update(cx, |buffer, cx| {
+            buffer.edit([(0..0, "1")], None, cx);
+        })
+    });
+    deterministic.run_until_parked();
+
+    let has_buffer_changed = cx_b.read(|cx| {
+        client_b
+            .channel_store()
+            .read(cx)
+            .has_channel_buffer_changed(channel_id)
+            .unwrap()
+    });
+
+    assert!(has_buffer_changed);
+}
+
 #[track_caller]
 fn assert_collaborators(collaborators: &HashMap<PeerId, Collaborator>, ids: &[Option<UserId>]) {
     let mut user_ids = collaborators

crates/collab_ui/src/collab_panel.rs 🔗

@@ -1816,12 +1816,19 @@ impl CollabPanel {
                         .left(),
                 )
                 .with_child(
-                    Label::new(channel.name.clone(), theme.channel_name.text.clone())
-                        .contained()
-                        .with_style(theme.channel_name.container)
-                        .aligned()
-                        .left()
-                        .flex(1., true),
+                    Label::new(
+                        channel.name.clone(),
+                        theme
+                            .channel_name
+                            .in_state(channel.has_changed)
+                            .text
+                            .clone(),
+                    )
+                    .contained()
+                    .with_style(theme.channel_name.container)
+                    .aligned()
+                    .left()
+                    .flex(1., true),
                 )
                 .with_child(
                     MouseEventHandler::new::<ChannelCall, _>(ix, cx, move |_, cx| {

crates/rpc/proto/zed.proto 🔗

@@ -955,6 +955,7 @@ message UpdateChannels {
     repeated uint64 remove_channel_invitations = 6;
     repeated ChannelParticipants channel_participants = 7;
     repeated ChannelPermission channel_permissions = 8;
+    repeated uint64 notes_changed = 9;
 }
 
 message ChannelEdge {

crates/theme/src/theme.rs 🔗

@@ -251,7 +251,7 @@ pub struct CollabPanel {
     pub leave_call: Interactive<ContainedText>,
     pub contact_row: Toggleable<Interactive<ContainerStyle>>,
     pub channel_row: Toggleable<Interactive<ContainerStyle>>,
-    pub channel_name: ContainedText,
+    pub channel_name: Toggleable<ContainedText>,
     pub row_height: f32,
     pub project_row: Toggleable<Interactive<ProjectRow>>,
     pub tree_branch: Toggleable<Interactive<TreeBranch>>,

styles/src/style_tree/collab_panel.ts 🔗

@@ -267,10 +267,18 @@ export default function contacts_panel(): any {
         }),
         channel_row: item_row,
         channel_name: {
-            ...text(layer, "sans", { size: "sm" }),
-            margin: {
-                left: CHANNEL_SPACING,
+            active: {
+                ...text(layer, "sans", { size: "sm", weight: "bold" }),
+                margin: {
+                    left: CHANNEL_SPACING,
+                },
             },
+            inactive: {
+                ...text(layer, "sans", { size: "sm" }),
+                margin: {
+                    left: CHANNEL_SPACING,
+                },
+            }
         },
         list_empty_label_container: {
             margin: {