matrix_sdk_base/room/
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
15#[cfg(feature = "e2e-encryption")]
16use std::{collections::BTreeMap, num::NonZeroUsize};
17
18use ruma::MilliSecondsSinceUnixEpoch;
19#[cfg(feature = "e2e-encryption")]
20use ruma::{OwnedRoomId, events::AnySyncTimelineEvent, serde::Raw};
21
22use super::Room;
23#[cfg(feature = "e2e-encryption")]
24use super::RoomInfoNotableUpdateReasons;
25use crate::latest_event::{LatestEvent, LatestEventValue};
26
27impl Room {
28    /// The size of the latest_encrypted_events RingBuffer
29    #[cfg(feature = "e2e-encryption")]
30    pub(super) const MAX_ENCRYPTED_EVENTS: NonZeroUsize = NonZeroUsize::new(10).unwrap();
31
32    /// Return the last event in this room, if one has been cached during
33    /// sliding sync.
34    pub fn latest_event(&self) -> Option<LatestEvent> {
35        self.info.read().latest_event.as_deref().cloned()
36    }
37
38    /// Return the [`LatestEventValue`] of this room.
39    ///
40    /// Note that it clones the [`LatestEventValue`]! This can be add pressure
41    /// on the memory if used in a hot path.
42    pub fn new_latest_event(&self) -> LatestEventValue {
43        self.info.read().new_latest_event.clone()
44    }
45
46    /// Return the value of [`LatestEventValue::timestamp`].
47    pub fn new_latest_event_timestamp(&self) -> Option<MilliSecondsSinceUnixEpoch> {
48        self.info.read().new_latest_event.timestamp()
49    }
50
51    /// Return the value of [`LatestEventValue::is_local`].
52    pub fn new_latest_event_is_local(&self) -> bool {
53        self.info.read().new_latest_event.is_local()
54    }
55
56    /// Return the most recent few encrypted events. When the keys come through
57    /// to decrypt these, the most recent relevant one will replace
58    /// latest_event. (We can't tell which one is relevant until
59    /// they are decrypted.)
60    #[cfg(feature = "e2e-encryption")]
61    pub(crate) fn latest_encrypted_events(&self) -> Vec<Raw<AnySyncTimelineEvent>> {
62        self.latest_encrypted_events.read().unwrap().iter().cloned().collect()
63    }
64
65    /// Replace our latest_event with the supplied event, and delete it and all
66    /// older encrypted events from latest_encrypted_events, given that the
67    /// new event was at the supplied index in the latest_encrypted_events
68    /// list.
69    ///
70    /// Panics if index is not a valid index in the latest_encrypted_events
71    /// list.
72    ///
73    /// It is the responsibility of the caller to apply the changes into the
74    /// state store after calling this function.
75    #[cfg(feature = "e2e-encryption")]
76    pub(crate) fn on_latest_event_decrypted(
77        &self,
78        latest_event: Box<LatestEvent>,
79        index: usize,
80        changes: &mut crate::StateChanges,
81        room_info_notable_updates: &mut BTreeMap<OwnedRoomId, RoomInfoNotableUpdateReasons>,
82    ) {
83        self.latest_encrypted_events.write().unwrap().drain(0..=index);
84
85        let room_info = changes
86            .room_infos
87            .entry(self.room_id().to_owned())
88            .or_insert_with(|| self.clone_info());
89
90        room_info.latest_event = Some(latest_event);
91
92        room_info_notable_updates
93            .entry(self.room_id().to_owned())
94            .or_default()
95            .insert(RoomInfoNotableUpdateReasons::LATEST_EVENT);
96    }
97}
98
99#[cfg(all(test, feature = "e2e-encryption"))]
100mod tests_with_e2e_encryption {
101    use std::sync::Arc;
102
103    use assert_matches::assert_matches;
104    use matrix_sdk_common::deserialized_responses::TimelineEvent;
105    use matrix_sdk_test::async_test;
106    use ruma::{room_id, serde::Raw, user_id};
107    use serde_json::json;
108
109    use crate::{
110        BaseClient, Room, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomState,
111        SessionMeta, StateChanges,
112        client::ThreadingSupport,
113        latest_event::LatestEvent,
114        response_processors as processors,
115        store::{MemoryStore, RoomLoadSettings, StoreConfig},
116    };
117
118    fn make_room_test_helper(room_type: RoomState) -> (Arc<MemoryStore>, Room) {
119        let store = Arc::new(MemoryStore::new());
120        let user_id = user_id!("@me:example.org");
121        let room_id = room_id!("!test:localhost");
122        let (sender, _receiver) = tokio::sync::broadcast::channel(1);
123
124        (store.clone(), Room::new(user_id, store, room_id, room_type, sender))
125    }
126
127    #[async_test]
128    async fn test_setting_the_latest_event_doesnt_cause_a_room_info_notable_update() {
129        // Given a room,
130        let client = BaseClient::new(
131            StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
132            ThreadingSupport::Disabled,
133        );
134
135        client
136            .activate(
137                SessionMeta {
138                    user_id: user_id!("@alice:example.org").into(),
139                    device_id: ruma::device_id!("AYEAYEAYE").into(),
140                },
141                RoomLoadSettings::default(),
142                None,
143            )
144            .await
145            .unwrap();
146
147        let room_id = room_id!("!test:localhost");
148        let room = client.get_or_create_room(room_id, RoomState::Joined);
149
150        // That has an encrypted event,
151        add_encrypted_event(&room, "$A");
152        // Sanity: it has no latest_event
153        assert!(room.latest_event().is_none());
154
155        // When I set up an observer on the latest_event,
156        let mut room_info_notable_update = client.room_info_notable_update_receiver();
157
158        // And I provide a decrypted event to replace the encrypted one,
159        let event = make_latest_event("$A");
160
161        let mut context = processors::Context::default();
162        room.on_latest_event_decrypted(
163            event.clone(),
164            0,
165            &mut context.state_changes,
166            &mut context.room_info_notable_updates,
167        );
168
169        assert!(context.room_info_notable_updates.contains_key(room_id));
170
171        // The subscriber isn't notified at this point.
172        assert!(room_info_notable_update.is_empty());
173
174        // Then updating the room info will store the event,
175        processors::changes::save_and_apply(
176            context,
177            &client.state_store,
178            &client.ignore_user_list_changes,
179            None,
180        )
181        .await
182        .unwrap();
183
184        assert_eq!(room.latest_event().unwrap().event_id(), event.event_id());
185
186        // And wake up the subscriber.
187        assert_matches!(
188            room_info_notable_update.recv().await,
189            Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons }) => {
190                assert_eq!(received_room_id, room_id);
191                assert!(reasons.contains(RoomInfoNotableUpdateReasons::LATEST_EVENT));
192            }
193        );
194    }
195
196    #[async_test]
197    async fn test_when_we_provide_a_newly_decrypted_event_it_replaces_latest_event() {
198        use std::collections::BTreeMap;
199
200        // Given a room with an encrypted event
201        let (_store, room) = make_room_test_helper(RoomState::Joined);
202        add_encrypted_event(&room, "$A");
203        // Sanity: it has no latest_event
204        assert!(room.latest_event().is_none());
205
206        // When I provide a decrypted event to replace the encrypted one
207        let event = make_latest_event("$A");
208        let mut changes = StateChanges::default();
209        let mut room_info_notable_updates = BTreeMap::new();
210        room.on_latest_event_decrypted(
211            event.clone(),
212            0,
213            &mut changes,
214            &mut room_info_notable_updates,
215        );
216        room.set_room_info(
217            changes.room_infos.get(room.room_id()).cloned().unwrap(),
218            room_info_notable_updates.get(room.room_id()).copied().unwrap(),
219        );
220
221        // Then is it stored
222        assert_eq!(room.latest_event().unwrap().event_id(), event.event_id());
223    }
224
225    #[cfg(feature = "e2e-encryption")]
226    #[async_test]
227    async fn test_when_a_newly_decrypted_event_appears_we_delete_all_older_encrypted_events() {
228        // Given a room with some encrypted events and a latest event
229
230        use std::collections::BTreeMap;
231        let (_store, room) = make_room_test_helper(RoomState::Joined);
232        room.info.update(|info| info.latest_event = Some(make_latest_event("$A")));
233        add_encrypted_event(&room, "$0");
234        add_encrypted_event(&room, "$1");
235        add_encrypted_event(&room, "$2");
236        add_encrypted_event(&room, "$3");
237
238        // When I provide a latest event
239        let new_event = make_latest_event("$1");
240        let new_event_index = 1;
241        let mut changes = StateChanges::default();
242        let mut room_info_notable_updates = BTreeMap::new();
243        room.on_latest_event_decrypted(
244            new_event.clone(),
245            new_event_index,
246            &mut changes,
247            &mut room_info_notable_updates,
248        );
249        room.set_room_info(
250            changes.room_infos.get(room.room_id()).cloned().unwrap(),
251            room_info_notable_updates.get(room.room_id()).copied().unwrap(),
252        );
253
254        // Then the encrypted events list is shortened to only newer events
255        let enc_evs = room.latest_encrypted_events();
256        assert_eq!(enc_evs.len(), 2);
257        assert_eq!(enc_evs[0].get_field::<&str>("event_id").unwrap().unwrap(), "$2");
258        assert_eq!(enc_evs[1].get_field::<&str>("event_id").unwrap().unwrap(), "$3");
259
260        // And the event is stored
261        assert_eq!(room.latest_event().unwrap().event_id(), new_event.event_id());
262    }
263
264    #[async_test]
265    async fn test_replacing_the_newest_event_leaves_none_left() {
266        use std::collections::BTreeMap;
267
268        // Given a room with some encrypted events
269        let (_store, room) = make_room_test_helper(RoomState::Joined);
270        add_encrypted_event(&room, "$0");
271        add_encrypted_event(&room, "$1");
272        add_encrypted_event(&room, "$2");
273        add_encrypted_event(&room, "$3");
274
275        // When I provide a latest event and say it was the very latest
276        let new_event = make_latest_event("$3");
277        let new_event_index = 3;
278        let mut changes = StateChanges::default();
279        let mut room_info_notable_updates = BTreeMap::new();
280        room.on_latest_event_decrypted(
281            new_event,
282            new_event_index,
283            &mut changes,
284            &mut room_info_notable_updates,
285        );
286        room.set_room_info(
287            changes.room_infos.get(room.room_id()).cloned().unwrap(),
288            room_info_notable_updates.get(room.room_id()).copied().unwrap(),
289        );
290
291        // Then the encrypted events list ie empty
292        let enc_evs = room.latest_encrypted_events();
293        assert_eq!(enc_evs.len(), 0);
294    }
295
296    fn add_encrypted_event(room: &Room, event_id: &str) {
297        room.latest_encrypted_events
298            .write()
299            .unwrap()
300            .push(Raw::from_json_string(json!({ "event_id": event_id }).to_string()).unwrap());
301    }
302
303    fn make_latest_event(event_id: &str) -> Box<LatestEvent> {
304        Box::new(LatestEvent::new(TimelineEvent::from_plaintext(
305            Raw::from_json_string(json!({ "event_id": event_id }).to_string()).unwrap(),
306        )))
307    }
308}