Skip to content

Commit ab98028

Browse files
committed
feat(sdk): An edit can be a LatestEventValue if it targets the immediate previous event.
This patch changes the rule of what is a `LatestEventValue` candidate in case of an edit. An edit must target/relate to its immediate previous event to be a candidate. Otherwise it's easy to edit an old message and create a “broken” `LatestEventValue` because it points to an older message that the user may not be able to find easily.
1 parent 7c7cbb2 commit ab98028

File tree

2 files changed

+127
-37
lines changed

2 files changed

+127
-37
lines changed

crates/matrix-sdk/src/event_cache/room/mod.rs

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -322,11 +322,14 @@ impl RoomEventCache {
322322
/// Try to find a single event in this room, starting from the most recent
323323
/// event.
324324
///
325+
/// The `predicate` receives two arguments: the current event, and the
326+
/// ID of the _previous_ (older) event.
327+
///
325328
/// **Warning**! It looks into the loaded events from the in-memory linked
326329
/// chunk **only**. It doesn't look inside the storage.
327330
pub async fn rfind_map_event_in_memory_by<O, P>(&self, predicate: P) -> Result<Option<O>>
328331
where
329-
P: FnMut(&Event) -> Option<O>,
332+
P: FnMut(&Event, Option<OwnedEventId>) -> Option<O>,
330333
{
331334
Ok(self.inner.state.read().await?.rfind_map_event_in_memory_by(predicate))
332335
}
@@ -632,6 +635,7 @@ mod private {
632635

633636
use eyeball::SharedObservable;
634637
use eyeball_im::VectorDiff;
638+
use itertools::Itertools;
635639
use matrix_sdk_base::{
636640
apply_redaction,
637641
deserialized_responses::{ThreadSummary, ThreadSummaryStatus, TimelineEventKind},
@@ -1096,14 +1100,30 @@ mod private {
10961100

10971101
//// Find a single event in this room, starting from the most recent event.
10981102
///
1103+
/// The `predicate` receives two arguments: the current event, and the
1104+
/// ID of the _previous_ (older) event.
1105+
///
10991106
/// **Warning**! It looks into the loaded events from the in-memory
11001107
/// linked chunk **only**. It doesn't look inside the storage,
11011108
/// contrary to [`Self::find_event`].
11021109
pub fn rfind_map_event_in_memory_by<O, P>(&self, mut predicate: P) -> Option<O>
11031110
where
1104-
P: FnMut(&Event) -> Option<O>,
1111+
P: FnMut(&Event, Option<OwnedEventId>) -> Option<O>,
11051112
{
1106-
self.state.room_linked_chunk.revents().find_map(|(_position, event)| predicate(event))
1113+
self.state
1114+
.room_linked_chunk
1115+
.revents()
1116+
.peekable()
1117+
.batching(|iter| {
1118+
iter.next().map(|(_position, event)| {
1119+
(
1120+
event,
1121+
iter.peek()
1122+
.and_then(|(_next_position, next_event)| next_event.event_id()),
1123+
)
1124+
})
1125+
})
1126+
.find_map(|(event, next_event_id)| predicate(event, next_event_id))
11071127
}
11081128

11091129
#[cfg(test)]
@@ -3788,32 +3808,34 @@ mod timed_tests {
37883808
// Look for an event from `BOB`: it must be `event_0`.
37893809
assert_matches!(
37903810
room_event_cache
3791-
.rfind_map_event_in_memory_by(|event| {
3792-
(event.raw().get_field::<OwnedUserId>("sender").unwrap().as_deref() == Some(*BOB)).then(|| event.event_id())
3811+
.rfind_map_event_in_memory_by(|event, previous_event_id| {
3812+
(event.raw().get_field::<OwnedUserId>("sender").unwrap().as_deref() == Some(*BOB)).then(|| (event.event_id(), previous_event_id))
37933813
})
37943814
.await,
3795-
Ok(Some(event_id)) => {
3815+
Ok(Some((event_id, previous_event_id))) => {
37963816
assert_eq!(event_id.as_deref(), Some(event_id_0));
3817+
assert!(previous_event_id.is_none());
37973818
}
37983819
);
37993820

38003821
// Look for an event from `ALICE`: it must be `event_2`, right before `event_1`
38013822
// because events are looked for in reverse order.
38023823
assert_matches!(
38033824
room_event_cache
3804-
.rfind_map_event_in_memory_by(|event| {
3805-
(event.raw().get_field::<OwnedUserId>("sender").unwrap().as_deref() == Some(*ALICE)).then(|| event.event_id())
3825+
.rfind_map_event_in_memory_by(|event, previous_event_id| {
3826+
(event.raw().get_field::<OwnedUserId>("sender").unwrap().as_deref() == Some(*ALICE)).then(|| (event.event_id(), previous_event_id))
38063827
})
38073828
.await,
3808-
Ok(Some(event_id)) => {
3829+
Ok(Some((event_id, previous_event_id))) => {
38093830
assert_eq!(event_id.as_deref(), Some(event_id_2));
3831+
assert_eq!(previous_event_id.as_deref(), Some(event_id_1));
38103832
}
38113833
);
38123834

38133835
// Look for an event that is inside the storage, but not loaded.
38143836
assert!(
38153837
room_event_cache
3816-
.rfind_map_event_in_memory_by(|event| {
3838+
.rfind_map_event_in_memory_by(|event, _| {
38173839
(event.raw().get_field::<OwnedUserId>("sender").unwrap().as_deref()
38183840
== Some(user_id))
38193841
.then(|| event.event_id())
@@ -3825,7 +3847,11 @@ mod timed_tests {
38253847

38263848
// Look for an event that doesn't exist.
38273849
assert!(
3828-
room_event_cache.rfind_map_event_in_memory_by(|_| None::<()>).await.unwrap().is_none()
3850+
room_event_cache
3851+
.rfind_map_event_in_memory_by(|_, _| None::<()>)
3852+
.await
3853+
.unwrap()
3854+
.is_none()
38293855
);
38303856
}
38313857

@@ -4412,7 +4438,7 @@ mod timed_tests {
44124438

44134439
async fn event_loaded(room_event_cache: &RoomEventCache, event_id: &EventId) -> bool {
44144440
room_event_cache
4415-
.rfind_map_event_in_memory_by(|event| {
4441+
.rfind_map_event_in_memory_by(|event, _previous_event_id| {
44164442
(event.event_id().as_deref() == Some(event_id)).then_some(())
44174443
})
44184444
.await

crates/matrix-sdk/src/latest_events/latest_event.rs

Lines changed: 89 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,12 @@ use ruma::{
2929
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedTransactionId, TransactionId, UserId,
3030
events::{
3131
AnyMessageLikeEventContent, AnySyncStateEvent, AnySyncTimelineEvent, SyncStateEvent,
32-
room::{member::MembershipState, message::MessageType, power_levels::RoomPowerLevels},
32+
relation::Replacement,
33+
room::{
34+
member::MembershipState,
35+
message::{MessageType, Relation},
36+
power_levels::RoomPowerLevels,
37+
},
3338
},
3439
};
3540
use tracing::{error, instrument, warn};
@@ -519,8 +524,9 @@ impl LatestEventValueBuilder {
519524
power_levels: Option<&RoomPowerLevels>,
520525
) -> LatestEventValue {
521526
if let Ok(Some(event)) = room_event_cache
522-
.rfind_map_event_in_memory_by(|event| {
523-
filter_timeline_event(event, own_user_id, power_levels).then(|| event.clone())
527+
.rfind_map_event_in_memory_by(|event, previous_event_id| {
528+
filter_timeline_event(event, previous_event_id, own_user_id, power_levels)
529+
.then(|| event.clone())
524530
})
525531
.await
526532
{
@@ -552,7 +558,7 @@ impl LatestEventValueBuilder {
552558
LocalEchoContent::Event { serialized_event: serialized_event_content, .. } => {
553559
match serialized_event_content.deserialize() {
554560
Ok(content) => {
555-
if filter_any_message_like_event_content(content) {
561+
if filter_any_message_like_event_content(content, None) {
556562
let local_value = LocalLatestEventValue {
557563
timestamp: MilliSecondsSinceUnixEpoch::now(),
558564
content: serialized_event_content.clone(),
@@ -660,7 +666,7 @@ impl LatestEventValueBuilder {
660666
if let Some(position) = buffer_of_values_for_local_events.position(transaction_id) {
661667
match new_serialized_event_content.deserialize() {
662668
Ok(content) => {
663-
if filter_any_message_like_event_content(content) {
669+
if filter_any_message_like_event_content(content, None) {
664670
buffer_of_values_for_local_events.replace_content(
665671
position,
666672
new_serialized_event_content.clone(),
@@ -933,6 +939,7 @@ impl LatestEventValuesForLocalEvents {
933939

934940
fn filter_timeline_event(
935941
event: &TimelineEvent,
942+
previous_event_id: Option<OwnedEventId>,
936943
own_user_id: Option<&UserId>,
937944
power_levels: Option<&RoomPowerLevels>,
938945
) -> bool {
@@ -953,9 +960,10 @@ fn filter_timeline_event(
953960
match event {
954961
AnySyncTimelineEvent::MessageLike(message_like_event) => {
955962
match message_like_event.original_content() {
956-
Some(any_message_like_event_content) => {
957-
filter_any_message_like_event_content(any_message_like_event_content)
958-
}
963+
Some(any_message_like_event_content) => filter_any_message_like_event_content(
964+
any_message_like_event_content,
965+
previous_event_id,
966+
),
959967

960968
// The event has been redacted.
961969
None => true,
@@ -968,15 +976,27 @@ fn filter_timeline_event(
968976
}
969977
}
970978

971-
fn filter_any_message_like_event_content(event: AnyMessageLikeEventContent) -> bool {
979+
fn filter_any_message_like_event_content(
980+
event: AnyMessageLikeEventContent,
981+
previous_event_id: Option<OwnedEventId>,
982+
) -> bool {
972983
match event {
973984
AnyMessageLikeEventContent::RoomMessage(message) => {
974985
// Don't show incoming verification requests.
975986
if let MessageType::VerificationRequest(_) = message.msgtype {
976987
return false;
977988
}
978989

979-
true
990+
// Not all relations are accepted. Let's filter them.
991+
match &message.relates_to {
992+
Some(Relation::Replacement(Replacement { event_id, .. })) => {
993+
// If the edit relates to the immediate previous event, this is an acceptable
994+
// latest event, otherwise let's ignore it.
995+
Some(event_id) == previous_event_id.as_ref()
996+
}
997+
998+
_ => true,
999+
}
9801000
}
9811001

9821002
AnyMessageLikeEventContent::UnstablePollStart(_)
@@ -1074,7 +1094,7 @@ mod tests_latest_event_content {
10741094
$event_builder
10751095
};
10761096

1077-
assert_eq!(filter_timeline_event(&event, Some(user_id!("@mnt_io:matrix.org")), None), $expect );
1097+
assert_eq!(filter_timeline_event(&event, None, Some(user_id!("@mnt_io:matrix.org")), None), $expect );
10781098
};
10791099
}
10801100

@@ -1103,15 +1123,59 @@ mod tests_latest_event_content {
11031123

11041124
#[test]
11051125
fn test_room_message_replacement() {
1106-
assert_latest_event_content!(
1107-
event | event_factory | {
1108-
event_factory
1109-
.text_msg("bonjour")
1110-
.edit(event_id!("$ev0"), RoomMessageEventContent::text_plain("hello").into())
1111-
.into_event()
1112-
}
1113-
is a candidate
1114-
);
1126+
let user_id = user_id!("@mnt_io:matrix.org");
1127+
let event_factory = EventFactory::new().sender(user_id);
1128+
let event = event_factory
1129+
.text_msg("bonjour")
1130+
.edit(event_id!("$ev0"), RoomMessageEventContent::text_plain("hello").into())
1131+
.into_event();
1132+
1133+
// Without a previous event.
1134+
//
1135+
// This is an edge case where either the event cache has been emptied and only
1136+
// the edit is received via the sync for example, or either the previous event
1137+
// is part of another chunk that is not loaded in memory yet. In this case,
1138+
// let's not consider the event as a `LatestEventValue` candidate.
1139+
{
1140+
let previous_event_id = None;
1141+
1142+
assert!(
1143+
filter_timeline_event(
1144+
&event,
1145+
previous_event_id,
1146+
Some(user_id!("@mnt_io:matrix.org")),
1147+
None
1148+
)
1149+
.not()
1150+
);
1151+
}
1152+
1153+
// With a previous event, but the one being replaced.
1154+
{
1155+
let previous_event_id = Some(event_id!("$ev1").to_owned());
1156+
1157+
assert!(
1158+
filter_timeline_event(
1159+
&event,
1160+
previous_event_id,
1161+
Some(user_id!("@mnt_io:matrix.org")),
1162+
None
1163+
)
1164+
.not()
1165+
);
1166+
}
1167+
1168+
// With a previous event, and that's the one being replaced!
1169+
{
1170+
let previous_event_id = Some(event_id!("$ev0").to_owned());
1171+
1172+
assert!(filter_timeline_event(
1173+
&event,
1174+
previous_event_id,
1175+
Some(user_id!("@mnt_io:matrix.org")),
1176+
None
1177+
));
1178+
}
11151179
}
11161180

11171181
#[test]
@@ -1258,7 +1322,7 @@ mod tests_latest_event_content {
12581322
room_power_levels.invite = 10.into();
12591323
room_power_levels.kick = 10.into();
12601324
assert!(
1261-
filter_timeline_event(&event, Some(user_id), Some(&room_power_levels)).not(),
1325+
filter_timeline_event(&event, None, Some(user_id), Some(&room_power_levels)).not(),
12621326
"cannot accept, cannot decline",
12631327
);
12641328
}
@@ -1268,7 +1332,7 @@ mod tests_latest_event_content {
12681332
room_power_levels.invite = 0.into();
12691333
room_power_levels.kick = 10.into();
12701334
assert!(
1271-
filter_timeline_event(&event, Some(user_id), Some(&room_power_levels)),
1335+
filter_timeline_event(&event, None, Some(user_id), Some(&room_power_levels)),
12721336
"can accept, cannot decline",
12731337
);
12741338
}
@@ -1278,7 +1342,7 @@ mod tests_latest_event_content {
12781342
room_power_levels.invite = 10.into();
12791343
room_power_levels.kick = 0.into();
12801344
assert!(
1281-
filter_timeline_event(&event, Some(user_id), Some(&room_power_levels)),
1345+
filter_timeline_event(&event, None, Some(user_id), Some(&room_power_levels)),
12821346
"cannot accept, can decline",
12831347
);
12841348
}
@@ -1288,7 +1352,7 @@ mod tests_latest_event_content {
12881352
room_power_levels.invite = 0.into();
12891353
room_power_levels.kick = 0.into();
12901354
assert!(
1291-
filter_timeline_event(&event, Some(user_id), Some(&room_power_levels)),
1355+
filter_timeline_event(&event, None, Some(user_id), Some(&room_power_levels)),
12921356
"can accept, can decline",
12931357
);
12941358
}
@@ -1304,7 +1368,7 @@ mod tests_latest_event_content {
13041368
room_power_levels.kick = 0.into();
13051369

13061370
assert!(
1307-
filter_timeline_event(&event, Some(user_id), Some(&room_power_levels)).not(),
1371+
filter_timeline_event(&event, None, Some(user_id), Some(&room_power_levels)).not(),
13081372
"cannot accept, can decline, at least same user levels",
13091373
);
13101374
}

0 commit comments

Comments
 (0)