diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index a8f6dd67b6..19fc4f35ee 100644 --- a/crates/channel/src/channel_store.rs +++ b/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 { + 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); } diff --git a/crates/channel/src/channel_store/channel_index.rs b/crates/channel/src/channel_store/channel_index.rs index d0c49dc298..88c6b85698 100644 --- a/crates/channel/src/channel_store/channel_index.rs +++ b/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); diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 982d2b290e..7553392f39 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/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"); diff --git a/crates/collab/migrations/20230925210437_add_observed_notes.sql b/crates/collab/migrations/20230925210437_add_observed_notes.sql index ce3547b281..4574fe215b 100644 --- a/crates/collab/migrations/20230925210437_add_observed_notes.sql +++ b/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"); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index ab2fbe3945..f0896f8732 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -436,6 +436,7 @@ pub struct Channel { pub struct ChannelsForUser { pub channels: ChannelGraph, pub channel_participants: HashMap>, + pub channels_with_changed_notes: HashSet, pub channels_with_admin_privileges: HashSet, } diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index 51848f1e61..f2993c516d 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/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> { + ) -> Result<(Vec, Vec)> { 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::>(); + 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 { - 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 { + 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 { + 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) } } diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 16a0891d16..6274550c25 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/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, tx: &DatabaseTransaction, ) -> Result { @@ -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, }) } diff --git a/crates/collab/src/db/tables.rs b/crates/collab/src/db/tables.rs index e8deb1501e..4068606546 100644 --- a/crates/collab/src/db/tables.rs +++ b/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; diff --git a/crates/collab/src/db/tables/observed_note_edits.rs b/crates/collab/src/db/tables/observed_buffer_edits.rs similarity index 66% rename from crates/collab/src/db/tables/observed_note_edits.rs rename to crates/collab/src/db/tables/observed_buffer_edits.rs index 01e0e212ef..db027f78b2 100644 --- a/crates/collab/src/db/tables/observed_note_edits.rs +++ b/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 for Entity { +impl Related for Entity { fn to() -> RelationDef { - Relation::Channel.def() + Relation::Buffer.def() } } diff --git a/crates/collab/src/db/tests/buffer_tests.rs b/crates/collab/src/db/tests/buffer_tests.rs index 76edc13136..115a20ffa6 100644 --- a/crates/collab/src/db/tests/buffer_tests.rs +++ b/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()); } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 9e14c48473..b9dae999cd 100644 --- a/crates/collab/src/rpc.rs +++ b/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 { diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index 05abda5af3..0f87aeb48f 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/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, @@ -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, + 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, ids: &[Option]) { let mut user_ids = collaborators diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 16a9ec563b..4f81d07ea7 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/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::(ix, cx, move |_, cx| { diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index a62a9f06c3..c53db447d3 100644 --- a/crates/rpc/proto/zed.proto +++ b/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 { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 1ca2d839c0..6c32bfd129 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -251,7 +251,7 @@ pub struct CollabPanel { pub leave_call: Interactive, pub contact_row: Toggleable>, pub channel_row: Toggleable>, - pub channel_name: ContainedText, + pub channel_name: Toggleable, pub row_height: f32, pub project_row: Toggleable>, pub tree_branch: Toggleable>, diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index 4d605d118c..33b36e36f4 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/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: {