diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index d02c22d797..8299b7c6e4 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -400,6 +400,12 @@ impl UserStore { &self.incoming_contact_requests } + pub fn has_incoming_contact_request(&self, user_id: u64) -> bool { + self.incoming_contact_requests + .iter() + .any(|user| user.id == user_id) + } + pub fn outgoing_contact_requests(&self) -> &[Arc] { &self.outgoing_contact_requests } diff --git a/crates/collab/src/db/queries/notifications.rs b/crates/collab/src/db/queries/notifications.rs index 718e41de58..6f2511c23e 100644 --- a/crates/collab/src/db/queries/notifications.rs +++ b/crates/collab/src/db/queries/notifications.rs @@ -150,6 +150,28 @@ impl Database { .await } + pub async fn mark_notification_as_read_by_id( + &self, + recipient_id: UserId, + notification_id: NotificationId, + ) -> Result { + self.transaction(|tx| async move { + let row = notification::Entity::update(notification::ActiveModel { + id: ActiveValue::Unchanged(notification_id), + recipient_id: ActiveValue::Unchanged(recipient_id), + is_read: ActiveValue::Set(true), + ..Default::default() + }) + .exec(&*tx) + .await?; + Ok(model_to_proto(self, row) + .map(|notification| (recipient_id, notification)) + .into_iter() + .collect()) + }) + .await + } + async fn mark_notification_as_read_internal( &self, recipient_id: UserId, diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index eb4f9394e3..a6af089a0a 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -4,7 +4,7 @@ use crate::{ auth, db::{ self, BufferId, ChannelId, ChannelVisibility, ChannelsForUser, CreatedChannelMessage, - Database, MessageId, ProjectId, RoomId, ServerId, User, UserId, + Database, MessageId, NotificationId, ProjectId, RoomId, ServerId, User, UserId, }, executor::Executor, AppState, Result, @@ -273,6 +273,7 @@ impl Server { .add_request_handler(get_channel_messages) .add_request_handler(get_channel_messages_by_id) .add_request_handler(get_notifications) + .add_request_handler(mark_notification_as_read) .add_request_handler(link_channel) .add_request_handler(unlink_channel) .add_request_handler(move_channel) @@ -3187,6 +3188,27 @@ async fn get_notifications( Ok(()) } +async fn mark_notification_as_read( + request: proto::MarkNotificationRead, + response: Response, + session: Session, +) -> Result<()> { + let database = &session.db().await; + let notifications = database + .mark_notification_as_read_by_id( + session.user_id, + NotificationId::from_proto(request.notification_id), + ) + .await?; + send_notifications( + &*session.connection_pool().await, + &session.peer, + notifications, + ); + response.send(proto::Ack {})?; + Ok(()) +} + async fn update_diff_base(request: proto::UpdateDiffBase, session: Session) -> Result<()> { let project_id = ProjectId::from_proto(request.project_id); let project_connection_ids = session diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index 35d216cf58..fb0b393a42 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -5,6 +5,7 @@ use crate::{ use anyhow::Result; use channel::ChannelStore; use client::{Client, Notification, User, UserStore}; +use collections::HashMap; use db::kvp::KEY_VALUE_STORE; use futures::StreamExt; use gpui::{ @@ -16,6 +17,7 @@ use gpui::{ }; use notifications::{NotificationEntry, NotificationEvent, NotificationStore}; use project::Fs; +use rpc::proto; use serde::{Deserialize, Serialize}; use settings::SettingsStore; use std::{sync::Arc, time::Duration}; @@ -27,6 +29,7 @@ use workspace::{ Workspace, }; +const MARK_AS_READ_DELAY: Duration = Duration::from_secs(1); const TOAST_DURATION: Duration = Duration::from_secs(5); const NOTIFICATION_PANEL_KEY: &'static str = "NotificationPanel"; @@ -45,6 +48,7 @@ pub struct NotificationPanel { current_notification_toast: Option<(u64, Task<()>)>, local_timezone: UtcOffset, has_focus: bool, + mark_as_read_tasks: HashMap>>, } #[derive(Serialize, Deserialize)] @@ -114,6 +118,7 @@ impl NotificationPanel { current_notification_toast: None, subscriptions: Vec::new(), active: false, + mark_as_read_tasks: HashMap::default(), width: None, }; @@ -186,6 +191,7 @@ impl NotificationPanel { cx: &mut ViewContext, ) -> Option> { let entry = self.notification_store.read(cx).notification_at(ix)?; + let notification_id = entry.id; let now = OffsetDateTime::now_utc(); let timestamp = entry.timestamp; let NotificationPresenter { @@ -207,6 +213,10 @@ impl NotificationPanel { style.unread_text.clone() }; + if self.active && !entry.is_read { + self.did_render_notification(notification_id, ¬ification, cx); + } + enum Decline {} enum Accept {} @@ -322,7 +332,7 @@ impl NotificationPanel { Some(NotificationPresenter { icon: "icons/plus.svg", text: format!("{} wants to add you as a contact", requester.github_login), - needs_response: user_store.is_contact_request_pending(&requester), + needs_response: user_store.has_incoming_contact_request(requester.id), actor: Some(requester), can_navigate: false, }) @@ -379,6 +389,38 @@ impl NotificationPanel { } } + fn did_render_notification( + &mut self, + notification_id: u64, + notification: &Notification, + cx: &mut ViewContext, + ) { + let should_mark_as_read = match notification { + Notification::ContactRequestAccepted { .. } => true, + Notification::ContactRequest { .. } + | Notification::ChannelInvitation { .. } + | Notification::ChannelMessageMention { .. } => false, + }; + + if should_mark_as_read { + self.mark_as_read_tasks + .entry(notification_id) + .or_insert_with(|| { + let client = self.client.clone(); + cx.spawn(|this, mut cx| async move { + cx.background().timer(MARK_AS_READ_DELAY).await; + client + .request(proto::MarkNotificationRead { notification_id }) + .await?; + this.update(&mut cx, |this, _| { + this.mark_as_read_tasks.remove(¬ification_id); + })?; + Ok(()) + }) + }); + } + } + fn did_click_notification(&mut self, notification: &Notification, cx: &mut ViewContext) { if let Notification::ChannelMessageMention { message_id, diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 854c3f0f9a..efdbf0e56c 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -180,7 +180,7 @@ message Envelope { GetNotifications get_notifications = 150; GetNotificationsResponse get_notifications_response = 151; DeleteNotification delete_notification = 152; - MarkNotificationsRead mark_notifications_read = 153; // Current max + MarkNotificationRead mark_notification_read = 153; // Current max } } @@ -1622,8 +1622,8 @@ message DeleteNotification { uint64 notification_id = 1; } -message MarkNotificationsRead { - repeated uint64 notification_ids = 1; +message MarkNotificationRead { + uint64 notification_id = 1; } message Notification { diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 571fbec865..c501c85107 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -211,7 +211,7 @@ messages!( (LeaveProject, Foreground), (LeaveRoom, Foreground), (LinkChannel, Foreground), - (MarkNotificationsRead, Foreground), + (MarkNotificationRead, Foreground), (MoveChannel, Foreground), (OnTypeFormatting, Background), (OnTypeFormattingResponse, Background), @@ -328,7 +328,7 @@ request_messages!( (LeaveChannelBuffer, Ack), (LeaveRoom, Ack), (LinkChannel, Ack), - (MarkNotificationsRead, Ack), + (MarkNotificationRead, Ack), (MoveChannel, Ack), (OnTypeFormatting, OnTypeFormattingResponse), (OpenBufferById, OpenBufferResponse),