Skip to main content

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