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::{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}