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::{
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/// A simplified version of [`matrix_sdk_base::latest_event::LatestEventValue`]
32/// tailored for this `timeline` module.
33#[derive(Debug)]
34pub enum LatestEventValue {
35    /// No value has been computed yet, or no candidate value was found.
36    None,
37
38    /// The latest event represents a remote event.
39    Remote {
40        /// The timestamp of the remote event.
41        timestamp: MilliSecondsSinceUnixEpoch,
42
43        /// The sender of the remote event.
44        sender: OwnedUserId,
45
46        /// Has this event been sent by the current logged user?
47        is_own: bool,
48
49        /// The sender's profile.
50        profile: TimelineDetails<Profile>,
51
52        /// The content of the remote event.
53        content: TimelineItemContent,
54    },
55
56    /// The latest event represents an invite to a room.
57    RemoteInvite {
58        /// The timestamp of the invite.
59        timestamp: MilliSecondsSinceUnixEpoch,
60
61        /// The inviter (can be unknown).
62        inviter: Option<OwnedUserId>,
63
64        /// The inviter's profile (can be unknown).
65        inviter_profile: TimelineDetails<Profile>,
66    },
67
68    /// The latest event represents a local event that is sending, or that
69    /// cannot be sent, either because a previous local event, or this local
70    /// event cannot be sent.
71    Local {
72        /// The timestamp of the local event.
73        timestamp: MilliSecondsSinceUnixEpoch,
74
75        /// The sender of the remote event.
76        sender: OwnedUserId,
77
78        /// The sender's profile.
79        profile: TimelineDetails<Profile>,
80
81        /// The content of the local event.
82        content: TimelineItemContent,
83
84        /// Whether the local event is sending, has been sent or cannot be sent.
85        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                    // Easy path: no aggregation, direct event.
136                    Some(TimelineAction::AddItem { content }) => {
137                        Self::Remote { timestamp, sender, is_own, profile, content }
138                    }
139
140                    // Aggregated event.
141                    //
142                    // Only edits are supported for the moment.
143                    Some(TimelineAction::HandleAggregation {
144                        kind:
145                            HandleAggregationKind::Edit { replacement: Replacement { new_content, .. } },
146                        ..
147                    }) => {
148                        // Let's map the edit into a regular message.
149                        match TimelineAction::from_content(
150                            AnyMessageLikeEventContent::RoomMessage(RoomMessageEventContent::new(
151                                new_content.msgtype,
152                            )),
153                            // We don't care about the `InReplyToDetails` in the context of a
154                            // `LatestEventValue`.
155                            None,
156                            // We don't care about the thread information in the context of a
157                            // `LatestEventValue`.
158                            None,
159                            None,
160                        ) {
161                            // The expected case.
162                            TimelineAction::AddItem { content } => {
163                                Self::Remote { timestamp, sender, is_own, profile, content }
164                            }
165
166                            // Supposedly unreachable, but let's pretend there is no
167                            // `LatestEventValue` if it happens.
168                            _ => {
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                        // Add some debug logging here to help diagnose issues with the latest
234                        // event.
235                        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}