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 a local event that is sending, or that
57    /// cannot be sent, either because a previous local event, or this local
58    /// event cannot be sent.
59    Local {
60        /// The timestamp of the local event.
61        timestamp: MilliSecondsSinceUnixEpoch,
62
63        /// The sender of the remote event.
64        sender: OwnedUserId,
65
66        /// The sender's profile.
67        profile: TimelineDetails<Profile>,
68
69        /// The content of the local event.
70        content: TimelineItemContent,
71
72        /// Whether the local event is sending if it is set to `true`, otherwise
73        /// it cannot be sent.
74        is_sending: bool,
75    },
76}
77
78impl LatestEventValue {
79    pub(crate) async fn from_base_latest_event_value(
80        value: BaseLatestEventValue,
81        room: &Room,
82        client: &Client,
83    ) -> Self {
84        match value {
85            BaseLatestEventValue::None => Self::None,
86            BaseLatestEventValue::Remote(timeline_event) => {
87                let raw_any_sync_timeline_event = timeline_event.into_raw();
88                let Ok(any_sync_timeline_event) = raw_any_sync_timeline_event.deserialize() else {
89                    return Self::None;
90                };
91
92                let timestamp = any_sync_timeline_event.origin_server_ts();
93                let sender = any_sync_timeline_event.sender().to_owned();
94                let is_own = client.user_id().map(|user_id| user_id == sender).unwrap_or(false);
95                let profile = room
96                    .profile_from_user_id(&sender)
97                    .await
98                    .map(TimelineDetails::Ready)
99                    .unwrap_or(TimelineDetails::Unavailable);
100
101                match TimelineAction::from_event(
102                    any_sync_timeline_event,
103                    &raw_any_sync_timeline_event,
104                    room,
105                    None,
106                    None,
107                    None,
108                    None,
109                )
110                .await
111                {
112                    // Easy path: no aggregation, direct event.
113                    Some(TimelineAction::AddItem { content }) => {
114                        Self::Remote { timestamp, sender, is_own, profile, content }
115                    }
116
117                    // Aggregated event.
118                    //
119                    // Only edits are supported for the moment.
120                    Some(TimelineAction::HandleAggregation {
121                        kind:
122                            HandleAggregationKind::Edit { replacement: Replacement { new_content, .. } },
123                        ..
124                    }) => {
125                        // Let's map the edit into a regular message.
126                        match TimelineAction::from_content(
127                            AnyMessageLikeEventContent::RoomMessage(RoomMessageEventContent::new(
128                                new_content.msgtype,
129                            )),
130                            // We don't care about the `InReplyToDetails` in the context of a
131                            // `LatestEventValue`.
132                            None,
133                            // We don't care about the thread information in the context of a
134                            // `LatestEventValue`.
135                            None,
136                            None,
137                        ) {
138                            // The expected case.
139                            TimelineAction::AddItem { content } => {
140                                Self::Remote { timestamp, sender, is_own, profile, content }
141                            }
142
143                            // Supposedly unreachable, but let's pretend there is no
144                            // `LatestEventValue` if it happens.
145                            _ => {
146                                trace!("latest event was an edit that failed to be un-aggregated");
147
148                                Self::None
149                            }
150                        }
151                    }
152
153                    _ => Self::None,
154                }
155            }
156            BaseLatestEventValue::LocalIsSending(LocalLatestEventValue {
157                timestamp,
158                content: ref serialized_content,
159            })
160            | BaseLatestEventValue::LocalCannotBeSent(LocalLatestEventValue {
161                timestamp,
162                content: ref serialized_content,
163            }) => {
164                let Ok(message_like_event_content) = serialized_content.deserialize() else {
165                    return Self::None;
166                };
167
168                let sender =
169                    client.user_id().expect("The `Client` is supposed to be logged").to_owned();
170                let profile = room
171                    .profile_from_user_id(&sender)
172                    .await
173                    .map(TimelineDetails::Ready)
174                    .unwrap_or(TimelineDetails::Unavailable);
175                let is_sending = matches!(value, BaseLatestEventValue::LocalIsSending(_));
176
177                match TimelineAction::from_content(message_like_event_content, None, None, None) {
178                    TimelineAction::AddItem { content } => {
179                        Self::Local { timestamp, sender, profile, content, is_sending }
180                    }
181
182                    TimelineAction::HandleAggregation { kind, .. } => {
183                        // Add some debug logging here to help diagnose issues with the latest
184                        // event.
185                        trace!("latest event is an aggregation: {}", kind.debug_string());
186                        Self::None
187                    }
188                }
189            }
190        }
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use std::ops::Not;
197
198    use assert_matches::assert_matches;
199    use matrix_sdk::{
200        latest_events::{LocalLatestEventValue, RemoteLatestEventValue},
201        store::SerializableEventContent,
202        test_utils::mocks::MatrixMockServer,
203    };
204    use matrix_sdk_test::{JoinedRoomBuilder, async_test, event_factory::EventFactory};
205    use ruma::{
206        MilliSecondsSinceUnixEpoch, event_id,
207        events::{AnyMessageLikeEventContent, room::message::RoomMessageEventContent},
208        room_id, uint, user_id,
209    };
210
211    use super::{
212        super::{MsgLikeContent, MsgLikeKind, TimelineItemContent},
213        BaseLatestEventValue, LatestEventValue, TimelineDetails,
214    };
215
216    #[async_test]
217    async fn test_none() {
218        let server = MatrixMockServer::new().await;
219        let client = server.client_builder().build().await;
220        let room = server.sync_room(&client, JoinedRoomBuilder::new(room_id!("!r0"))).await;
221
222        let base_value = BaseLatestEventValue::None;
223        let value =
224            LatestEventValue::from_base_latest_event_value(base_value, &room, &client).await;
225
226        assert_matches!(value, LatestEventValue::None);
227    }
228
229    #[async_test]
230    async fn test_remote() {
231        let server = MatrixMockServer::new().await;
232        let client = server.client_builder().build().await;
233        let room = server.sync_room(&client, JoinedRoomBuilder::new(room_id!("!r0"))).await;
234        let sender = user_id!("@mnt_io:matrix.org");
235        let event_factory = EventFactory::new();
236
237        let base_value = BaseLatestEventValue::Remote(RemoteLatestEventValue::from_plaintext(
238            event_factory
239                .server_ts(42)
240                .sender(sender)
241                .text_msg("raclette")
242                .event_id(event_id!("$ev0"))
243                .into_raw_sync(),
244        ));
245        let value =
246            LatestEventValue::from_base_latest_event_value(base_value, &room, &client).await;
247
248        assert_matches!(value, LatestEventValue::Remote { timestamp, sender: received_sender, is_own, profile, content } => {
249            assert_eq!(u64::from(timestamp.get()), 42u64);
250            assert_eq!(received_sender, sender);
251            assert!(is_own.not());
252            assert_matches!(profile, TimelineDetails::Unavailable);
253            assert_matches!(
254                content,
255                TimelineItemContent::MsgLike(MsgLikeContent { kind: MsgLikeKind::Message(message), .. }) => {
256                    assert_eq!(message.body(), "raclette");
257                }
258            );
259        })
260    }
261
262    #[async_test]
263    async fn test_local_is_sending() {
264        let server = MatrixMockServer::new().await;
265        let client = server.client_builder().build().await;
266        let room = server.sync_room(&client, JoinedRoomBuilder::new(room_id!("!r0"))).await;
267
268        let base_value = BaseLatestEventValue::LocalIsSending(LocalLatestEventValue {
269            timestamp: MilliSecondsSinceUnixEpoch(uint!(42)),
270            content: SerializableEventContent::new(&AnyMessageLikeEventContent::RoomMessage(
271                RoomMessageEventContent::text_plain("raclette"),
272            ))
273            .unwrap(),
274        });
275        let value =
276            LatestEventValue::from_base_latest_event_value(base_value, &room, &client).await;
277
278        assert_matches!(value, LatestEventValue::Local { timestamp, sender, profile, content, is_sending } => {
279            assert_eq!(u64::from(timestamp.get()), 42u64);
280            assert_eq!(sender, "@example:localhost");
281            assert_matches!(profile, TimelineDetails::Unavailable);
282            assert_matches!(
283                content,
284                TimelineItemContent::MsgLike(MsgLikeContent { kind: MsgLikeKind::Message(_), .. })
285            );
286            assert!(is_sending);
287        })
288    }
289
290    #[async_test]
291    async fn test_local_cannot_be_sent() {
292        let server = MatrixMockServer::new().await;
293        let client = server.client_builder().build().await;
294        let room = server.sync_room(&client, JoinedRoomBuilder::new(room_id!("!r0"))).await;
295
296        let base_value = BaseLatestEventValue::LocalCannotBeSent(LocalLatestEventValue {
297            timestamp: MilliSecondsSinceUnixEpoch(uint!(42)),
298            content: SerializableEventContent::new(&AnyMessageLikeEventContent::RoomMessage(
299                RoomMessageEventContent::text_plain("raclette"),
300            ))
301            .unwrap(),
302        });
303        let value =
304            LatestEventValue::from_base_latest_event_value(base_value, &room, &client).await;
305
306        assert_matches!(value, LatestEventValue::Local { timestamp, sender, profile, content, is_sending } => {
307            assert_eq!(u64::from(timestamp.get()), 42u64);
308            assert_eq!(sender, "@example:localhost");
309            assert_matches!(profile, TimelineDetails::Unavailable);
310            assert_matches!(
311                content,
312                TimelineItemContent::MsgLike(MsgLikeContent { kind: MsgLikeKind::Message(_), .. })
313            );
314            assert!(is_sending.not());
315        })
316    }
317
318    #[async_test]
319    async fn test_remote_edit() {
320        let server = MatrixMockServer::new().await;
321        let client = server.client_builder().build().await;
322        let room = server.sync_room(&client, JoinedRoomBuilder::new(room_id!("!r0"))).await;
323        let sender = user_id!("@mnt_io:matrix.org");
324        let event_factory = EventFactory::new();
325
326        let base_value = BaseLatestEventValue::Remote(RemoteLatestEventValue::from_plaintext(
327            event_factory
328                .server_ts(42)
329                .sender(sender)
330                .text_msg("bonjour")
331                .event_id(event_id!("$ev1"))
332                .edit(event_id!("$ev0"), RoomMessageEventContent::text_plain("fondue").into())
333                .into_raw_sync(),
334        ));
335        let value =
336            LatestEventValue::from_base_latest_event_value(base_value, &room, &client).await;
337
338        assert_matches!(value, LatestEventValue::Remote { timestamp, sender: received_sender, is_own, profile, content } => {
339            assert_eq!(u64::from(timestamp.get()), 42u64);
340            assert_eq!(received_sender, sender);
341            assert!(is_own.not());
342            assert_matches!(profile, TimelineDetails::Unavailable);
343            assert_matches!(
344                content,
345                TimelineItemContent::MsgLike(MsgLikeContent { kind: MsgLikeKind::Message(message), .. }) => {
346                    assert_eq!(message.body(), "fondue");
347                }
348            );
349        })
350    }
351}