From 129275556f2a6c0603bf4270dd73c49e565909f2 Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Thu, 18 Sep 2025 16:59:24 +0200 Subject: [PATCH] feat(push): add experimental support for applying MSC4359 ("Do not Disturb" notification settings) Signed-off-by: Johannes Marbach --- Cargo.lock | 179 ++++++++++------- Cargo.toml | 7 +- bindings/matrix-sdk-ffi/CHANGELOG.md | 1 + bindings/matrix-sdk-ffi/Cargo.toml | 1 + bindings/matrix-sdk-ffi/src/client.rs | 4 + bindings/matrix-sdk-ffi/src/ruma.rs | 64 ++++++ crates/matrix-sdk-base/Cargo.toml | 3 + crates/matrix-sdk-base/src/client.rs | 51 +++++ crates/matrix-sdk-ui/CHANGELOG.md | 6 + crates/matrix-sdk-ui/Cargo.toml | 4 + .../matrix-sdk-ui/src/notification_client.rs | 15 +- .../tests/integration/notification_client.rs | 185 ++++++++++++++++++ crates/matrix-sdk/Cargo.toml | 3 + crates/matrix-sdk/src/client/mod.rs | 7 + testing/matrix-sdk-test/src/event_factory.rs | 11 ++ 15 files changed, 466 insertions(+), 75 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8f0061137ff..dfda673b13f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2253,16 +2253,13 @@ dependencies = [ [[package]] name = "html5ever" -version = "0.29.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e15626aaf9c351bc696217cbe29cb9b5e86c43f8a46b5e2f5c6c5cf7cb904ce" +checksum = "55d958c2f74b664487a2035fe1dadb032c48718a03b63f3ab0b8537db8549ed4" dependencies = [ "log", - "mac", "markup5ever", - "proc-macro2", - "quote", - "syn", + "match_token", ] [[package]] @@ -2836,6 +2833,15 @@ dependencies = [ "serde", ] +[[package]] +name = "js_option" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7dd3e281add16813cf673bf74a32249b0aa0d1c8117519a17b3ada5e8552b3c" +dependencies = [ + "serde_core", +] + [[package]] name = "json-structural-diff" version = "0.2.0" @@ -3051,16 +3057,24 @@ checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] name = "markup5ever" -version = "0.14.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82c88c6129bd24319e62a0359cb6b958fa7e8be6e19bb1663bc396b90883aca5" +checksum = "311fe69c934650f8f19652b3946075f0fc41ad8757dbb68f1ca14e7900ecc1c3" dependencies = [ "log", - "phf 0.11.2", - "phf_codegen", - "string_cache", - "string_cache_codegen", "tendril", + "web_atoms", +] + +[[package]] +name = "match_token" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -3277,7 +3291,7 @@ dependencies = [ "indoc", "insta", "itertools 0.14.0", - "js_option", + "js_option 0.1.1", "matrix-sdk-common", "matrix-sdk-qrcode", "matrix-sdk-test", @@ -3593,6 +3607,7 @@ dependencies = [ "async-stream", "async_cell", "bitflags", + "cfg-if", "chrono", "emojis", "eyeball", @@ -4129,20 +4144,10 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" dependencies = [ - "phf_generator 0.11.2", + "phf_generator", "phf_shared 0.11.2", ] -[[package]] -name = "phf_generator" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" -dependencies = [ - "phf_shared 0.10.0", - "rand 0.8.5", -] - [[package]] name = "phf_generator" version = "0.11.2" @@ -4153,15 +4158,6 @@ dependencies = [ "rand 0.8.5", ] -[[package]] -name = "phf_shared" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" -dependencies = [ - "siphasher 0.3.11", -] - [[package]] name = "phf_shared" version = "0.11.2" @@ -4749,11 +4745,11 @@ dependencies = [ [[package]] name = "ruma" version = "0.13.0" -source = "git+https://github.com/ruma/ruma?rev=2f64faeabb85950de27e9829faeb389d2779ac57#2f64faeabb85950de27e9829faeb389d2779ac57" +source = "git+https://github.com/ruma/ruma?rev=de227ef5c5b2d2ecff89ab021ebe53c5d565577d#de227ef5c5b2d2ecff89ab021ebe53c5d565577d" dependencies = [ "assign", "js_int", - "js_option", + "js_option 0.2.0", "ruma-client-api", "ruma-common", "ruma-events", @@ -4766,7 +4762,7 @@ dependencies = [ [[package]] name = "ruma-client-api" version = "0.21.0" -source = "git+https://github.com/ruma/ruma?rev=2f64faeabb85950de27e9829faeb389d2779ac57#2f64faeabb85950de27e9829faeb389d2779ac57" +source = "git+https://github.com/ruma/ruma?rev=de227ef5c5b2d2ecff89ab021ebe53c5d565577d#de227ef5c5b2d2ecff89ab021ebe53c5d565577d" dependencies = [ "as_variant", "assign", @@ -4774,7 +4770,7 @@ dependencies = [ "date_header", "http", "js_int", - "js_option", + "js_option 0.2.0", "maplit", "ruma-common", "ruma-events", @@ -4789,7 +4785,7 @@ dependencies = [ [[package]] name = "ruma-common" version = "0.16.0" -source = "git+https://github.com/ruma/ruma?rev=2f64faeabb85950de27e9829faeb389d2779ac57#2f64faeabb85950de27e9829faeb389d2779ac57" +source = "git+https://github.com/ruma/ruma?rev=de227ef5c5b2d2ecff89ab021ebe53c5d565577d#de227ef5c5b2d2ecff89ab021ebe53c5d565577d" dependencies = [ "as_variant", "base64", @@ -4822,12 +4818,12 @@ dependencies = [ [[package]] name = "ruma-events" version = "0.31.0" -source = "git+https://github.com/ruma/ruma?rev=2f64faeabb85950de27e9829faeb389d2779ac57#2f64faeabb85950de27e9829faeb389d2779ac57" +source = "git+https://github.com/ruma/ruma?rev=de227ef5c5b2d2ecff89ab021ebe53c5d565577d#de227ef5c5b2d2ecff89ab021ebe53c5d565577d" dependencies = [ "as_variant", "indexmap", "js_int", - "js_option", + "js_option 0.2.0", "percent-encoding", "pulldown-cmark", "regex", @@ -4848,7 +4844,7 @@ dependencies = [ [[package]] name = "ruma-federation-api" version = "0.12.0" -source = "git+https://github.com/ruma/ruma?rev=2f64faeabb85950de27e9829faeb389d2779ac57#2f64faeabb85950de27e9829faeb389d2779ac57" +source = "git+https://github.com/ruma/ruma?rev=de227ef5c5b2d2ecff89ab021ebe53c5d565577d#de227ef5c5b2d2ecff89ab021ebe53c5d565577d" dependencies = [ "headers", "http", @@ -4868,7 +4864,7 @@ dependencies = [ [[package]] name = "ruma-html" version = "0.5.0" -source = "git+https://github.com/ruma/ruma?rev=2f64faeabb85950de27e9829faeb389d2779ac57#2f64faeabb85950de27e9829faeb389d2779ac57" +source = "git+https://github.com/ruma/ruma?rev=de227ef5c5b2d2ecff89ab021ebe53c5d565577d#de227ef5c5b2d2ecff89ab021ebe53c5d565577d" dependencies = [ "as_variant", "html5ever", @@ -4879,7 +4875,7 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" version = "0.11.0" -source = "git+https://github.com/ruma/ruma?rev=2f64faeabb85950de27e9829faeb389d2779ac57#2f64faeabb85950de27e9829faeb389d2779ac57" +source = "git+https://github.com/ruma/ruma?rev=de227ef5c5b2d2ecff89ab021ebe53c5d565577d#de227ef5c5b2d2ecff89ab021ebe53c5d565577d" dependencies = [ "js_int", "thiserror 2.0.16", @@ -4888,7 +4884,7 @@ dependencies = [ [[package]] name = "ruma-macros" version = "0.16.0" -source = "git+https://github.com/ruma/ruma?rev=2f64faeabb85950de27e9829faeb389d2779ac57#2f64faeabb85950de27e9829faeb389d2779ac57" +source = "git+https://github.com/ruma/ruma?rev=de227ef5c5b2d2ecff89ab021ebe53c5d565577d#de227ef5c5b2d2ecff89ab021ebe53c5d565577d" dependencies = [ "cfg-if", "proc-macro-crate", @@ -4897,13 +4893,13 @@ dependencies = [ "ruma-identifiers-validation", "serde", "syn", - "toml 0.8.15", + "toml 0.9.6", ] [[package]] name = "ruma-signatures" version = "0.18.0" -source = "git+https://github.com/ruma/ruma?rev=2f64faeabb85950de27e9829faeb389d2779ac57#2f64faeabb85950de27e9829faeb389d2779ac57" +source = "git+https://github.com/ruma/ruma?rev=de227ef5c5b2d2ecff89ab021ebe53c5d565577d#de227ef5c5b2d2ecff89ab021ebe53c5d565577d" dependencies = [ "base64", "ed25519-dalek", @@ -5241,10 +5237,11 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.219" +version = "1.0.225" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "fd6c24dee235d0da097043389623fb913daddf92c76e9f5a1db88607a0bcbd1d" dependencies = [ + "serde_core", "serde_derive", ] @@ -5268,11 +5265,20 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_core" +version = "1.0.225" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "659356f9a0cb1e529b24c01e43ad2bdf520ec4ceaf83047b83ddcc2251f96383" +dependencies = [ + "serde_derive", +] + [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.225" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516" dependencies = [ "proc-macro2", "quote", @@ -5316,11 +5322,11 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.8" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "2789234a13a53fc4be1b51ea1bab45a3c338bdb884862a257d10e5a74ae009e6" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -5536,26 +5542,25 @@ dependencies = [ [[package]] name = "string_cache" -version = "0.8.7" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" dependencies = [ "new_debug_unreachable", - "once_cell", "parking_lot", - "phf_shared 0.10.0", + "phf_shared 0.11.2", "precomputed-hash", "serde", ] [[package]] name = "string_cache_codegen" -version = "0.5.2" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", + "phf_generator", + "phf_shared 0.11.2", "proc-macro2", "quote", ] @@ -6054,14 +6059,15 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.15" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac2caab0bf757388c6c0ae23b3293fdb463fee59434529014f85e3263b995c28" +checksum = "ae2a4cf385da23d1d53bc15cdfa5c2109e93d8d362393c801e87da2f72f0e201" dependencies = [ - "serde", + "serde_core", "serde_spanned", - "toml_datetime", - "toml_edit", + "toml_datetime 0.7.1", + "toml_parser", + "winnow 0.7.13", ] [[package]] @@ -6069,8 +6075,14 @@ name = "toml_datetime" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" + +[[package]] +name = "toml_datetime" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a197c0ec7d131bfc6f7e82c8442ba1595aeab35da7adbf05b6b73cd06a16b6be" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -6080,10 +6092,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "winnow", + "toml_datetime 0.6.8", + "winnow 0.6.20", +] + +[[package]] +name = "toml_parser" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" +dependencies = [ + "winnow 0.7.13", ] [[package]] @@ -6857,6 +6876,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web_atoms" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57ffde1dc01240bdf9992e3205668b235e59421fd085e8a317ed98da0178d414" +dependencies = [ + "phf 0.11.2", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + [[package]] name = "webpki-root-certs" version = "1.0.2" @@ -7207,6 +7238,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" + [[package]] name = "wiremock" version = "0.6.5" diff --git a/Cargo.toml b/Cargo.toml index ef99abbb995..fb236ee976f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,7 +65,7 @@ rand = "0.8.5" regex = "1.11.2" reqwest = { version = "0.12.23", default-features = false } rmp-serde = "1.3.0" -ruma = { git = "https://github.com/ruma/ruma", rev = "2f64faeabb85950de27e9829faeb389d2779ac57", features = [ +ruma = { git = "https://github.com/ruma/ruma", rev = "de227ef5c5b2d2ecff89ab021ebe53c5d565577d", features = [ "client-api-c", "compat-upload-signatures", "compat-arbitrary-length-ids", @@ -85,11 +85,12 @@ ruma = { git = "https://github.com/ruma/ruma", rev = "2f64faeabb85950de27e9829fa "unstable-msc4286", "unstable-msc4306", "unstable-msc4308", - "unstable-msc4310" + "unstable-msc4310", + "unstable-msc4359" ] } sentry = { version = "0.42.0", default-features = false } sentry-tracing = "0.42.0" -serde = { version = "1.0.219", features = ["rc"] } +serde = { version = "1.0.221", features = ["rc"] } serde_html_form = "0.2.7" serde_json = "1.0.143" sha2 = "0.10.9" diff --git a/bindings/matrix-sdk-ffi/CHANGELOG.md b/bindings/matrix-sdk-ffi/CHANGELOG.md index 1dec9d79900..6e9252d08e4 100644 --- a/bindings/matrix-sdk-ffi/CHANGELOG.md +++ b/bindings/matrix-sdk-ffi/CHANGELOG.md @@ -19,6 +19,7 @@ All notable changes to this project will be documented in this file. - Add `Room::load_or_fetch_event` so we can get a `TimelineEvent` given its event id ([#5678](https://github.com/matrix-org/matrix-rust-sdk/pull/5678)). - Add `TimelineEvent::thread_root_event_id` to expose the thread root event id for this type too ([#5678](https://github.com/matrix-org/matrix-rust-sdk/pull/5678)). +- Add support for "Do not Disturb" room list in account data as per MSC4359 ([#5687](https://github.com/matrix-org/matrix-rust-sdk/pull/5687)) ## [0.14.0] - 2025-09-04 diff --git a/bindings/matrix-sdk-ffi/Cargo.toml b/bindings/matrix-sdk-ffi/Cargo.toml index 0702b802fe5..a0a697c7a37 100644 --- a/bindings/matrix-sdk-ffi/Cargo.toml +++ b/bindings/matrix-sdk-ffi/Cargo.toml @@ -27,6 +27,7 @@ crate-type = [ default = ["bundled-sqlite", "unstable-msc4274", "experimental-element-recent-emojis"] bundled-sqlite = ["matrix-sdk/bundled-sqlite"] unstable-msc4274 = ["matrix-sdk-ui/unstable-msc4274"] +unstable-msc4359 = ["matrix-sdk-ui/unstable-msc4359"] # Required when targeting a Javascript environment, like Wasm in a browser. js = ["matrix-sdk-ui/js"] # Use the TLS implementation provided by the host system, necessary on iOS and Wasm platforms. diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 1712cacd3fc..e2cf22a82a3 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -61,6 +61,7 @@ use ruma::{ }, events::{ direct::DirectEventContent, + do_not_disturb::DoNotDisturbEventContent, fully_read::FullyReadEventContent, identity_server::IdentityServerEventContent, ignored_user_list::IgnoredUserListEventContent, @@ -699,6 +700,9 @@ impl Client { AccountDataEventType::Direct => { observe!(DirectEventContent) } + AccountDataEventType::DoNotDisturbRoomList => { + observe!(DoNotDisturbEventContent) + } AccountDataEventType::IdentityServer => { observe!(IdentityServerEventContent) } diff --git a/bindings/matrix-sdk-ffi/src/ruma.rs b/bindings/matrix-sdk-ffi/src/ruma.rs index b44414abd3d..f1f19ac910d 100644 --- a/bindings/matrix-sdk-ffi/src/ruma.rs +++ b/bindings/matrix-sdk-ffi/src/ruma.rs @@ -24,6 +24,10 @@ use ruma::{ assign, events::{ direct::DirectEventContent, + do_not_disturb::{ + DoNotDisturbEventContent, DoNotDisturbRoom as RumaDoNotDisturbRoom, + DoNotDisturbRoomKey as RumaDoNotDisturbRoomKey, + }, fully_read::FullyReadEventContent, identity_server::IdentityServerEventContent, ignored_user_list::{IgnoredUser as RumaIgnoredUser, IgnoredUserListEventContent}, @@ -1090,6 +1094,8 @@ pub fn content_without_relation_from_message( pub enum AccountDataEventType { /// m.direct Direct, + /// dm.filament.do_not_disturb + DoNotDisturbRoomList, /// m.identity_server IdentityServer, /// m.ignored_user_list @@ -1108,6 +1114,7 @@ impl TryFrom for AccountDataEventType { fn try_from(value: RumaGlobalAccountDataEventType) -> Result { match value { RumaGlobalAccountDataEventType::Direct => Ok(Self::Direct), + RumaGlobalAccountDataEventType::DoNotDisturb => Ok(Self::DoNotDisturbRoomList), RumaGlobalAccountDataEventType::IdentityServer => Ok(Self::IdentityServer), RumaGlobalAccountDataEventType::IgnoredUserList => Ok(Self::IgnoredUserList), RumaGlobalAccountDataEventType::PushRules => Ok(Self::PushRules), @@ -1131,6 +1138,12 @@ pub enum AccountDataEvent { /// for that user ID. map: HashMap>, }, + /// dm.filament.do_not_disturb + DoNotDisturbRoomList { + /// The map of rooms in "Do not Disturb" mode. This is a mapping from + /// [`DoNotDisturbRoomKey`] to empty object. + rooms: HashMap, + }, /// m.identity_server IdentityServer { /// The base URL for the identity server for client-server connections. @@ -1232,6 +1245,42 @@ impl From for RumaInviteAvatars { } } +/// The key for a "Do not Disturb" setting. +/// +/// This either matches a single room or all rooms. +#[derive(Clone, Eq, Hash, PartialEq, uniffi::Enum)] +pub enum DoNotDisturbRoomKey { + /// Match any room. + AllRooms, + + /// Match a single room based on its room ID. + SingleRoom(String), +} + +impl From for DoNotDisturbRoomKey { + fn from(value: RumaDoNotDisturbRoomKey) -> Self { + match value { + RumaDoNotDisturbRoomKey::AllRooms => DoNotDisturbRoomKey::AllRooms, + RumaDoNotDisturbRoomKey::SingleRoom(room_id) => { + DoNotDisturbRoomKey::SingleRoom(room_id.into()) + } + _ => panic!("Unexpected DoNotDisturbRoomKey: {value:?}"), + } + } +} + +/// Details about a room in "Do not Disturb" mode. +/// +/// This is currently empty. +#[derive(Clone, uniffi::Record)] +pub struct DoNotDisturbRoom {} + +impl From for DoNotDisturbRoom { + fn from(_value: RumaDoNotDisturbRoom) -> Self { + DoNotDisturbRoom {} + } +} + /// Details about an ignored user. /// /// This is currently empty. @@ -1567,6 +1616,21 @@ impl From> for AccountDataEvent { } } +impl From> for AccountDataEvent { + fn from(value: RumaGlobalAccountDataEvent) -> Self { + Self::DoNotDisturbRoomList { + rooms: value + .content + .rooms + .into_iter() + .map(|(key, do_not_disturb_room)| { + (key.into(), DoNotDisturbRoom::from(do_not_disturb_room)) + }) + .collect(), + } + } +} + impl From> for AccountDataEvent { fn from(value: RumaGlobalAccountDataEvent) -> Self { Self::IdentityServer { base_url: value.content.base_url.into_option() } diff --git a/crates/matrix-sdk-base/Cargo.toml b/crates/matrix-sdk-base/Cargo.toml index 92b9f1f2326..44cee37b0ee 100644 --- a/crates/matrix-sdk-base/Cargo.toml +++ b/crates/matrix-sdk-base/Cargo.toml @@ -65,6 +65,9 @@ testing = [ # Add support for inline media galleries via msgtypes unstable-msc4274 = [] +# "Do not Disturb" notification settings +unstable-msc4359 = ["ruma/unstable-msc4359"] + experimental-element-recent-emojis = [] [dependencies] diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 4837483ac8e..86bb015c05a 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -32,6 +32,8 @@ use matrix_sdk_crypto::{ }; #[cfg(doc)] use ruma::DeviceId; +#[cfg(feature = "unstable-msc4359")] +use ruma::events::do_not_disturb::{DoNotDisturbEventContent, DoNotDisturbRoomKey}; #[cfg(feature = "e2e-encryption")] use ruma::events::room::{history_visibility::HistoryVisibility, member::MembershipState}; use ruma::{ @@ -1135,6 +1137,37 @@ impl BaseClient { } } } + + /// Checks whether the provided `room_id` belongs to a room in "Do not + /// Disturb" mode. + #[cfg(feature = "unstable-msc4359")] + pub async fn is_room_in_do_not_disturb_mode(&self, room_id: &RoomId) -> bool { + match self.state_store.get_account_data_event_static::().await { + Ok(Some(raw_do_not_disturb_room_list)) => { + match raw_do_not_disturb_room_list.deserialize() { + Ok(current_do_not_disturb_room_list) => { + current_do_not_disturb_room_list + .content + .rooms + .contains_key(&DoNotDisturbRoomKey::AllRooms) + || current_do_not_disturb_room_list + .content + .rooms + .contains_key(&DoNotDisturbRoomKey::SingleRoom(room_id.to_owned())) + } + Err(error) => { + warn!(?error, "Failed to deserialize the 'Do not Disturb' room list event"); + false + } + } + } + Ok(None) => false, + Err(error) => { + warn!(?error, "Could not get the 'Do not Disturb' room list from the state store"); + false + } + } + } } /// Represent the `required_state` values sent by a sync request. @@ -1772,6 +1805,24 @@ mod tests { assert!(client.is_user_ignored(ignored_user_id).await); } + #[cfg(feature = "unstable-msc4359")] + #[async_test] + async fn test_is_room_in_do_not_disturb() { + let dnd_room_id = room_id!("!sonofagun"); + let client = logged_in_base_client(None).await; + + assert!(!client.is_room_in_do_not_disturb_mode(dnd_room_id).await); + + let mut sync_builder = SyncResponseBuilder::new(); + let f = EventFactory::new(); + let response = sync_builder + .add_global_account_data(f.do_not_disturb_room_list([dnd_room_id.to_owned()])) + .build_sync_response(); + client.receive_sync_response(response).await.unwrap(); + + assert!(client.is_room_in_do_not_disturb_mode(dnd_room_id).await); + } + #[async_test] async fn test_invite_details_are_set() { let user_id = user_id!("@alice:localhost"); diff --git a/crates/matrix-sdk-ui/CHANGELOG.md b/crates/matrix-sdk-ui/CHANGELOG.md index 4d5b08cb997..fe3aa922ffe 100644 --- a/crates/matrix-sdk-ui/CHANGELOG.md +++ b/crates/matrix-sdk-ui/CHANGELOG.md @@ -25,6 +25,12 @@ All notable changes to this project will be documented in this file. `RoomListItem`. ([#5684](https://github.com/matrix-org/matrix-rust-sdk/pull/5684)) +### Features + +- Apply "Do not Disturb" room list when filtering notifications as + per MSC4359 + ([#5687](https://github.com/matrix-org/matrix-rust-sdk/pull/5687)) + ## [0.14.0] - 2025-09-04 ### Features diff --git a/crates/matrix-sdk-ui/Cargo.toml b/crates/matrix-sdk-ui/Cargo.toml index b5df3f8ba88..9ddd3a401ad 100644 --- a/crates/matrix-sdk-ui/Cargo.toml +++ b/crates/matrix-sdk-ui/Cargo.toml @@ -25,6 +25,9 @@ unstable-msc3956 = ["ruma/unstable-msc3956"] # Add support for inline media galleries via msgtypes unstable-msc4274 = ["matrix-sdk/unstable-msc4274"] +# "Do not Disturb" notification settings +unstable-msc4359 = ["matrix-sdk/unstable-msc4359"] + # Enable experimental support for encrypting state events; see # https://github.com/matrix-org/matrix-rust-sdk/issues/5397. experimental-encrypted-state-events = [ @@ -38,6 +41,7 @@ async-rx.workspace = true async-stream.workspace = true async_cell = "0.2.3" bitflags.workspace = true +cfg-if.workspace = true chrono.workspace = true eyeball.workspace = true eyeball-im.workspace = true diff --git a/crates/matrix-sdk-ui/src/notification_client.rs b/crates/matrix-sdk-ui/src/notification_client.rs index 45604efc0ba..8893a50e499 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, @@ -637,7 +638,19 @@ impl NotificationClient { let notification_item = NotificationItem::new(room, raw_event, push_actions, state_events).await?; - if self.client.is_user_ignored(notification_item.event.sender()).await { + let is_room_in_do_not_disturb = { + cfg_if! { + if #[cfg(feature = "unstable-msc4359")] { + self.client.is_room_in_do_not_disturb_mode(room.room_id()).await + } else { + false + } + } + }; + + if self.client.is_user_ignored(notification_item.event.sender()).await + || is_room_in_do_not_disturb + { Ok(NotificationStatus::EventFilteredOut) } else { Ok(NotificationStatus::Event(Box::new(notification_item))) diff --git a/crates/matrix-sdk-ui/tests/integration/notification_client.rs b/crates/matrix-sdk-ui/tests/integration/notification_client.rs index b21885ffa4b..eb05494e14a 100644 --- a/crates/matrix-sdk-ui/tests/integration/notification_client.rs +++ b/crates/matrix-sdk-ui/tests/integration/notification_client.rs @@ -1095,6 +1095,112 @@ async fn test_notification_client_sliding_sync_filters_out_events_from_ignored_u }; } +#[cfg(feature = "unstable-msc4359")] +#[async_test] +async fn test_notification_client_sliding_sync_filters_out_events_from_do_not_disturb_rooms() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + let sender = user_id!("@user:example.org"); + let my_user_id = client.user_id().unwrap().to_owned(); + + let room_id = room_id!("!a98sd12bjh:example.org"); + let room_name = "The Maltese Falcon"; + let sender_display_name = "John Mastodon"; + let event_id = event_id!("$example_event_id"); + + let raw_event = EventFactory::new() + .room(room_id) + .sender(sender) + .text_msg("Heya") + .event_id(event_id) + .into_raw_sync(); + + let event_factory = EventFactory::new().room(room_id); + + let sender_member_event = event_factory + .member(sender) + .display_name(sender_display_name) + .membership(MembershipState::Join) + .into_raw_sync(); + + let own_member_event = event_factory + .member(&my_user_id) + .display_name("My self") + .membership(MembershipState::Join) + .into_raw_sync(); + + let power_levels_event = + event_factory.sender(sender).power_levels(&mut BTreeMap::new()).into_raw_sync(); + + let pos = Mutex::new(0); + Mock::given(SlidingSyncMatcher) + .respond_with(move |request: &Request| { + let partial_request: PartialSlidingSyncRequest = request.body_json().unwrap(); + // Repeat the transaction id in the response, to validate sticky parameters. + let mut pos = pos.lock().unwrap(); + *pos += 1; + let pos_as_str = (*pos).to_string(); + ResponseTemplate::new(200).set_body_json(json!({ + "txn_id": partial_request.txn_id, + "pos": pos_as_str, + "rooms": { + room_id: { + "name": room_name, + "initial": true, + + "required_state": [ + // Sender's member information. + sender_member_event, + + // Own member information. + own_member_event, + + // Power levels. + power_levels_event, + ], + + "timeline": [ + raw_event, + ] + } + }, + + "extensions": { + "account_data": { + "global": [{ + "type": "dm.filament.do_not_disturb", + "content": { + "rooms": { room_id: {} } + } + }] + } + } + })) + }) + .mount(server.server()) + .await; + + let dummy_sync_service = Arc::new(SyncService::builder(client.clone()).build().await.unwrap()); + let process_setup = + NotificationProcessSetup::SingleProcess { sync_service: dummy_sync_service }; + let notification_client = NotificationClient::new(client, process_setup).await.unwrap(); + let mut result = notification_client + .get_notifications_with_sliding_sync(&[NotificationItemsRequest { + room_id: room_id.to_owned(), + event_ids: vec![event_id.to_owned()], + }]) + .await + .unwrap(); + + let Some(Ok(item)) = result.remove(event_id) else { + panic!("fetching notification for {event_id} failed"); + }; + let NotificationStatus::EventFilteredOut = item else { + panic!("notification for {event_id} was not filtered out"); + }; +} + #[async_test] async fn test_notification_client_context_filters_out_events_from_ignored_users() { let server = MatrixMockServer::new().await; @@ -1172,3 +1278,82 @@ async fn test_notification_client_context_filters_out_events_from_ignored_users( assert_matches!(result, NotificationStatus::EventFilteredOut); } + +#[cfg(feature = "unstable-msc4359")] +#[async_test] +async fn test_notification_client_context_filters_out_events_from_do_not_disturb_rooms() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + let sender = user_id!("@user:example.org"); + let room_id = room_id!("!a98sd12bjh:example.org"); + let event_id = event_id!("$example_event_id"); + + server.sync_joined_room(&client, room_id).await; + + // Add mock for sliding sync so we get the ignored user list from its account + // data + let pos = Mutex::new(0); + Mock::given(SlidingSyncMatcher) + .respond_with(move |request: &Request| { + let partial_request: PartialSlidingSyncRequest = request.body_json().unwrap(); + // Repeat the transaction id in the response, to validate sticky parameters. + let mut pos = pos.lock().unwrap(); + *pos += 1; + let pos_as_str = (*pos).to_string(); + ResponseTemplate::new(200).set_body_json(json!({ + "txn_id": partial_request.txn_id, + "pos": pos_as_str, + "rooms": {}, + + "extensions": { + "account_data": { + "global": [{ + "type": "dm.filament.do_not_disturb", + "content": { + "rooms": { room_id: {} } + } + }] + } + } + })) + }) + .mount(server.server()) + .await; + + let event = EventFactory::new() + .room(room_id) + .sender(sender) + .text_msg("Heya") + .event_id(event_id) + .into_event(); + + // Mock the /context response + server + .mock_room_event_context() + .ok(RoomContextResponseTemplate::new(event).start("start").end("end")) + .mock_once() + .mount() + .await; + + let dummy_sync_service = Arc::new(SyncService::builder(client.clone()).build().await.unwrap()); + let process_setup = + NotificationProcessSetup::SingleProcess { sync_service: dummy_sync_service }; + let notification_client = NotificationClient::new(client, process_setup).await.unwrap(); + + // Call sync first so we get the list of ignored users in the notification + // client This should still work in a real life usage + let _ = notification_client + .get_notifications_with_sliding_sync(&[NotificationItemsRequest { + room_id: room_id.to_owned(), + event_ids: vec![event_id.to_owned()], + }]) + .await; + + // If the event is not found even though there was a mocked response for it, it + // was discarded as expected. + let result = + notification_client.get_notification_with_context(room_id, event_id).await.unwrap(); + + assert_matches!(result, NotificationStatus::EventFilteredOut); +} diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index c02835106a0..e8949580cc0 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -71,6 +71,9 @@ docsrs = ["e2e-encryption", "sqlite", "indexeddb", "sso-login", "qrcode"] # Add support for inline media galleries via msgtypes unstable-msc4274 = ["ruma/unstable-msc4274", "matrix-sdk-base/unstable-msc4274"] +# "Do not Disturb" notification settings +unstable-msc4359 = ["matrix-sdk-base/unstable-msc4359"] + experimental-search = ["matrix-sdk-search"] experimental-element-recent-emojis = ["matrix-sdk-base/experimental-element-recent-emojis"] diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index 8e353fb4fe4..07d935e0572 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -2860,6 +2860,13 @@ impl Client { self.base_client().is_user_ignored(user_id).await } + /// Checks whether the provided `room_id` belongs to a room in "Do not + /// Disturb" mode. + #[cfg(feature = "unstable-msc4359")] + pub async fn is_room_in_do_not_disturb_mode(&self, room_id: &RoomId) -> bool { + self.base_client().is_room_in_do_not_disturb_mode(room_id).await + } + /// Gets the `max_upload_size` value from the homeserver, getting either a /// cached value or with a `/_matrix/client/v1/media/config` request if it's /// missing. diff --git a/testing/matrix-sdk-test/src/event_factory.rs b/testing/matrix-sdk-test/src/event_factory.rs index 92f361145c0..6e406c058b7 100644 --- a/testing/matrix-sdk-test/src/event_factory.rs +++ b/testing/matrix-sdk-test/src/event_factory.rs @@ -36,6 +36,7 @@ use ruma::{ beacon::BeaconEventContent, call::{SessionDescription, invite::CallInviteEventContent}, direct::{DirectEventContent, OwnedDirectUserIdentifier}, + do_not_disturb::DoNotDisturbEventContent, ignored_user_list::IgnoredUserListEventContent, macros::EventContent, member_hints::MemberHintsEventContent, @@ -1046,6 +1047,16 @@ impl EventFactory { builder } + /// Create a new `dm.filament.do_not_disturb` global account data event. + pub fn do_not_disturb_room_list( + &self, + rooms: impl IntoIterator, + ) -> EventBuilder { + let mut builder = self.event(rooms.into_iter().collect()); + builder.is_global = true; + builder + } + /// Create a new `m.push_rules` global account data event. pub fn push_rules(&self, rules: Ruleset) -> EventBuilder { let mut builder = self.event(PushRulesEventContent::new(rules));