1use matrix_sdk::{Client, Room, latest_events::LocalLatestEventValue};
16use matrix_sdk_base::latest_event::LatestEventValue as BaseLatestEventValue;
17use ruma::{
18 MilliSecondsSinceUnixEpoch, OwnedUserId,
19 events::{
20 AnyMessageLikeEventContent, relation::Replacement, room::message::RoomMessageEventContent,
21 },
22};
23use tracing::trace;
24
25use crate::timeline::{
26 Profile, TimelineDetails, TimelineItemContent,
27 event_handler::{HandleAggregationKind, TimelineAction},
28 traits::RoomDataProvider,
29};
30
31#[derive(Debug)]
34pub enum LatestEventValue {
35 None,
37
38 Remote {
40 timestamp: MilliSecondsSinceUnixEpoch,
42
43 sender: OwnedUserId,
45
46 is_own: bool,
48
49 profile: TimelineDetails<Profile>,
51
52 content: TimelineItemContent,
54 },
55
56 RemoteInvite {
58 timestamp: MilliSecondsSinceUnixEpoch,
60
61 inviter: Option<OwnedUserId>,
63
64 inviter_profile: TimelineDetails<Profile>,
66 },
67
68 Local {
72 timestamp: MilliSecondsSinceUnixEpoch,
74
75 sender: OwnedUserId,
77
78 profile: TimelineDetails<Profile>,
80
81 content: TimelineItemContent,
83
84 state: LatestEventValueLocalState,
86 },
87}
88
89#[derive(Debug)]
90#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
91pub enum LatestEventValueLocalState {
92 IsSending,
93 HasBeenSent,
94 CannotBeSent,
95}
96
97impl LatestEventValue {
98 pub(crate) async fn from_base_latest_event_value(
99 value: BaseLatestEventValue,
100 room: &Room,
101 client: &Client,
102 ) -> Self {
103 match value {
104 BaseLatestEventValue::None => Self::None,
105 BaseLatestEventValue::Remote(timeline_event) => {
106 let raw_any_sync_timeline_event = timeline_event.into_raw();
107 let Ok(any_sync_timeline_event) = raw_any_sync_timeline_event.deserialize() else {
108 return Self::None;
109 };
110
111 let timestamp = any_sync_timeline_event.origin_server_ts();
112 let sender = any_sync_timeline_event.sender().to_owned();
113 let is_own = client.user_id().map(|user_id| user_id == sender).unwrap_or(false);
114 let profile = room
115 .profile_from_user_id(&sender)
116 .await
117 .map(TimelineDetails::Ready)
118 .unwrap_or(TimelineDetails::Unavailable);
119
120 match TimelineAction::from_event(
121 any_sync_timeline_event,
122 &raw_any_sync_timeline_event,
123 room,
124 None,
125 None,
126 None,
127 None,
128 )
129 .await
130 {
131 Some(TimelineAction::AddItem { content }) => {
133 Self::Remote { timestamp, sender, is_own, profile, content }
134 }
135
136 Some(TimelineAction::HandleAggregation {
140 kind:
141 HandleAggregationKind::Edit { replacement: Replacement { new_content, .. } },
142 ..
143 }) => {
144 match TimelineAction::from_content(
146 AnyMessageLikeEventContent::RoomMessage(RoomMessageEventContent::new(
147 new_content.msgtype,
148 )),
149 None,
152 None,
155 None,
156 ) {
157 TimelineAction::AddItem { content } => {
159 Self::Remote { timestamp, sender, is_own, profile, content }
160 }
161
162 _ => {
165 trace!("latest event was an edit that failed to be un-aggregated");
166
167 Self::None
168 }
169 }
170 }
171
172 _ => Self::None,
173 }
174 }
175 BaseLatestEventValue::RemoteInvite { timestamp, inviter, .. } => {
176 let inviter_profile = if let Some(inviter_id) = &inviter {
177 room.profile_from_user_id(inviter_id)
178 .await
179 .map(TimelineDetails::Ready)
180 .unwrap_or(TimelineDetails::Unavailable)
181 } else {
182 TimelineDetails::Unavailable
183 };
184
185 Self::RemoteInvite { timestamp, inviter, inviter_profile }
186 }
187 BaseLatestEventValue::LocalIsSending(ref local_value)
188 | BaseLatestEventValue::LocalHasBeenSent { value: ref local_value, .. }
189 | BaseLatestEventValue::LocalCannotBeSent(ref local_value) => {
190 let LocalLatestEventValue { timestamp, content: serialized_content } = local_value;
191
192 let Ok(message_like_event_content) = serialized_content.deserialize() else {
193 return Self::None;
194 };
195
196 let sender =
197 client.user_id().expect("The `Client` is supposed to be logged").to_owned();
198 let profile = room
199 .profile_from_user_id(&sender)
200 .await
201 .map(TimelineDetails::Ready)
202 .unwrap_or(TimelineDetails::Unavailable);
203
204 match TimelineAction::from_content(message_like_event_content, None, None, None) {
205 TimelineAction::AddItem { content } => Self::Local {
206 timestamp: *timestamp,
207 sender,
208 profile,
209 content,
210 state: match value {
211 BaseLatestEventValue::LocalIsSending(_) => {
212 LatestEventValueLocalState::IsSending
213 }
214 BaseLatestEventValue::LocalHasBeenSent { .. } => {
215 LatestEventValueLocalState::HasBeenSent
216 }
217 BaseLatestEventValue::LocalCannotBeSent(_) => {
218 LatestEventValueLocalState::CannotBeSent
219 }
220 BaseLatestEventValue::Remote(_)
221 | BaseLatestEventValue::RemoteInvite { .. }
222 | BaseLatestEventValue::None => {
223 unreachable!("Only local latest events are supposed to be handled");
224 }
225 },
226 },
227
228 TimelineAction::HandleAggregation { kind, .. } => {
229 trace!("latest event is an aggregation: {}", kind.debug_string());
232 Self::None
233 }
234 }
235 }
236 }
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use std::ops::Not;
243
244 use assert_matches::assert_matches;
245 use matrix_sdk::{
246 latest_events::{LocalLatestEventValue, RemoteLatestEventValue},
247 store::SerializableEventContent,
248 test_utils::mocks::MatrixMockServer,
249 };
250 use matrix_sdk_test::{JoinedRoomBuilder, async_test, event_factory::EventFactory};
251 use ruma::{
252 MilliSecondsSinceUnixEpoch, event_id,
253 events::{AnyMessageLikeEventContent, room::message::RoomMessageEventContent},
254 room_id, uint, user_id,
255 };
256
257 use super::{
258 super::{MsgLikeContent, MsgLikeKind, TimelineItemContent},
259 BaseLatestEventValue, LatestEventValue, LatestEventValueLocalState, TimelineDetails,
260 };
261
262 #[async_test]
263 async fn test_none() {
264 let server = MatrixMockServer::new().await;
265 let client = server.client_builder().build().await;
266 let room = server.sync_room(&client, JoinedRoomBuilder::new(room_id!("!r0"))).await;
267
268 let base_value = BaseLatestEventValue::None;
269 let value =
270 LatestEventValue::from_base_latest_event_value(base_value, &room, &client).await;
271
272 assert_matches!(value, LatestEventValue::None);
273 }
274
275 #[async_test]
276 async fn test_remote() {
277 let server = MatrixMockServer::new().await;
278 let client = server.client_builder().build().await;
279 let room = server.sync_room(&client, JoinedRoomBuilder::new(room_id!("!r0"))).await;
280 let sender = user_id!("@mnt_io:matrix.org");
281 let event_factory = EventFactory::new();
282
283 let base_value = BaseLatestEventValue::Remote(RemoteLatestEventValue::from_plaintext(
284 event_factory
285 .server_ts(42)
286 .sender(sender)
287 .text_msg("raclette")
288 .event_id(event_id!("$ev0"))
289 .into_raw_sync(),
290 ));
291 let value =
292 LatestEventValue::from_base_latest_event_value(base_value, &room, &client).await;
293
294 assert_matches!(value, LatestEventValue::Remote { timestamp, sender: received_sender, is_own, profile, content } => {
295 assert_eq!(u64::from(timestamp.get()), 42u64);
296 assert_eq!(received_sender, sender);
297 assert!(is_own.not());
298 assert_matches!(profile, TimelineDetails::Unavailable);
299 assert_matches!(
300 content,
301 TimelineItemContent::MsgLike(MsgLikeContent { kind: MsgLikeKind::Message(message), .. }) => {
302 assert_eq!(message.body(), "raclette");
303 }
304 );
305 })
306 }
307
308 #[async_test]
309 async fn test_remote_invite() {
310 let server = MatrixMockServer::new().await;
311 let client = server.client_builder().build().await;
312 let room = server.sync_room(&client, JoinedRoomBuilder::new(room_id!("!r0"))).await;
313 let user_id = user_id!("@mnt_io:matrix.org");
314
315 let base_value = BaseLatestEventValue::RemoteInvite {
316 event_id: None,
317 timestamp: MilliSecondsSinceUnixEpoch(42u32.into()),
318 inviter: Some(user_id.to_owned()),
319 };
320 let value =
321 LatestEventValue::from_base_latest_event_value(base_value, &room, &client).await;
322
323 assert_matches!(value, LatestEventValue::RemoteInvite { timestamp, inviter, inviter_profile} => {
324 assert_eq!(u64::from(timestamp.get()), 42u64);
325 assert_eq!(inviter.as_deref(), Some(user_id));
326 assert_matches!(inviter_profile, TimelineDetails::Unavailable);
327 })
328 }
329
330 #[async_test]
331 async fn test_local_is_sending() {
332 let server = MatrixMockServer::new().await;
333 let client = server.client_builder().build().await;
334 let room = server.sync_room(&client, JoinedRoomBuilder::new(room_id!("!r0"))).await;
335
336 let base_value = BaseLatestEventValue::LocalIsSending(LocalLatestEventValue {
337 timestamp: MilliSecondsSinceUnixEpoch(uint!(42)),
338 content: SerializableEventContent::new(&AnyMessageLikeEventContent::RoomMessage(
339 RoomMessageEventContent::text_plain("raclette"),
340 ))
341 .unwrap(),
342 });
343 let value =
344 LatestEventValue::from_base_latest_event_value(base_value, &room, &client).await;
345
346 assert_matches!(value, LatestEventValue::Local { timestamp, sender, profile, content, state } => {
347 assert_eq!(u64::from(timestamp.get()), 42u64);
348 assert_eq!(sender, "@example:localhost");
349 assert_matches!(profile, TimelineDetails::Unavailable);
350 assert_matches!(
351 content,
352 TimelineItemContent::MsgLike(MsgLikeContent { kind: MsgLikeKind::Message(_), .. })
353 );
354 assert_matches!(state, LatestEventValueLocalState::IsSending);
355 })
356 }
357
358 #[async_test]
359 async fn test_local_has_been_sent() {
360 let server = MatrixMockServer::new().await;
361 let client = server.client_builder().build().await;
362 let room = server.sync_room(&client, JoinedRoomBuilder::new(room_id!("!r0"))).await;
363
364 let base_value = BaseLatestEventValue::LocalHasBeenSent {
365 event_id: event_id!("$ev0").to_owned(),
366 value: LocalLatestEventValue {
367 timestamp: MilliSecondsSinceUnixEpoch(uint!(42)),
368 content: SerializableEventContent::new(&AnyMessageLikeEventContent::RoomMessage(
369 RoomMessageEventContent::text_plain("raclette"),
370 ))
371 .unwrap(),
372 },
373 };
374 let value =
375 LatestEventValue::from_base_latest_event_value(base_value, &room, &client).await;
376
377 assert_matches!(value, LatestEventValue::Local { timestamp, sender, profile, content, state } => {
378 assert_eq!(u64::from(timestamp.get()), 42u64);
379 assert_eq!(sender, "@example:localhost");
380 assert_matches!(profile, TimelineDetails::Unavailable);
381 assert_matches!(
382 content,
383 TimelineItemContent::MsgLike(MsgLikeContent { kind: MsgLikeKind::Message(_), .. })
384 );
385 assert_matches!(state, LatestEventValueLocalState::HasBeenSent);
386 })
387 }
388
389 #[async_test]
390 async fn test_local_cannot_be_sent() {
391 let server = MatrixMockServer::new().await;
392 let client = server.client_builder().build().await;
393 let room = server.sync_room(&client, JoinedRoomBuilder::new(room_id!("!r0"))).await;
394
395 let base_value = BaseLatestEventValue::LocalCannotBeSent(LocalLatestEventValue {
396 timestamp: MilliSecondsSinceUnixEpoch(uint!(42)),
397 content: SerializableEventContent::new(&AnyMessageLikeEventContent::RoomMessage(
398 RoomMessageEventContent::text_plain("raclette"),
399 ))
400 .unwrap(),
401 });
402 let value =
403 LatestEventValue::from_base_latest_event_value(base_value, &room, &client).await;
404
405 assert_matches!(value, LatestEventValue::Local { timestamp, sender, profile, content, state } => {
406 assert_eq!(u64::from(timestamp.get()), 42u64);
407 assert_eq!(sender, "@example:localhost");
408 assert_matches!(profile, TimelineDetails::Unavailable);
409 assert_matches!(
410 content,
411 TimelineItemContent::MsgLike(MsgLikeContent { kind: MsgLikeKind::Message(_), .. })
412 );
413 assert_matches!(state, LatestEventValueLocalState::CannotBeSent);
414 })
415 }
416
417 #[async_test]
418 async fn test_remote_edit() {
419 let server = MatrixMockServer::new().await;
420 let client = server.client_builder().build().await;
421 let room = server.sync_room(&client, JoinedRoomBuilder::new(room_id!("!r0"))).await;
422 let sender = user_id!("@mnt_io:matrix.org");
423 let event_factory = EventFactory::new();
424
425 let base_value = BaseLatestEventValue::Remote(RemoteLatestEventValue::from_plaintext(
426 event_factory
427 .server_ts(42)
428 .sender(sender)
429 .text_msg("bonjour")
430 .event_id(event_id!("$ev1"))
431 .edit(event_id!("$ev0"), RoomMessageEventContent::text_plain("fondue").into())
432 .into_raw_sync(),
433 ));
434 let value =
435 LatestEventValue::from_base_latest_event_value(base_value, &room, &client).await;
436
437 assert_matches!(value, LatestEventValue::Remote { timestamp, sender: received_sender, is_own, profile, content } => {
438 assert_eq!(u64::from(timestamp.get()), 42u64);
439 assert_eq!(received_sender, sender);
440 assert!(is_own.not());
441 assert_matches!(profile, TimelineDetails::Unavailable);
442 assert_matches!(
443 content,
444 TimelineItemContent::MsgLike(MsgLikeContent { kind: MsgLikeKind::Message(message), .. }) => {
445 assert_eq!(message.body(), "fondue");
446 }
447 );
448 })
449 }
450}