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