Commit 6b04099c763c
tarball5 files changed+162-69
@@ -18,7 +18,7 @@ use crate::{
1818
engine::MediaService,1919
normalise::{2020
MediaRegistration, MediaScope, apply_profiles, member_sender, normalise_context,21
- normalise_observed, resolve_replies, stored_member_sender,21
+ normalise_observed, resolve_replies,2222
},2323
transport::EventSender,2424
};@@ -550,52 +550,6 @@ async fn fetch_reply(
550550
}551551
}552552
553
-/// settles an event built outside any page from what the client already holds, so it never554
-/// waits on the network. the sender gets its stored profile and the reply comes from `known`555
-/// or the event cache, or is unavailable556
-pub(crate) async fn settle_offline(557
- room: &Room,558
- current_user_id: Option<&ruma::UserId>,559
- item: &mut TimelineItem,560
- known: impl Fn(&ruma::EventId) -> Option<TimelineEvent>,561
-) {562
- let TimelineItem::Event(event) = item else {563
- return;564
- };565
- if let Some(sender) = stored_member_sender(room, &event.sender.user_id).await {566
- event.sender = sender;567
- }568
- let Some(reply) = &event.reply_to else {569
- return;570
- };571
- let target_id = reply.event_id.clone();572
- let mut target = known(&target_id);573
- if target.is_none()574
- && let Ok((cache, _handles)) = room.event_cache().await575
- && let Ok(Some(cached)) = cache.find_event(&target_id).await576
- {577
- target = serde_json::from_str::<Value>(cached.raw().json().get())578
- .ok()579
- .and_then(|value| {580
- let identity = format!("{}\n{target_id}", room.room_id());581
- match normalise_observed(&value, current_user_id, &identity)? {582
- (TimelineItem::Event(event), _) => Some(*event),583
- _ => None,584
- }585
- });586
- }587
- if let Some(target) = &mut target588
- && let Some(sender) = stored_member_sender(room, &target.sender.user_id).await589
- {590
- target.sender = sender;591
- }592
- let targets: HashMap<_, _> = target593
- .map(|target| (target_id, target))594
- .into_iter()595
- .collect();596
- resolve_replies(std::iter::once(&mut **event), &targets);597
-}598
-599553
/// under lazy-loaded members a sender may be unknown locally, and `get_member` fetches the600554
/// member list once for the room when it is601555
async fn profiles<'a>(@@ -1,5 +1,5 @@
11
use std::{2
- collections::{BTreeMap, BTreeSet},2
+ collections::{BTreeMap, BTreeSet, HashMap},33
sync::{44
Arc,55
atomic::{AtomicBool, Ordering},@@ -32,7 +32,7 @@ use crate::{
3232
MentionGapReason, MentionKey, MentionReason, MentionsCommand, MentionsPage, MentionsUpdate,3333
TimelineContent, TimelineItem,3434
engine::{MediaService, MentionsStore, context, executor::EngineBoxFuture},35
- normalise::{MediaRegistration, MediaScope, normalise_observed},35
+ normalise::{MediaRegistration, MediaScope, normalise_observed, resolve_replies},3636
transport::EventSender,3737
};3838
@@ -897,18 +897,22 @@ impl Service {
897897
else {898898
return;899899
};900
- if let Some(room) = self.client.get_room(room_id) {901
- let known = |event_id: &ruma::EventId| {902
- let key = id(&MentionKey {903
- room_id: room_id.clone(),904
- event_id: event_id.to_owned(),905
- });906
- match &data.events.get(&key)?.item {907
- TimelineItem::Event(event) => Some((**event).clone()),908
- _ => None,900
+ // the commit hook waits on this actor, so a reply settles here only from what the901
+ // inbox already holds, and the capture fetches profiles and anything else later902
+ if let TimelineItem::Event(event) = &mut item903
+ && let Some(reply) = &event.reply_to904
+ {905
+ let target = id(&MentionKey {906
+ room_id: room_id.clone(),907
+ event_id: reply.event_id.clone(),908
+ });909
+ let targets: HashMap<_, _> = match data.events.get(&target).map(|saved| &saved.item) {910
+ Some(TimelineItem::Event(saved)) => {911
+ HashMap::from([(reply.event_id.clone(), (**saved).clone())])909912
}913
+ _ => HashMap::new(),910914
};911
- context::settle_offline(&room, Some(&self.account), &mut item, known).await;915
+ resolve_replies(std::iter::once(&mut **event), &targets);912916
}913917
self.supplement(data, &key, &item, &media, &value);914918
let (reasons, suppressed, unresolved) = self.classify(data, room_id, &value).await;@@ -33,7 +33,7 @@ pub(crate) async fn member_sender(room: &Room, user_id: &OwnedUserId) -> Option<
3333
}3434
3535
/// what the state store already knows, without asking the server36
-pub(crate) async fn stored_member_sender(room: &Room, user_id: &OwnedUserId) -> Option<Sender> {36
+async fn stored_member_sender(room: &Room, user_id: &OwnedUserId) -> Option<Sender> {3737
let member = room.get_member_no_sync(user_id).await.ok().flatten()?;3838
Some(sender(user_id, &member))3939
}@@ -15,7 +15,7 @@ pub(crate) use image_pack::{
1515
};1616
pub(crate) use link_preview::{ROOM_PREVIEW_URLS, server_preview, url_previews_allowed};1717
pub(crate) use media::{MediaRegistration, MediaScope, MediaSourceInfo};18
-pub(crate) use member::{member_sender, member_summary, stored_member_sender};18
+pub(crate) use member::{member_sender, member_summary};1919
pub(crate) use observed::{normalise_observed, poll_question};2020
pub(crate) use room::notification_mode;2121
pub(crate) use room::{room_summaries, space_child, stored_space_order};@@ -1154,14 +1154,43 @@ async fn inbox_rows_carry_sender_profiles_and_never_leave_replies_pending() {
11541154
event["content"]["m.relates_to"] = json!({ "m.in_reply_to": { "event_id": target } });11551155
event11561156
};1157
- feed.replace(1158
- room_id,1159
- vec![1160
- reply("$answer", "$parent", 2_000),1161
- reply("$orphan", "$missing", 2_100),1162
- ],1163
- );1164
- let page = mentions_page(&mut events, room_id, |page| page.entries.len() == 2).await;1157
+ let answer = reply("$answer", "$parent", 2_000);1158
+ let orphan = reply("$orphan", "$missing", 2_100);1159
+ for event in [&answer, &orphan] {1160
+ let mut event = event.clone();1161
+ event["room_id"] = json!(room_id);1162
+ server1163
+ .mock_room_event_context()1164
+ .room(room_id.to_owned())1165
+ .match_event_id()1166
+ .ok(RoomContextResponseTemplate::new(1167
+ matrix_sdk::deserialized_responses::TimelineEvent::from_plaintext(1168
+ ruma::serde::Raw::new(&event).unwrap().cast_unchecked(),1169
+ ),1170
+ ))1171
+ .mount()1172
+ .await;1173
+ }1174
+ feed.replace(room_id, vec![answer, orphan]);1175
+ let settled = |page: &MentionsPage| {1176
+ page.entries.len() == 21177
+ && page.entries.iter().all(|entry| {1178
+ matches!(&entry.event, macltok::TimelineItem::Event(event)1179
+ if event.sender.display_name.is_some()1180
+ && event.reply_to.as_ref().is_some_and(|reply| {1181
+ !matches!(reply.details, macltok::ReplyDetails::Pending)1182
+ }))1183
+ })1184
+ };1185
+ let page = mentions_page(&mut events, room_id, |page| {1186
+ settled(page)1187
+ && page.entries.iter().any(|entry| {1188
+ matches!(&entry.event, macltok::TimelineItem::Event(event)1189
+ if matches!(event.reply_to.as_ref().unwrap().details,1190
+ macltok::ReplyDetails::Ready { .. }))1191
+ })1192
+ })1193
+ .await;11651194
for entry in &page.entries {11661195
let macltok::TimelineItem::Event(event) = &entry.event else {11671196
panic!("a mention should be an event");@@ -1179,3 +1208,109 @@ async fn inbox_rows_carry_sender_profiles_and_never_leave_replies_pending() {
11791208
}11801209
shut_down(&handle, &mut events, running).await;11811210
}1211
+1212
+#[cfg(feature = "sqlite")]1213
+#[tokio::test]1214
+async fn a_resumed_inbox_with_threaded_replies_still_answers_a_page() {1215
+ static NEXT_TEMPORARY_PATH: AtomicUsize = AtomicUsize::new(0);1216
+1217
+ let server = MatrixMockServer::new().await;1218
+ server1219
+ .mock_versions()1220
+ .with_simplified_sliding_sync()1221
+ .ok()1222
+ .mount()1223
+ .await;1224
+ server.mock_login().ok().mount().await;1225
+ let root = std::env::temp_dir().join(format!(1226
+ "macltok-mentions-threads-{}-{}",1227
+ std::process::id(),1228
+ NEXT_TEMPORARY_PATH.fetch_add(1, Ordering::Relaxed)1229
+ ));1230
+ let sessions = Arc::new(EncryptedSessionStore::file(1231
+ root.join("records").join("session"),1232
+ StoreConfig::sqlite(root.join("store"), "mentions thread test passphrase"),1233
+ ));1234
+ let room_id = room_id!("!mentions-threads:matrix.org");1235
+ let own_user = user_id!("@cheeky_monkey:matrix.org");1236
+ let alice = "@alice:matrix.org";1237
+ let threaded = |event_id: &str, timestamp| {1238
+ let mut event = message(1239
+ event_id,1240
+ alice,1241
+ timestamp,1242
+ "threaded mention",1243
+ Some(own_user.as_str()),1244
+ );1245
+ event["content"]["m.relates_to"] = json!({1246
+ "rel_type": "m.thread",1247
+ "event_id": "$root",1248
+ "is_falling_back": true,1249
+ "m.in_reply_to": { "event_id": "$root" },1250
+ });1251
+ event1252
+ };1253
+ let feed = SlidingFeed::new(1254
+ room_id.to_owned(),1255
+ vec![1256
+ message("$root", alice, 6_000, "thread root", None),1257
+ threaded("$thread-a", 7_000),1258
+ threaded("$thread-b", 7_100),1259
+ message(1260
+ "$plain",1261
+ alice,1262
+ 7_200,1263
+ "plain mention",1264
+ Some(own_user.as_str()),1265
+ ),1266
+ ],1267
+ );1268
+ feed.set_required_state(1269
+ room_id,1270
+ vec![json!({1271
+ "type": "m.room.member",1272
+ "event_id": "$joined-member",1273
+ "sender": own_user,1274
+ "state_key": own_user,1275
+ "origin_server_ts": 1,1276
+ "content": { "membership": "join" },1277
+ })],1278
+ );1279
+ feed.mount(&server).await;1280
+1281
+ let config = EngineConfig::default()1282
+ .with_initial_history_limit(0)1283
+ .with_search_backfill_pages_per_room(0);1284
+ let session = Session::login_password(1285
+ PasswordLogin::new(server.uri(), "example", "wordpass").allow_insecure_loopback(true),1286
+ sessions.clone(),1287
+ )1288
+ .await1289
+ .unwrap();1290
+ let (engine, handle, mut events) = session.build(config.clone()).await.unwrap();1291
+ let running = tokio::spawn(engine.run());1292
+ mentions_page(&mut events, room_id, |page| page.entries.len() == 3).await;1293
+ shut_down(&handle, &mut events, running).await;1294
+ drop(handle);1295
+1296
+ let Some(Restore::Resumed(session)) = Session::restore(sessions.clone()).await.unwrap() else {1297
+ panic!("the saved session did not resume");1298
+ };1299
+ let (engine, handle, mut events) = session.build(config).await.unwrap();1300
+ let running = tokio::spawn(engine.run());1301
+ receive_until(&mut events, |event| {1302
+ matches!(event, Event::Authenticated { .. })1303
+ })1304
+ .await;1305
+ wait_for_requests(&feed, 4).await;1306
+ let resumed = tokio::time::timeout(Duration::from_secs(5), handle.mentions_page(None, 50))1307
+ .await1308
+ .expect("the resumed inbox did not answer a page")1309
+ .unwrap();1310
+ assert_eq!(resumed.entries.len(), 3);1311
+ shut_down(&handle, &mut events, running).await;1312
+ drop(handle);1313
+1314
+ sessions.discard().await.unwrap();1315
+ std::fs::remove_dir_all(root).unwrap();1316
+}