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};
18
19use crate::timeline::{
20    Profile, TimelineDetails, TimelineItemContent, event_handler::TimelineAction,
21    traits::RoomDataProvider,
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 a local event that is sending, or that
50    /// cannot be sent, either because a previous local event, or this local
51    /// event cannot be sent.
52    Local {
53        /// The timestamp of the local event.
54        timestamp: MilliSecondsSinceUnixEpoch,
55
56        /// The content of the local event.
57        content: TimelineItemContent,
58
59        /// Whether the local event is sending if it is set to `true`, otherwise
60        /// it cannot be sent.
61        is_sending: bool,
62    },
63}
64
65impl LatestEventValue {
66    pub(crate) async fn from_base_latest_event_value(
67        value: BaseLatestEventValue,
68        room: &Room,
69        client: &Client,
70    ) -> Self {
71        match value {
72            BaseLatestEventValue::None => Self::None,
73            BaseLatestEventValue::Remote(timeline_event) => {
74                let raw_any_sync_timeline_event = timeline_event.into_raw();
75                let Ok(any_sync_timeline_event) = raw_any_sync_timeline_event.deserialize() else {
76                    return Self::None;
77                };
78
79                let timestamp = any_sync_timeline_event.origin_server_ts();
80                let sender = any_sync_timeline_event.sender().to_owned();
81                let is_own = client.user_id().map(|user_id| user_id == sender).unwrap_or(false);
82                let profile = room
83                    .profile_from_user_id(&sender)
84                    .await
85                    .map(TimelineDetails::Ready)
86                    .unwrap_or(TimelineDetails::Unavailable);
87
88                let Some(TimelineAction::AddItem { content }) = TimelineAction::from_event(
89                    any_sync_timeline_event,
90                    &raw_any_sync_timeline_event,
91                    room,
92                    None,
93                    None,
94                    None,
95                    None,
96                )
97                .await
98                else {
99                    return Self::None;
100                };
101
102                Self::Remote { timestamp, sender, is_own, profile, content }
103            }
104            BaseLatestEventValue::LocalIsSending(LocalLatestEventValue {
105                timestamp,
106                content: ref serialized_content,
107            })
108            | BaseLatestEventValue::LocalCannotBeSent(LocalLatestEventValue {
109                timestamp,
110                content: ref serialized_content,
111            }) => {
112                let Ok(message_like_event_content) = serialized_content.deserialize() else {
113                    return Self::None;
114                };
115
116                let is_sending = matches!(value, BaseLatestEventValue::LocalIsSending(_));
117
118                let Some(TimelineAction::AddItem { content }) =
119                    TimelineAction::from_content(message_like_event_content, None, None, None)
120                else {
121                    return Self::None;
122                };
123
124                Self::Local { timestamp, content, is_sending }
125            }
126        }
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use std::ops::Not;
133
134    use assert_matches::assert_matches;
135    use matrix_sdk::{
136        latest_events::{LocalLatestEventValue, RemoteLatestEventValue},
137        store::SerializableEventContent,
138        test_utils::mocks::MatrixMockServer,
139    };
140    use matrix_sdk_test::{JoinedRoomBuilder, async_test};
141    use ruma::{
142        MilliSecondsSinceUnixEpoch,
143        events::{AnyMessageLikeEventContent, room::message::RoomMessageEventContent},
144        room_id,
145        serde::Raw,
146        uint, user_id,
147    };
148    use serde_json::json;
149
150    use super::{
151        super::{MsgLikeContent, MsgLikeKind, TimelineItemContent},
152        BaseLatestEventValue, LatestEventValue, TimelineDetails,
153    };
154
155    #[async_test]
156    async fn test_none() {
157        let server = MatrixMockServer::new().await;
158        let client = server.client_builder().build().await;
159        let room = server.sync_room(&client, JoinedRoomBuilder::new(room_id!("!r0"))).await;
160
161        let base_value = BaseLatestEventValue::None;
162        let value =
163            LatestEventValue::from_base_latest_event_value(base_value, &room, &client).await;
164
165        assert_matches!(value, LatestEventValue::None);
166    }
167
168    #[async_test]
169    async fn test_remote() {
170        let server = MatrixMockServer::new().await;
171        let client = server.client_builder().build().await;
172        let room = server.sync_room(&client, JoinedRoomBuilder::new(room_id!("!r0"))).await;
173        let sender = user_id!("@mnt_io:matrix.org");
174
175        let base_value = BaseLatestEventValue::Remote(RemoteLatestEventValue::from_plaintext(
176            Raw::from_json_string(
177                json!({
178                    "content": RoomMessageEventContent::text_plain("raclette"),
179                    "type": "m.room.message",
180                    "event_id": "$ev0",
181                    "room_id": "!r0",
182                    "origin_server_ts": 42,
183                    "sender": sender,
184                })
185                .to_string(),
186            )
187            .unwrap(),
188        ));
189        let value =
190            LatestEventValue::from_base_latest_event_value(base_value, &room, &client).await;
191
192        assert_matches!(value, LatestEventValue::Remote { timestamp, sender: received_sender, is_own, profile, content } => {
193            assert_eq!(u64::from(timestamp.get()), 42u64);
194            assert_eq!(received_sender, sender);
195            assert!(is_own.not());
196            assert_matches!(profile, TimelineDetails::Unavailable);
197            assert_matches!(
198                content,
199                TimelineItemContent::MsgLike(MsgLikeContent { kind: MsgLikeKind::Message(_), .. })
200            );
201        })
202    }
203
204    #[async_test]
205    async fn test_local_is_sending() {
206        let server = MatrixMockServer::new().await;
207        let client = server.client_builder().build().await;
208        let room = server.sync_room(&client, JoinedRoomBuilder::new(room_id!("!r0"))).await;
209
210        let base_value = BaseLatestEventValue::LocalIsSending(LocalLatestEventValue {
211            timestamp: MilliSecondsSinceUnixEpoch(uint!(42)),
212            content: SerializableEventContent::from_raw(
213                Raw::new(&AnyMessageLikeEventContent::RoomMessage(
214                    RoomMessageEventContent::text_plain("raclette"),
215                ))
216                .unwrap(),
217                "m.room.message".to_owned(),
218            ),
219        });
220        let value =
221            LatestEventValue::from_base_latest_event_value(base_value, &room, &client).await;
222
223        assert_matches!(value, LatestEventValue::Local { timestamp, content, is_sending } => {
224            assert_eq!(u64::from(timestamp.get()), 42u64);
225            assert_matches!(
226                content,
227                TimelineItemContent::MsgLike(MsgLikeContent { kind: MsgLikeKind::Message(_), .. })
228            );
229            assert!(is_sending);
230        })
231    }
232
233    #[async_test]
234    async fn test_local_cannot_be_sent() {
235        let server = MatrixMockServer::new().await;
236        let client = server.client_builder().build().await;
237        let room = server.sync_room(&client, JoinedRoomBuilder::new(room_id!("!r0"))).await;
238
239        let base_value = BaseLatestEventValue::LocalCannotBeSent(LocalLatestEventValue {
240            timestamp: MilliSecondsSinceUnixEpoch(uint!(42)),
241            content: SerializableEventContent::from_raw(
242                Raw::new(&AnyMessageLikeEventContent::RoomMessage(
243                    RoomMessageEventContent::text_plain("raclette"),
244                ))
245                .unwrap(),
246                "m.room.message".to_owned(),
247            ),
248        });
249        let value =
250            LatestEventValue::from_base_latest_event_value(base_value, &room, &client).await;
251
252        assert_matches!(value, LatestEventValue::Local { timestamp, content, is_sending } => {
253            assert_eq!(u64::from(timestamp.get()), 42u64);
254            assert_matches!(
255                content,
256                TimelineItemContent::MsgLike(MsgLikeContent { kind: MsgLikeKind::Message(_), .. })
257            );
258            assert!(is_sending.not());
259        })
260    }
261}