atagen

macltok

public
Star0
Watch0

Commit 97fb5720d881

tarball

atagen <boss@atagen.co>2026-10-25 14:19 UTC

3 files changed+149-7
threads: date an entry from the cache when its latest has no time

continuwuity 26.9's bare-content latest_event leaves a page entry undated, and the replies already in the event cache never arrive again, so latest_activity_ms fell back to the root's timestamp and the list sorted threads by their roots. build_entry now takes the newest reply the event cache holds for that root when the bundled latest is unusable.
Modified file: docs/design-decisions.md+2 additions−1 deletions
@@ -144,7 +144,8 @@ corrected timestamp. the latest alone can't date a count. a summary from the
144144 event cache names its latest reply without the event, and continuwuity 26.9
145145 bundles the latest as bare content, so with only that check every reply the
146146 cache re-inserted read as new and a page's count doubled. a newer reply that
147-doesn't count still becomes the latest. replays, reloads from the store and
147+doesn't count still becomes the latest, and an entry built without a dated
148+latest takes the newest reply the event cache holds. replays, reloads from the store and
148149 back pagination never count. a reply sync brings to a thread the list hasn't
149150 got fetches its root, four at a time, and adds it. one from anywhere else
150151 doesn't, since that
Modified file: src/engine/thread_list.rs+34 additions−4 deletions
@@ -12,7 +12,9 @@ use matrix_sdk::{
1212 serde_helpers::extract_thread_root,
1313 };
1414 use matrix_sdk_ui::eyeball_im::VectorDiff;
15-use ruma::{OwnedEventId, OwnedRoomId, OwnedUserId, UInt, push::Action};
15+use ruma::{
16+ OwnedEventId, OwnedRoomId, OwnedUserId, UInt, events::relation::RelationType, push::Action,
17+};
1618 use tokio::sync::{broadcast::error::RecvError, mpsc};
1719
1820 use super::{
@@ -801,11 +803,16 @@ async fn build_entry(
801803 Some(latest) => normalise(room, &latest, current_user_id, media_prefix).await,
802804 None => None,
803805 };
806+ let latest = match latest {
807+ Some(latest) => Some(latest),
808+ None => cached_latest(room, &root.event_id, current_user_id, media_prefix).await,
809+ };
804810 let summary = ThreadSummary {
805811 reply_count: summary.as_ref().map_or(0, |summary| summary.num_replies),
806- latest_event_id: summary
807- .and_then(|summary| summary.latest_reply)
808- .or_else(|| latest.as_ref().map(|latest| latest.event_id.clone())),
812+ latest_event_id: latest
813+ .as_ref()
814+ .map(|latest| latest.event_id.clone())
815+ .or_else(|| summary.and_then(|summary| summary.latest_reply)),
809816 latest_event_timestamp_ms: latest.as_ref().map(|latest| latest.timestamp_ms),
810817 };
811818 let mut media = root.media;
@@ -829,6 +836,29 @@ async fn build_entry(
829836 })
830837 }
831838
839+/// the newest reply the event cache holds, for a summary with no usable latest event.
840+/// continuwuity 26.9 bundles only the latest's content, which dates nothing
841+async fn cached_latest(
842+ room: &Room,
843+ root_event_id: &OwnedEventId,
844+ current_user_id: Option<&ruma::UserId>,
845+ media_prefix: &str,
846+) -> Option<Normalised> {
847+ let (cache, _drop_handles) = room.event_cache().await.ok()?;
848+ let replies = cache
849+ .find_event_relations(root_event_id, Some(vec![RelationType::Thread]))
850+ .await
851+ .inspect_err(
852+ |error| tracing::debug!(%root_event_id, %error, "reading a thread's replies failed"),
853+ )
854+ .ok()?;
855+ let newest = replies
856+ .iter()
857+ .filter(|reply| extract_thread_root(reply.raw()).as_ref() == Some(root_event_id))
858+ .max_by_key(|reply| reply.timestamp())?;
859+ normalise(room, newest, current_user_id, media_prefix).await
860+}
861+
832862 fn now_ms() -> u64 {
833863 u64::from(ruma::MilliSecondsSinceUnixEpoch::now().get())
834864 }
Modified file: tests/state.rs+113 additions−2 deletions
@@ -4,12 +4,12 @@ use std::time::Duration;
44
55 use macltok::{
66 ConnectionState, Engine, EngineConfig, EngineError, Event, PackUsage, Retain, RoomMembership,
7- Termination, ThreadUnread,
7+ Termination, ThreadListEntry, ThreadUnread,
88 };
99 use matrix_sdk::{
1010 ThreadingSupport,
1111 ruma::{
12- MilliSecondsSinceUnixEpoch, RoomId,
12+ EventId, MilliSecondsSinceUnixEpoch, RoomId,
1313 api::client::sync::sync_events::v5,
1414 event_id,
1515 events::{
@@ -493,3 +493,114 @@ async fn a_thread_list_page_keeps_the_servers_count() {
493493
494494 support::shut_down(&handle, &mut events, running).await;
495495 }
496+
497+/// with continuwuity's undated latest, the page has to date each thread from the replies the
498+/// cache already holds, or it sorts by the root
499+#[tokio::test]
500+async fn a_thread_list_page_dates_threads_from_cached_replies() {
501+ let server = MatrixMockServer::new().await;
502+ server
503+ .mock_sliding_sync()
504+ .ok(v5::Response::new("1".to_owned()))
505+ .mount()
506+ .await;
507+ let client = server
508+ .client_builder()
509+ .on_builder(|builder| {
510+ builder.with_threading_support(ThreadingSupport::Enabled {
511+ with_subscriptions: false,
512+ })
513+ })
514+ .build()
515+ .await;
516+ let room_id = room_id!("!thread-dates:localhost");
517+ let own = client.user_id().unwrap().to_owned();
518+ server.mock_room_state_encryption().plain().mount().await;
519+ client.event_cache().subscribe().unwrap();
520+ let factory = EventFactory::new().room(room_id).sender(&ALICE);
521+ let (early, first, second, late) = (
522+ event_id!("$dates-early"),
523+ event_id!("$dates-first"),
524+ event_id!("$dates-second"),
525+ event_id!("$dates-late"),
526+ );
527+ server
528+ .sync_room(
529+ &client,
530+ JoinedRoomBuilder::new(room_id)
531+ .add_state_event(EventFactory::new().room(room_id).member(&own))
532+ .add_timeline_event(factory.text_msg("early").event_id(early).server_ts(1_000))
533+ .add_timeline_event(
534+ factory
535+ .text_msg("one")
536+ .in_thread(early, early)
537+ .event_id(first)
538+ .server_ts(5_000),
539+ )
540+ .add_timeline_event(factory.text_msg("late").event_id(late).server_ts(7_000))
541+ .add_timeline_event(
542+ factory
543+ .text_msg("two")
544+ .in_thread(early, first)
545+ .event_id(second)
546+ .server_ts(9_000),
547+ ),
548+ )
549+ .await;
550+ let listed = |root: &EventId, timestamp: u64, count: u64| -> Raw<AnyTimelineEvent> {
551+ serde_json::from_value(json!({
552+ "type": "m.room.message",
553+ "event_id": root,
554+ "room_id": room_id,
555+ "sender": &*ALICE,
556+ "origin_server_ts": timestamp,
557+ "content": { "msgtype": "m.text", "body": "root" },
558+ "unsigned": { "m.relations": { "m.thread": {
559+ "latest_event": { "msgtype": "m.text", "body": "latest" },
560+ "count": count,
561+ "current_user_participated": false,
562+ } } },
563+ }))
564+ .unwrap()
565+ };
566+ server
567+ .mock_room_threads()
568+ .ok(vec![listed(late, 7_000, 1), listed(early, 1_000, 2)], None)
569+ .mount()
570+ .await;
571+ let config = EngineConfig::default().with_initial_history_limit(0);
572+ let (engine, handle, mut events) = Engine::from_client(client.clone(), config).await.unwrap();
573+ let running = tokio::spawn(engine.run());
574+
575+ handle.open_thread_list(room_id.to_owned()).await.unwrap();
576+ handle
577+ .paginate_thread_list(room_id.to_owned(), 20)
578+ .await
579+ .unwrap();
580+ let changed = support::receive_until(&mut events, |event| {
581+ matches!(event, Event::ThreadListChanged { .. })
582+ })
583+ .await;
584+ let Event::ThreadListChanged { upserts, .. } = changed else {
585+ unreachable!()
586+ };
587+ let mut entries = macltok::imbl::Vector::new();
588+ for entry in upserts {
589+ ThreadListEntry::upsert(&mut entries, entry);
590+ }
591+ let order: Vec<_> = entries
592+ .iter()
593+ .map(|entry| (entry.root_event_id.as_str(), entry.latest_activity_ms))
594+ .collect();
595+ assert_eq!(
596+ order,
597+ [(early.as_str(), 9_000), (late.as_str(), 7_000)],
598+ "a thread was dated by its root rather than its newest cached reply"
599+ );
600+ let dated = &entries[0];
601+ assert_eq!(dated.summary.latest_event_id.as_deref(), Some(second));
602+ assert_eq!(dated.summary.latest_event_timestamp_ms, Some(9_000));
603+ assert_eq!(dated.summary.reply_count, 2);
604+
605+ support::shut_down(&handle, &mut events, running).await;
606+}