Skip to main content

matrix_sdk_ui/timeline/
latest_event.rs

1// Copyright 2025 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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/// A simplified version of [`matrix_sdk_base::latest_event::LatestEventValue`]
25/// tailored for this `timeline` module.
26#[derive(Debug)]
27pub enum LatestEventValue {
28    /// No value has been computed yet, or no candidate value was found.
29    None,
30
31    /// The latest event represents a remote event.
32    Remote {
33        /// The timestamp of the remote event.
34        timestamp: MilliSecondsSinceUnixEpoch,
35
36        /// The sender of the remote event.
37        sender: OwnedUserId,
38
39        /// Has this event been sent by the current logged user?
40        is_own: bool,
41
42        /// The sender's profile.
43        profile: TimelineDetails<Profile>,
44
45        /// The content of the remote event.
46        content: TimelineItemContent,
47    },
48
49    /// The latest event represents an invite to a room.
50    RemoteInvite {
51        /// The timestamp of the invite.
52        timestamp: MilliSecondsSinceUnixEpoch,
53
54        /// The inviter (can be unknown).
55        inviter: Option<OwnedUserId>,
56
57        /// The inviter's profile (can be unknown).
58        inviter_profile: TimelineDetails<Profile>,
59    },
60
61    /// The latest event represents a local event that is sending, or that
62    /// cannot be sent, either because a previous local event, or this local
63    /// event cannot be sent.
64    Local {
65        /// The timestamp of the local event.
66        timestamp: MilliSecondsSinceUnixEpoch,
67
68        /// The sender of the remote event.
69        sender: OwnedUserId,
70
71        /// The sender's profile.
72        profile: TimelineDetails<Profile>,
73
74        /// The content of the local event.
75        content: TimelineItemContent,
76
77        /// Whether the local event is sending, has been sent or cannot be sent.
78        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                        // Add some debug logging here to help diagnose issues with the latest
164                        // event.
165                        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}