Channel notifications from the server works

This commit is contained in:
Mikayla 2023-09-30 22:30:36 -07:00
parent 1469c02998
commit 9ba975d6ad
No known key found for this signature in database
16 changed files with 266 additions and 107 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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>,
}

View File

@ -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,11 +483,24 @@ 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(
OnConflict::columns([
@ -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,11 +715,36 @@ 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))
#[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
}
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?
.map(|model| (model.epoch, model.lamport_timestamp));
@ -734,9 +775,7 @@ impl Database {
// 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)),
)
.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)
@ -745,8 +784,6 @@ impl Database {
}
Ok(user_max != channel_max)
})
.await
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1816,7 +1816,14 @@ impl CollabPanel {
.left(),
)
.with_child(
Label::new(channel.name.clone(), theme.channel_name.text.clone())
Label::new(
channel.name.clone(),
theme
.channel_name
.in_state(channel.has_changed)
.text
.clone(),
)
.contained()
.with_style(theme.channel_name.container)
.aligned()

View File

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

View File

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

View File

@ -267,10 +267,18 @@ export default function contacts_panel(): any {
}),
channel_row: item_row,
channel_name: {
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: {