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::{AnyMessageLikeEventContent, room::message::RoomMessageEventContent},
188 owned_event_id, room_id, uint, user_id,
189 };
190
191 use super::{
192 super::{MsgLikeContent, MsgLikeKind, TimelineItemContent},
193 BaseLatestEventValue, LatestEventValue, LatestEventValueLocalState, TimelineDetails,
194 };
195
196 #[async_test]
197 async fn test_none() {
198 let server = MatrixMockServer::new().await;
199 let client = server.client_builder().build().await;
200 let room = server.sync_room(&client, JoinedRoomBuilder::new(room_id!("!r0"))).await;
201
202 let base_value = BaseLatestEventValue::None;
203 let value =
204 LatestEventValue::from_base_latest_event_value(base_value, &room, &client).await;
205
206 assert_matches!(value, LatestEventValue::None);
207 }
208
209 #[async_test]
210 async fn test_remote() {
211 let server = MatrixMockServer::new().await;
212 let client = server.client_builder().build().await;
213 let room = server.sync_room(&client, JoinedRoomBuilder::new(room_id!("!r0"))).await;
214 let sender = user_id!("@mnt_io:matrix.org");
215 let event_factory = EventFactory::new();
216
217 let base_value = BaseLatestEventValue::Remote(RemoteLatestEventValue::from_plaintext(
218 event_factory
219 .server_ts(42)
220 .sender(sender)
221 .text_msg("raclette")
222 .event_id(event_id!("$ev0"))
223 .into_raw_sync(),
224 ));
225 let value =
226 LatestEventValue::from_base_latest_event_value(base_value, &room, &client).await;
227
228 assert_matches!(value, LatestEventValue::Remote { timestamp, sender: received_sender, is_own, profile, content } => {
229 assert_eq!(u64::from(timestamp.get()), 42u64);
230 assert_eq!(received_sender, sender);
231 assert!(is_own.not());
232 assert_matches!(profile, TimelineDetails::Unavailable);
233 assert_matches!(
234 content,
235 TimelineItemContent::MsgLike(MsgLikeContent { kind: MsgLikeKind::Message(message), .. }) => {
236 assert_eq!(message.body(), "raclette");
237 }
238 );
239 })
240 }
241
242 #[async_test]
243 async fn test_remote_invite() {
244 let server = MatrixMockServer::new().await;
245 let client = server.client_builder().build().await;
246 let room = server.sync_room(&client, JoinedRoomBuilder::new(room_id!("!r0"))).await;
247 let user_id = user_id!("@mnt_io:matrix.org");
248
249 let base_value = BaseLatestEventValue::RemoteInvite {
250 event_id: None,
251 timestamp: MilliSecondsSinceUnixEpoch(42u32.into()),
252 inviter: Some(user_id.to_owned()),
253 };
254 let value =
255 LatestEventValue::from_base_latest_event_value(base_value, &room, &client).await;
256
257 assert_matches!(value, LatestEventValue::RemoteInvite { timestamp, inviter, inviter_profile} => {
258 assert_eq!(u64::from(timestamp.get()), 42u64);
259 assert_eq!(inviter.as_deref(), Some(user_id));
260 assert_matches!(inviter_profile, TimelineDetails::Unavailable);
261 })
262 }
263
264 #[async_test]
265 async fn test_local_is_sending() {
266 let server = MatrixMockServer::new().await;
267 let client = server.client_builder().build().await;
268 let room = server.sync_room(&client, JoinedRoomBuilder::new(room_id!("!r0"))).await;
269
270 let base_value = BaseLatestEventValue::LocalIsSending(LocalLatestEventValue {
271 timestamp: MilliSecondsSinceUnixEpoch(uint!(42)),
272 content: SerializableEventContent::new(&AnyMessageLikeEventContent::RoomMessage(
273 RoomMessageEventContent::text_plain("raclette"),
274 ))
275 .unwrap(),
276 });
277 let value =
278 LatestEventValue::from_base_latest_event_value(base_value, &room, &client).await;
279
280 assert_matches!(value, LatestEventValue::Local { timestamp, sender, profile, content, state } => {
281 assert_eq!(u64::from(timestamp.get()), 42u64);
282 assert_eq!(sender, "@example:localhost");
283 assert_matches!(profile, TimelineDetails::Unavailable);
284 assert_matches!(
285 content,
286 TimelineItemContent::MsgLike(MsgLikeContent { kind: MsgLikeKind::Message(_), .. })
287 );
288 assert_matches!(state, LatestEventValueLocalState::IsSending);
289 })
290 }
291
292 #[async_test]
293 async fn test_local_has_been_sent() {
294 let server = MatrixMockServer::new().await;
295 let client = server.client_builder().build().await;
296 let room = server.sync_room(&client, JoinedRoomBuilder::new(room_id!("!r0"))).await;
297
298 let base_value = BaseLatestEventValue::LocalHasBeenSent {
299 event_id: owned_event_id!("$ev0"),
300 value: LocalLatestEventValue {
301 timestamp: MilliSecondsSinceUnixEpoch(uint!(42)),
302 content: SerializableEventContent::new(&AnyMessageLikeEventContent::RoomMessage(
303 RoomMessageEventContent::text_plain("raclette"),
304 ))
305 .unwrap(),
306 },
307 };
308 let value =
309 LatestEventValue::from_base_latest_event_value(base_value, &room, &client).await;
310
311 assert_matches!(value, LatestEventValue::Local { timestamp, sender, profile, content, state } => {
312 assert_eq!(u64::from(timestamp.get()), 42u64);
313 assert_eq!(sender, "@example:localhost");
314 assert_matches!(profile, TimelineDetails::Unavailable);
315 assert_matches!(
316 content,
317 TimelineItemContent::MsgLike(MsgLikeContent { kind: MsgLikeKind::Message(_), .. })
318 );
319 assert_matches!(state, LatestEventValueLocalState::HasBeenSent);
320 })
321 }
322
323 #[async_test]
324 async fn test_local_cannot_be_sent() {
325 let server = MatrixMockServer::new().await;
326 let client = server.client_builder().build().await;
327 let room = server.sync_room(&client, JoinedRoomBuilder::new(room_id!("!r0"))).await;
328
329 let base_value = BaseLatestEventValue::LocalCannotBeSent(LocalLatestEventValue {
330 timestamp: MilliSecondsSinceUnixEpoch(uint!(42)),
331 content: SerializableEventContent::new(&AnyMessageLikeEventContent::RoomMessage(
332 RoomMessageEventContent::text_plain("raclette"),
333 ))
334 .unwrap(),
335 });
336 let value =
337 LatestEventValue::from_base_latest_event_value(base_value, &room, &client).await;
338
339 assert_matches!(value, LatestEventValue::Local { timestamp, sender, profile, content, state } => {
340 assert_eq!(u64::from(timestamp.get()), 42u64);
341 assert_eq!(sender, "@example:localhost");
342 assert_matches!(profile, TimelineDetails::Unavailable);
343 assert_matches!(
344 content,
345 TimelineItemContent::MsgLike(MsgLikeContent { kind: MsgLikeKind::Message(_), .. })
346 );
347 assert_matches!(state, LatestEventValueLocalState::CannotBeSent);
348 })
349 }
350
351 #[async_test]
352 async fn test_remote_edit() {
353 let server = MatrixMockServer::new().await;
354 let client = server.client_builder().build().await;
355 let room = server.sync_room(&client, JoinedRoomBuilder::new(room_id!("!r0"))).await;
356 let sender = user_id!("@mnt_io:matrix.org");
357 let event_factory = EventFactory::new();
358
359 let base_value = BaseLatestEventValue::Remote(RemoteLatestEventValue::from_plaintext(
360 event_factory
361 .server_ts(42)
362 .sender(sender)
363 .text_msg("bonjour")
364 .event_id(event_id!("$ev1"))
365 .edit(event_id!("$ev0"), RoomMessageEventContent::text_plain("fondue").into())
366 .into_raw_sync(),
367 ));
368 let value =
369 LatestEventValue::from_base_latest_event_value(base_value, &room, &client).await;
370
371 assert_matches!(value, LatestEventValue::Remote { timestamp, sender: received_sender, is_own, profile, content } => {
372 assert_eq!(u64::from(timestamp.get()), 42u64);
373 assert_eq!(received_sender, sender);
374 assert!(is_own.not());
375 assert_matches!(profile, TimelineDetails::Unavailable);
376 assert_matches!(
377 content,
378 TimelineItemContent::MsgLike(MsgLikeContent { kind: MsgLikeKind::Message(message), .. }) => {
379 assert_eq!(message.body(), "fondue");
380 }
381 );
382 })
383 }
384
385 #[async_test]
386 async fn test_remote_beacon_stop() {
387 let server = MatrixMockServer::new().await;
388 let client = server.client_builder().build().await;
389 let room = server.sync_room(&client, JoinedRoomBuilder::new(room_id!("!r0"))).await;
390 let sender = user_id!("@mnt_io:matrix.org");
391 let event_factory = EventFactory::new();
392
393 let base_value = BaseLatestEventValue::Remote(RemoteLatestEventValue::from_plaintext(
394 event_factory
395 .server_ts(42)
396 .sender(sender)
397 .beacon_info(Some("Alice's walk".to_owned()), Duration::from_secs(60), false, None)
398 .state_key(sender)
399 .event_id(event_id!("$beacon-stop"))
400 .into_raw_sync(),
401 ));
402 let value =
403 LatestEventValue::from_base_latest_event_value(base_value, &room, &client).await;
404
405 assert_matches!(value, LatestEventValue::Remote { timestamp, sender: received_sender, is_own, profile, content } => {
406 assert_eq!(u64::from(timestamp.get()), 42u64);
407 assert_eq!(received_sender, sender);
408 assert!(is_own.not());
409 assert_matches!(profile, TimelineDetails::Unavailable);
410 assert_matches!(
411 content,
412 TimelineItemContent::MsgLike(MsgLikeContent { kind: MsgLikeKind::LiveLocation(state), .. }) => {
413 assert!(!state.is_live(), "stop beacon should not be live");
414 assert_eq!(state.description(), Some("Alice's walk"));
415 }
416 );
417 })
418 }
419}