1#![allow(clippy::assign_op_pattern)] mod call;
18mod create;
19mod display_name;
20mod encryption;
21mod knock;
22mod latest_event;
23mod members;
24mod room_info;
25mod state;
26mod tags;
27mod tombstone;
28
29use std::collections::{BTreeMap, BTreeSet, HashSet};
30#[cfg(feature = "unstable-msc4354")]
31use std::sync::{Arc, OnceLock};
32
33pub use call::CallIntentConsensus;
34pub use create::*;
35pub use display_name::{RoomDisplayName, RoomHero, RoomHeroWithProfile};
36pub(crate) use display_name::{RoomSummary, UpdatedRoomDisplayName};
37pub use encryption::EncryptionState;
38use eyeball::{AsyncLock, SharedObservable};
39use futures_util::{Stream, StreamExt};
40pub use members::{RoomMember, RoomMembersUpdate, RoomMemberships};
41pub(crate) use room_info::SyncInfo;
42pub use room_info::{
43 BaseRoomInfo, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomRecencyStamp,
44 apply_redaction,
45};
46#[cfg(feature = "unstable-msc4426")]
47use ruma::profile::{Call, Status};
48use ruma::{
49 EventId, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomId,
50 RoomVersionId, UserId,
51 events::{
52 direct::OwnedDirectUserIdentifier,
53 receipt::{Receipt, ReceiptThread, ReceiptType},
54 room::{
55 avatar,
56 guest_access::GuestAccess,
57 history_visibility::HistoryVisibility,
58 join_rules::JoinRule,
59 member::MembershipState,
60 power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent, RoomPowerLevelsSource},
61 retention::RoomRetentionEventContent,
62 },
63 },
64 room::RoomType,
65};
66use serde::{Deserialize, Serialize};
67pub use state::{RoomState, RoomStateFilter};
68pub(crate) use tags::RoomNotableTags;
69use tokio::sync::broadcast;
70pub use tombstone::{PredecessorRoom, SuccessorRoom};
71use tracing::{info, instrument, trace, warn};
72
73use crate::{
74 DmRoomDefinition, Error, StateStore,
75 deserialized_responses::MemberEvent,
76 notification_settings::RoomNotificationMode,
77 read_receipts::ReadReceipts,
78 store::{Result as StoreResult, SaveLockedStateStore, StateStoreExt},
79 sync::UnreadNotificationsCount,
80};
81
82#[derive(Debug, Clone)]
85pub struct Room {
86 pub(super) room_id: OwnedRoomId,
88
89 pub(super) own_user_id: OwnedUserId,
91
92 pub(super) info: SharedObservable<RoomInfo>,
93
94 pub(super) room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
98
99 pub(super) store: SaveLockedStateStore,
101
102 pub seen_knock_request_ids_map:
106 SharedObservable<Option<BTreeMap<OwnedEventId, OwnedUserId>>, AsyncLock>,
107
108 pub room_member_updates_sender: broadcast::Sender<RoomMembersUpdate>,
110
111 #[cfg(feature = "unstable-msc4354")]
116 pub(super) sticky_events: Arc<OnceLock<crate::sticky::StickyEvents>>,
117}
118
119impl Room {
120 pub(crate) fn new(
121 own_user_id: &UserId,
122 store: SaveLockedStateStore,
123 room_id: &RoomId,
124 room_state: RoomState,
125 room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
126 ) -> Self {
127 let room_info = RoomInfo::new(room_id, room_state);
128 Self::restore(own_user_id, store, room_info, room_info_notable_update_sender)
129 }
130
131 pub(crate) fn restore(
132 own_user_id: &UserId,
133 store: SaveLockedStateStore,
134 room_info: RoomInfo,
135 room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
136 ) -> Self {
137 let (room_member_updates_sender, _) = broadcast::channel(10);
138 Self {
139 own_user_id: own_user_id.into(),
140 room_id: room_info.room_id.clone(),
141 store,
142 info: SharedObservable::new(room_info),
143 room_info_notable_update_sender,
144 seen_knock_request_ids_map: SharedObservable::new_async(None),
145 room_member_updates_sender,
146 #[cfg(feature = "unstable-msc4354")]
147 sticky_events: Default::default(),
148 }
149 }
150
151 pub fn room_id(&self) -> &RoomId {
153 &self.room_id
154 }
155
156 #[cfg(feature = "unstable-msc4354")]
160 pub fn sticky_events(&self) -> &crate::sticky::StickyEvents {
161 self.sticky_events.get_or_init(|| crate::sticky::StickyEvents::new(self.room_id.clone()))
162 }
163
164 #[cfg(feature = "unstable-msc4354")]
169 pub(crate) fn sticky_events_if_any(&self) -> Option<&crate::sticky::StickyEvents> {
170 self.sticky_events.get()
171 }
172
173 pub fn creators(&self) -> Option<Vec<OwnedUserId>> {
175 self.info.read().creators()
176 }
177
178 pub fn own_user_id(&self) -> &UserId {
180 &self.own_user_id
181 }
182
183 pub fn is_space(&self) -> bool {
185 self.info.read().room_type().is_some_and(|t| *t == RoomType::Space)
186 }
187
188 pub fn is_call(&self) -> bool {
192 self.info.read().room_type().is_some_and(|t| *t == RoomType::Call)
193 }
194
195 pub fn room_type(&self) -> Option<RoomType> {
198 self.info.read().room_type().map(ToOwned::to_owned)
199 }
200
201 pub fn unread_notification_counts(&self) -> UnreadNotificationsCount {
210 self.info.read().notification_counts
211 }
212
213 pub fn num_unread_messages(&self) -> u64 {
218 self.info.read().read_receipts.num_unread
219 }
220
221 pub fn num_unread_notifications(&self) -> u64 {
226 self.info.read().read_receipts.num_notifications
227 }
228
229 pub fn num_unread_mentions(&self) -> u64 {
235 self.info.read().read_receipts.num_mentions
236 }
237
238 pub fn read_receipts(&self) -> ReadReceipts {
240 self.info.read().read_receipts.clone()
241 }
242
243 pub fn is_state_fully_synced(&self) -> bool {
251 self.info.read().sync_info == SyncInfo::FullySynced
252 }
253
254 pub fn is_state_partially_or_fully_synced(&self) -> bool {
258 self.info.read().sync_info != SyncInfo::NoState
259 }
260
261 pub fn last_prev_batch(&self) -> Option<String> {
264 self.info.read().last_prev_batch.clone()
265 }
266
267 pub fn avatar_url(&self) -> Option<OwnedMxcUri> {
269 self.info.read().avatar_url().map(ToOwned::to_owned)
270 }
271
272 pub fn avatar_info(&self) -> Option<avatar::ImageInfo> {
274 self.info.read().avatar_info().map(ToOwned::to_owned)
275 }
276
277 pub fn canonical_alias(&self) -> Option<OwnedRoomAliasId> {
279 self.info.read().canonical_alias().map(ToOwned::to_owned)
280 }
281
282 pub fn alt_aliases(&self) -> Vec<OwnedRoomAliasId> {
284 self.info.read().alt_aliases().to_owned()
285 }
286
287 pub fn create_content(&self) -> Option<RoomCreateWithCreatorEventContent> {
297 Some(self.info.read().base_info.create.as_ref()?.content.clone())
298 }
299
300 #[instrument(skip_all, fields(room_id = ?self.room_id))]
304 pub async fn is_direct(&self) -> StoreResult<bool> {
305 match self.state() {
306 RoomState::Joined | RoomState::Left | RoomState::Banned => {
307 Ok(!self.info.read().base_info.dm_targets.is_empty())
308 }
309
310 RoomState::Invited => {
311 let member = self.get_member(self.own_user_id()).await?;
312
313 match member {
314 None => {
315 info!("RoomMember not found for the user's own id");
316 Ok(false)
317 }
318 Some(member) => match member.event.as_ref() {
319 MemberEvent::Sync(_) => {
320 warn!("Got MemberEvent::Sync in an invited room");
321 Ok(false)
322 }
323 MemberEvent::Stripped(event) => {
324 Ok(event.content.is_direct.unwrap_or(false))
325 }
326 },
327 }
328 }
329
330 RoomState::Knocked => Ok(false),
332 }
333 }
334
335 pub async fn compute_is_dm(&self, dm_room_definition: &DmRoomDefinition) -> StoreResult<bool> {
338 let is_direct = self.is_direct().await?;
339
340 match *dm_room_definition {
341 DmRoomDefinition::MatrixSpec => Ok(is_direct),
342 DmRoomDefinition::TwoMembers => {
343 if !is_direct {
344 return Ok(false);
345 }
346 let active_service_member_count =
347 self.update_active_service_members().await?.unwrap_or_default().len() as u64;
348 let has_at_most_two_members =
349 self.active_members_count().saturating_sub(active_service_member_count) <= 2;
350 Ok(has_at_most_two_members)
351 }
352 }
353 }
354
355 pub fn direct_targets(&self) -> HashSet<OwnedDirectUserIdentifier> {
364 self.info.read().base_info.dm_targets.clone()
365 }
366
367 pub fn direct_targets_length(&self) -> usize {
370 self.info.read().base_info.dm_targets.len()
371 }
372
373 pub fn guest_access(&self) -> GuestAccess {
375 self.info.read().guest_access().clone()
376 }
377
378 pub fn history_visibility(&self) -> Option<HistoryVisibility> {
380 self.info.read().history_visibility().cloned()
381 }
382
383 pub fn history_visibility_or_default(&self) -> HistoryVisibility {
386 self.info.read().history_visibility_or_default().clone()
387 }
388
389 pub fn is_public(&self) -> Option<bool> {
393 self.info.read().join_rule().map(|join_rule| matches!(join_rule, JoinRule::Public))
394 }
395
396 pub fn join_rule(&self) -> Option<JoinRule> {
398 self.info.read().join_rule().cloned()
399 }
400
401 pub fn max_power_level(&self) -> i64 {
406 self.info.read().base_info.max_power_level
407 }
408
409 pub fn retention(&self) -> Option<RoomRetentionEventContent> {
411 self.info.read().retention().cloned()
412 }
413
414 pub fn service_members(&self) -> Option<BTreeSet<OwnedUserId>> {
416 self.info.read().service_members().cloned()
417 }
418
419 pub async fn power_levels(&self) -> Result<RoomPowerLevels, Error> {
421 let power_levels_content = self
422 .store
423 .get_state_event_static::<RoomPowerLevelsEventContent>(self.room_id())
424 .await?
425 .ok_or(Error::InsufficientData)?
426 .deserialize()?;
427 let creators = self.creators().ok_or(Error::InsufficientData)?;
428 let rules = self.info.read().room_version_rules_or_default();
429
430 Ok(power_levels_content.power_levels(&rules.authorization, creators))
431 }
432
433 pub async fn power_levels_or_default(&self) -> RoomPowerLevels {
436 if let Ok(power_levels) = self.power_levels().await {
437 return power_levels;
438 }
439
440 let rules = self.info.read().room_version_rules_or_default();
442 RoomPowerLevels::new(
443 RoomPowerLevelsSource::None,
444 &rules.authorization,
445 self.creators().into_iter().flatten(),
446 )
447 }
448
449 pub fn name(&self) -> Option<String> {
454 self.info.read().name().map(ToOwned::to_owned)
455 }
456
457 pub fn topic(&self) -> Option<String> {
459 self.info.read().topic().map(ToOwned::to_owned)
460 }
461
462 pub fn update_cached_user_defined_notification_mode(&self, mode: RoomNotificationMode) {
468 self.info.update_if(|info| {
469 if info.cached_user_defined_notification_mode.as_ref() != Some(&mode) {
470 info.cached_user_defined_notification_mode = Some(mode);
471
472 true
473 } else {
474 false
475 }
476 });
477 }
478
479 pub fn cached_user_defined_notification_mode(&self) -> Option<RoomNotificationMode> {
484 self.info.read().cached_user_defined_notification_mode
485 }
486
487 pub fn clear_user_defined_notification_mode(&self) {
490 self.info.update_if(|info| {
491 if info.cached_user_defined_notification_mode.is_some() {
492 info.cached_user_defined_notification_mode = None;
493 true
494 } else {
495 false
496 }
497 })
498 }
499
500 pub async fn joined_user_ids(&self) -> StoreResult<Vec<OwnedUserId>> {
503 self.store.get_user_ids(self.room_id(), RoomMemberships::JOIN).await
504 }
505
506 #[cfg(feature = "unstable-msc4426")]
509 pub(crate) fn hero_user_ids(&self) -> Vec<OwnedUserId> {
510 self.info.read().heroes().iter().map(|hero| hero.user_id.clone()).collect()
511 }
512
513 #[cfg_attr(not(feature = "unstable-msc4426"), allow(clippy::unused_async))]
518 pub async fn heroes(&self) -> Vec<RoomHeroWithProfile> {
519 let heroes: Vec<RoomHero> = {
520 let guard = self.info.read();
521 let heroes = guard.heroes();
522
523 if let Some(service_members) = guard.service_members() {
524 heroes
525 .iter()
526 .filter(|hero| !service_members.contains(&hero.user_id))
527 .cloned()
528 .collect()
529 } else {
530 heroes.to_vec()
531 }
532 };
533
534 if heroes.is_empty() {
536 return Vec::new();
537 }
538
539 #[cfg(not(feature = "unstable-msc4426"))]
542 {
543 heroes.into_iter().map(RoomHeroWithProfile::from).collect()
544 }
545
546 #[cfg(feature = "unstable-msc4426")]
548 {
549 let user_ids = heroes.iter().map(|hero| hero.user_id.clone()).collect::<Vec<_>>();
550
551 let mut global_profiles =
552 self.store.get_global_profiles(&user_ids).await.unwrap_or_else(|error| {
553 tracing::warn!(?error, "Failed to load global profiles for room heroes");
554 Default::default()
555 });
556
557 heroes
558 .into_iter()
559 .map(|hero| {
560 let (status, call) = global_profiles
561 .remove(&*hero.user_id)
562 .map(|profile| {
563 (
564 profile.get_static::<Status>().ok().flatten(),
565 profile.get_static::<Call>().ok().flatten(),
566 )
567 })
568 .unwrap_or_default();
569
570 RoomHeroWithProfile {
571 user_id: hero.user_id,
572 display_name: hero.display_name,
573 avatar_url: hero.avatar_url,
574 status,
575 call,
576 }
577 })
578 .collect()
579 }
580 }
581
582 pub async fn load_user_receipt(
585 &self,
586 receipt_type: ReceiptType,
587 receipt_thread: &ReceiptThread,
588 user_id: &UserId,
589 ) -> StoreResult<Option<(OwnedEventId, Receipt)>> {
590 self.store
591 .get_user_room_receipt_event(self.room_id(), receipt_type, receipt_thread, user_id)
592 .await
593 }
594
595 pub async fn load_event_receipts(
599 &self,
600 receipt_type: ReceiptType,
601 receipt_thread: &ReceiptThread,
602 event_id: &EventId,
603 ) -> StoreResult<Vec<(OwnedUserId, Receipt)>> {
604 self.store
605 .get_event_room_receipt_events(self.room_id(), receipt_type, receipt_thread, event_id)
606 .await
607 }
608
609 pub fn is_marked_unread(&self) -> bool {
612 self.info.read().base_info.is_marked_unread
613 }
614
615 pub fn fully_read_event_id(&self) -> Option<OwnedEventId> {
618 self.info.read().fully_read_event_id().map(ToOwned::to_owned)
619 }
620
621 pub fn version(&self) -> Option<RoomVersionId> {
623 self.info.read().room_version().cloned()
624 }
625
626 pub fn recency_stamp(&self) -> Option<RoomRecencyStamp> {
630 self.info.read().recency_stamp
631 }
632
633 pub fn pinned_event_ids_stream(&self) -> impl Stream<Item = Vec<OwnedEventId>> + use<> {
636 self.info
637 .subscribe()
638 .map(|i| i.base_info.pinned_events.and_then(|c| c.pinned).unwrap_or_default())
639 }
640
641 pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
643 self.info.read().pinned_event_ids()
644 }
645
646 pub async fn update_active_service_members(&self) -> StoreResult<Option<Vec<RoomMember>>> {
650 if let Some(service_members) = self.service_members() {
651 let mut found = Vec::new();
652 for user_id in service_members {
653 match self.get_member(&user_id).await {
654 Ok(Some(member)) => {
655 if matches!(
657 member.membership(),
658 MembershipState::Join | MembershipState::Invite
659 ) {
660 found.push(member);
661 }
662 }
663 Ok(None) => (),
664 Err(error) => return Err(error),
665 }
666 }
667
668 trace!("Updating active service members ({}) in room {}", found.len(), self.room_id());
669
670 let new_active_service_member_count = found.len() as u64;
671 let current_active_service_member_count =
672 self.info.read().summary.active_service_members.unwrap_or_default();
673 if new_active_service_member_count != current_active_service_member_count {
674 self.update_and_save_room_info(|mut info| {
675 info.update_active_service_member_count(Some(new_active_service_member_count));
676 (info, RoomInfoNotableUpdateReasons::ACTIVE_SERVICE_MEMBERS)
677 })
678 .await?;
679 }
680
681 Ok(Some(found))
682 } else {
683 if self.info.read().summary.active_service_members.is_some() {
684 self.update_and_save_room_info(|mut info| {
685 info.update_active_service_member_count(None);
686 (info, RoomInfoNotableUpdateReasons::ACTIVE_SERVICE_MEMBERS)
687 })
688 .await?;
689 }
690 Ok(None)
691 }
692 }
693
694 #[instrument(skip_all, fields(room_id = ?self.room_id))]
698 pub async fn compute_joined_service_members(&self) -> StoreResult<Option<Vec<RoomMember>>> {
699 if !self.are_members_synced() {
700 trace!("Tried to compute joined service members in a room that is not synced");
701 return Ok(None);
702 }
703 if let Some(service_member_ids) = self.service_members() {
704 let mut ret = vec![];
705 for user_id in service_member_ids.iter() {
706 if let Some(member) = self.get_member(user_id).await.unwrap()
707 && matches!(member.membership(), MembershipState::Join)
708 {
709 trace!("Found a joined service member ({})", user_id);
710 ret.push(member);
711 } else {
712 trace!("Did not find a joined service member ({})", user_id);
713 }
714 }
715 trace!(
716 "Computed joined service members ({}) for service member count {}",
717 ret.len(),
718 service_member_ids.len()
719 );
720 Ok(Some(ret))
721 } else {
722 trace!("Tried to compute joined service members in a room that has no service members",);
723 Ok(None)
724 }
725 }
726
727 pub fn active_service_members_count(&self) -> Option<u64> {
730 self.info.read().summary.active_service_members
731 }
732}
733
734#[cfg(not(feature = "test-send-sync"))]
736unsafe impl Send for Room {}
737
738#[cfg(not(feature = "test-send-sync"))]
740unsafe impl Sync for Room {}
741
742#[cfg(feature = "test-send-sync")]
743#[test]
744fn test_send_sync_for_room() {
746 fn assert_send_sync<
747 T: matrix_sdk_common::SendOutsideWasm + matrix_sdk_common::SyncOutsideWasm,
748 >() {
749 }
750
751 assert_send_sync::<Room>();
752}
753
754#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
756pub(crate) enum AccountDataSource {
757 Stable,
759
760 #[default]
762 Unstable,
763}
764
765#[cfg(test)]
766mod tests {
767 use matrix_sdk_test::{
768 JoinedRoomBuilder, SyncResponseBuilder, async_test, event_factory::EventFactory,
769 };
770 use ruma::{room_id, user_id};
771 use serde_json::json;
772
773 use super::*;
774 use crate::test_utils::logged_in_base_client;
775
776 #[async_test]
777 async fn test_room_heroes_filters_out_service_members() {
778 let client = logged_in_base_client(None).await;
779 let user_id = &client.session_meta().unwrap().user_id;
780 let service_member_id = user_id!("@service:example.org");
781 let alice_id = user_id!("@alice:example.org");
782 let room_id = room_id!("!room:example.org");
783
784 let room = client.get_or_create_room(room_id, RoomState::Joined);
785
786 let mut sync_builder = SyncResponseBuilder::new();
788 let response = sync_builder
789 .add_joined_room(
790 JoinedRoomBuilder::new(room_id)
791 .set_room_summary(json!({
792 "m.joined_member_count": 3,
793 "m.invited_member_count": 0,
794 "m.heroes": [alice_id.to_owned(), service_member_id.to_owned()],
795 }))
796 .add_state_event(
797 EventFactory::new()
798 .sender(user_id)
799 .member_hints(BTreeSet::from([service_member_id.to_owned()])),
800 ),
801 )
802 .build_sync_response();
803
804 client.receive_sync_response(response).await.unwrap();
805
806 let heroes = room.heroes().await;
808 assert_eq!(heroes.len(), 1);
809 assert_eq!(heroes[0].user_id, alice_id);
810 }
811
812 #[async_test]
813 async fn test_human_member_ids_filters_out_service_members() {
814 let client = logged_in_base_client(None).await;
815 let user_id = &client.session_meta().unwrap().user_id;
816 let service_member_id = user_id!("@service:example.org");
817 let alice_id = user_id!("@alice:example.org");
818 let bob_id = user_id!("@bob:example.org");
819 let room_id = room_id!("!room:example.org");
820
821 let room = client.get_or_create_room(room_id, RoomState::Joined);
822 let factory = EventFactory::new().room(room_id);
823
824 let service_member_hint =
825 factory.member_hints(BTreeSet::from([service_member_id.to_owned()])).sender(user_id);
826 let alice_joins = factory.member(alice_id);
827 let service_member_joins = factory.member(service_member_id);
828 let bob_leaves = factory.member(bob_id).membership(MembershipState::Leave);
829
830 let mut sync_builder = SyncResponseBuilder::new();
831 let response = sync_builder
832 .add_joined_room(
833 JoinedRoomBuilder::new(room_id)
834 .add_state_event(service_member_hint)
835 .add_state_event(alice_joins)
836 .add_state_event(service_member_joins)
837 .add_state_event(bob_leaves),
838 )
839 .build_sync_response();
840
841 client.receive_sync_response(response).await.unwrap();
842
843 assert_eq!(
844 room.human_member_ids(RoomMemberships::ACTIVE).await.unwrap(),
845 vec![alice_id.to_owned()]
846 );
847 assert_eq!(
848 room.human_member_ids(RoomMemberships::empty())
849 .await
850 .unwrap()
851 .into_iter()
852 .collect::<BTreeSet<_>>(),
853 BTreeSet::from([alice_id.to_owned(), bob_id.to_owned()])
854 );
855 }
856
857 #[async_test]
858 async fn test_human_member_ids_without_member_hints() {
859 let client = logged_in_base_client(None).await;
860 let alice_id = user_id!("@alice:example.org");
861 let room_id = room_id!("!room:example.org");
862
863 let room = client.get_or_create_room(room_id, RoomState::Joined);
864 let factory = EventFactory::new().room(room_id);
865
866 let alice_joins = factory.member(alice_id);
867
868 let mut sync_builder = SyncResponseBuilder::new();
869 let response = sync_builder
870 .add_joined_room(JoinedRoomBuilder::new(room_id).add_state_event(alice_joins))
871 .build_sync_response();
872
873 client.receive_sync_response(response).await.unwrap();
874
875 assert_eq!(
876 room.human_member_ids(RoomMemberships::ACTIVE).await.unwrap(),
877 vec![alice_id.to_owned()]
878 );
879 }
880
881 #[cfg(feature = "unstable-msc4426")]
882 #[async_test]
883 async fn test_room_heroes_carry_global_profile() {
884 use ruma::{
885 SecondsSinceUnixEpoch,
886 profile::{
887 CallProfileField, ProfileFieldValue, StatusProfileField, UserProfileChanges,
888 UserProfileUpdate,
889 },
890 };
891
892 use crate::store::StateChanges;
893
894 let client = logged_in_base_client(None).await;
895 let alice_id = user_id!("@alice:example.org");
896 let room_id = room_id!("!room:example.org");
897
898 let room = client.get_or_create_room(room_id, RoomState::Joined);
899
900 let mut sync_builder = SyncResponseBuilder::new();
901 let response = sync_builder
902 .add_joined_room(JoinedRoomBuilder::new(room_id).set_room_summary(json!({
903 "m.joined_member_count": 2,
904 "m.invited_member_count": 0,
905 "m.heroes": [alice_id.to_owned()],
906 })))
907 .build_sync_response();
908 client.receive_sync_response(response).await.unwrap();
909
910 let heroes = room.heroes().await;
912 assert_eq!(heroes.len(), 1);
913 assert_eq!(heroes[0].user_id, alice_id);
914 assert!(heroes[0].status.is_none());
915 assert!(heroes[0].call.is_none());
916
917 let mut call = CallProfileField::new();
920 call.call_joined_ts = Some(SecondsSinceUnixEpoch(1_700_000_000u32.into()));
921 let mut changes = StateChanges::default();
922 changes.global_profiles.insert(alice_id.to_owned(), {
923 let mut profile_changes = UserProfileChanges::new();
924 profile_changes.insert_updated_value(ProfileFieldValue::Status(
925 StatusProfileField::new("Working".to_owned(), "💻".to_owned()),
926 ));
927 profile_changes.insert_updated_value(ProfileFieldValue::Call(call));
928 UserProfileUpdate::Updated(profile_changes)
929 });
930 client.state_store().save_changes(&changes).await.unwrap();
931
932 let heroes = room.heroes().await;
934 let hero = &heroes[0];
935 let status = hero.status.as_ref().expect("status is set");
936 assert_eq!(status.text, "Working");
937 assert_eq!(status.emoji, "💻");
938 assert_eq!(
939 hero.call.as_ref().expect("call is set").call_joined_ts,
940 Some(SecondsSinceUnixEpoch(1_700_000_000u32.into()))
941 );
942 }
943}