Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 98 additions & 23 deletions crates/matrix-sdk-ui/src/timeline/latest_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,17 @@

use matrix_sdk::{Client, Room, latest_events::LocalLatestEventValue};
use matrix_sdk_base::latest_event::LatestEventValue as BaseLatestEventValue;
use ruma::{MilliSecondsSinceUnixEpoch, OwnedUserId};
use ruma::{
MilliSecondsSinceUnixEpoch, OwnedUserId,
events::{
AnyMessageLikeEventContent, relation::Replacement, room::message::RoomMessageEventContent,
},
};
use tracing::trace;

use crate::timeline::{
Profile, TimelineDetails, TimelineItemContent, event_handler::TimelineAction,
Profile, TimelineDetails, TimelineItemContent,
event_handler::{HandleAggregationKind, TimelineAction},
traits::RoomDataProvider,
};

Expand Down Expand Up @@ -92,7 +98,7 @@ impl LatestEventValue {
.map(TimelineDetails::Ready)
.unwrap_or(TimelineDetails::Unavailable);

let Some(TimelineAction::AddItem { content }) = TimelineAction::from_event(
match TimelineAction::from_event(
any_sync_timeline_event,
&raw_any_sync_timeline_event,
room,
Expand All @@ -102,11 +108,50 @@ impl LatestEventValue {
None,
)
.await
else {
return Self::None;
};
{
// Easy path: no aggregation, direct event.
Some(TimelineAction::AddItem { content }) => {
Self::Remote { timestamp, sender, is_own, profile, content }
}

Self::Remote { timestamp, sender, is_own, profile, content }
// Aggregated event.
//
// Only edits are supported for the moment.
Some(TimelineAction::HandleAggregation {
kind:
HandleAggregationKind::Edit { replacement: Replacement { new_content, .. } },
..
}) => {
// Let's map the edit into a regular message.
match TimelineAction::from_content(
AnyMessageLikeEventContent::RoomMessage(RoomMessageEventContent::new(
new_content.msgtype,
)),
// We don't care about the `InReplyToDetails` in the context of a
// `LatestEventValue`.
None,
// We don't care about the thread information in the context of a
// `LatestEventValue`.
None,
None,
) {
// The expected case.
TimelineAction::AddItem { content } => {
Self::Remote { timestamp, sender, is_own, profile, content }
}

// Supposedly unreachable, but let's pretend there is no
// `LatestEventValue` if it happens.
_ => {
trace!("latest event was an edit that failed to be un-aggregated");

Self::None
}
}
}

_ => Self::None,
}
}
BaseLatestEventValue::LocalIsSending(LocalLatestEventValue {
timestamp,
Expand Down Expand Up @@ -156,15 +201,14 @@ mod tests {
store::SerializableEventContent,
test_utils::mocks::MatrixMockServer,
};
use matrix_sdk_test::{JoinedRoomBuilder, async_test};
use matrix_sdk_test::{JoinedRoomBuilder, async_test, event_factory::EventFactory};
use ruma::{
MilliSecondsSinceUnixEpoch,
MilliSecondsSinceUnixEpoch, event_id,
events::{AnyMessageLikeEventContent, room::message::RoomMessageEventContent},
room_id,
serde::Raw,
uint, user_id,
};
use serde_json::json;

use super::{
super::{MsgLikeContent, MsgLikeKind, TimelineItemContent},
Expand All @@ -190,20 +234,15 @@ mod tests {
let client = server.client_builder().build().await;
let room = server.sync_room(&client, JoinedRoomBuilder::new(room_id!("!r0"))).await;
let sender = user_id!("@mnt_io:matrix.org");
let event_factory = EventFactory::new();

let base_value = BaseLatestEventValue::Remote(RemoteLatestEventValue::from_plaintext(
Raw::from_json_string(
json!({
"content": RoomMessageEventContent::text_plain("raclette"),
"type": "m.room.message",
"event_id": "$ev0",
"room_id": "!r0",
"origin_server_ts": 42,
"sender": sender,
})
.to_string(),
)
.unwrap(),
event_factory
.server_ts(42)
.sender(sender)
.text_msg("raclette")
.event_id(event_id!("$ev0"))
.into_raw_sync(),
));
let value =
LatestEventValue::from_base_latest_event_value(base_value, &room, &client).await;
Expand All @@ -215,7 +254,9 @@ mod tests {
assert_matches!(profile, TimelineDetails::Unavailable);
assert_matches!(
content,
TimelineItemContent::MsgLike(MsgLikeContent { kind: MsgLikeKind::Message(_), .. })
TimelineItemContent::MsgLike(MsgLikeContent { kind: MsgLikeKind::Message(message), .. }) => {
assert_eq!(message.body(), "raclette");
}
);
})
}
Expand Down Expand Up @@ -281,4 +322,38 @@ mod tests {
assert!(is_sending.not());
})
}

#[async_test]
async fn test_remote_edit() {
let server = MatrixMockServer::new().await;
let client = server.client_builder().build().await;
let room = server.sync_room(&client, JoinedRoomBuilder::new(room_id!("!r0"))).await;
let sender = user_id!("@mnt_io:matrix.org");
let event_factory = EventFactory::new();

let base_value = BaseLatestEventValue::Remote(RemoteLatestEventValue::from_plaintext(
event_factory
.server_ts(42)
.sender(sender)
.text_msg("bonjour")
.event_id(event_id!("$ev1"))
.edit(event_id!("$ev0"), RoomMessageEventContent::text_plain("fondue").into())
.into_raw_sync(),
));
let value =
LatestEventValue::from_base_latest_event_value(base_value, &room, &client).await;

assert_matches!(value, LatestEventValue::Remote { timestamp, sender: received_sender, is_own, profile, content } => {
assert_eq!(u64::from(timestamp.get()), 42u64);
assert_eq!(received_sender, sender);
assert!(is_own.not());
assert_matches!(profile, TimelineDetails::Unavailable);
assert_matches!(
content,
TimelineItemContent::MsgLike(MsgLikeContent { kind: MsgLikeKind::Message(message), .. }) => {
assert_eq!(message.body(), "fondue");
}
);
})
}
}
50 changes: 38 additions & 12 deletions crates/matrix-sdk/src/event_cache/room/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,11 +322,14 @@ impl RoomEventCache {
/// Try to find a single event in this room, starting from the most recent
/// event.
///
/// The `predicate` receives two arguments: the current event, and the
/// ID of the _previous_ (older) event.
///
/// **Warning**! It looks into the loaded events from the in-memory linked
/// chunk **only**. It doesn't look inside the storage.
pub async fn rfind_map_event_in_memory_by<O, P>(&self, predicate: P) -> Result<Option<O>>
where
P: FnMut(&Event) -> Option<O>,
P: FnMut(&Event, Option<OwnedEventId>) -> Option<O>,
{
Ok(self.inner.state.read().await?.rfind_map_event_in_memory_by(predicate))
}
Expand Down Expand Up @@ -632,6 +635,7 @@ mod private {

use eyeball::SharedObservable;
use eyeball_im::VectorDiff;
use itertools::Itertools;
use matrix_sdk_base::{
apply_redaction,
deserialized_responses::{ThreadSummary, ThreadSummaryStatus, TimelineEventKind},
Expand Down Expand Up @@ -1096,14 +1100,30 @@ mod private {

//// Find a single event in this room, starting from the most recent event.
///
/// The `predicate` receives two arguments: the current event, and the
/// ID of the _previous_ (older) event.
///
/// **Warning**! It looks into the loaded events from the in-memory
/// linked chunk **only**. It doesn't look inside the storage,
/// contrary to [`Self::find_event`].
pub fn rfind_map_event_in_memory_by<O, P>(&self, mut predicate: P) -> Option<O>
where
P: FnMut(&Event) -> Option<O>,
P: FnMut(&Event, Option<OwnedEventId>) -> Option<O>,
{
self.state.room_linked_chunk.revents().find_map(|(_position, event)| predicate(event))
self.state
.room_linked_chunk
.revents()
.peekable()
.batching(|iter| {
iter.next().map(|(_position, event)| {
(
event,
iter.peek()
.and_then(|(_next_position, next_event)| next_event.event_id()),
)
})
})
.find_map(|(event, next_event_id)| predicate(event, next_event_id))
}

#[cfg(test)]
Expand Down Expand Up @@ -3788,32 +3808,34 @@ mod timed_tests {
// Look for an event from `BOB`: it must be `event_0`.
assert_matches!(
room_event_cache
.rfind_map_event_in_memory_by(|event| {
(event.raw().get_field::<OwnedUserId>("sender").unwrap().as_deref() == Some(*BOB)).then(|| event.event_id())
.rfind_map_event_in_memory_by(|event, previous_event_id| {
(event.raw().get_field::<OwnedUserId>("sender").unwrap().as_deref() == Some(*BOB)).then(|| (event.event_id(), previous_event_id))
})
.await,
Ok(Some(event_id)) => {
Ok(Some((event_id, previous_event_id))) => {
assert_eq!(event_id.as_deref(), Some(event_id_0));
assert!(previous_event_id.is_none());
}
);

// Look for an event from `ALICE`: it must be `event_2`, right before `event_1`
// because events are looked for in reverse order.
assert_matches!(
room_event_cache
.rfind_map_event_in_memory_by(|event| {
(event.raw().get_field::<OwnedUserId>("sender").unwrap().as_deref() == Some(*ALICE)).then(|| event.event_id())
.rfind_map_event_in_memory_by(|event, previous_event_id| {
(event.raw().get_field::<OwnedUserId>("sender").unwrap().as_deref() == Some(*ALICE)).then(|| (event.event_id(), previous_event_id))
})
.await,
Ok(Some(event_id)) => {
Ok(Some((event_id, previous_event_id))) => {
assert_eq!(event_id.as_deref(), Some(event_id_2));
assert_eq!(previous_event_id.as_deref(), Some(event_id_1));
}
);

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

// Look for an event that doesn't exist.
assert!(
room_event_cache.rfind_map_event_in_memory_by(|_| None::<()>).await.unwrap().is_none()
room_event_cache
.rfind_map_event_in_memory_by(|_, _| None::<()>)
.await
.unwrap()
.is_none()
);
}

Expand Down Expand Up @@ -4412,7 +4438,7 @@ mod timed_tests {

async fn event_loaded(room_event_cache: &RoomEventCache, event_id: &EventId) -> bool {
room_event_cache
.rfind_map_event_in_memory_by(|event| {
.rfind_map_event_in_memory_by(|event, _previous_event_id| {
(event.event_id().as_deref() == Some(event_id)).then_some(())
})
.await
Expand Down
Loading
Loading