diff --git a/Cargo.lock b/Cargo.lock index 8d33fd1c2af..10d30c179e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3353,6 +3353,7 @@ dependencies = [ "async-stream", "async_cell", "bitflags", + "cfg-if", "chrono", "emojis", "eyeball", diff --git a/bindings/matrix-sdk-ffi/CHANGELOG.md b/bindings/matrix-sdk-ffi/CHANGELOG.md index c231d22428b..fb7a21a72a0 100644 --- a/bindings/matrix-sdk-ffi/CHANGELOG.md +++ b/bindings/matrix-sdk-ffi/CHANGELOG.md @@ -29,6 +29,9 @@ All notable changes to this project will be documented in this file. This is primarily for Element X to give a dedicated error message in case it connects a homeserver with only this method available. ([#5222](https://github.com/matrix-org/matrix-rust-sdk/pull/5222)) +- Add `Action::NotifyInApp` and `RoomNotificationMode::PushMentionsAndKeywordsOnly` behind + a new feature `unstable-msc3768`. + ([#5441](https://github.com/matrix-org/matrix-rust-sdk/pull/5441)) ### Breaking changes: diff --git a/bindings/matrix-sdk-ffi/Cargo.toml b/bindings/matrix-sdk-ffi/Cargo.toml index 36e9a75ec41..7b99979ce1f 100644 --- a/bindings/matrix-sdk-ffi/Cargo.toml +++ b/bindings/matrix-sdk-ffi/Cargo.toml @@ -26,6 +26,7 @@ crate-type = [ [features] default = ["bundled-sqlite", "unstable-msc4274"] bundled-sqlite = ["matrix-sdk/bundled-sqlite"] +unstable-msc3768 = ["matrix-sdk-ui/unstable-msc3768"] unstable-msc4274 = ["matrix-sdk-ui/unstable-msc4274"] # Required when targeting a Javascript environment, like Wasm in a browser. js = ["matrix-sdk-ui/js"] diff --git a/bindings/matrix-sdk-ffi/src/notification_settings.rs b/bindings/matrix-sdk-ffi/src/notification_settings.rs index 9cce328bfae..cb1e053948a 100644 --- a/bindings/matrix-sdk-ffi/src/notification_settings.rs +++ b/bindings/matrix-sdk-ffi/src/notification_settings.rs @@ -322,8 +322,13 @@ impl TryFrom for SdkTweak { #[derive(Clone, uniffi::Enum)] /// Enum representing the push notification actions for a rule. pub enum Action { - /// Causes matching events to generate a notification. + /// Causes matching events to generate a notification (both in-app and + /// remote / push). Notify, + /// Causes matching events to generate an in-app notification but no remote + /// (push) notification. + #[cfg(feature = "unstable-msc3768")] + NotifyInApp, /// Sets an entry in the 'tweaks' dictionary sent to the push gateway. SetTweak { value: Tweak }, } @@ -334,6 +339,8 @@ impl TryFrom for Action { fn try_from(value: SdkAction) -> Result { Ok(match value { SdkAction::Notify => Self::Notify, + #[cfg(feature = "unstable-msc3768")] + SdkAction::NotifyInApp => Self::NotifyInApp, SdkAction::SetTweak(tweak) => Self::SetTweak { value: tweak.try_into().map_err(|e| format!("Failed to convert tweak: {e}"))?, }, @@ -348,6 +355,8 @@ impl TryFrom for SdkAction { fn try_from(value: Action) -> Result { Ok(match value { Action::Notify => Self::Notify, + #[cfg(feature = "unstable-msc3768")] + Action::NotifyInApp => Self::NotifyInApp, Action::SetTweak { value } => Self::SetTweak( value.try_into().map_err(|e| format!("Failed to convert tweak: {e}"))?, ), @@ -358,10 +367,14 @@ impl TryFrom for SdkAction { /// Enum representing the push notification modes for a room. #[derive(Clone, uniffi::Enum)] pub enum RoomNotificationMode { - /// Receive notifications for all messages. + /// Receive remote and in-app notifications for all messages. AllMessages, - /// Receive notifications for mentions and keywords only. + /// Receive remote and in-app notifications for mentions and keywords only. MentionsAndKeywordsOnly, + /// Receive remote and in-app notifications for mentions and keywords and + /// in-app notifications only for other room messages. + #[cfg(feature = "unstable-msc3768")] + PushMentionsAndKeywordsOnly, /// Do not receive any notifications. Mute, } @@ -371,6 +384,10 @@ impl From for RoomNotificationMode { match value { SdkRoomNotificationMode::AllMessages => Self::AllMessages, SdkRoomNotificationMode::MentionsAndKeywordsOnly => Self::MentionsAndKeywordsOnly, + #[cfg(feature = "unstable-msc3768")] + SdkRoomNotificationMode::PushMentionsAndKeywordsOnly => { + Self::PushMentionsAndKeywordsOnly + } SdkRoomNotificationMode::Mute => Self::Mute, } } @@ -381,6 +398,8 @@ impl From for SdkRoomNotificationMode { match value { RoomNotificationMode::AllMessages => Self::AllMessages, RoomNotificationMode::MentionsAndKeywordsOnly => Self::MentionsAndKeywordsOnly, + #[cfg(feature = "unstable-msc3768")] + RoomNotificationMode::PushMentionsAndKeywordsOnly => Self::PushMentionsAndKeywordsOnly, RoomNotificationMode::Mute => Self::Mute, } } diff --git a/crates/matrix-sdk-base/CHANGELOG.md b/crates/matrix-sdk-base/CHANGELOG.md index 0441ecef6ab..97b3474368d 100644 --- a/crates/matrix-sdk-base/CHANGELOG.md +++ b/crates/matrix-sdk-base/CHANGELOG.md @@ -18,6 +18,9 @@ All notable changes to this project will be documented in this file. `RoomInfo::invite_details` method returns both the timestamp and the inviter. ([#5390](https://github.com/matrix-org/matrix-rust-sdk/pull/5390)) +- Add `RoomNotificationMode::PushMentionsAndKeywordsOnly` behind a new + feature `unstable-msc3768`. + ([#5441](https://github.com/matrix-org/matrix-rust-sdk/pull/5441 ### Refactor - [**breaking**] `RelationalLinkedChunk::items` now takes a `RoomId` instead of an diff --git a/crates/matrix-sdk-base/Cargo.toml b/crates/matrix-sdk-base/Cargo.toml index a74665b096e..31fa5a739f1 100644 --- a/crates/matrix-sdk-base/Cargo.toml +++ b/crates/matrix-sdk-base/Cargo.toml @@ -28,6 +28,7 @@ qrcode = ["matrix-sdk-crypto?/qrcode"] automatic-room-key-forwarding = ["matrix-sdk-crypto?/automatic-room-key-forwarding"] experimental-send-custom-to-device = ["matrix-sdk-crypto?/experimental-send-custom-to-device"] uniffi = ["dep:uniffi", "matrix-sdk-crypto?/uniffi", "matrix-sdk-common/uniffi"] +unstable-msc3768 = ["ruma/unstable-msc3768"] # Private feature, see # https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823 for the gory diff --git a/crates/matrix-sdk-base/src/notification_settings.rs b/crates/matrix-sdk-base/src/notification_settings.rs index 8a733d1e607..f8144eb1233 100644 --- a/crates/matrix-sdk-base/src/notification_settings.rs +++ b/crates/matrix-sdk-base/src/notification_settings.rs @@ -19,10 +19,14 @@ use serde::{Deserialize, Serialize}; /// Enum representing the push notification modes for a room. #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] pub enum RoomNotificationMode { - /// Receive notifications for all messages. + /// Receive remote and in-app notifications for all messages. AllMessages, - /// Receive notifications for mentions and keywords only. + /// Receive remote and in-app notifications for mentions and keywords only. MentionsAndKeywordsOnly, + /// Receive remote and in-app notifications for mentions and keywords and + /// in-app notifications only for other room messages. + #[cfg(feature = "unstable-msc3768")] + PushMentionsAndKeywordsOnly, /// Do not receive any notifications. Mute, } diff --git a/crates/matrix-sdk-base/src/read_receipts.rs b/crates/matrix-sdk-base/src/read_receipts.rs index 253bfdafdcf..3264078f1f2 100644 --- a/crates/matrix-sdk-base/src/read_receipts.rs +++ b/crates/matrix-sdk-base/src/read_receipts.rs @@ -787,6 +787,17 @@ mod tests { assert_eq!(receipts.num_mentions, 1); assert_eq!(receipts.num_notifications, 1); + // NotifyInApp is treated like Notify + #[cfg(feature = "unstable-msc3768")] + { + let event = make_event(user_id!("@bob:example.org"), vec![Action::NotifyInApp]); + let mut receipts = RoomReadReceipts::default(); + receipts.process_event(&event, user_id, ThreadingSupport::Disabled); + assert_eq!(receipts.num_unread, 1); + assert_eq!(receipts.num_mentions, 0); + assert_eq!(receipts.num_notifications, 1); + } + // Technically this `push_actions` set would be a bug somewhere else, but let's // make sure to resist against it. let event = make_event(user_id!("@bob:example.org"), vec![Action::Notify, Action::Notify]); diff --git a/crates/matrix-sdk-ui/Cargo.toml b/crates/matrix-sdk-ui/Cargo.toml index ddacc9e1927..6310f6759f7 100644 --- a/crates/matrix-sdk-ui/Cargo.toml +++ b/crates/matrix-sdk-ui/Cargo.toml @@ -19,6 +19,9 @@ rustls-tls = ["matrix-sdk/rustls-tls"] js = ["matrix-sdk/js"] uniffi = ["dep:uniffi", "matrix-sdk/uniffi", "matrix-sdk-base/uniffi"] +# Add support for in-app only notifications +unstable-msc3768 = ["matrix-sdk/unstable-msc3768"] + # Add support for encrypted extensible events. unstable-msc3956 = ["ruma/unstable-msc3956"] @@ -58,6 +61,7 @@ tracing = { workspace = true, features = ["attributes"] } unicode-normalization.workspace = true uniffi = { workspace = true, optional = true } +cfg-if = "1.0.0" emojis = "0.6.4" unicode-segmentation = "1.12.0" diff --git a/crates/matrix-sdk-ui/src/notification_client.rs b/crates/matrix-sdk-ui/src/notification_client.rs index de960b87167..4472fcbd98b 100644 --- a/crates/matrix-sdk-ui/src/notification_client.rs +++ b/crates/matrix-sdk-ui/src/notification_client.rs @@ -18,6 +18,7 @@ use std::{ time::Duration, }; +use cfg_if::cfg_if; use futures_util::{StreamExt as _, pin_mut}; use matrix_sdk::{ Client, ClientBuildError, SlidingSyncList, SlidingSyncMode, room::Room, sleep::sleep, @@ -677,7 +678,7 @@ impl NotificationClient { let should_notify = push_actions .as_ref() - .is_some_and(|actions| actions.iter().any(|a| a.should_notify())); + .is_some_and(|actions| actions.iter().any(should_action_notify_remote)); if !should_notify { // The event has been filtered out by the user's push rules. @@ -749,7 +750,7 @@ impl NotificationClient { } if let Some(actions) = timeline_event.push_actions() - && !actions.iter().any(|a| a.should_notify()) + && !actions.iter().any(should_action_notify_remote) { return Ok(NotificationStatus::EventFilteredOut); } @@ -771,6 +772,17 @@ impl NotificationClient { } } +fn should_action_notify_remote(action: &Action) -> bool { + cfg_if! { + if #[cfg(feature = "unstable-msc3768")] { + action.should_notify_remote() + } else { + // Before MSC3768 only combined remote/local notifications existed + action.should_notify() + } + } +} + fn is_event_encrypted(event_type: TimelineEventType) -> bool { let is_still_encrypted = matches!(event_type, TimelineEventType::RoomEncrypted); diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index b1077fac54a..2dc63cd4e60 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -59,6 +59,9 @@ experimental-widgets = ["dep:uuid", "experimental-send-custom-to-device"] docsrs = ["e2e-encryption", "sqlite", "indexeddb", "sso-login", "qrcode"] +# Add support for in-app only notifications +unstable-msc3768 = ["matrix-sdk-base/unstable-msc3768"] + # Add support for inline media galleries via msgtypes unstable-msc4274 = ["ruma/unstable-msc4274", "matrix-sdk-base/unstable-msc4274"] diff --git a/crates/matrix-sdk/src/notification_settings/command.rs b/crates/matrix-sdk/src/notification_settings/command.rs index 65ce37b71c7..12f9bfffc81 100644 --- a/crates/matrix-sdk/src/notification_settings/command.rs +++ b/crates/matrix-sdk/src/notification_settings/command.rs @@ -14,9 +14,9 @@ use crate::NotificationSettingsError; #[derive(Clone, Debug)] pub(crate) enum Command { /// Set a new `Room` push rule - SetRoomPushRule { room_id: OwnedRoomId, notify: bool }, + SetRoomPushRule { room_id: OwnedRoomId, notify: Notify }, /// Set a new `Override` push rule matching a `RoomId` - SetOverridePushRule { rule_id: String, room_id: OwnedRoomId, notify: bool }, + SetOverridePushRule { rule_id: String, room_id: OwnedRoomId, notify: Notify }, /// Set a new push rule for a keyword. SetKeywordPushRule { keyword: String }, /// Set whether a push rule is enabled @@ -29,21 +29,13 @@ pub(crate) enum Command { SetCustomPushRule { rule: NewPushRule }, } -fn get_notify_actions(notify: bool) -> Vec { - if notify { - vec![Action::Notify, Action::SetTweak(Tweak::Sound("default".into()))] - } else { - vec![] - } -} - impl Command { /// Tries to create a push rule corresponding to this command pub(crate) fn to_push_rule(&self) -> Result { match self { Self::SetRoomPushRule { room_id, notify } => { // `Room` push rule for this `room_id` - let new_rule = NewSimplePushRule::new(room_id.clone(), get_notify_actions(*notify)); + let new_rule = NewSimplePushRule::new(room_id.clone(), notify.get_actions()); Ok(NewPushRule::Room(new_rule)) } @@ -55,7 +47,7 @@ impl Command { key: "room_id".to_owned(), pattern: room_id.to_string(), }], - get_notify_actions(*notify), + notify.get_actions(), ); Ok(NewPushRule::Override(new_rule)) } @@ -65,7 +57,7 @@ impl Command { let new_rule = NewPatternedPushRule::new( keyword.clone(), keyword.clone(), - get_notify_actions(true), + Notify::All.get_actions(), ); Ok(NewPushRule::Content(new_rule)) } @@ -80,3 +72,28 @@ impl Command { } } } + +/// Enum describing if and how to deliver a notification. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum Notify { + /// Generate a notification both in-app and remote / push. + All, + + /// Only generate an in-app notification but no remote / push notification. + #[cfg(feature = "unstable-msc3768")] + InAppOnly, + + /// Don't notify at all. + None, +} + +impl Notify { + fn get_actions(&self) -> Vec { + match self { + Self::All => vec![Action::Notify, Action::SetTweak(Tweak::Sound("default".into()))], + #[cfg(feature = "unstable-msc3768")] + Self::InAppOnly => vec![Action::NotifyInApp], + Self::None => Vec::new(), + } + } +} diff --git a/crates/matrix-sdk/src/notification_settings/mod.rs b/crates/matrix-sdk/src/notification_settings/mod.rs index eebe73e951e..a29c7d6117d 100644 --- a/crates/matrix-sdk/src/notification_settings/mod.rs +++ b/crates/matrix-sdk/src/notification_settings/mod.rs @@ -41,7 +41,7 @@ pub use matrix_sdk_base::notification_settings::RoomNotificationMode; use crate::{ config::RequestConfig, error::NotificationSettingsError, event_handler::EventHandlerDropGuard, - Client, Result, + notification_settings::command::Notify, Client, Result, }; /// Whether or not a room is encrypted @@ -320,15 +320,20 @@ impl NotificationSettings { let (new_rule_kind, notify) = match mode { RoomNotificationMode::AllMessages => { // insert a `Room` rule which notifies - (RuleKind::Room, true) + (RuleKind::Room, Notify::All) } RoomNotificationMode::MentionsAndKeywordsOnly => { // insert a `Room` rule which doesn't notify - (RuleKind::Room, false) + (RuleKind::Room, Notify::None) + } + #[cfg(feature = "unstable-msc3768")] + RoomNotificationMode::PushMentionsAndKeywordsOnly => { + // insert a `Room` rule which notifies in-app only + (RuleKind::Room, Notify::InAppOnly) } RoomNotificationMode::Mute => { // insert an `Override` rule which doesn't notify - (RuleKind::Override, false) + (RuleKind::Override, Notify::None) } }; @@ -923,6 +928,8 @@ mod tests { let new_modes = [ RoomNotificationMode::AllMessages, RoomNotificationMode::MentionsAndKeywordsOnly, + #[cfg(feature = "unstable-msc3768")] + RoomNotificationMode::PushMentionsAndKeywordsOnly, RoomNotificationMode::Mute, ]; for new_mode in new_modes { diff --git a/crates/matrix-sdk/src/notification_settings/rule_commands.rs b/crates/matrix-sdk/src/notification_settings/rule_commands.rs index a545e23b94b..dd43ce3b43e 100644 --- a/crates/matrix-sdk/src/notification_settings/rule_commands.rs +++ b/crates/matrix-sdk/src/notification_settings/rule_commands.rs @@ -7,7 +7,7 @@ use ruma::{ }; use super::command::Command; -use crate::NotificationSettingsError; +use crate::{notification_settings::command::Notify, NotificationSettingsError}; /// A `RuleCommand` allows to generate a list of `Command` needed to modify a /// `Ruleset` @@ -27,7 +27,7 @@ impl RuleCommands { &mut self, kind: RuleKind, room_id: &RoomId, - notify: bool, + notify: Notify, ) -> Result<(), NotificationSettingsError> { let command = match kind { RuleKind::Room => Command::SetRoomPushRule { room_id: room_id.to_owned(), notify }, @@ -210,7 +210,10 @@ mod tests { }; use super::RuleCommands; - use crate::{error::NotificationSettingsError, notification_settings::command::Command}; + use crate::{ + error::NotificationSettingsError, + notification_settings::command::{Command, Notify}, + }; fn get_server_default_ruleset() -> Ruleset { let user_id = UserId::parse("@user:matrix.org").unwrap(); @@ -225,7 +228,7 @@ mod tests { async fn test_insert_rule_room() { let room_id = get_test_room_id(); let mut rule_commands = RuleCommands::new(get_server_default_ruleset()); - rule_commands.insert_rule(RuleKind::Room, &room_id, true).unwrap(); + rule_commands.insert_rule(RuleKind::Room, &room_id, Notify::All).unwrap(); // A rule must have been inserted in the ruleset. assert!(rule_commands.rules.get(RuleKind::Room, &room_id).is_some()); @@ -235,7 +238,7 @@ mod tests { assert_matches!(&rule_commands.commands[0], Command::SetRoomPushRule { room_id: command_room_id, notify } => { assert_eq!(command_room_id, &room_id); - assert!(notify); + assert_eq!(*notify, Notify::All); } ); } @@ -244,7 +247,7 @@ mod tests { async fn test_insert_rule_override() { let room_id = get_test_room_id(); let mut rule_commands = RuleCommands::new(get_server_default_ruleset()); - rule_commands.insert_rule(RuleKind::Override, &room_id, true).unwrap(); + rule_commands.insert_rule(RuleKind::Override, &room_id, Notify::All).unwrap(); // A rule must have been inserted in the ruleset. assert!(rule_commands.rules.get(RuleKind::Override, &room_id).is_some()); @@ -255,7 +258,7 @@ mod tests { Command::SetOverridePushRule {room_id: command_room_id, rule_id, notify } => { assert_eq!(command_room_id, &room_id); assert_eq!(rule_id, room_id.as_str()); - assert!(notify); + assert_eq!(*notify, Notify::All); } ); } @@ -266,17 +269,17 @@ mod tests { let mut rule_commands = RuleCommands::new(get_server_default_ruleset()); assert_matches!( - rule_commands.insert_rule(RuleKind::Underride, &room_id, true), + rule_commands.insert_rule(RuleKind::Underride, &room_id, Notify::All), Err(NotificationSettingsError::InvalidParameter(_)) => {} ); assert_matches!( - rule_commands.insert_rule(RuleKind::Content, &room_id, true), + rule_commands.insert_rule(RuleKind::Content, &room_id, Notify::All), Err(NotificationSettingsError::InvalidParameter(_)) => {} ); assert_matches!( - rule_commands.insert_rule(RuleKind::Sender, &room_id, true), + rule_commands.insert_rule(RuleKind::Sender, &room_id, Notify::All), Err(NotificationSettingsError::InvalidParameter(_)) => {} ); } diff --git a/crates/matrix-sdk/src/notification_settings/rules.rs b/crates/matrix-sdk/src/notification_settings/rules.rs index 7a022c429e3..df3efc1dea2 100644 --- a/crates/matrix-sdk/src/notification_settings/rules.rs +++ b/crates/matrix-sdk/src/notification_settings/rules.rs @@ -85,8 +85,14 @@ impl Rules { // Search for an enabled `Room` rule where `rule_id` is the `room_id` if let Some(rule) = self.ruleset.get(RuleKind::Room, room_id) { - // if this rule contains a `Notify` action if rule.triggers_notification() { + #[cfg(feature = "unstable-msc3768")] + if !rule.triggers_remote_notification() { + // This rule contains a `NotifyInApp` action. + return Some(RoomNotificationMode::PushMentionsAndKeywordsOnly); + } + + // This rule contains a `Notify` action. return Some(RoomNotificationMode::AllMessages); } return Some(RoomNotificationMode::MentionsAndKeywordsOnly); @@ -112,18 +118,21 @@ impl Rules { let predefined_rule_id = get_predefined_underride_room_rule_id(is_encrypted, is_one_to_one); let rule_id = predefined_rule_id.as_str(); - // If there is an `Underride` rule that should trigger a notification, the mode - // is `AllMessages` - if self - .ruleset - .get(RuleKind::Underride, rule_id) - .is_some_and(|r| r.enabled() && r.triggers_notification()) - { - RoomNotificationMode::AllMessages - } else { - // Otherwise, the mode is `MentionsAndKeywordsOnly` - RoomNotificationMode::MentionsAndKeywordsOnly + if let Some(rule) = self.ruleset.get(RuleKind::Underride, rule_id).filter(|r| r.enabled()) { + if rule.triggers_notification() { + #[cfg(feature = "unstable-msc3768")] + if !rule.triggers_remote_notification() { + // This rule contains a `NotifyInApp` action. + return RoomNotificationMode::PushMentionsAndKeywordsOnly; + } + // If there is an `Underride` rule that should trigger a notification, the mode + // is `AllMessages` + return RoomNotificationMode::AllMessages; + } } + + // Otherwise, the mode is `MentionsAndKeywordsOnly` + RoomNotificationMode::MentionsAndKeywordsOnly } /// Get all room IDs for which a user-defined rule exists. @@ -330,6 +339,7 @@ pub(crate) mod tests { use crate::{ error::NotificationSettingsError, notification_settings::{ + command::Notify, rules::{self, Rules}, IsEncrypted, IsOneToOne, RoomNotificationMode, }, @@ -614,7 +624,7 @@ pub(crate) mod tests { // Build a `RuleCommands` inserting a rule let mut rules_commands = RuleCommands::new(rules.ruleset.clone()); - rules_commands.insert_rule(RuleKind::Override, &room_id, false).unwrap(); + rules_commands.insert_rule(RuleKind::Override, &room_id, Notify::None).unwrap(); rules.apply(rules_commands);