diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index d8dc7896ea..ae8a797d06 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -213,6 +213,12 @@ impl ChannelStore { self.channel_index.by_id().values().nth(ix) } + pub fn has_channel_invitation(&self, channel_id: ChannelId) -> bool { + self.channel_invitations + .iter() + .any(|channel| channel.id == channel_id) + } + pub fn channel_invitations(&self) -> &[Arc] { &self.channel_invitations } diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 745bd6e3ab..d2499ab3ce 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -187,6 +187,7 @@ impl Database { rpc::Notification::ChannelInvitation { channel_id: channel_id.to_proto(), channel_name: channel.name, + inviter_id: inviter_id.to_proto(), }, true, &*tx, @@ -276,6 +277,7 @@ impl Database { &rpc::Notification::ChannelInvitation { channel_id: channel_id.to_proto(), channel_name: Default::default(), + inviter_id: Default::default(), }, accept, &*tx, @@ -292,7 +294,7 @@ impl Database { channel_id: ChannelId, member_id: UserId, remover_id: UserId, - ) -> Result<()> { + ) -> Result> { self.transaction(|tx| async move { self.check_user_is_channel_admin(channel_id, remover_id, &*tx) .await?; @@ -310,7 +312,17 @@ impl Database { Err(anyhow!("no such member"))?; } - Ok(()) + Ok(self + .remove_notification( + member_id, + rpc::Notification::ChannelInvitation { + channel_id: channel_id.to_proto(), + channel_name: Default::default(), + inviter_id: Default::default(), + }, + &*tx, + ) + .await?) }) .await } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 9f3c22ce97..053058e06e 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2331,7 +2331,8 @@ async fn remove_channel_member( let channel_id = ChannelId::from_proto(request.channel_id); let member_id = UserId::from_proto(request.user_id); - db.remove_channel_member(channel_id, member_id, session.user_id) + let removed_notification_id = db + .remove_channel_member(channel_id, member_id, session.user_id) .await?; let mut update = proto::UpdateChannels::default(); @@ -2342,7 +2343,18 @@ async fn remove_channel_member( .await .user_connection_ids(member_id) { - session.peer.send(connection_id, update.clone())?; + session.peer.send(connection_id, update.clone()).trace_err(); + if let Some(notification_id) = removed_notification_id { + session + .peer + .send( + connection_id, + proto::DeleteNotification { + notification_id: notification_id.to_proto(), + }, + ) + .trace_err(); + } } response.send(proto::Ack {})?; diff --git a/crates/collab/src/tests/notification_tests.rs b/crates/collab/src/tests/notification_tests.rs index da94bd6fad..518208c0c7 100644 --- a/crates/collab/src/tests/notification_tests.rs +++ b/crates/collab/src/tests/notification_tests.rs @@ -1,5 +1,7 @@ use crate::tests::TestServer; use gpui::{executor::Deterministic, TestAppContext}; +use notifications::NotificationEvent; +use parking_lot::Mutex; use rpc::Notification; use std::sync::Arc; @@ -14,6 +16,23 @@ async fn test_notifications( let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; + let notification_events_a = Arc::new(Mutex::new(Vec::new())); + let notification_events_b = Arc::new(Mutex::new(Vec::new())); + client_a.notification_store().update(cx_a, |_, cx| { + let events = notification_events_a.clone(); + cx.subscribe(&cx.handle(), move |_, _, event, _| { + events.lock().push(event.clone()); + }) + .detach() + }); + client_b.notification_store().update(cx_b, |_, cx| { + let events = notification_events_b.clone(); + cx.subscribe(&cx.handle(), move |_, _, event, _| { + events.lock().push(event.clone()); + }) + .detach() + }); + // Client A sends a contact request to client B. client_a .user_store() @@ -36,6 +55,18 @@ async fn test_notifications( } ); assert!(!entry.is_read); + assert_eq!( + ¬ification_events_b.lock()[0..], + &[ + NotificationEvent::NewNotification { + entry: entry.clone(), + }, + NotificationEvent::NotificationsUpdated { + old_range: 0..0, + new_count: 1 + } + ] + ); store.respond_to_notification(entry.notification.clone(), true, cx); }); @@ -49,6 +80,18 @@ async fn test_notifications( let entry = store.notification_at(0).unwrap(); assert!(entry.is_read); assert_eq!(entry.response, Some(true)); + assert_eq!( + ¬ification_events_b.lock()[2..], + &[ + NotificationEvent::NotificationRead { + entry: entry.clone(), + }, + NotificationEvent::NotificationsUpdated { + old_range: 0..1, + new_count: 1 + } + ] + ); }); // Client A receives a notification that client B accepted their request. @@ -89,12 +132,13 @@ async fn test_notifications( assert_eq!(store.notification_count(), 2); assert_eq!(store.unread_notification_count(), 1); - let entry = store.notification_at(1).unwrap(); + let entry = store.notification_at(0).unwrap(); assert_eq!( entry.notification, Notification::ChannelInvitation { channel_id, - channel_name: "the-channel".to_string() + channel_name: "the-channel".to_string(), + inviter_id: client_a.id() } ); assert!(!entry.is_read); @@ -108,7 +152,7 @@ async fn test_notifications( assert_eq!(store.notification_count(), 2); assert_eq!(store.unread_notification_count(), 0); - let entry = store.notification_at(1).unwrap(); + let entry = store.notification_at(0).unwrap(); assert!(entry.is_read); assert_eq!(entry.response, Some(true)); }); diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index 30242d6360..93ba05a671 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -1,11 +1,9 @@ use crate::{ - format_timestamp, is_channels_feature_enabled, - notifications::contact_notification::ContactNotification, render_avatar, - NotificationPanelSettings, + format_timestamp, is_channels_feature_enabled, render_avatar, NotificationPanelSettings, }; use anyhow::Result; use channel::ChannelStore; -use client::{Client, Notification, UserStore}; +use client::{Client, Notification, User, UserStore}; use db::kvp::KEY_VALUE_STORE; use futures::StreamExt; use gpui::{ @@ -19,7 +17,7 @@ use notifications::{NotificationEntry, NotificationEvent, NotificationStore}; use project::Fs; use serde::{Deserialize, Serialize}; use settings::SettingsStore; -use std::sync::Arc; +use std::{sync::Arc, time::Duration}; use theme::{IconButton, Theme}; use time::{OffsetDateTime, UtcOffset}; use util::{ResultExt, TryFutureExt}; @@ -28,6 +26,7 @@ use workspace::{ Workspace, }; +const TOAST_DURATION: Duration = Duration::from_secs(5); const NOTIFICATION_PANEL_KEY: &'static str = "NotificationPanel"; pub struct NotificationPanel { @@ -42,6 +41,7 @@ pub struct NotificationPanel { pending_serialization: Task>, subscriptions: Vec, workspace: WeakViewHandle, + current_notification_toast: Option<(u64, Task<()>)>, local_timezone: UtcOffset, has_focus: bool, } @@ -58,7 +58,7 @@ pub enum Event { Dismissed, } -actions!(chat_panel, [ToggleFocus]); +actions!(notification_panel, [ToggleFocus]); pub fn init(_cx: &mut AppContext) {} @@ -69,14 +69,8 @@ impl NotificationPanel { let user_store = workspace.app_state().user_store.clone(); let workspace_handle = workspace.weak_handle(); - let notification_list = - ListState::::new(0, Orientation::Top, 1000., move |this, ix, cx| { - this.render_notification(ix, cx) - }); - cx.add_view(|cx| { let mut status = client.status(); - cx.spawn(|this, mut cx| async move { while let Some(_) = status.next().await { if this @@ -91,6 +85,12 @@ impl NotificationPanel { }) .detach(); + let notification_list = + ListState::::new(0, Orientation::Top, 1000., move |this, ix, cx| { + this.render_notification(ix, cx) + .unwrap_or_else(|| Empty::new().into_any()) + }); + let mut this = Self { fs, client, @@ -102,6 +102,7 @@ impl NotificationPanel { pending_serialization: Task::ready(None), workspace: workspace_handle, has_focus: false, + current_notification_toast: None, subscriptions: Vec::new(), active: false, width: None, @@ -169,73 +170,20 @@ impl NotificationPanel { ); } - fn render_notification(&mut self, ix: usize, cx: &mut ViewContext) -> AnyElement { - self.try_render_notification(ix, cx) - .unwrap_or_else(|| Empty::new().into_any()) - } - - fn try_render_notification( + fn render_notification( &mut self, ix: usize, cx: &mut ViewContext, ) -> Option> { - let notification_store = self.notification_store.read(cx); - let user_store = self.user_store.read(cx); - let channel_store = self.channel_store.read(cx); - let entry = notification_store.notification_at(ix)?; - let notification = entry.notification.clone(); + let entry = self.notification_store.read(cx).notification_at(ix)?; let now = OffsetDateTime::now_utc(); let timestamp = entry.timestamp; - - let icon; - let text; - let actor; - let needs_acceptance; - match notification { - Notification::ContactRequest { sender_id } => { - let requester = user_store.get_cached_user(sender_id)?; - icon = "icons/plus.svg"; - text = format!("{} wants to add you as a contact", requester.github_login); - needs_acceptance = true; - actor = Some(requester); - } - Notification::ContactRequestAccepted { responder_id } => { - let responder = user_store.get_cached_user(responder_id)?; - icon = "icons/plus.svg"; - text = format!("{} accepted your contact invite", responder.github_login); - needs_acceptance = false; - actor = Some(responder); - } - Notification::ChannelInvitation { - ref channel_name, .. - } => { - actor = None; - icon = "icons/hash.svg"; - text = format!("you were invited to join the #{channel_name} channel"); - needs_acceptance = true; - } - Notification::ChannelMessageMention { - sender_id, - channel_id, - message_id, - } => { - let sender = user_store.get_cached_user(sender_id)?; - let channel = channel_store.channel_for_id(channel_id)?; - let message = notification_store.channel_message_for_id(message_id)?; - - icon = "icons/conversations.svg"; - text = format!( - "{} mentioned you in the #{} channel:\n{}", - sender.github_login, channel.name, message.body, - ); - needs_acceptance = false; - actor = Some(sender); - } - } + let (actor, text, icon, needs_response) = self.present_notification(entry, cx)?; let theme = theme::current(cx); let style = &theme.notification_panel; let response = entry.response; + let notification = entry.notification.clone(); let message_style = if entry.is_read { style.read_text.clone() @@ -276,7 +224,7 @@ impl NotificationPanel { ) .into_any(), ) - } else if needs_acceptance { + } else if needs_response { Some( Flex::row() .with_children([ @@ -336,6 +284,69 @@ impl NotificationPanel { ) } + fn present_notification( + &self, + entry: &NotificationEntry, + cx: &AppContext, + ) -> Option<(Option>, String, &'static str, bool)> { + let user_store = self.user_store.read(cx); + let channel_store = self.channel_store.read(cx); + let icon; + let text; + let actor; + let needs_response; + match entry.notification { + Notification::ContactRequest { sender_id } => { + let requester = user_store.get_cached_user(sender_id)?; + 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); + actor = Some(requester); + } + Notification::ContactRequestAccepted { responder_id } => { + let responder = user_store.get_cached_user(responder_id)?; + icon = "icons/plus.svg"; + text = format!("{} accepted your contact invite", responder.github_login); + needs_response = false; + actor = Some(responder); + } + Notification::ChannelInvitation { + ref channel_name, + channel_id, + inviter_id, + } => { + let inviter = user_store.get_cached_user(inviter_id)?; + icon = "icons/hash.svg"; + text = format!( + "{} invited you to join the #{channel_name} channel", + inviter.github_login + ); + needs_response = channel_store.has_channel_invitation(channel_id); + actor = Some(inviter); + } + Notification::ChannelMessageMention { + sender_id, + channel_id, + message_id, + } => { + let sender = user_store.get_cached_user(sender_id)?; + let channel = channel_store.channel_for_id(channel_id)?; + let message = self + .notification_store + .read(cx) + .channel_message_for_id(message_id)?; + icon = "icons/conversations.svg"; + text = format!( + "{} mentioned you in the #{} channel:\n{}", + sender.github_login, channel.name, message.body, + ); + needs_response = false; + actor = Some(sender); + } + } + Some((actor, text, icon, needs_response)) + } + fn render_sign_in_prompt( &self, theme: &Arc, @@ -387,7 +398,7 @@ impl NotificationPanel { match event { NotificationEvent::NewNotification { entry } => self.add_toast(entry, cx), NotificationEvent::NotificationRemoved { entry } - | NotificationEvent::NotificationRead { entry } => self.remove_toast(entry, cx), + | NotificationEvent::NotificationRead { entry } => self.remove_toast(entry.id, cx), NotificationEvent::NotificationsUpdated { old_range, new_count, @@ -399,49 +410,44 @@ impl NotificationPanel { } fn add_toast(&mut self, entry: &NotificationEntry, cx: &mut ViewContext) { - let id = entry.id as usize; - match entry.notification { - Notification::ContactRequest { - sender_id: actor_id, - } - | Notification::ContactRequestAccepted { - responder_id: actor_id, - } => { - let user_store = self.user_store.clone(); - let Some(user) = user_store.read(cx).get_cached_user(actor_id) else { - return; - }; - self.workspace - .update(cx, |workspace, cx| { - workspace.show_notification(id, cx, |cx| { - cx.add_view(|_| { - ContactNotification::new( - user, - entry.notification.clone(), - user_store, - ) - }) - }) - }) + let Some((actor, text, _, _)) = self.present_notification(entry, cx) else { + return; + }; + + let id = entry.id; + self.current_notification_toast = Some(( + id, + cx.spawn(|this, mut cx| async move { + cx.background().timer(TOAST_DURATION).await; + this.update(&mut cx, |this, cx| this.remove_toast(id, cx)) .ok(); - } - Notification::ChannelInvitation { .. } => {} - Notification::ChannelMessageMention { .. } => {} - } + }), + )); + + self.workspace + .update(cx, |workspace, cx| { + workspace.show_notification(0, cx, |cx| { + let workspace = cx.weak_handle(); + cx.add_view(|_| NotificationToast { + actor, + text, + workspace, + }) + }) + }) + .ok(); } - fn remove_toast(&mut self, entry: &NotificationEntry, cx: &mut ViewContext) { - let id = entry.id as usize; - match entry.notification { - Notification::ContactRequest { .. } | Notification::ContactRequestAccepted { .. } => { + fn remove_toast(&mut self, notification_id: u64, cx: &mut ViewContext) { + if let Some((current_id, _)) = &self.current_notification_toast { + if *current_id == notification_id { + self.current_notification_toast.take(); self.workspace .update(cx, |workspace, cx| { - workspace.dismiss_notification::(id, cx) + workspace.dismiss_notification::(0, cx) }) .ok(); } - Notification::ChannelInvitation { .. } => {} - Notification::ChannelMessageMention { .. } => {} } } @@ -582,3 +588,111 @@ fn render_icon_button(style: &IconButton, svg_path: &'static str) -> im .contained() .with_style(style.container) } + +pub struct NotificationToast { + actor: Option>, + text: String, + workspace: WeakViewHandle, +} + +pub enum ToastEvent { + Dismiss, +} + +impl NotificationToast { + fn focus_notification_panel(&self, cx: &mut AppContext) { + let workspace = self.workspace.clone(); + cx.defer(move |cx| { + workspace + .update(cx, |workspace, cx| { + workspace.focus_panel::(cx); + }) + .ok(); + }) + } +} + +impl Entity for NotificationToast { + type Event = ToastEvent; +} + +impl View for NotificationToast { + fn ui_name() -> &'static str { + "ContactNotification" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + let user = self.actor.clone(); + let theme = theme::current(cx).clone(); + let theme = &theme.contact_notification; + + MouseEventHandler::new::(0, cx, |_, cx| { + Flex::row() + .with_children(user.and_then(|user| { + Some( + Image::from_data(user.avatar.clone()?) + .with_style(theme.header_avatar) + .aligned() + .constrained() + .with_height( + cx.font_cache() + .line_height(theme.header_message.text.font_size), + ) + .aligned() + .top(), + ) + })) + .with_child( + Text::new(self.text.clone(), theme.header_message.text.clone()) + .contained() + .with_style(theme.header_message.container) + .aligned() + .top() + .left() + .flex(1., true), + ) + .with_child( + MouseEventHandler::new::(0, cx, |state, _| { + let style = theme.dismiss_button.style_for(state); + Svg::new("icons/x.svg") + .with_color(style.color) + .constrained() + .with_width(style.icon_width) + .aligned() + .contained() + .with_style(style.container) + .constrained() + .with_width(style.button_width) + .with_height(style.button_width) + }) + .with_cursor_style(CursorStyle::PointingHand) + .with_padding(Padding::uniform(5.)) + .on_click(MouseButton::Left, move |_, _, cx| { + cx.emit(ToastEvent::Dismiss) + }) + .aligned() + .constrained() + .with_height( + cx.font_cache() + .line_height(theme.header_message.text.font_size), + ) + .aligned() + .top() + .flex_float(), + ) + .contained() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, this, cx| { + this.focus_notification_panel(cx); + cx.emit(ToastEvent::Dismiss); + }) + .into_any() + } +} + +impl workspace::notifications::Notification for NotificationToast { + fn should_dismiss_notification_on_event(&self, event: &::Event) -> bool { + matches!(event, ToastEvent::Dismiss) + } +} diff --git a/crates/collab_ui/src/notifications.rs b/crates/collab_ui/src/notifications.rs index e4456163c6..5c184ec5c8 100644 --- a/crates/collab_ui/src/notifications.rs +++ b/crates/collab_ui/src/notifications.rs @@ -1,120 +1,11 @@ -use client::User; -use gpui::{ - elements::*, - platform::{CursorStyle, MouseButton}, - AnyElement, AppContext, Element, ViewContext, -}; +use gpui::AppContext; use std::sync::Arc; use workspace::AppState; -pub mod contact_notification; pub mod incoming_call_notification; pub mod project_shared_notification; -enum Dismiss {} -enum Button {} - pub fn init(app_state: &Arc, cx: &mut AppContext) { incoming_call_notification::init(app_state, cx); project_shared_notification::init(app_state, cx); } - -pub fn render_user_notification( - user: Arc, - title: &'static str, - body: Option<&'static str>, - on_dismiss: F, - buttons: Vec<(&'static str, Box)>)>, - cx: &mut ViewContext, -) -> AnyElement -where - F: 'static + Fn(&mut V, &mut ViewContext), -{ - let theme = theme::current(cx).clone(); - let theme = &theme.contact_notification; - - Flex::column() - .with_child( - Flex::row() - .with_children(user.avatar.clone().map(|avatar| { - Image::from_data(avatar) - .with_style(theme.header_avatar) - .aligned() - .constrained() - .with_height( - cx.font_cache() - .line_height(theme.header_message.text.font_size), - ) - .aligned() - .top() - })) - .with_child( - Text::new( - format!("{} {}", user.github_login, title), - theme.header_message.text.clone(), - ) - .contained() - .with_style(theme.header_message.container) - .aligned() - .top() - .left() - .flex(1., true), - ) - .with_child( - MouseEventHandler::new::(user.id as usize, cx, |state, _| { - let style = theme.dismiss_button.style_for(state); - Svg::new("icons/x.svg") - .with_color(style.color) - .constrained() - .with_width(style.icon_width) - .aligned() - .contained() - .with_style(style.container) - .constrained() - .with_width(style.button_width) - .with_height(style.button_width) - }) - .with_cursor_style(CursorStyle::PointingHand) - .with_padding(Padding::uniform(5.)) - .on_click(MouseButton::Left, move |_, view, cx| on_dismiss(view, cx)) - .aligned() - .constrained() - .with_height( - cx.font_cache() - .line_height(theme.header_message.text.font_size), - ) - .aligned() - .top() - .flex_float(), - ) - .into_any_named("contact notification header"), - ) - .with_children(body.map(|body| { - Label::new(body, theme.body_message.text.clone()) - .contained() - .with_style(theme.body_message.container) - })) - .with_children(if buttons.is_empty() { - None - } else { - Some( - Flex::row() - .with_children(buttons.into_iter().enumerate().map( - |(ix, (message, handler))| { - MouseEventHandler::new::(ix, cx, |state, _| { - let button = theme.button.style_for(state); - Label::new(message, button.text.clone()) - .contained() - .with_style(button.container) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, view, cx| handler(view, cx)) - }, - )) - .aligned() - .right(), - ) - }) - .contained() - .into_any() -} diff --git a/crates/collab_ui/src/notifications/contact_notification.rs b/crates/collab_ui/src/notifications/contact_notification.rs deleted file mode 100644 index 2e3c3ca58a..0000000000 --- a/crates/collab_ui/src/notifications/contact_notification.rs +++ /dev/null @@ -1,106 +0,0 @@ -use crate::notifications::render_user_notification; -use client::{User, UserStore}; -use gpui::{elements::*, Entity, ModelHandle, View, ViewContext}; -use std::sync::Arc; -use workspace::notifications::Notification; - -pub struct ContactNotification { - user_store: ModelHandle, - user: Arc, - notification: rpc::Notification, -} - -#[derive(Clone, PartialEq)] -struct Dismiss(u64); - -#[derive(Clone, PartialEq)] -pub struct RespondToContactRequest { - pub user_id: u64, - pub accept: bool, -} - -pub enum Event { - Dismiss, -} - -impl Entity for ContactNotification { - type Event = Event; -} - -impl View for ContactNotification { - fn ui_name() -> &'static str { - "ContactNotification" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - match self.notification { - rpc::Notification::ContactRequest { .. } => render_user_notification( - self.user.clone(), - "wants to add you as a contact", - Some("They won't be alerted if you decline."), - |notification, cx| notification.dismiss(cx), - vec![ - ( - "Decline", - Box::new(|notification, cx| { - notification.respond_to_contact_request(false, cx) - }), - ), - ( - "Accept", - Box::new(|notification, cx| { - notification.respond_to_contact_request(true, cx) - }), - ), - ], - cx, - ), - rpc::Notification::ContactRequestAccepted { .. } => render_user_notification( - self.user.clone(), - "accepted your contact request", - None, - |notification, cx| notification.dismiss(cx), - vec![], - cx, - ), - _ => unreachable!(), - } - } -} - -impl Notification for ContactNotification { - fn should_dismiss_notification_on_event(&self, event: &::Event) -> bool { - matches!(event, Event::Dismiss) - } -} - -impl ContactNotification { - pub fn new( - user: Arc, - notification: rpc::Notification, - user_store: ModelHandle, - ) -> Self { - Self { - user, - notification, - user_store, - } - } - - fn dismiss(&mut self, cx: &mut ViewContext) { - self.user_store.update(cx, |store, cx| { - store - .dismiss_contact_request(self.user.id, cx) - .detach_and_log_err(cx); - }); - cx.emit(Event::Dismiss); - } - - fn respond_to_contact_request(&mut self, accept: bool, cx: &mut ViewContext) { - self.user_store - .update(cx, |store, cx| { - store.respond_to_contact_request(self.user.id, accept, cx) - }) - .detach(); - } -} diff --git a/crates/notifications/src/notification_store.rs b/crates/notifications/src/notification_store.rs index 5a1ed2677e..0ee4ad35f1 100644 --- a/crates/notifications/src/notification_store.rs +++ b/crates/notifications/src/notification_store.rs @@ -25,6 +25,7 @@ pub struct NotificationStore { _subscriptions: Vec, } +#[derive(Clone, PartialEq, Eq, Debug)] pub enum NotificationEvent { NotificationsUpdated { old_range: Range, @@ -118,7 +119,13 @@ impl NotificationStore { self.channel_messages.get(&id) } + // Get the nth newest notification. pub fn notification_at(&self, ix: usize) -> Option<&NotificationEntry> { + let count = self.notifications.summary().count; + if ix >= count { + return None; + } + let ix = count - 1 - ix; let mut cursor = self.notifications.cursor::(); cursor.seek(&Count(ix), Bias::Right, &()); cursor.item() @@ -200,7 +207,9 @@ impl NotificationStore { for entry in ¬ifications { match entry.notification { - Notification::ChannelInvitation { .. } => {} + Notification::ChannelInvitation { inviter_id, .. } => { + user_ids.push(inviter_id); + } Notification::ContactRequest { sender_id: requester_id, } => { @@ -273,8 +282,11 @@ impl NotificationStore { old_range.start = cursor.start().1 .0; } - if let Some(existing_notification) = cursor.item() { - if existing_notification.id == id { + let old_notification = cursor.item(); + if let Some(old_notification) = old_notification { + if old_notification.id == id { + cursor.next(&()); + if let Some(new_notification) = &new_notification { if new_notification.is_read { cx.emit(NotificationEvent::NotificationRead { @@ -283,20 +295,19 @@ impl NotificationStore { } } else { cx.emit(NotificationEvent::NotificationRemoved { - entry: existing_notification.clone(), + entry: old_notification.clone(), }); } - cursor.next(&()); + } + } else if let Some(new_notification) = &new_notification { + if is_new { + cx.emit(NotificationEvent::NewNotification { + entry: new_notification.clone(), + }); } } if let Some(notification) = new_notification { - if is_new { - cx.emit(NotificationEvent::NewNotification { - entry: notification.clone(), - }); - } - new_notifications.push(notification, &()); } } diff --git a/crates/rpc/src/notification.rs b/crates/rpc/src/notification.rs index 06dff82b75..c5476469be 100644 --- a/crates/rpc/src/notification.rs +++ b/crates/rpc/src/notification.rs @@ -30,12 +30,13 @@ pub enum Notification { #[serde(rename = "entity_id")] channel_id: u64, channel_name: String, + inviter_id: u64, }, ChannelMessageMention { - sender_id: u64, - channel_id: u64, #[serde(rename = "entity_id")] message_id: u64, + sender_id: u64, + channel_id: u64, }, } @@ -84,6 +85,7 @@ fn test_notification() { Notification::ChannelInvitation { channel_id: 100, channel_name: "the-channel".into(), + inviter_id: 50, }, Notification::ChannelMessageMention { sender_id: 200,