Mark contact acceptance notifications as read automatically

Max Brunsfeld created

Change summary

crates/client/src/user.rs                     |  6 ++
crates/collab/src/db/queries/notifications.rs | 22 ++++++++++
crates/collab/src/rpc.rs                      | 24 ++++++++++
crates/collab_ui/src/notification_panel.rs    | 44 ++++++++++++++++++++
crates/rpc/proto/zed.proto                    |  6 +-
crates/rpc/src/proto.rs                       |  4 
6 files changed, 99 insertions(+), 7 deletions(-)

Detailed changes

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<User>] {
         &self.outgoing_contact_requests
     }

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<NotificationBatch> {
+        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,

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<proto::MarkNotificationRead>,
+    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

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<u64, Task<Result<()>>>,
 }
 
 #[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<Self>,
     ) -> Option<AnyElement<Self>> {
         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, &notification, 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<Self>,
+    ) {
+        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(&notification_id);
+                        })?;
+                        Ok(())
+                    })
+                });
+        }
+    }
+
     fn did_click_notification(&mut self, notification: &Notification, cx: &mut ViewContext<Self>) {
         if let Notification::ChannelMessageMention {
             message_id,

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 {

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