matrix_sdk_base/room/
tags.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 bitflags::bitflags;
16use ruma::events::{tag::Tags, AnyRoomAccountDataEvent, RoomAccountDataEventType};
17use serde::{Deserialize, Serialize};
18
19use super::Room;
20use crate::store::Result as StoreResult;
21
22impl Room {
23    /// Get the `Tags` for this room.
24    pub async fn tags(&self) -> StoreResult<Option<Tags>> {
25        if let Some(AnyRoomAccountDataEvent::Tag(event)) = self
26            .store
27            .get_room_account_data_event(self.room_id(), RoomAccountDataEventType::Tag)
28            .await?
29            .and_then(|raw| raw.deserialize().ok())
30        {
31            Ok(Some(event.content.tags))
32        } else {
33            Ok(None)
34        }
35    }
36
37    /// Check whether the room is marked as favourite.
38    ///
39    /// A room is considered favourite if it has received the `m.favourite` tag.
40    pub fn is_favourite(&self) -> bool {
41        self.inner.read().base_info.notable_tags.contains(RoomNotableTags::FAVOURITE)
42    }
43
44    /// Check whether the room is marked as low priority.
45    ///
46    /// A room is considered low priority if it has received the `m.lowpriority`
47    /// tag.
48    pub fn is_low_priority(&self) -> bool {
49        self.inner.read().base_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY)
50    }
51}
52
53bitflags! {
54    /// Notable tags, i.e. subset of tags that we are more interested by.
55    ///
56    /// We are not interested by all the tags. Some tags are more important than
57    /// others, and this struct describes them.
58    #[repr(transparent)]
59    #[derive(Debug, Default, Clone, Copy, Deserialize, Serialize)]
60    pub(crate) struct RoomNotableTags: u8 {
61        /// The `m.favourite` tag.
62        const FAVOURITE = 0b0000_0001;
63
64        /// THe `m.lowpriority` tag.
65        const LOW_PRIORITY = 0b0000_0010;
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use std::ops::Not;
72
73    use matrix_sdk_test::async_test;
74    use ruma::{
75        events::tag::{TagInfo, TagName, Tags},
76        room_id,
77        serde::Raw,
78        user_id,
79    };
80    use serde_json::json;
81    use stream_assert::{assert_pending, assert_ready};
82
83    use super::{super::BaseRoomInfo, RoomNotableTags};
84    use crate::{
85        response_processors as processors,
86        store::{RoomLoadSettings, StoreConfig},
87        BaseClient, RoomState, SessionMeta,
88    };
89
90    #[async_test]
91    async fn test_is_favourite() {
92        // Given a room,
93        let client =
94            BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
95
96        client
97            .activate(
98                SessionMeta {
99                    user_id: user_id!("@alice:example.org").into(),
100                    device_id: ruma::device_id!("AYEAYEAYE").into(),
101                },
102                RoomLoadSettings::default(),
103                #[cfg(feature = "e2e-encryption")]
104                None,
105            )
106            .await
107            .unwrap();
108
109        let room_id = room_id!("!test:localhost");
110        let room = client.get_or_create_room(room_id, RoomState::Joined);
111
112        // Sanity checks to ensure the room isn't marked as favourite.
113        assert!(room.is_favourite().not());
114
115        // Subscribe to the `RoomInfo`.
116        let mut room_info_subscriber = room.subscribe_info();
117
118        assert_pending!(room_info_subscriber);
119
120        // Create the tag.
121        let tag_raw = Raw::new(&json!({
122            "content": {
123                "tags": {
124                    "m.favourite": {
125                        "order": 0.0
126                    },
127                },
128            },
129            "type": "m.tag",
130        }))
131        .unwrap()
132        .cast();
133
134        // When the new tag is handled and applied.
135        let mut context = processors::Context::default();
136
137        processors::account_data::for_room(&mut context, room_id, &[tag_raw], &client.state_store)
138            .await;
139
140        processors::changes::save_and_apply(
141            context.clone(),
142            &client.state_store,
143            &client.ignore_user_list_changes,
144            None,
145        )
146        .await
147        .unwrap();
148
149        // The `RoomInfo` is getting notified.
150        assert_ready!(room_info_subscriber);
151        assert_pending!(room_info_subscriber);
152
153        // The room is now marked as favourite.
154        assert!(room.is_favourite());
155
156        // Now, let's remove the tag.
157        let tag_raw = Raw::new(&json!({
158            "content": {
159                "tags": {},
160            },
161            "type": "m.tag"
162        }))
163        .unwrap()
164        .cast();
165
166        processors::account_data::for_room(&mut context, room_id, &[tag_raw], &client.state_store)
167            .await;
168
169        processors::changes::save_and_apply(
170            context,
171            &client.state_store,
172            &client.ignore_user_list_changes,
173            None,
174        )
175        .await
176        .unwrap();
177
178        // The `RoomInfo` is getting notified.
179        assert_ready!(room_info_subscriber);
180        assert_pending!(room_info_subscriber);
181
182        // The room is now marked as _not_ favourite.
183        assert!(room.is_favourite().not());
184    }
185
186    #[async_test]
187    async fn test_is_low_priority() {
188        // Given a room,
189        let client =
190            BaseClient::new(StoreConfig::new("cross-process-store-locks-holder-name".to_owned()));
191
192        client
193            .activate(
194                SessionMeta {
195                    user_id: user_id!("@alice:example.org").into(),
196                    device_id: ruma::device_id!("AYEAYEAYE").into(),
197                },
198                RoomLoadSettings::default(),
199                #[cfg(feature = "e2e-encryption")]
200                None,
201            )
202            .await
203            .unwrap();
204
205        let room_id = room_id!("!test:localhost");
206        let room = client.get_or_create_room(room_id, RoomState::Joined);
207
208        // Sanity checks to ensure the room isn't marked as low priority.
209        assert!(!room.is_low_priority());
210
211        // Subscribe to the `RoomInfo`.
212        let mut room_info_subscriber = room.subscribe_info();
213
214        assert_pending!(room_info_subscriber);
215
216        // Create the tag.
217        let tag_raw = Raw::new(&json!({
218            "content": {
219                "tags": {
220                    "m.lowpriority": {
221                        "order": 0.0
222                    },
223                }
224            },
225            "type": "m.tag"
226        }))
227        .unwrap()
228        .cast();
229
230        // When the new tag is handled and applied.
231        let mut context = processors::Context::default();
232
233        processors::account_data::for_room(&mut context, room_id, &[tag_raw], &client.state_store)
234            .await;
235
236        processors::changes::save_and_apply(
237            context.clone(),
238            &client.state_store,
239            &client.ignore_user_list_changes,
240            None,
241        )
242        .await
243        .unwrap();
244
245        // The `RoomInfo` is getting notified.
246        assert_ready!(room_info_subscriber);
247        assert_pending!(room_info_subscriber);
248
249        // The room is now marked as low priority.
250        assert!(room.is_low_priority());
251
252        // Now, let's remove the tag.
253        let tag_raw = Raw::new(&json!({
254            "content": {
255                "tags": {},
256            },
257            "type": "m.tag"
258        }))
259        .unwrap()
260        .cast();
261
262        processors::account_data::for_room(&mut context, room_id, &[tag_raw], &client.state_store)
263            .await;
264
265        processors::changes::save_and_apply(
266            context,
267            &client.state_store,
268            &client.ignore_user_list_changes,
269            None,
270        )
271        .await
272        .unwrap();
273
274        // The `RoomInfo` is getting notified.
275        assert_ready!(room_info_subscriber);
276        assert_pending!(room_info_subscriber);
277
278        // The room is now marked as _not_ low priority.
279        assert!(room.is_low_priority().not());
280    }
281
282    #[test]
283    fn test_handle_notable_tags_favourite() {
284        let mut base_room_info = BaseRoomInfo::default();
285
286        let mut tags = Tags::new();
287        tags.insert(TagName::Favorite, TagInfo::default());
288
289        assert!(base_room_info.notable_tags.contains(RoomNotableTags::FAVOURITE).not());
290        base_room_info.handle_notable_tags(&tags);
291        assert!(base_room_info.notable_tags.contains(RoomNotableTags::FAVOURITE));
292        tags.clear();
293        base_room_info.handle_notable_tags(&tags);
294        assert!(base_room_info.notable_tags.contains(RoomNotableTags::FAVOURITE).not());
295    }
296
297    #[test]
298    fn test_handle_notable_tags_low_priority() {
299        let mut base_room_info = BaseRoomInfo::default();
300
301        let mut tags = Tags::new();
302        tags.insert(TagName::LowPriority, TagInfo::default());
303
304        assert!(base_room_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY).not());
305        base_room_info.handle_notable_tags(&tags);
306        assert!(base_room_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY));
307        tags.clear();
308        base_room_info.handle_notable_tags(&tags);
309        assert!(base_room_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY).not());
310    }
311}