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