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        client::ThreadingSupport,
86        response_processors as processors,
87        store::{RoomLoadSettings, StoreConfig},
88        BaseClient, RoomState, SessionMeta,
89    };
90
91    #[async_test]
92    async fn test_is_favourite() {
93        // Given a room,
94        let client = BaseClient::new(
95            StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
96            ThreadingSupport::Disabled,
97        );
98
99        client
100            .activate(
101                SessionMeta {
102                    user_id: user_id!("@alice:example.org").into(),
103                    device_id: ruma::device_id!("AYEAYEAYE").into(),
104                },
105                RoomLoadSettings::default(),
106                #[cfg(feature = "e2e-encryption")]
107                None,
108            )
109            .await
110            .unwrap();
111
112        let room_id = room_id!("!test:localhost");
113        let room = client.get_or_create_room(room_id, RoomState::Joined);
114
115        // Sanity checks to ensure the room isn't marked as favourite.
116        assert!(room.is_favourite().not());
117
118        // Subscribe to the `RoomInfo`.
119        let mut room_info_subscriber = room.subscribe_info();
120
121        assert_pending!(room_info_subscriber);
122
123        // Create the tag.
124        let tag_raw = Raw::new(&json!({
125            "content": {
126                "tags": {
127                    "m.favourite": {
128                        "order": 0.0
129                    },
130                },
131            },
132            "type": "m.tag",
133        }))
134        .unwrap()
135        .cast();
136
137        // When the new tag is handled and applied.
138        let mut context = processors::Context::default();
139
140        processors::account_data::for_room(&mut context, room_id, &[tag_raw], &client.state_store)
141            .await;
142
143        processors::changes::save_and_apply(
144            context.clone(),
145            &client.state_store,
146            &client.ignore_user_list_changes,
147            None,
148        )
149        .await
150        .unwrap();
151
152        // The `RoomInfo` is getting notified.
153        assert_ready!(room_info_subscriber);
154        assert_pending!(room_info_subscriber);
155
156        // The room is now marked as favourite.
157        assert!(room.is_favourite());
158
159        // Now, let's remove the tag.
160        let tag_raw = Raw::new(&json!({
161            "content": {
162                "tags": {},
163            },
164            "type": "m.tag"
165        }))
166        .unwrap()
167        .cast();
168
169        processors::account_data::for_room(&mut context, room_id, &[tag_raw], &client.state_store)
170            .await;
171
172        processors::changes::save_and_apply(
173            context,
174            &client.state_store,
175            &client.ignore_user_list_changes,
176            None,
177        )
178        .await
179        .unwrap();
180
181        // The `RoomInfo` is getting notified.
182        assert_ready!(room_info_subscriber);
183        assert_pending!(room_info_subscriber);
184
185        // The room is now marked as _not_ favourite.
186        assert!(room.is_favourite().not());
187    }
188
189    #[async_test]
190    async fn test_is_low_priority() {
191        // Given a room,
192        let client = BaseClient::new(
193            StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
194            ThreadingSupport::Disabled,
195        );
196
197        client
198            .activate(
199                SessionMeta {
200                    user_id: user_id!("@alice:example.org").into(),
201                    device_id: ruma::device_id!("AYEAYEAYE").into(),
202                },
203                RoomLoadSettings::default(),
204                #[cfg(feature = "e2e-encryption")]
205                None,
206            )
207            .await
208            .unwrap();
209
210        let room_id = room_id!("!test:localhost");
211        let room = client.get_or_create_room(room_id, RoomState::Joined);
212
213        // Sanity checks to ensure the room isn't marked as low priority.
214        assert!(!room.is_low_priority());
215
216        // Subscribe to the `RoomInfo`.
217        let mut room_info_subscriber = room.subscribe_info();
218
219        assert_pending!(room_info_subscriber);
220
221        // Create the tag.
222        let tag_raw = Raw::new(&json!({
223            "content": {
224                "tags": {
225                    "m.lowpriority": {
226                        "order": 0.0
227                    },
228                }
229            },
230            "type": "m.tag"
231        }))
232        .unwrap()
233        .cast();
234
235        // When the new tag is handled and applied.
236        let mut context = processors::Context::default();
237
238        processors::account_data::for_room(&mut context, room_id, &[tag_raw], &client.state_store)
239            .await;
240
241        processors::changes::save_and_apply(
242            context.clone(),
243            &client.state_store,
244            &client.ignore_user_list_changes,
245            None,
246        )
247        .await
248        .unwrap();
249
250        // The `RoomInfo` is getting notified.
251        assert_ready!(room_info_subscriber);
252        assert_pending!(room_info_subscriber);
253
254        // The room is now marked as low priority.
255        assert!(room.is_low_priority());
256
257        // Now, let's remove the tag.
258        let tag_raw = Raw::new(&json!({
259            "content": {
260                "tags": {},
261            },
262            "type": "m.tag"
263        }))
264        .unwrap()
265        .cast();
266
267        processors::account_data::for_room(&mut context, room_id, &[tag_raw], &client.state_store)
268            .await;
269
270        processors::changes::save_and_apply(
271            context,
272            &client.state_store,
273            &client.ignore_user_list_changes,
274            None,
275        )
276        .await
277        .unwrap();
278
279        // The `RoomInfo` is getting notified.
280        assert_ready!(room_info_subscriber);
281        assert_pending!(room_info_subscriber);
282
283        // The room is now marked as _not_ low priority.
284        assert!(room.is_low_priority().not());
285    }
286
287    #[test]
288    fn test_handle_notable_tags_favourite() {
289        let mut base_room_info = BaseRoomInfo::default();
290
291        let mut tags = Tags::new();
292        tags.insert(TagName::Favorite, TagInfo::default());
293
294        assert!(base_room_info.notable_tags.contains(RoomNotableTags::FAVOURITE).not());
295        base_room_info.handle_notable_tags(&tags);
296        assert!(base_room_info.notable_tags.contains(RoomNotableTags::FAVOURITE));
297        tags.clear();
298        base_room_info.handle_notable_tags(&tags);
299        assert!(base_room_info.notable_tags.contains(RoomNotableTags::FAVOURITE).not());
300    }
301
302    #[test]
303    fn test_handle_notable_tags_low_priority() {
304        let mut base_room_info = BaseRoomInfo::default();
305
306        let mut tags = Tags::new();
307        tags.insert(TagName::LowPriority, TagInfo::default());
308
309        assert!(base_room_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY).not());
310        base_room_info.handle_notable_tags(&tags);
311        assert!(base_room_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY));
312        tags.clear();
313        base_room_info.handle_notable_tags(&tags);
314        assert!(base_room_info.notable_tags.contains(RoomNotableTags::LOW_PRIORITY).not());
315    }
316}