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