use matrix_sdk::{Client, Room, latest_events::LocalLatestEventValue};
use matrix_sdk_base::latest_event::LatestEventValue as BaseLatestEventValue;
use ruma::{
MilliSecondsSinceUnixEpoch, OwnedUserId,
events::{
AnyMessageLikeEventContent, relation::Replacement, room::message::RoomMessageEventContent,
},
};
use tracing::trace;
use crate::timeline::{
Profile, TimelineDetails, TimelineItemContent,
event_handler::{HandleAggregationKind, TimelineAction},
traits::RoomDataProvider,
};
#[derive(Debug)]
pub enum LatestEventValue {
None,
Remote {
timestamp: MilliSecondsSinceUnixEpoch,
sender: OwnedUserId,
is_own: bool,
profile: TimelineDetails<Profile>,
content: TimelineItemContent,
},
Local {
timestamp: MilliSecondsSinceUnixEpoch,
sender: OwnedUserId,
profile: TimelineDetails<Profile>,
content: TimelineItemContent,
is_sending: bool,
},
}
impl LatestEventValue {
pub(crate) async fn from_base_latest_event_value(
value: BaseLatestEventValue,
room: &Room,
client: &Client,
) -> Self {
match value {
BaseLatestEventValue::None => Self::None,
BaseLatestEventValue::Remote(timeline_event) => {
let raw_any_sync_timeline_event = timeline_event.into_raw();
let Ok(any_sync_timeline_event) = raw_any_sync_timeline_event.deserialize() else {
return Self::None;
};
let timestamp = any_sync_timeline_event.origin_server_ts();
let sender = any_sync_timeline_event.sender().to_owned();
let is_own = client.user_id().map(|user_id| user_id == sender).unwrap_or(false);
let profile = room
.profile_from_user_id(&sender)
.await
.map(TimelineDetails::Ready)
.unwrap_or(TimelineDetails::Unavailable);
match TimelineAction::from_event(
any_sync_timeline_event,
&raw_any_sync_timeline_event,
room,
None,
None,
None,
None,
)
.await
{
Some(TimelineAction::AddItem { content }) => {
Self::Remote { timestamp, sender, is_own, profile, content }
}
Some(TimelineAction::HandleAggregation {
kind:
HandleAggregationKind::Edit { replacement: Replacement { new_content, .. } },
..
}) => {
match TimelineAction::from_content(
AnyMessageLikeEventContent::RoomMessage(RoomMessageEventContent::new(
new_content.msgtype,
)),
None,
None,
None,
) {
TimelineAction::AddItem { content } => {
Self::Remote { timestamp, sender, is_own, profile, content }
}
_ => {
trace!("latest event was an edit that failed to be un-aggregated");
Self::None
}
}
}
_ => Self::None,
}
}
BaseLatestEventValue::LocalIsSending(LocalLatestEventValue {
timestamp,
content: ref serialized_content,
})
| BaseLatestEventValue::LocalCannotBeSent(LocalLatestEventValue {
timestamp,
content: ref serialized_content,
}) => {
let Ok(message_like_event_content) = serialized_content.deserialize() else {
return Self::None;
};
let sender =
client.user_id().expect("The `Client` is supposed to be logged").to_owned();
let profile = room
.profile_from_user_id(&sender)
.await
.map(TimelineDetails::Ready)
.unwrap_or(TimelineDetails::Unavailable);
let is_sending = matches!(value, BaseLatestEventValue::LocalIsSending(_));
match TimelineAction::from_content(message_like_event_content, None, None, None) {
TimelineAction::AddItem { content } => {
Self::Local { timestamp, sender, profile, content, is_sending }
}
TimelineAction::HandleAggregation { kind, .. } => {
trace!("latest event is an aggregation: {}", kind.debug_string());
Self::None
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use std::ops::Not;
use assert_matches::assert_matches;
use matrix_sdk::{
latest_events::{LocalLatestEventValue, RemoteLatestEventValue},
store::SerializableEventContent,
test_utils::mocks::MatrixMockServer,
};
use matrix_sdk_test::{JoinedRoomBuilder, async_test, event_factory::EventFactory};
use ruma::{
MilliSecondsSinceUnixEpoch, event_id,
events::{AnyMessageLikeEventContent, room::message::RoomMessageEventContent},
room_id, uint, user_id,
};
use super::{
super::{MsgLikeContent, MsgLikeKind, TimelineItemContent},
BaseLatestEventValue, LatestEventValue, TimelineDetails,
};
#[async_test]
async fn test_none() {
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 base_value = BaseLatestEventValue::None;
let value =
LatestEventValue::from_base_latest_event_value(base_value, &room, &client).await;
assert_matches!(value, LatestEventValue::None);
}
#[async_test]
async fn test_remote() {
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("raclette")
.event_id(event_id!("$ev0"))
.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(), "raclette");
}
);
})
}
#[async_test]
async fn test_local_is_sending() {
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 base_value = BaseLatestEventValue::LocalIsSending(LocalLatestEventValue {
timestamp: MilliSecondsSinceUnixEpoch(uint!(42)),
content: SerializableEventContent::new(&AnyMessageLikeEventContent::RoomMessage(
RoomMessageEventContent::text_plain("raclette"),
))
.unwrap(),
});
let value =
LatestEventValue::from_base_latest_event_value(base_value, &room, &client).await;
assert_matches!(value, LatestEventValue::Local { timestamp, sender, profile, content, is_sending } => {
assert_eq!(u64::from(timestamp.get()), 42u64);
assert_eq!(sender, "@example:localhost");
assert_matches!(profile, TimelineDetails::Unavailable);
assert_matches!(
content,
TimelineItemContent::MsgLike(MsgLikeContent { kind: MsgLikeKind::Message(_), .. })
);
assert!(is_sending);
})
}
#[async_test]
async fn test_local_cannot_be_sent() {
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 base_value = BaseLatestEventValue::LocalCannotBeSent(LocalLatestEventValue {
timestamp: MilliSecondsSinceUnixEpoch(uint!(42)),
content: SerializableEventContent::new(&AnyMessageLikeEventContent::RoomMessage(
RoomMessageEventContent::text_plain("raclette"),
))
.unwrap(),
});
let value =
LatestEventValue::from_base_latest_event_value(base_value, &room, &client).await;
assert_matches!(value, LatestEventValue::Local { timestamp, sender, profile, content, is_sending } => {
assert_eq!(u64::from(timestamp.get()), 42u64);
assert_eq!(sender, "@example:localhost");
assert_matches!(profile, TimelineDetails::Unavailable);
assert_matches!(
content,
TimelineItemContent::MsgLike(MsgLikeContent { kind: MsgLikeKind::Message(_), .. })
);
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");
}
);
})
}
}