Skip to content

Commit 7118250

Browse files
committed
feat(push): add experimental support for applying MSC4359 ("Do not Disturb" notification settings)
Signed-off-by: Johannes Marbach <[email protected]>
1 parent 943b048 commit 7118250

File tree

11 files changed

+391
-75
lines changed

11 files changed

+391
-75
lines changed

Cargo.lock

Lines changed: 108 additions & 71 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ rand = "0.8.5"
6565
regex = "1.11.2"
6666
reqwest = { version = "0.12.23", default-features = false }
6767
rmp-serde = "1.3.0"
68-
ruma = { git = "https://github.com/ruma/ruma", rev = "2f64faeabb85950de27e9829faeb389d2779ac57", features = [
68+
ruma = { git = "https://github.com/ruma/ruma", rev = "de227ef5c5b2d2ecff89ab021ebe53c5d565577d", features = [
6969
"client-api-c",
7070
"compat-upload-signatures",
7171
"compat-arbitrary-length-ids",
@@ -85,11 +85,12 @@ ruma = { git = "https://github.com/ruma/ruma", rev = "2f64faeabb85950de27e9829fa
8585
"unstable-msc4286",
8686
"unstable-msc4306",
8787
"unstable-msc4308",
88-
"unstable-msc4310"
88+
"unstable-msc4310",
89+
"unstable-msc4359"
8990
] }
9091
sentry = { version = "0.42.0", default-features = false }
9192
sentry-tracing = "0.42.0"
92-
serde = { version = "1.0.219", features = ["rc"] }
93+
serde = { version = "1.0.221", features = ["rc"] }
9394
serde_html_form = "0.2.7"
9495
serde_json = "1.0.143"
9596
sha2 = "0.10.9"

bindings/matrix-sdk-ffi/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ crate-type = [
2727
default = ["bundled-sqlite", "unstable-msc4274", "experimental-element-recent-emojis"]
2828
bundled-sqlite = ["matrix-sdk/bundled-sqlite"]
2929
unstable-msc4274 = ["matrix-sdk-ui/unstable-msc4274"]
30+
unstable-msc4359 = ["matrix-sdk-ui/unstable-msc4359"]
3031
# Required when targeting a Javascript environment, like Wasm in a browser.
3132
js = ["matrix-sdk-ui/js"]
3233
# Use the TLS implementation provided by the host system, necessary on iOS and Wasm platforms.

crates/matrix-sdk-base/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ testing = [
6565
# Add support for inline media galleries via msgtypes
6666
unstable-msc4274 = []
6767

68+
# "Do not Disturb" notification settings
69+
unstable-msc4359 = ["ruma/unstable-msc4359"]
70+
6871
experimental-element-recent-emojis = []
6972

7073
[dependencies]

crates/matrix-sdk-base/src/client.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ use matrix_sdk_crypto::{
3232
};
3333
#[cfg(doc)]
3434
use ruma::DeviceId;
35+
#[cfg(feature = "unstable-msc4359")]
36+
use ruma::events::do_not_disturb::{DoNotDisturbEventContent, DoNotDisturbRoomKey};
3537
#[cfg(feature = "e2e-encryption")]
3638
use ruma::events::room::{history_visibility::HistoryVisibility, member::MembershipState};
3739
use ruma::{
@@ -1135,6 +1137,37 @@ impl BaseClient {
11351137
}
11361138
}
11371139
}
1140+
1141+
/// Checks whether the provided `room_id` belongs to a room in "Do not
1142+
/// Disturb" mode.
1143+
#[cfg(feature = "unstable-msc4359")]
1144+
pub async fn is_room_in_do_not_disturb_mode(&self, room_id: &RoomId) -> bool {
1145+
match self.state_store.get_account_data_event_static::<DoNotDisturbEventContent>().await {
1146+
Ok(Some(raw_do_not_disturb_room_list)) => {
1147+
match raw_do_not_disturb_room_list.deserialize() {
1148+
Ok(current_do_not_disturb_room_list) => {
1149+
current_do_not_disturb_room_list
1150+
.content
1151+
.rooms
1152+
.contains_key(&DoNotDisturbRoomKey::AllRooms)
1153+
|| current_do_not_disturb_room_list
1154+
.content
1155+
.rooms
1156+
.contains_key(&DoNotDisturbRoomKey::SingleRoom(room_id.to_owned()))
1157+
}
1158+
Err(error) => {
1159+
warn!(?error, "Failed to deserialize the 'Do not Disturb' room list event");
1160+
false
1161+
}
1162+
}
1163+
}
1164+
Ok(None) => false,
1165+
Err(error) => {
1166+
warn!(?error, "Could not get the 'Do not Disturb' room list from the state store");
1167+
false
1168+
}
1169+
}
1170+
}
11381171
}
11391172

11401173
/// Represent the `required_state` values sent by a sync request.
@@ -1772,6 +1805,24 @@ mod tests {
17721805
assert!(client.is_user_ignored(ignored_user_id).await);
17731806
}
17741807

1808+
#[cfg(feature = "unstable-msc4359")]
1809+
#[async_test]
1810+
async fn test_is_room_in_do_not_disturb() {
1811+
let dnd_room_id = room_id!("!sonofagun");
1812+
let client = logged_in_base_client(None).await;
1813+
1814+
assert!(!client.is_room_in_do_not_disturb_mode(dnd_room_id).await);
1815+
1816+
let mut sync_builder = SyncResponseBuilder::new();
1817+
let f = EventFactory::new();
1818+
let response = sync_builder
1819+
.add_global_account_data(f.do_not_disturb_room_list([dnd_room_id.to_owned()]))
1820+
.build_sync_response();
1821+
client.receive_sync_response(response).await.unwrap();
1822+
1823+
assert!(client.is_room_in_do_not_disturb_mode(dnd_room_id).await);
1824+
}
1825+
17751826
#[async_test]
17761827
async fn test_invite_details_are_set() {
17771828
let user_id = user_id!("@alice:localhost");

crates/matrix-sdk-ui/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ unstable-msc3956 = ["ruma/unstable-msc3956"]
2525
# Add support for inline media galleries via msgtypes
2626
unstable-msc4274 = ["matrix-sdk/unstable-msc4274"]
2727

28+
# "Do not Disturb" notification settings
29+
unstable-msc4359 = ["matrix-sdk/unstable-msc4359"]
30+
2831
# Enable experimental support for encrypting state events; see
2932
# https://github.com/matrix-org/matrix-rust-sdk/issues/5397.
3033
experimental-encrypted-state-events = [
@@ -38,6 +41,7 @@ async-rx.workspace = true
3841
async-stream.workspace = true
3942
async_cell = "0.2.3"
4043
bitflags.workspace = true
44+
cfg-if.workspace = true
4145
chrono.workspace = true
4246
eyeball.workspace = true
4347
eyeball-im.workspace = true

crates/matrix-sdk-ui/src/notification_client.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use std::{
1818
time::Duration,
1919
};
2020

21+
use cfg_if::cfg_if;
2122
use futures_util::{StreamExt as _, pin_mut};
2223
use matrix_sdk::{
2324
Client, ClientBuildError, SlidingSyncList, SlidingSyncMode, room::Room, sleep::sleep,
@@ -637,7 +638,19 @@ impl NotificationClient {
637638
let notification_item =
638639
NotificationItem::new(room, raw_event, push_actions, state_events).await?;
639640

640-
if self.client.is_user_ignored(notification_item.event.sender()).await {
641+
let is_room_in_do_not_disturb = {
642+
cfg_if! {
643+
if #[cfg(feature = "unstable-msc4359")] {
644+
self.client.is_room_in_do_not_disturb_mode(room.room_id()).await
645+
} else {
646+
false
647+
}
648+
}
649+
};
650+
651+
if self.client.is_user_ignored(notification_item.event.sender()).await
652+
|| is_room_in_do_not_disturb
653+
{
641654
Ok(NotificationStatus::EventFilteredOut)
642655
} else {
643656
Ok(NotificationStatus::Event(Box::new(notification_item)))

crates/matrix-sdk-ui/tests/integration/notification_client.rs

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1095,6 +1095,112 @@ async fn test_notification_client_sliding_sync_filters_out_events_from_ignored_u
10951095
};
10961096
}
10971097

1098+
#[cfg(feature = "unstable-msc4359")]
1099+
#[async_test]
1100+
async fn test_notification_client_sliding_sync_filters_out_events_from_do_not_disturb_rooms() {
1101+
let server = MatrixMockServer::new().await;
1102+
let client = server.client_builder().build().await;
1103+
1104+
let sender = user_id!("@user:example.org");
1105+
let my_user_id = client.user_id().unwrap().to_owned();
1106+
1107+
let room_id = room_id!("!a98sd12bjh:example.org");
1108+
let room_name = "The Maltese Falcon";
1109+
let sender_display_name = "John Mastodon";
1110+
let event_id = event_id!("$example_event_id");
1111+
1112+
let raw_event = EventFactory::new()
1113+
.room(room_id)
1114+
.sender(sender)
1115+
.text_msg("Heya")
1116+
.event_id(event_id)
1117+
.into_raw_sync();
1118+
1119+
let event_factory = EventFactory::new().room(room_id);
1120+
1121+
let sender_member_event = event_factory
1122+
.member(sender)
1123+
.display_name(sender_display_name)
1124+
.membership(MembershipState::Join)
1125+
.into_raw_sync();
1126+
1127+
let own_member_event = event_factory
1128+
.member(&my_user_id)
1129+
.display_name("My self")
1130+
.membership(MembershipState::Join)
1131+
.into_raw_sync();
1132+
1133+
let power_levels_event =
1134+
event_factory.sender(sender).power_levels(&mut BTreeMap::new()).into_raw_sync();
1135+
1136+
let pos = Mutex::new(0);
1137+
Mock::given(SlidingSyncMatcher)
1138+
.respond_with(move |request: &Request| {
1139+
let partial_request: PartialSlidingSyncRequest = request.body_json().unwrap();
1140+
// Repeat the transaction id in the response, to validate sticky parameters.
1141+
let mut pos = pos.lock().unwrap();
1142+
*pos += 1;
1143+
let pos_as_str = (*pos).to_string();
1144+
ResponseTemplate::new(200).set_body_json(json!({
1145+
"txn_id": partial_request.txn_id,
1146+
"pos": pos_as_str,
1147+
"rooms": {
1148+
room_id: {
1149+
"name": room_name,
1150+
"initial": true,
1151+
1152+
"required_state": [
1153+
// Sender's member information.
1154+
sender_member_event,
1155+
1156+
// Own member information.
1157+
own_member_event,
1158+
1159+
// Power levels.
1160+
power_levels_event,
1161+
],
1162+
1163+
"timeline": [
1164+
raw_event,
1165+
]
1166+
}
1167+
},
1168+
1169+
"extensions": {
1170+
"account_data": {
1171+
"global": [{
1172+
"type": "dm.filament.do_not_disturb",
1173+
"content": {
1174+
"rooms": { room_id: {} }
1175+
}
1176+
}]
1177+
}
1178+
}
1179+
}))
1180+
})
1181+
.mount(server.server())
1182+
.await;
1183+
1184+
let dummy_sync_service = Arc::new(SyncService::builder(client.clone()).build().await.unwrap());
1185+
let process_setup =
1186+
NotificationProcessSetup::SingleProcess { sync_service: dummy_sync_service };
1187+
let notification_client = NotificationClient::new(client, process_setup).await.unwrap();
1188+
let mut result = notification_client
1189+
.get_notifications_with_sliding_sync(&[NotificationItemsRequest {
1190+
room_id: room_id.to_owned(),
1191+
event_ids: vec![event_id.to_owned()],
1192+
}])
1193+
.await
1194+
.unwrap();
1195+
1196+
let Some(Ok(item)) = result.remove(event_id) else {
1197+
panic!("fetching notification for {event_id} failed");
1198+
};
1199+
let NotificationStatus::EventFilteredOut = item else {
1200+
panic!("notification for {event_id} was not filtered out");
1201+
};
1202+
}
1203+
10981204
#[async_test]
10991205
async fn test_notification_client_context_filters_out_events_from_ignored_users() {
11001206
let server = MatrixMockServer::new().await;
@@ -1172,3 +1278,82 @@ async fn test_notification_client_context_filters_out_events_from_ignored_users(
11721278

11731279
assert_matches!(result, NotificationStatus::EventFilteredOut);
11741280
}
1281+
1282+
#[cfg(feature = "unstable-msc4359")]
1283+
#[async_test]
1284+
async fn test_notification_client_context_filters_out_events_from_do_not_disturb_rooms() {
1285+
let server = MatrixMockServer::new().await;
1286+
let client = server.client_builder().build().await;
1287+
1288+
let sender = user_id!("@user:example.org");
1289+
let room_id = room_id!("!a98sd12bjh:example.org");
1290+
let event_id = event_id!("$example_event_id");
1291+
1292+
server.sync_joined_room(&client, room_id).await;
1293+
1294+
// Add mock for sliding sync so we get the ignored user list from its account
1295+
// data
1296+
let pos = Mutex::new(0);
1297+
Mock::given(SlidingSyncMatcher)
1298+
.respond_with(move |request: &Request| {
1299+
let partial_request: PartialSlidingSyncRequest = request.body_json().unwrap();
1300+
// Repeat the transaction id in the response, to validate sticky parameters.
1301+
let mut pos = pos.lock().unwrap();
1302+
*pos += 1;
1303+
let pos_as_str = (*pos).to_string();
1304+
ResponseTemplate::new(200).set_body_json(json!({
1305+
"txn_id": partial_request.txn_id,
1306+
"pos": pos_as_str,
1307+
"rooms": {},
1308+
1309+
"extensions": {
1310+
"account_data": {
1311+
"global": [{
1312+
"type": "dm.filament.do_not_disturb",
1313+
"content": {
1314+
"rooms": { room_id: {} }
1315+
}
1316+
}]
1317+
}
1318+
}
1319+
}))
1320+
})
1321+
.mount(server.server())
1322+
.await;
1323+
1324+
let event = EventFactory::new()
1325+
.room(room_id)
1326+
.sender(sender)
1327+
.text_msg("Heya")
1328+
.event_id(event_id)
1329+
.into_event();
1330+
1331+
// Mock the /context response
1332+
server
1333+
.mock_room_event_context()
1334+
.ok(RoomContextResponseTemplate::new(event).start("start").end("end"))
1335+
.mock_once()
1336+
.mount()
1337+
.await;
1338+
1339+
let dummy_sync_service = Arc::new(SyncService::builder(client.clone()).build().await.unwrap());
1340+
let process_setup =
1341+
NotificationProcessSetup::SingleProcess { sync_service: dummy_sync_service };
1342+
let notification_client = NotificationClient::new(client, process_setup).await.unwrap();
1343+
1344+
// Call sync first so we get the list of ignored users in the notification
1345+
// client This should still work in a real life usage
1346+
let _ = notification_client
1347+
.get_notifications_with_sliding_sync(&[NotificationItemsRequest {
1348+
room_id: room_id.to_owned(),
1349+
event_ids: vec![event_id.to_owned()],
1350+
}])
1351+
.await;
1352+
1353+
// If the event is not found even though there was a mocked response for it, it
1354+
// was discarded as expected.
1355+
let result =
1356+
notification_client.get_notification_with_context(room_id, event_id).await.unwrap();
1357+
1358+
assert_matches!(result, NotificationStatus::EventFilteredOut);
1359+
}

crates/matrix-sdk/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ docsrs = ["e2e-encryption", "sqlite", "indexeddb", "sso-login", "qrcode"]
7171
# Add support for inline media galleries via msgtypes
7272
unstable-msc4274 = ["ruma/unstable-msc4274", "matrix-sdk-base/unstable-msc4274"]
7373

74+
# "Do not Disturb" notification settings
75+
unstable-msc4359 = ["matrix-sdk-base/unstable-msc4359"]
76+
7477
experimental-search = ["matrix-sdk-search"]
7578

7679
experimental-element-recent-emojis = ["matrix-sdk-base/experimental-element-recent-emojis"]

crates/matrix-sdk/src/client/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2860,6 +2860,13 @@ impl Client {
28602860
self.base_client().is_user_ignored(user_id).await
28612861
}
28622862

2863+
/// Checks whether the provided `room_id` belongs to a room in "Do not
2864+
/// Disturb" mode.
2865+
#[cfg(feature = "unstable-msc4359")]
2866+
pub async fn is_room_in_do_not_disturb_mode(&self, room_id: &RoomId) -> bool {
2867+
self.base_client().is_room_in_do_not_disturb_mode(room_id).await
2868+
}
2869+
28632870
/// Gets the `max_upload_size` value from the homeserver, getting either a
28642871
/// cached value or with a `/_matrix/client/v1/media/config` request if it's
28652872
/// missing.

0 commit comments

Comments
 (0)