Skip to content

Commit 0b9daa2

Browse files
committed
timeline: when deduplicating event meta, keep the most recent instead of the oldest
Not doing this leads to an invalid ordering of events, as shown in the test: if we increase the timeline limit of a room using sliding sync, we'll receive a duplicate event, and if we ignore it, it'll be in an invalid position. The solution is to keep the most recent event (and remove the old one from event_meta).
1 parent 304350f commit 0b9daa2

File tree

3 files changed

+86
-8
lines changed

3 files changed

+86
-8
lines changed

crates/matrix-sdk-ui/src/timeline/inner/state.rs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -664,26 +664,35 @@ impl TimelineInnerStateTransaction<'_> {
664664
room_data_provider: &P,
665665
settings: &TimelineInnerSettings,
666666
) -> bool {
667-
// Detect if an event already exists in [`TimelineInnerMeta::all_events`]
668-
fn event_already_exists(new_event_id: &EventId, all_events: &VecDeque<EventMeta>) -> bool {
669-
all_events.iter().any(|EventMeta { event_id, .. }| event_id == new_event_id)
667+
// Detect if an event already exists in [`TimelineInnerMeta::all_events`].
668+
//
669+
// Returns its position, in this case.
670+
fn event_already_exists(
671+
new_event_id: &EventId,
672+
all_events: &VecDeque<EventMeta>,
673+
) -> Option<usize> {
674+
all_events.iter().position(|EventMeta { event_id, .. }| event_id == new_event_id)
670675
}
671676

672677
match position {
673678
TimelineItemPosition::Start { .. } => {
674-
if event_already_exists(event_meta.event_id, &self.meta.all_events) {
675-
return false;
679+
if let Some(pos) = event_already_exists(event_meta.event_id, &self.meta.all_events)
680+
{
681+
self.meta.all_events.remove(pos);
676682
}
677683

678684
self.meta.all_events.push_front(event_meta.base_meta())
679685
}
686+
680687
TimelineItemPosition::End { .. } => {
681-
if event_already_exists(event_meta.event_id, &self.meta.all_events) {
682-
return false;
688+
if let Some(pos) = event_already_exists(event_meta.event_id, &self.meta.all_events)
689+
{
690+
self.meta.all_events.remove(pos);
683691
}
684692

685693
self.meta.all_events.push_back(event_meta.base_meta());
686694
}
695+
687696
#[cfg(feature = "e2e-encryption")]
688697
TimelineItemPosition::Update(_) => {
689698
if let Some(event) =

crates/matrix-sdk-ui/tests/integration/timeline/mod.rs

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ use matrix_sdk_test::{
2828
};
2929
use matrix_sdk_ui::timeline::{EventSendState, RoomExt, TimelineItemContent, VirtualTimelineItem};
3030
use ruma::{
31-
events::room::message::RoomMessageEventContent, room_id, user_id, MilliSecondsSinceUnixEpoch,
31+
event_id, events::room::message::RoomMessageEventContent, room_id, user_id,
32+
MilliSecondsSinceUnixEpoch,
3233
};
3334
use serde_json::json;
3435
use wiremock::{
@@ -442,3 +443,67 @@ async fn test_sync_highlighted() {
442443
// `m.room.tombstone` should be highlighted by default.
443444
assert!(remote_event.is_highlighted());
444445
}
446+
447+
#[async_test]
448+
async fn test_duplicate_maintains_correct_order() {
449+
let room_id = room_id!("!a98sd12bjh:example.org");
450+
let (client, server) = logged_in_client_with_server().await;
451+
let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000));
452+
453+
let mut sync_builder = SyncResponseBuilder::new();
454+
sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id));
455+
456+
mock_sync(&server, sync_builder.build_json_sync_response(), None).await;
457+
let _response = client.sync_once(sync_settings.clone()).await.unwrap();
458+
server.reset().await;
459+
460+
let room = client.get_room(room_id).unwrap();
461+
let timeline = room.timeline().await.unwrap();
462+
463+
// At the beginning, the timeline is empty.
464+
assert!(timeline.items().await.is_empty());
465+
466+
let f = EventFactory::new().sender(user_id!("@a:b.c"));
467+
468+
// We receive an event F, from a sliding sync with timeline limit=1.
469+
sync_builder.add_joined_room(
470+
JoinedRoomBuilder::new(room_id)
471+
.add_timeline_event(f.text_msg("C").event_id(event_id!("$c")).into_raw_sync()),
472+
);
473+
474+
mock_sync(&server, sync_builder.build_json_sync_response(), None).await;
475+
let _response = client.sync_once(sync_settings.clone()).await.unwrap();
476+
server.reset().await;
477+
478+
// The timeline item represents the message we just received.
479+
let items = timeline.items().await;
480+
assert_eq!(items.len(), 2);
481+
482+
assert!(items[0].is_day_divider());
483+
let content = items[1].as_event().unwrap().content().as_message().unwrap().body();
484+
assert_eq!(content, "C");
485+
486+
// We receive multiple events, and C is now the last one (because we supposedly
487+
// increased the timeline limit).
488+
sync_builder.add_joined_room(
489+
JoinedRoomBuilder::new(room_id)
490+
.add_timeline_event(f.text_msg("A").event_id(event_id!("$a")).into_raw_sync())
491+
.add_timeline_event(f.text_msg("B").event_id(event_id!("$b")).into_raw_sync())
492+
.add_timeline_event(f.text_msg("C").event_id(event_id!("$c")).into_raw_sync()),
493+
);
494+
495+
mock_sync(&server, sync_builder.build_json_sync_response(), None).await;
496+
let _response = client.sync_once(sync_settings.clone()).await.unwrap();
497+
server.reset().await;
498+
499+
let items = timeline.items().await;
500+
assert_eq!(items.len(), 4, "{items:?}");
501+
502+
assert!(items[0].is_day_divider());
503+
let content = items[1].as_event().unwrap().content().as_message().unwrap().body();
504+
assert_eq!(content, "A");
505+
let content = items[2].as_event().unwrap().content().as_message().unwrap().body();
506+
assert_eq!(content, "B");
507+
let content = items[3].as_event().unwrap().content().as_message().unwrap().body();
508+
assert_eq!(content, "C");
509+
}

crates/matrix-sdk-ui/tests/integration/timeline/sliding_sync.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,10 @@ async fn test_timeline_duplicated_events() -> Result<()> {
416416
assert_timeline_stream! {
417417
[timeline_stream]
418418
update[3] "$x3:bar.org";
419+
update[1] "$x1:bar.org";
420+
remove[1];
421+
append "$x1:bar.org";
422+
update[3] "$x1:bar.org";
419423
append "$x4:bar.org";
420424
};
421425
}

0 commit comments

Comments
 (0)