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