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