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    traits::RoomDataProvider,
23};
24
25/// A simplified version of [`matrix_sdk_base::latest_event::LatestEventValue`]
26/// tailored for this `timeline` module.
27#[derive(Debug)]
28pub enum LatestEventValue {
29    /// No value has been computed yet, or no candidate value was found.
30    None,
31
32    /// The latest event represents a remote event.
33    Remote {
34        /// The timestamp of the remote event.
35        timestamp: MilliSecondsSinceUnixEpoch,
36
37        /// The sender of the remote event.
38        sender: OwnedUserId,
39
40        /// Has this event been sent by the current logged user?
41        is_own: bool,
42
43        /// The sender's profile.
44        profile: TimelineDetails<Profile>,
45
46        /// The content of the remote event.
47        content: TimelineItemContent,
48    },
49
50    /// The latest event represents a local event that is sending, or that
51    /// cannot be sent, either because a previous local event, or this local
52    /// event cannot be sent.
53    Local {
54        /// The timestamp of the local event.
55        timestamp: MilliSecondsSinceUnixEpoch,
56
57        /// The sender of the remote event.
58        sender: OwnedUserId,
59
60        /// The sender's profile.
61        profile: TimelineDetails<Profile>,
62
63        /// The content of the local event.
64        content: TimelineItemContent,
65
66        /// Whether the local event is sending if it is set to `true`, otherwise
67        /// it cannot be sent.
68        is_sending: bool,
69    },
70}
71
72impl LatestEventValue {
73    pub(crate) async fn from_base_latest_event_value(
74        value: BaseLatestEventValue,
75        room: &Room,
76        client: &Client,
77    ) -> Self {
78        match value {
79            BaseLatestEventValue::None => Self::None,
80            BaseLatestEventValue::Remote(timeline_event) => {
81                let raw_any_sync_timeline_event = timeline_event.into_raw();
82                let Ok(any_sync_timeline_event) = raw_any_sync_timeline_event.deserialize() else {
83                    return Self::None;
84                };
85
86                let timestamp = any_sync_timeline_event.origin_server_ts();
87                let sender = any_sync_timeline_event.sender().to_owned();
88                let is_own = client.user_id().map(|user_id| user_id == sender).unwrap_or(false);
89                let profile = room
90                    .profile_from_user_id(&sender)
91                    .await
92                    .map(TimelineDetails::Ready)
93                    .unwrap_or(TimelineDetails::Unavailable);
94
95                let Some(TimelineAction::AddItem { content }) = TimelineAction::from_event(
96                    any_sync_timeline_event,
97                    &raw_any_sync_timeline_event,
98                    room,
99                    None,
100                    None,
101                    None,
102                    None,
103                )
104                .await
105                else {
106                    return Self::None;
107                };
108
109                Self::Remote { timestamp, sender, is_own, profile, content }
110            }
111            BaseLatestEventValue::LocalIsSending(LocalLatestEventValue {
112                timestamp,
113                content: ref serialized_content,
114            })
115            | BaseLatestEventValue::LocalCannotBeSent(LocalLatestEventValue {
116                timestamp,
117                content: ref serialized_content,
118            }) => {
119                let Ok(message_like_event_content) = serialized_content.deserialize() else {
120                    return Self::None;
121                };
122
123                let sender =
124                    client.user_id().expect("The `Client` is supposed to be logged").to_owned();
125                let profile = room
126                    .profile_from_user_id(&sender)
127                    .await
128                    .map(TimelineDetails::Ready)
129                    .unwrap_or(TimelineDetails::Unavailable);
130                let is_sending = matches!(value, BaseLatestEventValue::LocalIsSending(_));
131
132                match TimelineAction::from_content(message_like_event_content, None, None, None) {
133                    TimelineAction::AddItem { content } => {
134                        Self::Local { timestamp, sender, profile, content, is_sending }
135                    }
136
137                    TimelineAction::HandleAggregation { kind, .. } => {
138                        // Add some debug logging here to help diagnose issues with the latest
139                        // event.
140                        trace!("latest event is an aggregation: {}", kind.debug_string());
141                        Self::None
142                    }
143                }
144            }
145        }
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use std::ops::Not;
152
153    use assert_matches::assert_matches;
154    use matrix_sdk::{
155        latest_events::{LocalLatestEventValue, RemoteLatestEventValue},
156        store::SerializableEventContent,
157        test_utils::mocks::MatrixMockServer,
158    };
159    use matrix_sdk_test::{JoinedRoomBuilder, async_test};
160    use ruma::{
161        MilliSecondsSinceUnixEpoch,
162        events::{AnyMessageLikeEventContent, room::message::RoomMessageEventContent},
163        room_id,
164        serde::Raw,
165        uint, user_id,
166    };
167    use serde_json::json;
168
169    use super::{
170        super::{MsgLikeContent, MsgLikeKind, TimelineItemContent},
171        BaseLatestEventValue, LatestEventValue, TimelineDetails,
172    };
173
174    #[async_test]
175    async fn test_none() {
176        let server = MatrixMockServer::new().await;
177        let client = server.client_builder().build().await;
178        let room = server.sync_room(&client, JoinedRoomBuilder::new(room_id!("!r0"))).await;
179
180        let base_value = BaseLatestEventValue::None;
181        let value =
182            LatestEventValue::from_base_latest_event_value(base_value, &room, &client).await;
183
184        assert_matches!(value, LatestEventValue::None);
185    }
186
187    #[async_test]
188    async fn test_remote() {
189        let server = MatrixMockServer::new().await;
190        let client = server.client_builder().build().await;
191        let room = server.sync_room(&client, JoinedRoomBuilder::new(room_id!("!r0"))).await;
192        let sender = user_id!("@mnt_io:matrix.org");
193
194        let base_value = BaseLatestEventValue::Remote(RemoteLatestEventValue::from_plaintext(
195            Raw::from_json_string(
196                json!({
197                    "content": RoomMessageEventContent::text_plain("raclette"),
198                    "type": "m.room.message",
199                    "event_id": "$ev0",
200                    "room_id": "!r0",
201                    "origin_server_ts": 42,
202                    "sender": sender,
203                })
204                .to_string(),
205            )
206            .unwrap(),
207        ));
208        let value =
209            LatestEventValue::from_base_latest_event_value(base_value, &room, &client).await;
210
211        assert_matches!(value, LatestEventValue::Remote { timestamp, sender: received_sender, is_own, profile, content } => {
212            assert_eq!(u64::from(timestamp.get()), 42u64);
213            assert_eq!(received_sender, sender);
214            assert!(is_own.not());
215            assert_matches!(profile, TimelineDetails::Unavailable);
216            assert_matches!(
217                content,
218                TimelineItemContent::MsgLike(MsgLikeContent { kind: MsgLikeKind::Message(_), .. })
219            );
220        })
221    }
222
223    #[async_test]
224    async fn test_local_is_sending() {
225        let server = MatrixMockServer::new().await;
226        let client = server.client_builder().build().await;
227        let room = server.sync_room(&client, JoinedRoomBuilder::new(room_id!("!r0"))).await;
228
229        let base_value = BaseLatestEventValue::LocalIsSending(LocalLatestEventValue {
230            timestamp: MilliSecondsSinceUnixEpoch(uint!(42)),
231            content: SerializableEventContent::from_raw(
232                Raw::new(&AnyMessageLikeEventContent::RoomMessage(
233                    RoomMessageEventContent::text_plain("raclette"),
234                ))
235                .unwrap(),
236                "m.room.message".to_owned(),
237            ),
238        });
239        let value =
240            LatestEventValue::from_base_latest_event_value(base_value, &room, &client).await;
241
242        assert_matches!(value, LatestEventValue::Local { timestamp, sender, profile, content, is_sending } => {
243            assert_eq!(u64::from(timestamp.get()), 42u64);
244            assert_eq!(sender, "@example:localhost");
245            assert_matches!(profile, TimelineDetails::Unavailable);
246            assert_matches!(
247                content,
248                TimelineItemContent::MsgLike(MsgLikeContent { kind: MsgLikeKind::Message(_), .. })
249            );
250            assert!(is_sending);
251        })
252    }
253
254    #[async_test]
255    async fn test_local_cannot_be_sent() {
256        let server = MatrixMockServer::new().await;
257        let client = server.client_builder().build().await;
258        let room = server.sync_room(&client, JoinedRoomBuilder::new(room_id!("!r0"))).await;
259
260        let base_value = BaseLatestEventValue::LocalCannotBeSent(LocalLatestEventValue {
261            timestamp: MilliSecondsSinceUnixEpoch(uint!(42)),
262            content: SerializableEventContent::from_raw(
263                Raw::new(&AnyMessageLikeEventContent::RoomMessage(
264                    RoomMessageEventContent::text_plain("raclette"),
265                ))
266                .unwrap(),
267                "m.room.message".to_owned(),
268            ),
269        });
270        let value =
271            LatestEventValue::from_base_latest_event_value(base_value, &room, &client).await;
272
273        assert_matches!(value, LatestEventValue::Local { timestamp, sender, profile, content, is_sending } => {
274            assert_eq!(u64::from(timestamp.get()), 42u64);
275            assert_eq!(sender, "@example:localhost");
276            assert_matches!(profile, TimelineDetails::Unavailable);
277            assert_matches!(
278                content,
279                TimelineItemContent::MsgLike(MsgLikeContent { kind: MsgLikeKind::Message(_), .. })
280            );
281            assert!(is_sending.not());
282        })
283    }
284}