diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index e78bbe3466..139910e1f6 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -6,6 +6,7 @@ mod channel_message_tests; mod channel_tests; mod following_tests; mod integration_tests; +mod notification_tests; mod random_channel_buffer_tests; mod random_project_collaboration_tests; mod randomized_test_helpers; diff --git a/crates/collab/src/tests/notification_tests.rs b/crates/collab/src/tests/notification_tests.rs new file mode 100644 index 0000000000..da94bd6fad --- /dev/null +++ b/crates/collab/src/tests/notification_tests.rs @@ -0,0 +1,115 @@ +use crate::tests::TestServer; +use gpui::{executor::Deterministic, TestAppContext}; +use rpc::Notification; +use std::sync::Arc; + +#[gpui::test] +async fn test_notifications( + 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; + + // Client A sends a contact request to client B. + client_a + .user_store() + .update(cx_a, |store, cx| store.request_contact(client_b.id(), cx)) + .await + .unwrap(); + + // Client B receives a contact request notification and responds to the + // request, accepting it. + deterministic.run_until_parked(); + client_b.notification_store().update(cx_b, |store, cx| { + assert_eq!(store.notification_count(), 1); + assert_eq!(store.unread_notification_count(), 1); + + let entry = store.notification_at(0).unwrap(); + assert_eq!( + entry.notification, + Notification::ContactRequest { + sender_id: client_a.id() + } + ); + assert!(!entry.is_read); + + store.respond_to_notification(entry.notification.clone(), true, cx); + }); + + // Client B sees the notification is now read, and that they responded. + deterministic.run_until_parked(); + client_b.notification_store().read_with(cx_b, |store, _| { + assert_eq!(store.notification_count(), 1); + assert_eq!(store.unread_notification_count(), 0); + + let entry = store.notification_at(0).unwrap(); + assert!(entry.is_read); + assert_eq!(entry.response, Some(true)); + }); + + // Client A receives a notification that client B accepted their request. + client_a.notification_store().read_with(cx_a, |store, _| { + assert_eq!(store.notification_count(), 1); + assert_eq!(store.unread_notification_count(), 1); + + let entry = store.notification_at(0).unwrap(); + assert_eq!( + entry.notification, + Notification::ContactRequestAccepted { + responder_id: client_b.id() + } + ); + assert!(!entry.is_read); + }); + + // Client A creates a channel and invites client B to be a member. + let channel_id = client_a + .channel_store() + .update(cx_a, |store, cx| { + store.create_channel("the-channel", None, cx) + }) + .await + .unwrap(); + client_a + .channel_store() + .update(cx_a, |store, cx| { + store.invite_member(channel_id, client_b.id(), false, cx) + }) + .await + .unwrap(); + + // Client B receives a channel invitation notification and responds to the + // invitation, accepting it. + deterministic.run_until_parked(); + client_b.notification_store().update(cx_b, |store, cx| { + assert_eq!(store.notification_count(), 2); + assert_eq!(store.unread_notification_count(), 1); + + let entry = store.notification_at(1).unwrap(); + assert_eq!( + entry.notification, + Notification::ChannelInvitation { + channel_id, + channel_name: "the-channel".to_string() + } + ); + assert!(!entry.is_read); + + store.respond_to_notification(entry.notification.clone(), true, cx); + }); + + // Client B sees the notification is now read, and that they responded. + deterministic.run_until_parked(); + client_b.notification_store().read_with(cx_b, |store, _| { + assert_eq!(store.notification_count(), 2); + assert_eq!(store.unread_notification_count(), 0); + + let entry = store.notification_at(1).unwrap(); + assert!(entry.is_read); + assert_eq!(entry.response, Some(true)); + }); +} diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 2dddd5961b..806b57bb59 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -16,6 +16,7 @@ use futures::{channel::oneshot, StreamExt as _}; use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext, WindowHandle}; use language::LanguageRegistry; use node_runtime::FakeNodeRuntime; +use notifications::NotificationStore; use parking_lot::Mutex; use project::{Project, WorktreeId}; use rpc::RECEIVE_TIMEOUT; @@ -46,6 +47,7 @@ pub struct TestClient { pub username: String, pub app_state: Arc, channel_store: ModelHandle, + notification_store: ModelHandle, state: RefCell, } @@ -244,6 +246,7 @@ impl TestServer { app_state, username: name.to_string(), channel_store: cx.read(ChannelStore::global).clone(), + notification_store: cx.read(NotificationStore::global).clone(), state: Default::default(), }; client.wait_for_current_user(cx).await; @@ -449,6 +452,10 @@ impl TestClient { &self.channel_store } + pub fn notification_store(&self) -> &ModelHandle { + &self.notification_store + } + pub fn user_store(&self) -> &ModelHandle { &self.app_state.user_store } diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index 3f1bafb10e..30242d6360 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -386,7 +386,8 @@ impl NotificationPanel { ) { match event { NotificationEvent::NewNotification { entry } => self.add_toast(entry, cx), - NotificationEvent::NotificationRemoved { entry } => self.remove_toast(entry, cx), + NotificationEvent::NotificationRemoved { entry } + | NotificationEvent::NotificationRead { entry } => self.remove_toast(entry, cx), NotificationEvent::NotificationsUpdated { old_range, new_count, @@ -450,25 +451,9 @@ impl NotificationPanel { response: bool, cx: &mut ViewContext, ) { - match notification { - Notification::ContactRequest { - sender_id: actor_id, - } => { - self.user_store - .update(cx, |store, cx| { - store.respond_to_contact_request(actor_id, response, cx) - }) - .detach(); - } - Notification::ChannelInvitation { channel_id, .. } => { - self.channel_store - .update(cx, |store, cx| { - store.respond_to_channel_invite(channel_id, response, cx) - }) - .detach(); - } - _ => {} - } + self.notification_store.update(cx, |store, cx| { + store.respond_to_notification(notification, response, cx); + }); } } diff --git a/crates/notifications/src/notification_store.rs b/crates/notifications/src/notification_store.rs index 43afb8181a..5a1ed2677e 100644 --- a/crates/notifications/src/notification_store.rs +++ b/crates/notifications/src/notification_store.rs @@ -36,6 +36,9 @@ pub enum NotificationEvent { NotificationRemoved { entry: NotificationEntry, }, + NotificationRead { + entry: NotificationEntry, + }, } #[derive(Debug, PartialEq, Eq, Clone)] @@ -272,7 +275,13 @@ impl NotificationStore { if let Some(existing_notification) = cursor.item() { if existing_notification.id == id { - if new_notification.is_none() { + if let Some(new_notification) = &new_notification { + if new_notification.is_read { + cx.emit(NotificationEvent::NotificationRead { + entry: new_notification.clone(), + }); + } + } else { cx.emit(NotificationEvent::NotificationRemoved { entry: existing_notification.clone(), }); @@ -303,6 +312,31 @@ impl NotificationStore { new_count, }); } + + pub fn respond_to_notification( + &mut self, + notification: Notification, + response: bool, + cx: &mut ModelContext, + ) { + match notification { + Notification::ContactRequest { sender_id } => { + self.user_store + .update(cx, |store, cx| { + store.respond_to_contact_request(sender_id, response, cx) + }) + .detach(); + } + Notification::ChannelInvitation { channel_id, .. } => { + self.channel_store + .update(cx, |store, cx| { + store.respond_to_channel_invite(channel_id, response, cx) + }) + .detach(); + } + _ => {} + } + } } impl Entity for NotificationStore {