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