Skip to main content

matrix_sdk_base/room/
room_info.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 std::{
16    collections::{BTreeMap, BTreeSet, HashSet},
17    sync::{Arc, atomic::AtomicBool},
18};
19
20use as_variant::as_variant;
21use bitflags::bitflags;
22use eyeball::Subscriber;
23use matrix_sdk_common::{ROOM_VERSION_FALLBACK, ROOM_VERSION_RULES_FALLBACK};
24use ruma::{
25    EventId, MxcUri, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedUserId,
26    RoomAliasId, RoomId, RoomVersionId,
27    api::client::sync::sync_events::v3::RoomSummary as RumaSummary,
28    events::{
29        AnyPossiblyRedactedStateEventContent, AnyStrippedStateEvent, AnySyncStateEvent,
30        AnySyncTimelineEvent, StateEventType,
31        call::member::{
32            CallMemberStateKey, MembershipData, PossiblyRedactedCallMemberEventContent,
33        },
34        direct::OwnedDirectUserIdentifier,
35        member_hints::PossiblyRedactedMemberHintsEventContent,
36        room::{
37            avatar::{self, PossiblyRedactedRoomAvatarEventContent},
38            canonical_alias::PossiblyRedactedRoomCanonicalAliasEventContent,
39            encryption::PossiblyRedactedRoomEncryptionEventContent,
40            guest_access::{GuestAccess, PossiblyRedactedRoomGuestAccessEventContent},
41            history_visibility::{
42                HistoryVisibility, PossiblyRedactedRoomHistoryVisibilityEventContent,
43            },
44            join_rules::{JoinRule, PossiblyRedactedRoomJoinRulesEventContent},
45            name::PossiblyRedactedRoomNameEventContent,
46            pinned_events::{
47                PossiblyRedactedRoomPinnedEventsEventContent, RoomPinnedEventsEventContent,
48            },
49            redaction::SyncRoomRedactionEvent,
50            tombstone::PossiblyRedactedRoomTombstoneEventContent,
51            topic::PossiblyRedactedRoomTopicEventContent,
52        },
53        rtc::notification::CallIntent,
54        tag::{TagEventContent, TagName, Tags},
55    },
56    room::RoomType,
57    room_version_rules::{RedactionRules, RoomVersionRules},
58    serde::Raw,
59};
60use serde::{Deserialize, Serialize};
61use tracing::{field::debug, info, instrument, warn};
62
63use super::{
64    AccountDataSource, EncryptionState, Room, RoomCreateWithCreatorEventContent, RoomDisplayName,
65    RoomHero, RoomNotableTags, RoomState, RoomSummary,
66};
67use crate::{
68    MinimalStateEvent,
69    deserialized_responses::RawSyncOrStrippedState,
70    latest_event::LatestEventValue,
71    notification_settings::RoomNotificationMode,
72    read_receipts::RoomReadReceipts,
73    room::call::CallIntentConsensus,
74    store::{DynStateStore, StateStoreExt},
75    sync::UnreadNotificationsCount,
76    utils::{AnyStateEventEnum, RawStateEventWithKeys},
77};
78
79/// The default value of the maximum power level.
80const DEFAULT_MAX_POWER_LEVEL: i64 = 100;
81
82impl Room {
83    /// Subscribe to the inner `RoomInfo`.
84    pub fn subscribe_info(&self) -> Subscriber<RoomInfo> {
85        self.info.subscribe()
86    }
87
88    /// Clone the inner `RoomInfo`.
89    pub fn clone_info(&self) -> RoomInfo {
90        self.info.get()
91    }
92
93    /// Update the summary with given RoomInfo.
94    pub fn set_room_info(
95        &self,
96        room_info: RoomInfo,
97        room_info_notable_update_reasons: RoomInfoNotableUpdateReasons,
98    ) {
99        self.info.set(room_info);
100
101        if !room_info_notable_update_reasons.is_empty() {
102            // Ignore error if no receiver exists.
103            let _ = self.room_info_notable_update_sender.send(RoomInfoNotableUpdate {
104                room_id: self.room_id.clone(),
105                reasons: room_info_notable_update_reasons,
106            });
107        } else {
108            // TODO: remove this block!
109            // Read `RoomInfoNotableUpdateReasons::NONE` to understand why it must be
110            // removed.
111            let _ = self.room_info_notable_update_sender.send(RoomInfoNotableUpdate {
112                room_id: self.room_id.clone(),
113                reasons: RoomInfoNotableUpdateReasons::NONE,
114            });
115        }
116    }
117}
118
119/// A base room info struct that is the backbone of normal as well as stripped
120/// rooms. Holds all the state events that are important to present a room to
121/// users.
122#[derive(Clone, Debug, Serialize, Deserialize)]
123pub struct BaseRoomInfo {
124    /// The avatar URL of this room.
125    pub(crate) avatar: Option<MinimalStateEvent<PossiblyRedactedRoomAvatarEventContent>>,
126    /// The canonical alias of this room.
127    pub(crate) canonical_alias:
128        Option<MinimalStateEvent<PossiblyRedactedRoomCanonicalAliasEventContent>>,
129    /// The `m.room.create` event content of this room.
130    pub(crate) create: Option<MinimalStateEvent<RoomCreateWithCreatorEventContent>>,
131    /// A list of user ids this room is considered as direct message, if this
132    /// room is a DM.
133    pub(crate) dm_targets: HashSet<OwnedDirectUserIdentifier>,
134    /// The `m.room.encryption` event content that enabled E2EE in this room.
135    pub(crate) encryption: Option<PossiblyRedactedRoomEncryptionEventContent>,
136    /// The guest access policy of this room.
137    pub(crate) guest_access: Option<MinimalStateEvent<PossiblyRedactedRoomGuestAccessEventContent>>,
138    /// The history visibility policy of this room.
139    pub(crate) history_visibility:
140        Option<MinimalStateEvent<PossiblyRedactedRoomHistoryVisibilityEventContent>>,
141    /// The join rule policy of this room.
142    pub(crate) join_rules: Option<MinimalStateEvent<PossiblyRedactedRoomJoinRulesEventContent>>,
143    /// The maximal power level that can be found in this room.
144    pub(crate) max_power_level: i64,
145    /// The member hints for the room as per MSC4171, including service members,
146    /// if available.
147    pub(crate) member_hints: Option<MinimalStateEvent<PossiblyRedactedMemberHintsEventContent>>,
148    /// The `m.room.name` of this room.
149    pub(crate) name: Option<MinimalStateEvent<PossiblyRedactedRoomNameEventContent>>,
150    /// The `m.room.tombstone` event content of this room.
151    pub(crate) tombstone: Option<MinimalStateEvent<PossiblyRedactedRoomTombstoneEventContent>>,
152    /// The topic of this room.
153    pub(crate) topic: Option<MinimalStateEvent<PossiblyRedactedRoomTopicEventContent>>,
154    /// All minimal state events that containing one or more running matrixRTC
155    /// memberships.
156    #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
157    pub(crate) rtc_member_events:
158        BTreeMap<CallMemberStateKey, MinimalStateEvent<PossiblyRedactedCallMemberEventContent>>,
159    /// Whether this room has been manually marked as unread.
160    #[serde(default)]
161    pub(crate) is_marked_unread: bool,
162    /// The source of is_marked_unread.
163    #[serde(default)]
164    pub(crate) is_marked_unread_source: AccountDataSource,
165    /// Some notable tags.
166    ///
167    /// We are not interested by all the tags. Some tags are more important than
168    /// others, and this field collects them.
169    #[serde(skip_serializing_if = "RoomNotableTags::is_empty", default)]
170    pub(crate) notable_tags: RoomNotableTags,
171    /// The `m.room.pinned_events` of this room.
172    pub(crate) pinned_events: Option<PossiblyRedactedRoomPinnedEventsEventContent>,
173}
174
175impl BaseRoomInfo {
176    /// Create a new, empty base room info.
177    pub fn new() -> Self {
178        Self::default()
179    }
180
181    /// Get the room version of this room.
182    ///
183    /// For room versions earlier than room version 11, if the event is
184    /// redacted, this will return the default of [`RoomVersionId::V1`].
185    pub fn room_version(&self) -> Option<&RoomVersionId> {
186        Some(&self.create.as_ref()?.content.room_version)
187    }
188
189    /// Handle a state event for this room and update our info accordingly.
190    ///
191    /// Returns true if the event modified the info, false otherwise.
192    pub fn handle_state_event<T: AnyStateEventEnum>(
193        &mut self,
194        raw_event: &mut RawStateEventWithKeys<T>,
195    ) -> bool {
196        match (&raw_event.event_type, raw_event.state_key.as_str()) {
197            (StateEventType::RoomEncryption, "") => {
198                // To avoid breaking encrypted rooms, we ignore `m.room.encryption` events that
199                // fail to deserialize or that are redacted (i.e. they don't contain the
200                // algorithm used for encryption).
201                if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
202                    as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomEncryption)
203                }) && event.content.algorithm.is_some()
204                {
205                    self.encryption = Some(event.content);
206                    true
207                } else {
208                    false
209                }
210            }
211            (StateEventType::RoomAvatar, "") => {
212                if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
213                    as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomAvatar)
214                }) {
215                    self.avatar = Some(event);
216                    true
217                } else {
218                    // Remove the previous content if the new content is unknown.
219                    self.avatar.take().is_some()
220                }
221            }
222            (StateEventType::RoomName, "") => {
223                if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
224                    as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomName)
225                }) {
226                    self.name = Some(event);
227                    true
228                } else {
229                    // Remove the previous content if the new content is unknown.
230                    self.name.take().is_some()
231                }
232            }
233            // `m.room.create` CANNOT be overwritten.
234            (StateEventType::RoomCreate, "") if self.create.is_none() => {
235                if let Some(any_event) = raw_event.deserialize()
236                    && let Some(content) = as_variant!(
237                        any_event.get_content(),
238                        AnyPossiblyRedactedStateEventContent::RoomCreate
239                    )
240                {
241                    self.create = Some(MinimalStateEvent {
242                        content: RoomCreateWithCreatorEventContent::from_event_content(
243                            content,
244                            any_event.get_sender().to_owned(),
245                        ),
246                        event_id: any_event.get_event_id().map(ToOwned::to_owned),
247                    });
248                    true
249                } else {
250                    false
251                }
252            }
253            (StateEventType::RoomHistoryVisibility, "") => {
254                if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
255                    as_variant!(
256                        any_event,
257                        AnyPossiblyRedactedStateEventContent::RoomHistoryVisibility
258                    )
259                }) {
260                    self.history_visibility = Some(event);
261                    true
262                } else {
263                    // Remove the previous content if the new content is unknown.
264                    self.history_visibility.take().is_some()
265                }
266            }
267            (StateEventType::RoomGuestAccess, "") => {
268                if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
269                    as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomGuestAccess)
270                }) {
271                    self.guest_access = Some(event);
272                    true
273                } else {
274                    // Remove the previous content if the new content is unknown.
275                    self.guest_access.take().is_some()
276                }
277            }
278            (StateEventType::MemberHints, "") => {
279                if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
280                    as_variant!(any_event, AnyPossiblyRedactedStateEventContent::MemberHints)
281                }) {
282                    self.member_hints = Some(event);
283                    true
284                } else {
285                    // Remove the previous content if the new content is unknown.
286                    self.member_hints.take().is_some()
287                }
288            }
289            (StateEventType::RoomJoinRules, "") => {
290                if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
291                    as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomJoinRules)
292                }) {
293                    match &event.content.join_rule {
294                        JoinRule::Invite
295                        | JoinRule::Knock
296                        | JoinRule::Private
297                        | JoinRule::Restricted(_)
298                        | JoinRule::KnockRestricted(_)
299                        | JoinRule::Public => {
300                            self.join_rules = Some(event);
301                            true
302                        }
303                        r => {
304                            warn!(join_rule = ?r.as_str(), "Encountered a custom join rule, skipping");
305                            // Remove the previous content if the new content is unsupported.
306                            self.join_rules.take().is_some()
307                        }
308                    }
309                } else {
310                    // Remove the previous content if the new content is unknown.
311                    self.join_rules.take().is_some()
312                }
313            }
314            (StateEventType::RoomCanonicalAlias, "") => {
315                if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
316                    as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomCanonicalAlias)
317                }) {
318                    self.canonical_alias = Some(event);
319                    true
320                } else {
321                    // Remove the previous content if the new content is unknown.
322                    self.canonical_alias.take().is_some()
323                }
324            }
325            (StateEventType::RoomTopic, "") => {
326                if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
327                    as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomTopic)
328                }) {
329                    self.topic = Some(event);
330                    true
331                } else {
332                    // Remove the previous content if the new content is unknown.
333                    self.topic.take().is_some()
334                }
335            }
336            (StateEventType::RoomTombstone, "") => {
337                if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
338                    as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomTombstone)
339                }) {
340                    self.tombstone = Some(event);
341                    true
342                } else {
343                    // Remove the previous content if the new content is unknown.
344                    self.tombstone.take().is_some()
345                }
346            }
347            (StateEventType::RoomPowerLevels, "") => {
348                if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
349                    as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomPowerLevels)
350                }) {
351                    let new_max = i64::from(
352                        event
353                            .content
354                            .users
355                            .values()
356                            .fold(event.content.users_default, |max_pl, user_pl| {
357                                max_pl.max(*user_pl)
358                            }),
359                    );
360
361                    if self.max_power_level != new_max {
362                        self.max_power_level = new_max;
363                        true
364                    } else {
365                        false
366                    }
367                } else if self.max_power_level != DEFAULT_MAX_POWER_LEVEL {
368                    // Reset the previous value if the new value is unknown.
369                    self.max_power_level = DEFAULT_MAX_POWER_LEVEL;
370                    true
371                } else {
372                    false
373                }
374            }
375            (StateEventType::CallMember, _) => {
376                if let Ok(call_member_key) = raw_event.state_key.parse::<CallMemberStateKey>() {
377                    if let Some(any_event) = raw_event.deserialize()
378                        && let Some(content) = as_variant!(
379                            any_event.get_content(),
380                            AnyPossiblyRedactedStateEventContent::CallMember
381                        )
382                    {
383                        let mut event = MinimalStateEvent {
384                            content,
385                            event_id: any_event.get_event_id().map(ToOwned::to_owned),
386                        };
387
388                        if let Some(origin_server_ts) = any_event.get_origin_server_ts() {
389                            event.content.set_created_ts_if_none(origin_server_ts);
390                        }
391
392                        // Add the new event.
393                        self.rtc_member_events.insert(call_member_key, event);
394
395                        // Remove all events that don't contain any memberships anymore.
396                        self.rtc_member_events
397                            .retain(|_, ev| !ev.content.active_memberships(None).is_empty());
398
399                        true
400                    } else {
401                        // Remove the previous content with the same state key if the new content is
402                        // unknown.
403                        self.rtc_member_events.remove(&call_member_key).is_some()
404                    }
405                } else {
406                    false
407                }
408            }
409            (StateEventType::RoomPinnedEvents, "") => {
410                if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
411                    as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomPinnedEvents)
412                }) {
413                    self.pinned_events = Some(event.content);
414                    true
415                } else {
416                    // Remove the previous content if the new content is unknown.
417                    self.pinned_events.take().is_some()
418                }
419            }
420            _ => false,
421        }
422    }
423
424    pub(super) fn handle_redaction(&mut self, redacts: &EventId) {
425        let redaction_rules = self
426            .room_version()
427            .and_then(|room_version| room_version.rules())
428            .unwrap_or(ROOM_VERSION_RULES_FALLBACK)
429            .redaction;
430
431        if let Some(ev) = &mut self.avatar
432            && ev.event_id.as_deref() == Some(redacts)
433        {
434            ev.redact(&redaction_rules);
435        } else if let Some(ev) = &mut self.canonical_alias
436            && ev.event_id.as_deref() == Some(redacts)
437        {
438            ev.redact(&redaction_rules);
439        } else if let Some(ev) = &mut self.create
440            && ev.event_id.as_deref() == Some(redacts)
441        {
442            ev.redact(&redaction_rules);
443        } else if let Some(ev) = &mut self.guest_access
444            && ev.event_id.as_deref() == Some(redacts)
445        {
446            ev.redact(&redaction_rules);
447        } else if let Some(ev) = &mut self.history_visibility
448            && ev.event_id.as_deref() == Some(redacts)
449        {
450            ev.redact(&redaction_rules);
451        } else if let Some(ev) = &mut self.join_rules
452            && ev.event_id.as_deref() == Some(redacts)
453        {
454            ev.redact(&redaction_rules);
455        } else if let Some(ev) = &mut self.name
456            && ev.event_id.as_deref() == Some(redacts)
457        {
458            ev.redact(&redaction_rules);
459        } else if let Some(ev) = &mut self.tombstone
460            && ev.event_id.as_deref() == Some(redacts)
461        {
462            ev.redact(&redaction_rules);
463        } else if let Some(ev) = &mut self.topic
464            && ev.event_id.as_deref() == Some(redacts)
465        {
466            ev.redact(&redaction_rules);
467        } else {
468            self.rtc_member_events
469                .retain(|_, member_event| member_event.event_id.as_deref() != Some(redacts));
470        }
471    }
472
473    pub fn handle_notable_tags(&mut self, tags: &Tags) {
474        let mut notable_tags = RoomNotableTags::empty();
475
476        if tags.contains_key(&TagName::Favorite) {
477            notable_tags.insert(RoomNotableTags::FAVOURITE);
478        }
479
480        if tags.contains_key(&TagName::LowPriority) {
481            notable_tags.insert(RoomNotableTags::LOW_PRIORITY);
482        }
483
484        self.notable_tags = notable_tags;
485    }
486}
487
488impl Default for BaseRoomInfo {
489    fn default() -> Self {
490        Self {
491            avatar: None,
492            canonical_alias: None,
493            create: None,
494            dm_targets: Default::default(),
495            member_hints: None,
496            encryption: None,
497            guest_access: None,
498            history_visibility: None,
499            join_rules: None,
500            max_power_level: DEFAULT_MAX_POWER_LEVEL,
501            name: None,
502            tombstone: None,
503            topic: None,
504            rtc_member_events: BTreeMap::new(),
505            is_marked_unread: false,
506            is_marked_unread_source: AccountDataSource::Unstable,
507            notable_tags: RoomNotableTags::empty(),
508            pinned_events: None,
509        }
510    }
511}
512
513/// The underlying pure data structure for joined and left rooms.
514///
515/// Holds all the info needed to persist a room into the state store.
516#[derive(Clone, Debug, Serialize, Deserialize)]
517pub struct RoomInfo {
518    /// The version of the room info type. It is used to migrate the `RoomInfo`
519    /// serialization from one version to another.
520    #[serde(default, alias = "version")]
521    pub(crate) data_format_version: u8,
522
523    /// The unique room id of the room.
524    pub(crate) room_id: OwnedRoomId,
525
526    /// The state of the room.
527    pub(crate) room_state: RoomState,
528
529    /// The unread notifications counts, as returned by the server.
530    ///
531    /// These might be incorrect for encrypted rooms, since the server doesn't
532    /// have access to the content of the encrypted events.
533    pub(crate) notification_counts: UnreadNotificationsCount,
534
535    /// The summary of this room.
536    pub(crate) summary: RoomSummary,
537
538    /// Flag remembering if the room members are synced.
539    pub(crate) members_synced: bool,
540
541    /// The prev batch of this room we received during the last sync.
542    pub(crate) last_prev_batch: Option<String>,
543
544    /// How much we know about this room.
545    pub(crate) sync_info: SyncInfo,
546
547    /// Whether or not the encryption info was been synced.
548    pub(crate) encryption_state_synced: bool,
549
550    /// The latest event value of this room.
551    #[serde(default)]
552    pub(crate) latest_event_value: LatestEventValue,
553
554    /// Information about read receipts for this room.
555    #[serde(default)]
556    pub(crate) read_receipts: RoomReadReceipts,
557
558    /// Base room info which holds some basic event contents important for the
559    /// room state.
560    pub(crate) base_info: Box<BaseRoomInfo>,
561
562    /// Whether we already warned about unknown room version rules in
563    /// [`RoomInfo::room_version_rules_or_default`]. This is done to avoid
564    /// spamming about unknown room versions rules in the log for the same room.
565    #[serde(skip)]
566    pub(crate) warned_about_unknown_room_version_rules: Arc<AtomicBool>,
567
568    /// Cached display name, useful for sync access.
569    ///
570    /// Filled by calling [`Room::compute_display_name`]. It's automatically
571    /// filled at start when creating a room, or on every successful sync.
572    #[serde(default, skip_serializing_if = "Option::is_none")]
573    pub(crate) cached_display_name: Option<RoomDisplayName>,
574
575    /// Cached user defined notification mode.
576    #[serde(default, skip_serializing_if = "Option::is_none")]
577    pub(crate) cached_user_defined_notification_mode: Option<RoomNotificationMode>,
578
579    /// The recency stamp of this room.
580    ///
581    /// It's not to be confused with the `origin_server_ts` value of an event.
582    /// Sliding Sync might “ignore” some events when computing the recency
583    /// stamp of the room. The recency stamp must be considered as an opaque
584    /// unsigned integer value.
585    ///
586    /// # Sorting rooms
587    ///
588    /// The recency stamp is designed to _sort_ rooms between them. The room
589    /// with the highest stamp should be at the top of a room list. However, in
590    /// some situation, it might be inaccurate (for example if the server and
591    /// the client disagree on which events should increment the recency stamp).
592    /// The [`LatestEventValue`] might be a useful alternative to sort rooms
593    /// between them as it's all computed client-side. In this case, the recency
594    /// stamp nicely acts as a default fallback.
595    #[serde(default)]
596    pub(crate) recency_stamp: Option<RoomRecencyStamp>,
597}
598
599impl RoomInfo {
600    #[doc(hidden)] // used by store tests, otherwise it would be pub(crate)
601    pub fn new(room_id: &RoomId, room_state: RoomState) -> Self {
602        Self {
603            data_format_version: 1,
604            room_id: room_id.into(),
605            room_state,
606            notification_counts: Default::default(),
607            summary: Default::default(),
608            members_synced: false,
609            last_prev_batch: None,
610            sync_info: SyncInfo::NoState,
611            encryption_state_synced: false,
612            latest_event_value: LatestEventValue::default(),
613            read_receipts: Default::default(),
614            base_info: Box::new(BaseRoomInfo::new()),
615            warned_about_unknown_room_version_rules: Arc::new(false.into()),
616            cached_display_name: None,
617            cached_user_defined_notification_mode: None,
618            recency_stamp: None,
619        }
620    }
621
622    /// Mark this Room as joined.
623    pub fn mark_as_joined(&mut self) {
624        self.set_state(RoomState::Joined);
625    }
626
627    /// Mark this Room as left.
628    pub fn mark_as_left(&mut self) {
629        self.set_state(RoomState::Left);
630    }
631
632    /// Mark this Room as invited.
633    pub fn mark_as_invited(&mut self) {
634        self.set_state(RoomState::Invited);
635    }
636
637    /// Mark this Room as knocked.
638    pub fn mark_as_knocked(&mut self) {
639        self.set_state(RoomState::Knocked);
640    }
641
642    /// Mark this Room as banned.
643    pub fn mark_as_banned(&mut self) {
644        self.set_state(RoomState::Banned);
645    }
646
647    /// Set the membership RoomState of this Room
648    pub fn set_state(&mut self, room_state: RoomState) {
649        self.room_state = room_state;
650    }
651
652    /// Mark this Room as having all the members synced.
653    pub fn mark_members_synced(&mut self) {
654        self.members_synced = true;
655    }
656
657    /// Mark this Room as still missing member information.
658    pub fn mark_members_missing(&mut self) {
659        self.members_synced = false;
660    }
661
662    /// Returns whether the room members are synced.
663    pub fn are_members_synced(&self) -> bool {
664        self.members_synced
665    }
666
667    /// Mark this Room as still missing some state information.
668    pub fn mark_state_partially_synced(&mut self) {
669        self.sync_info = SyncInfo::PartiallySynced;
670    }
671
672    /// Mark this Room as still having all state synced.
673    pub fn mark_state_fully_synced(&mut self) {
674        self.sync_info = SyncInfo::FullySynced;
675    }
676
677    /// Mark this Room as still having no state synced.
678    pub fn mark_state_not_synced(&mut self) {
679        self.sync_info = SyncInfo::NoState;
680    }
681
682    /// Mark this Room as having the encryption state synced.
683    pub fn mark_encryption_state_synced(&mut self) {
684        self.encryption_state_synced = true;
685    }
686
687    /// Mark this Room as still missing encryption state information.
688    pub fn mark_encryption_state_missing(&mut self) {
689        self.encryption_state_synced = false;
690    }
691
692    /// Set the `prev_batch`-token.
693    /// Returns whether the token has differed and thus has been upgraded:
694    /// `false` means no update was applied as the were the same
695    pub fn set_prev_batch(&mut self, prev_batch: Option<&str>) -> bool {
696        if self.last_prev_batch.as_deref() != prev_batch {
697            self.last_prev_batch = prev_batch.map(|p| p.to_owned());
698            true
699        } else {
700            false
701        }
702    }
703
704    /// Returns the state this room is in.
705    pub fn state(&self) -> RoomState {
706        self.room_state
707    }
708
709    /// Returns the encryption state of this room.
710    #[cfg(not(feature = "experimental-encrypted-state-events"))]
711    pub fn encryption_state(&self) -> EncryptionState {
712        if !self.encryption_state_synced {
713            EncryptionState::Unknown
714        } else if self.base_info.encryption.is_some() {
715            EncryptionState::Encrypted
716        } else {
717            EncryptionState::NotEncrypted
718        }
719    }
720
721    /// Returns the encryption state of this room.
722    #[cfg(feature = "experimental-encrypted-state-events")]
723    pub fn encryption_state(&self) -> EncryptionState {
724        if !self.encryption_state_synced {
725            EncryptionState::Unknown
726        } else {
727            self.base_info
728                .encryption
729                .as_ref()
730                .map(|state| {
731                    if state.encrypt_state_events {
732                        EncryptionState::StateEncrypted
733                    } else {
734                        EncryptionState::Encrypted
735                    }
736                })
737                .unwrap_or(EncryptionState::NotEncrypted)
738        }
739    }
740
741    /// Set the encryption event content in this room.
742    pub fn set_encryption_event(
743        &mut self,
744        event: Option<PossiblyRedactedRoomEncryptionEventContent>,
745    ) {
746        self.base_info.encryption = event;
747    }
748
749    /// Handle the encryption state.
750    pub fn handle_encryption_state(
751        &mut self,
752        requested_required_states: &[(StateEventType, String)],
753    ) {
754        if requested_required_states
755            .iter()
756            .any(|(state_event, _)| state_event == &StateEventType::RoomEncryption)
757        {
758            // The `m.room.encryption` event was requested during the sync. Whether we have
759            // received a `m.room.encryption` event in return doesn't matter: we must mark
760            // the encryption state as synced; if the event is present, it means the room
761            // _is_ encrypted, otherwise it means the room _is not_ encrypted.
762
763            self.mark_encryption_state_synced();
764        }
765    }
766
767    /// Handle the given state event.
768    ///
769    /// Returns true if the event modified the info, false otherwise.
770    pub fn handle_state_event(
771        &mut self,
772        raw_event: &mut RawStateEventWithKeys<AnySyncStateEvent>,
773    ) -> bool {
774        // Store the state event in the `BaseRoomInfo` first.
775        let base_info_has_been_modified = self.base_info.handle_state_event(raw_event);
776
777        if raw_event.event_type == StateEventType::RoomEncryption && raw_event.state_key.is_empty()
778        {
779            // The `m.room.encryption` event was or wasn't explicitly requested, we don't
780            // know here (see `Self::handle_encryption_state`) but we got one in
781            // return! In this case, we can deduce the room _is_ encrypted, but we cannot
782            // know if it _is not_ encrypted.
783
784            self.mark_encryption_state_synced();
785        }
786
787        base_info_has_been_modified
788    }
789
790    /// Handle the given stripped state event.
791    ///
792    /// Returns true if the event modified the info, false otherwise.
793    pub fn handle_stripped_state_event(
794        &mut self,
795        raw_event: &mut RawStateEventWithKeys<AnyStrippedStateEvent>,
796    ) -> bool {
797        self.base_info.handle_state_event(raw_event)
798    }
799
800    /// Handle the given redaction.
801    #[instrument(skip_all, fields(redacts))]
802    pub fn handle_redaction(
803        &mut self,
804        event: &SyncRoomRedactionEvent,
805        _raw: &Raw<SyncRoomRedactionEvent>,
806    ) {
807        let redaction_rules = self.room_version_rules_or_default().redaction;
808
809        let Some(redacts) = event.redacts(&redaction_rules) else {
810            info!("Can't apply redaction, redacts field is missing");
811            return;
812        };
813        tracing::Span::current().record("redacts", debug(redacts));
814
815        self.base_info.handle_redaction(redacts);
816    }
817
818    /// Returns the current room avatar.
819    pub fn avatar_url(&self) -> Option<&MxcUri> {
820        self.base_info.avatar.as_ref().and_then(|e| e.content.url.as_deref())
821    }
822
823    /// Update the room avatar.
824    pub fn update_avatar(&mut self, url: Option<OwnedMxcUri>) {
825        self.base_info.avatar = url.map(|url| {
826            let mut content = PossiblyRedactedRoomAvatarEventContent::new();
827            content.url = Some(url);
828
829            MinimalStateEvent { content, event_id: None }
830        });
831    }
832
833    /// Returns information about the current room avatar.
834    pub fn avatar_info(&self) -> Option<&avatar::ImageInfo> {
835        self.base_info.avatar.as_ref().and_then(|e| e.content.info.as_deref())
836    }
837
838    /// Update the notifications count.
839    pub fn update_notification_count(&mut self, notification_counts: UnreadNotificationsCount) {
840        self.notification_counts = notification_counts;
841    }
842
843    /// Update the RoomSummary from a Ruma `RoomSummary`.
844    ///
845    /// Returns true if any field has been updated, false otherwise.
846    pub fn update_from_ruma_summary(&mut self, summary: &RumaSummary) -> bool {
847        let mut changed = false;
848
849        if !summary.is_empty() {
850            if !summary.heroes.is_empty() {
851                self.summary.room_heroes = summary
852                    .heroes
853                    .iter()
854                    .map(|hero_id| RoomHero {
855                        user_id: hero_id.to_owned(),
856                        display_name: None,
857                        avatar_url: None,
858                    })
859                    .collect();
860
861                changed = true;
862            }
863
864            if let Some(joined) = summary.joined_member_count {
865                self.summary.joined_member_count = joined.into();
866                changed = true;
867            }
868
869            if let Some(invited) = summary.invited_member_count {
870                self.summary.invited_member_count = invited.into();
871                changed = true;
872            }
873        }
874
875        changed
876    }
877
878    /// Updates the joined member count.
879    pub(crate) fn update_joined_member_count(&mut self, count: u64) {
880        self.summary.joined_member_count = count;
881    }
882
883    /// Updates the invited member count.
884    pub(crate) fn update_invited_member_count(&mut self, count: u64) {
885        self.summary.invited_member_count = count;
886    }
887
888    /// Updates the room heroes.
889    pub(crate) fn update_heroes(&mut self, heroes: Vec<RoomHero>) {
890        self.summary.room_heroes = heroes;
891    }
892
893    /// The heroes for this room.
894    pub fn heroes(&self) -> &[RoomHero] {
895        &self.summary.room_heroes
896    }
897
898    /// The number of active members (invited + joined) in the room.
899    ///
900    /// The return value is saturated at `u64::MAX`.
901    pub fn active_members_count(&self) -> u64 {
902        self.summary.joined_member_count.saturating_add(self.summary.invited_member_count)
903    }
904
905    /// The number of invited members in the room
906    pub fn invited_members_count(&self) -> u64 {
907        self.summary.invited_member_count
908    }
909
910    /// The number of joined members in the room
911    pub fn joined_members_count(&self) -> u64 {
912        self.summary.joined_member_count
913    }
914
915    /// Get the canonical alias of this room.
916    pub fn canonical_alias(&self) -> Option<&RoomAliasId> {
917        self.base_info.canonical_alias.as_ref()?.content.alias.as_deref()
918    }
919
920    /// Get the alternative aliases of this room.
921    pub fn alt_aliases(&self) -> &[OwnedRoomAliasId] {
922        self.base_info
923            .canonical_alias
924            .as_ref()
925            .map(|ev| ev.content.alt_aliases.as_ref())
926            .unwrap_or_default()
927    }
928
929    /// Get the room ID of this room.
930    pub fn room_id(&self) -> &RoomId {
931        &self.room_id
932    }
933
934    /// Get the room version of this room.
935    pub fn room_version(&self) -> Option<&RoomVersionId> {
936        self.base_info.room_version()
937    }
938
939    /// Get the room version rules of this room, or a sensible default.
940    ///
941    /// Will warn (at most once) if the room create event is missing from this
942    /// [`RoomInfo`] or if the room version is unsupported.
943    pub fn room_version_rules_or_default(&self) -> RoomVersionRules {
944        use std::sync::atomic::Ordering;
945
946        self.base_info.room_version().and_then(|room_version| room_version.rules()).unwrap_or_else(
947            || {
948                if self
949                    .warned_about_unknown_room_version_rules
950                    .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed)
951                    .is_ok()
952                {
953                    warn!("Unable to get the room version rules, defaulting to rules for room version {ROOM_VERSION_FALLBACK}");
954                }
955
956                ROOM_VERSION_RULES_FALLBACK
957            },
958        )
959    }
960
961    /// Get the room type of this room.
962    pub fn room_type(&self) -> Option<&RoomType> {
963        self.base_info.create.as_ref()?.content.room_type.as_ref()
964    }
965
966    /// Get the creators of this room.
967    pub fn creators(&self) -> Option<Vec<OwnedUserId>> {
968        Some(self.base_info.create.as_ref()?.content.creators())
969    }
970
971    pub(super) fn guest_access(&self) -> &GuestAccess {
972        self.base_info
973            .guest_access
974            .as_ref()
975            .and_then(|event| event.content.guest_access.as_ref())
976            .unwrap_or(&GuestAccess::Forbidden)
977    }
978
979    /// Returns the history visibility for this room.
980    ///
981    /// Returns None if the event was never seen during sync.
982    pub fn history_visibility(&self) -> Option<&HistoryVisibility> {
983        Some(&self.base_info.history_visibility.as_ref()?.content.history_visibility)
984    }
985
986    /// Returns the history visibility for this room, or a sensible default.
987    ///
988    /// Returns `Shared`, the default specified by the [spec], when the event is
989    /// missing.
990    ///
991    /// [spec]: https://spec.matrix.org/latest/client-server-api/#server-behaviour-7
992    pub fn history_visibility_or_default(&self) -> &HistoryVisibility {
993        self.history_visibility().unwrap_or(&HistoryVisibility::Shared)
994    }
995
996    /// Return the join rule for this room, if the `m.room.join_rules` event is
997    /// available.
998    pub fn join_rule(&self) -> Option<&JoinRule> {
999        Some(&self.base_info.join_rules.as_ref()?.content.join_rule)
1000    }
1001
1002    /// Return the service members for this room if the `m.member_hints` event
1003    /// is available
1004    pub fn service_members(&self) -> Option<&BTreeSet<OwnedUserId>> {
1005        self.base_info.member_hints.as_ref()?.content.service_members.as_ref()
1006    }
1007
1008    /// Get the name of this room.
1009    pub fn name(&self) -> Option<&str> {
1010        self.base_info.name.as_ref()?.content.name.as_deref().filter(|name| !name.is_empty())
1011    }
1012
1013    /// Get the content of the `m.room.create` event if any.
1014    pub fn create(&self) -> Option<&RoomCreateWithCreatorEventContent> {
1015        Some(&self.base_info.create.as_ref()?.content)
1016    }
1017
1018    /// Get the content of the `m.room.tombstone` event if any.
1019    pub fn tombstone(&self) -> Option<&PossiblyRedactedRoomTombstoneEventContent> {
1020        Some(&self.base_info.tombstone.as_ref()?.content)
1021    }
1022
1023    /// Returns the topic for this room, if set.
1024    pub fn topic(&self) -> Option<&str> {
1025        self.base_info.topic.as_ref()?.content.topic.as_deref()
1026    }
1027
1028    /// Get a list of all the valid (non expired) matrixRTC memberships and
1029    /// associated UserId's in this room.
1030    ///
1031    /// The vector is ordered by oldest membership to newest.
1032    fn active_matrix_rtc_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
1033        let mut v = self
1034            .base_info
1035            .rtc_member_events
1036            .iter()
1037            .flat_map(|(state_key, ev)| {
1038                ev.content.active_memberships(None).into_iter().map(move |m| (state_key.clone(), m))
1039            })
1040            .collect::<Vec<_>>();
1041        v.sort_by_key(|(_, m)| m.created_ts());
1042        v
1043    }
1044
1045    /// Similar to
1046    /// [`matrix_rtc_memberships`](Self::active_matrix_rtc_memberships) but only
1047    /// returns Memberships with application "m.call" and scope "m.room".
1048    ///
1049    /// The vector is ordered by oldest membership user to newest.
1050    fn active_room_call_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
1051        self.active_matrix_rtc_memberships()
1052            .into_iter()
1053            .filter(|(_user_id, m)| m.is_room_call())
1054            .collect()
1055    }
1056
1057    /// Is there a non expired membership with application "m.call" and scope
1058    /// "m.room" in this room.
1059    pub fn has_active_room_call(&self) -> bool {
1060        !self.active_room_call_memberships().is_empty()
1061    }
1062
1063    /// Get the call intent consensus for the current call, based on what
1064    /// members are advertising.
1065    ///
1066    /// This provides detailed information about the consensus state (is it an
1067    /// audio or video call), including whether it's full (all members
1068    /// agree) or partial (only some members advertise), allowing callers to
1069    /// distinguish between different levels of consensus.
1070    ///
1071    /// # Returns
1072    ///
1073    /// - [`CallIntentConsensus::Full`] if all members advertise and agree on
1074    ///   the same intent
1075    /// - [`CallIntentConsensus::Partial`] if only some members advertise but
1076    ///   those who do agree
1077    /// - [`CallIntentConsensus::None`] if no one advertises or advertisers
1078    ///   disagree
1079    pub fn active_room_call_consensus_intent(&self) -> CallIntentConsensus {
1080        let memberships = self.active_room_call_memberships();
1081        let total_count: u64 = memberships.len() as u64;
1082
1083        if total_count == 0 {
1084            return CallIntentConsensus::None;
1085        }
1086
1087        // Track the first intent found and count how many members advertise it
1088        let mut consensus_intent: Option<CallIntent> = None;
1089        let mut agreeing_count: u64 = 0;
1090
1091        for (_, data) in memberships.iter() {
1092            if let Some(intent) = data.call_intent() {
1093                match &consensus_intent {
1094                    // First intent found, set it as consensus
1095                    None => {
1096                        consensus_intent = Some(intent.clone());
1097                        agreeing_count = 1;
1098                    }
1099                    // Check if this intent matches the consensus
1100                    Some(current) if current == intent => {
1101                        agreeing_count += 1;
1102                    }
1103                    // Intents differ, no consensus
1104                    Some(_) => return CallIntentConsensus::None,
1105                }
1106            }
1107        }
1108
1109        // Return the appropriate consensus type based on participation
1110        match consensus_intent {
1111            None => CallIntentConsensus::None,
1112            Some(intent) if agreeing_count == total_count => {
1113                // All members advertise and agree
1114                CallIntentConsensus::Full(intent)
1115            }
1116            Some(intent) => {
1117                // Some members advertise and agree, others don't advertise
1118                CallIntentConsensus::Partial { intent, agreeing_count, total_count }
1119            }
1120        }
1121    }
1122
1123    /// Returns a Vec of userId's that participate in the room call.
1124    ///
1125    /// matrix_rtc memberships with application "m.call" and scope "m.room" are
1126    /// considered. A user can occur twice if they join with two devices.
1127    /// convert to a set depending if the different users are required or the
1128    /// amount of sessions.
1129    ///
1130    /// The vector is ordered by oldest membership user to newest.
1131    pub fn active_room_call_participants(&self) -> Vec<OwnedUserId> {
1132        self.active_room_call_memberships()
1133            .iter()
1134            .map(|(call_member_state_key, _)| call_member_state_key.user_id().to_owned())
1135            .collect()
1136    }
1137
1138    /// Sets the new [`LatestEventValue`].
1139    pub fn set_latest_event(&mut self, new_value: LatestEventValue) {
1140        self.latest_event_value = new_value;
1141    }
1142
1143    /// Updates the recency stamp of this room.
1144    ///
1145    /// Please read `Self::recency_stamp` to learn more.
1146    pub fn update_recency_stamp(&mut self, stamp: RoomRecencyStamp) {
1147        self.recency_stamp = Some(stamp);
1148    }
1149
1150    /// Returns the current pinned event ids for this room.
1151    pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
1152        self.base_info.pinned_events.clone().and_then(|c| c.pinned)
1153    }
1154
1155    /// Checks if an `EventId` is currently pinned.
1156    /// It avoids having to clone the whole list of event ids to check a single
1157    /// value.
1158    ///
1159    /// Returns `true` if the provided `event_id` is pinned, `false` otherwise.
1160    pub fn is_pinned_event(&self, event_id: &EventId) -> bool {
1161        self.base_info
1162            .pinned_events
1163            .as_ref()
1164            .and_then(|content| content.pinned.as_deref())
1165            .is_some_and(|pinned| pinned.contains(&event_id.to_owned()))
1166    }
1167
1168    /// Returns the computed read receipts for this room.
1169    pub fn read_receipts(&self) -> &RoomReadReceipts {
1170        &self.read_receipts
1171    }
1172
1173    /// Set the computed read receipts for this room.
1174    pub fn set_read_receipts(&mut self, read_receipts: RoomReadReceipts) {
1175        self.read_receipts = read_receipts;
1176    }
1177
1178    /// Apply migrations to this `RoomInfo` if needed.
1179    ///
1180    /// This should be used to populate new fields with data from the state
1181    /// store.
1182    ///
1183    /// Returns `true` if migrations were applied and this `RoomInfo` needs to
1184    /// be persisted to the state store.
1185    #[instrument(skip_all, fields(room_id = ?self.room_id))]
1186    pub(crate) async fn apply_migrations(&mut self, store: Arc<DynStateStore>) -> bool {
1187        let mut migrated = false;
1188
1189        if self.data_format_version < 1 {
1190            info!("Migrating room info to version 1");
1191
1192            // notable_tags
1193            match store.get_room_account_data_event_static::<TagEventContent>(&self.room_id).await {
1194                // Pinned events are never in stripped state.
1195                Ok(Some(raw_event)) => match raw_event.deserialize() {
1196                    Ok(event) => {
1197                        self.base_info.handle_notable_tags(&event.content.tags);
1198                    }
1199                    Err(error) => {
1200                        warn!("Failed to deserialize room tags: {error}");
1201                    }
1202                },
1203                Ok(_) => {
1204                    // Nothing to do.
1205                }
1206                Err(error) => {
1207                    warn!("Failed to load room tags: {error}");
1208                }
1209            }
1210
1211            // pinned_events
1212            match store.get_state_event_static::<RoomPinnedEventsEventContent>(&self.room_id).await
1213            {
1214                // Pinned events are never in stripped state.
1215                Ok(Some(RawSyncOrStrippedState::Sync(raw_event))) => {
1216                    if let Some(mut raw_event) =
1217                        RawStateEventWithKeys::try_from_raw_state_event(raw_event.cast())
1218                    {
1219                        self.handle_state_event(&mut raw_event);
1220                    }
1221                }
1222                Ok(_) => {
1223                    // Nothing to do.
1224                }
1225                Err(error) => {
1226                    warn!("Failed to load room pinned events: {error}");
1227                }
1228            }
1229
1230            self.data_format_version = 1;
1231            migrated = true;
1232        }
1233
1234        migrated
1235    }
1236}
1237
1238/// Type to represent a `RoomInfo::recency_stamp`.
1239#[repr(transparent)]
1240#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
1241#[serde(transparent)]
1242pub struct RoomRecencyStamp(u64);
1243
1244impl From<u64> for RoomRecencyStamp {
1245    fn from(value: u64) -> Self {
1246        Self(value)
1247    }
1248}
1249
1250impl From<RoomRecencyStamp> for u64 {
1251    fn from(value: RoomRecencyStamp) -> Self {
1252        value.0
1253    }
1254}
1255
1256#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1257pub(crate) enum SyncInfo {
1258    /// We only know the room exists and whether it is in invite / joined / left
1259    /// state.
1260    ///
1261    /// This is the case when we have a limited sync or only seen the room
1262    /// because of a request we've done, like a room creation event.
1263    NoState,
1264
1265    /// Some states have been synced, but they might have been filtered or is
1266    /// stale, as it is from a room we've left.
1267    PartiallySynced,
1268
1269    /// We have all the latest state events.
1270    FullySynced,
1271}
1272
1273/// Apply a redaction to the given target `event`, given the raw redaction event
1274/// and the room version.
1275pub fn apply_redaction(
1276    event: &Raw<AnySyncTimelineEvent>,
1277    raw_redaction: &Raw<SyncRoomRedactionEvent>,
1278    rules: &RedactionRules,
1279) -> Option<Raw<AnySyncTimelineEvent>> {
1280    use ruma::canonical_json::{RedactedBecause, redact_in_place};
1281
1282    let mut event_json = match event.deserialize_as() {
1283        Ok(json) => json,
1284        Err(e) => {
1285            warn!("Failed to deserialize latest event: {e}");
1286            return None;
1287        }
1288    };
1289
1290    let redacted_because = match RedactedBecause::from_raw_event(raw_redaction) {
1291        Ok(rb) => rb,
1292        Err(e) => {
1293            warn!("Redaction event is not valid canonical JSON: {e}");
1294            return None;
1295        }
1296    };
1297
1298    let redact_result = redact_in_place(&mut event_json, rules, Some(redacted_because));
1299
1300    if let Err(e) = redact_result {
1301        warn!("Failed to redact event: {e}");
1302        return None;
1303    }
1304
1305    let raw = Raw::new(&event_json).expect("CanonicalJsonObject must be serializable");
1306    Some(raw.cast_unchecked())
1307}
1308
1309/// Indicates that a notable update of `RoomInfo` has been applied, and why.
1310///
1311/// A room info notable update is an update that can be interesting for other
1312/// parts of the code. This mechanism is used in coordination with
1313/// [`BaseClient::room_info_notable_update_receiver`][baseclient] (and
1314/// `Room::info` plus `Room::room_info_notable_update_sender`) where `RoomInfo`
1315/// can be observed and some of its updates can be spread to listeners.
1316///
1317/// [baseclient]: crate::BaseClient::room_info_notable_update_receiver
1318#[derive(Debug, Clone)]
1319pub struct RoomInfoNotableUpdate {
1320    /// The room which was updated.
1321    pub room_id: OwnedRoomId,
1322
1323    /// The reason for this update.
1324    pub reasons: RoomInfoNotableUpdateReasons,
1325}
1326
1327bitflags! {
1328    /// The reason why a [`RoomInfoNotableUpdate`] is emitted.
1329    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1330    pub struct RoomInfoNotableUpdateReasons: u8 {
1331        /// The recency stamp of the `Room` has changed.
1332        const RECENCY_STAMP = 0b0000_0001;
1333
1334        /// The latest event of the `Room` has changed.
1335        const LATEST_EVENT = 0b0000_0010;
1336
1337        /// A read receipt has changed.
1338        const READ_RECEIPT = 0b0000_0100;
1339
1340        /// The user-controlled unread marker value has changed.
1341        const UNREAD_MARKER = 0b0000_1000;
1342
1343        /// A membership change happened for the current user.
1344        const MEMBERSHIP = 0b0001_0000;
1345
1346        /// The display name has changed.
1347        const DISPLAY_NAME = 0b0010_0000;
1348
1349        /// This is a temporary hack.
1350        ///
1351        /// So here is the thing. Ideally, we DO NOT want to emit this reason. It does not
1352        /// makes sense. However, all notable update reasons are not clearly identified
1353        /// so far. Why is it a problem? The `matrix_sdk_ui::room_list_service::RoomList`
1354        /// is listening this stream of [`RoomInfoNotableUpdate`], and emits an update on a
1355        /// room item if it receives a notable reason. Because all reasons are not
1356        /// identified, we are likely to miss particular updates, and it can feel broken.
1357        /// Ultimately, we want to clearly identify all the notable update reasons, and
1358        /// remove this one.
1359        const NONE = 0b1000_0000;
1360    }
1361}
1362
1363impl Default for RoomInfoNotableUpdateReasons {
1364    fn default() -> Self {
1365        Self::empty()
1366    }
1367}
1368
1369#[cfg(test)]
1370mod tests {
1371    use std::{str::FromStr, sync::Arc};
1372
1373    use assert_matches::assert_matches;
1374    use matrix_sdk_test::{async_test, event_factory::EventFactory};
1375    use ruma::{
1376        assign,
1377        events::{
1378            AnyRoomAccountDataEvent,
1379            room::pinned_events::RoomPinnedEventsEventContent,
1380            tag::{TagInfo, TagName, Tags, UserTagName},
1381        },
1382        owned_event_id, owned_mxc_uri, owned_user_id, room_id,
1383        serde::Raw,
1384        user_id,
1385    };
1386    use serde_json::json;
1387    use similar_asserts::assert_eq;
1388
1389    use super::{BaseRoomInfo, LatestEventValue, RoomInfo, SyncInfo};
1390    use crate::{
1391        RoomDisplayName, RoomHero, RoomState, StateChanges,
1392        notification_settings::RoomNotificationMode,
1393        room::{RoomNotableTags, RoomSummary},
1394        store::{IntoStateStore, MemoryStore},
1395        sync::UnreadNotificationsCount,
1396    };
1397
1398    #[test]
1399    fn test_room_info_serialization() {
1400        // This test exists to make sure we don't accidentally change the
1401        // serialized format for `RoomInfo`.
1402
1403        let info = RoomInfo {
1404            data_format_version: 1,
1405            room_id: room_id!("!gda78o:server.tld").into(),
1406            room_state: RoomState::Invited,
1407            notification_counts: UnreadNotificationsCount {
1408                highlight_count: 1,
1409                notification_count: 2,
1410            },
1411            summary: RoomSummary {
1412                room_heroes: vec![RoomHero {
1413                    user_id: owned_user_id!("@somebody:example.org"),
1414                    display_name: None,
1415                    avatar_url: None,
1416                }],
1417                joined_member_count: 5,
1418                invited_member_count: 0,
1419            },
1420            members_synced: true,
1421            last_prev_batch: Some("pb".to_owned()),
1422            sync_info: SyncInfo::FullySynced,
1423            encryption_state_synced: true,
1424            latest_event_value: LatestEventValue::None,
1425            base_info: Box::new(
1426                assign!(BaseRoomInfo::new(), { pinned_events: Some(RoomPinnedEventsEventContent::new(vec![owned_event_id!("$a")]).into()) }),
1427            ),
1428            read_receipts: Default::default(),
1429            warned_about_unknown_room_version_rules: Arc::new(false.into()),
1430            cached_display_name: None,
1431            cached_user_defined_notification_mode: None,
1432            recency_stamp: Some(42.into()),
1433        };
1434
1435        let info_json = json!({
1436            "data_format_version": 1,
1437            "room_id": "!gda78o:server.tld",
1438            "room_state": "Invited",
1439            "notification_counts": {
1440                "highlight_count": 1,
1441                "notification_count": 2,
1442            },
1443            "summary": {
1444                "room_heroes": [{
1445                    "user_id": "@somebody:example.org",
1446                    "display_name": null,
1447                    "avatar_url": null
1448                }],
1449                "joined_member_count": 5,
1450                "invited_member_count": 0,
1451            },
1452            "members_synced": true,
1453            "last_prev_batch": "pb",
1454            "sync_info": "FullySynced",
1455            "encryption_state_synced": true,
1456            "latest_event_value": "None",
1457            "base_info": {
1458                "avatar": null,
1459                "canonical_alias": null,
1460                "create": null,
1461                "dm_targets": [],
1462                "encryption": null,
1463                "guest_access": null,
1464                "history_visibility": null,
1465                "is_marked_unread": false,
1466                "is_marked_unread_source": "Unstable",
1467                "join_rules": null,
1468                "max_power_level": 100,
1469                "member_hints": null,
1470                "name": null,
1471                "tombstone": null,
1472                "topic": null,
1473                "pinned_events": {
1474                    "pinned": ["$a"]
1475                },
1476            },
1477            "read_receipts": {
1478                "num_unread": 0,
1479                "num_mentions": 0,
1480                "num_notifications": 0,
1481                "latest_active": null,
1482                "pending": [],
1483            },
1484            "recency_stamp": 42,
1485        });
1486
1487        assert_eq!(serde_json::to_value(info).unwrap(), info_json);
1488    }
1489
1490    #[async_test]
1491    async fn test_room_info_migration_v1() {
1492        let store = MemoryStore::new().into_state_store();
1493
1494        let room_info_json = json!({
1495            "room_id": "!gda78o:server.tld",
1496            "room_state": "Joined",
1497            "notification_counts": {
1498                "highlight_count": 1,
1499                "notification_count": 2,
1500            },
1501            "summary": {
1502                "room_heroes": [{
1503                    "user_id": "@somebody:example.org",
1504                    "display_name": null,
1505                    "avatar_url": null
1506                }],
1507                "joined_member_count": 5,
1508                "invited_member_count": 0,
1509            },
1510            "members_synced": true,
1511            "last_prev_batch": "pb",
1512            "sync_info": "FullySynced",
1513            "encryption_state_synced": true,
1514            "latest_event": {
1515                "event": {
1516                    "encryption_info": null,
1517                    "event": {
1518                        "sender": "@u:i.uk",
1519                    },
1520                },
1521            },
1522            "base_info": {
1523                "avatar": null,
1524                "canonical_alias": null,
1525                "create": null,
1526                "dm_targets": [],
1527                "encryption": null,
1528                "guest_access": null,
1529                "history_visibility": null,
1530                "join_rules": null,
1531                "max_power_level": 100,
1532                "name": null,
1533                "tombstone": null,
1534                "topic": null,
1535            },
1536            "read_receipts": {
1537                "num_unread": 0,
1538                "num_mentions": 0,
1539                "num_notifications": 0,
1540                "latest_active": null,
1541                "pending": []
1542            },
1543            "recency_stamp": 42,
1544        });
1545        let mut room_info: RoomInfo = serde_json::from_value(room_info_json).unwrap();
1546
1547        assert_eq!(room_info.data_format_version, 0);
1548        assert!(room_info.base_info.notable_tags.is_empty());
1549        assert!(room_info.base_info.pinned_events.is_none());
1550
1551        // Apply migrations with an empty store.
1552        assert!(room_info.apply_migrations(store.clone()).await);
1553
1554        assert_eq!(room_info.data_format_version, 1);
1555        assert!(room_info.base_info.notable_tags.is_empty());
1556        assert!(room_info.base_info.pinned_events.is_none());
1557
1558        // Applying migrations again has no effect.
1559        assert!(!room_info.apply_migrations(store.clone()).await);
1560
1561        assert_eq!(room_info.data_format_version, 1);
1562        assert!(room_info.base_info.notable_tags.is_empty());
1563        assert!(room_info.base_info.pinned_events.is_none());
1564
1565        // Add events to the store.
1566        let mut changes = StateChanges::default();
1567
1568        let f = EventFactory::new().room(&room_info.room_id).sender(user_id!("@example:localhost"));
1569        let mut tags = Tags::new();
1570        tags.insert(TagName::Favorite, TagInfo::new());
1571        tags.insert(TagName::User(UserTagName::from_str("u.work").unwrap()), TagInfo::new());
1572        let raw_tag_event: Raw<AnyRoomAccountDataEvent> = f.tag(tags).into();
1573        let tag_event = raw_tag_event.deserialize().unwrap();
1574        changes.add_room_account_data(&room_info.room_id, tag_event, raw_tag_event);
1575
1576        let raw_pinned_events_event: Raw<_> = f
1577            .room_pinned_events(vec![owned_event_id!("$a"), owned_event_id!("$b")])
1578            .into_raw_sync_state();
1579        let pinned_events_event = raw_pinned_events_event.deserialize().unwrap();
1580        changes.add_state_event(&room_info.room_id, pinned_events_event, raw_pinned_events_event);
1581
1582        store.save_changes(&changes).await.unwrap();
1583
1584        // Reset to version 0 and reapply migrations.
1585        room_info.data_format_version = 0;
1586        assert!(room_info.apply_migrations(store.clone()).await);
1587
1588        assert_eq!(room_info.data_format_version, 1);
1589        assert!(room_info.base_info.notable_tags.contains(RoomNotableTags::FAVOURITE));
1590        assert!(room_info.base_info.pinned_events.is_some());
1591
1592        // Creating a new room info initializes it to version 1.
1593        let new_room_info = RoomInfo::new(room_id!("!new_room:localhost"), RoomState::Joined);
1594        assert_eq!(new_room_info.data_format_version, 1);
1595    }
1596
1597    #[test]
1598    fn test_room_info_deserialization() {
1599        let info_json = json!({
1600            "room_id": "!gda78o:server.tld",
1601            "room_state": "Joined",
1602            "notification_counts": {
1603                "highlight_count": 1,
1604                "notification_count": 2,
1605            },
1606            "summary": {
1607                "room_heroes": [{
1608                    "user_id": "@somebody:example.org",
1609                    "display_name": "Somebody",
1610                    "avatar_url": "mxc://example.org/abc"
1611                }],
1612                "joined_member_count": 5,
1613                "invited_member_count": 0,
1614            },
1615            "members_synced": true,
1616            "last_prev_batch": "pb",
1617            "sync_info": "FullySynced",
1618            "encryption_state_synced": true,
1619            "base_info": {
1620                "avatar": null,
1621                "canonical_alias": null,
1622                "create": null,
1623                "dm_targets": [],
1624                "encryption": null,
1625                "guest_access": null,
1626                "history_visibility": null,
1627                "join_rules": null,
1628                "max_power_level": 100,
1629                "member_hints": null,
1630                "name": null,
1631                "tombstone": null,
1632                "topic": null,
1633            },
1634            "cached_display_name": { "Calculated": "lol" },
1635            "cached_user_defined_notification_mode": "Mute",
1636            "recency_stamp": 42,
1637        });
1638
1639        let info: RoomInfo = serde_json::from_value(info_json).unwrap();
1640
1641        assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
1642        assert_eq!(info.room_state, RoomState::Joined);
1643        assert_eq!(info.notification_counts.highlight_count, 1);
1644        assert_eq!(info.notification_counts.notification_count, 2);
1645        assert_eq!(
1646            info.summary.room_heroes,
1647            vec![RoomHero {
1648                user_id: owned_user_id!("@somebody:example.org"),
1649                display_name: Some("Somebody".to_owned()),
1650                avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
1651            }]
1652        );
1653        assert_eq!(info.summary.joined_member_count, 5);
1654        assert_eq!(info.summary.invited_member_count, 0);
1655        assert!(info.members_synced);
1656        assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
1657        assert_eq!(info.sync_info, SyncInfo::FullySynced);
1658        assert!(info.encryption_state_synced);
1659        assert_matches!(info.latest_event_value, LatestEventValue::None);
1660        assert!(info.base_info.avatar.is_none());
1661        assert!(info.base_info.canonical_alias.is_none());
1662        assert!(info.base_info.create.is_none());
1663        assert_eq!(info.base_info.dm_targets.len(), 0);
1664        assert!(info.base_info.encryption.is_none());
1665        assert!(info.base_info.guest_access.is_none());
1666        assert!(info.base_info.history_visibility.is_none());
1667        assert!(info.base_info.join_rules.is_none());
1668        assert_eq!(info.base_info.max_power_level, 100);
1669        assert!(info.base_info.member_hints.is_none());
1670        assert!(info.base_info.name.is_none());
1671        assert!(info.base_info.tombstone.is_none());
1672        assert!(info.base_info.topic.is_none());
1673
1674        assert_eq!(
1675            info.cached_display_name.as_ref(),
1676            Some(&RoomDisplayName::Calculated("lol".to_owned())),
1677        );
1678        assert_eq!(
1679            info.cached_user_defined_notification_mode.as_ref(),
1680            Some(&RoomNotificationMode::Mute)
1681        );
1682        assert_eq!(info.recency_stamp.as_ref(), Some(&42.into()));
1683    }
1684
1685    // Ensure we can still deserialize RoomInfos before we added things to its
1686    // schema
1687    //
1688    // In an ideal world, we must not change this test. Please see
1689    // [`test_room_info_serialization`] if you want to test a “recent” `RoomInfo`
1690    // deserialization.
1691    #[test]
1692    fn test_room_info_deserialization_without_optional_items() {
1693        // The following JSON should never change if we want to be able to read in old
1694        // cached state
1695        let info_json = json!({
1696            "room_id": "!gda78o:server.tld",
1697            "room_state": "Invited",
1698            "notification_counts": {
1699                "highlight_count": 1,
1700                "notification_count": 2,
1701            },
1702            "summary": {
1703                "room_heroes": [{
1704                    "user_id": "@somebody:example.org",
1705                    "display_name": "Somebody",
1706                    "avatar_url": "mxc://example.org/abc"
1707                }],
1708                "joined_member_count": 5,
1709                "invited_member_count": 0,
1710            },
1711            "members_synced": true,
1712            "last_prev_batch": "pb",
1713            "sync_info": "FullySynced",
1714            "encryption_state_synced": true,
1715            "base_info": {
1716                "avatar": null,
1717                "canonical_alias": null,
1718                "create": null,
1719                "dm_targets": [],
1720                "encryption": null,
1721                "guest_access": null,
1722                "history_visibility": null,
1723                "join_rules": null,
1724                "max_power_level": 100,
1725                "name": null,
1726                "tombstone": null,
1727                "topic": null,
1728            },
1729        });
1730
1731        let info: RoomInfo = serde_json::from_value(info_json).unwrap();
1732
1733        assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
1734        assert_eq!(info.room_state, RoomState::Invited);
1735        assert_eq!(info.notification_counts.highlight_count, 1);
1736        assert_eq!(info.notification_counts.notification_count, 2);
1737        assert_eq!(
1738            info.summary.room_heroes,
1739            vec![RoomHero {
1740                user_id: owned_user_id!("@somebody:example.org"),
1741                display_name: Some("Somebody".to_owned()),
1742                avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
1743            }]
1744        );
1745        assert_eq!(info.summary.joined_member_count, 5);
1746        assert_eq!(info.summary.invited_member_count, 0);
1747        assert!(info.members_synced);
1748        assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
1749        assert_eq!(info.sync_info, SyncInfo::FullySynced);
1750        assert!(info.encryption_state_synced);
1751        assert!(info.base_info.avatar.is_none());
1752        assert!(info.base_info.canonical_alias.is_none());
1753        assert!(info.base_info.create.is_none());
1754        assert_eq!(info.base_info.dm_targets.len(), 0);
1755        assert!(info.base_info.encryption.is_none());
1756        assert!(info.base_info.guest_access.is_none());
1757        assert!(info.base_info.history_visibility.is_none());
1758        assert!(info.base_info.join_rules.is_none());
1759        assert_eq!(info.base_info.max_power_level, 100);
1760        assert!(info.base_info.name.is_none());
1761        assert!(info.base_info.tombstone.is_none());
1762        assert!(info.base_info.topic.is_none());
1763    }
1764}