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
31pub use call::CallIntentConsensus;
32pub use create::*;
33pub use display_name::{RoomDisplayName, RoomHero, RoomHeroWithProfile};
34pub(crate) use display_name::{RoomSummary, UpdatedRoomDisplayName};
35pub use encryption::EncryptionState;
36use eyeball::{AsyncLock, SharedObservable};
37use futures_util::{Stream, StreamExt};
38pub use members::{RoomMember, RoomMembersUpdate, RoomMemberships};
39pub(crate) use room_info::SyncInfo;
40pub use room_info::{
41 BaseRoomInfo, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomRecencyStamp,
42 apply_redaction,
43};
44#[cfg(feature = "unstable-msc4426")]
45use ruma::profile::{Call, Status};
46use ruma::{
47 EventId, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomId,
48 RoomVersionId, UserId,
49 events::{
50 direct::OwnedDirectUserIdentifier,
51 receipt::{Receipt, ReceiptThread, ReceiptType},
52 room::{
53 avatar,
54 guest_access::GuestAccess,
55 history_visibility::HistoryVisibility,
56 join_rules::JoinRule,
57 member::MembershipState,
58 power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent, RoomPowerLevelsSource},
59 retention::RoomRetentionEventContent,
60 },
61 },
62 room::RoomType,
63};
64use serde::{Deserialize, Serialize};
65pub use state::{RoomState, RoomStateFilter};
66pub(crate) use tags::RoomNotableTags;
67use tokio::sync::broadcast;
68pub use tombstone::{PredecessorRoom, SuccessorRoom};
69use tracing::{info, instrument, trace, warn};
70
71use crate::{
72 DmRoomDefinition, Error, StateStore,
73 deserialized_responses::MemberEvent,
74 notification_settings::RoomNotificationMode,
75 read_receipts::ReadReceipts,
76 store::{Result as StoreResult, SaveLockedStateStore, StateStoreExt},
77 sync::UnreadNotificationsCount,
78};
79
80#[derive(Debug, Clone)]
83pub struct Room {
84 pub(super) room_id: OwnedRoomId,
86
87 pub(super) own_user_id: OwnedUserId,
89
90 pub(super) info: SharedObservable<RoomInfo>,
91
92 pub(super) room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
96
97 pub(super) store: SaveLockedStateStore,
99
100 pub seen_knock_request_ids_map:
104 SharedObservable<Option<BTreeMap<OwnedEventId, OwnedUserId>>, AsyncLock>,
105
106 pub room_member_updates_sender: broadcast::Sender<RoomMembersUpdate>,
108}
109
110impl Room {
111 pub(crate) fn new(
112 own_user_id: &UserId,
113 store: SaveLockedStateStore,
114 room_id: &RoomId,
115 room_state: RoomState,
116 room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
117 ) -> Self {
118 let room_info = RoomInfo::new(room_id, room_state);
119 Self::restore(own_user_id, store, room_info, room_info_notable_update_sender)
120 }
121
122 pub(crate) fn restore(
123 own_user_id: &UserId,
124 store: SaveLockedStateStore,
125 room_info: RoomInfo,
126 room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
127 ) -> Self {
128 let (room_member_updates_sender, _) = broadcast::channel(10);
129 Self {
130 own_user_id: own_user_id.into(),
131 room_id: room_info.room_id.clone(),
132 store,
133 info: SharedObservable::new(room_info),
134 room_info_notable_update_sender,
135 seen_knock_request_ids_map: SharedObservable::new_async(None),
136 room_member_updates_sender,
137 }
138 }
139
140 pub fn room_id(&self) -> &RoomId {
142 &self.room_id
143 }
144
145 pub fn creators(&self) -> Option<Vec<OwnedUserId>> {
147 self.info.read().creators()
148 }
149
150 pub fn own_user_id(&self) -> &UserId {
152 &self.own_user_id
153 }
154
155 pub fn is_space(&self) -> bool {
157 self.info.read().room_type().is_some_and(|t| *t == RoomType::Space)
158 }
159
160 pub fn is_call(&self) -> bool {
164 self.info.read().room_type().is_some_and(|t| *t == RoomType::Call)
165 }
166
167 pub fn room_type(&self) -> Option<RoomType> {
170 self.info.read().room_type().map(ToOwned::to_owned)
171 }
172
173 pub fn unread_notification_counts(&self) -> UnreadNotificationsCount {
182 self.info.read().notification_counts
183 }
184
185 pub fn num_unread_messages(&self) -> u64 {
190 self.info.read().read_receipts.num_unread
191 }
192
193 pub fn num_unread_notifications(&self) -> u64 {
198 self.info.read().read_receipts.num_notifications
199 }
200
201 pub fn num_unread_mentions(&self) -> u64 {
207 self.info.read().read_receipts.num_mentions
208 }
209
210 pub fn read_receipts(&self) -> ReadReceipts {
212 self.info.read().read_receipts.clone()
213 }
214
215 pub fn is_state_fully_synced(&self) -> bool {
223 self.info.read().sync_info == SyncInfo::FullySynced
224 }
225
226 pub fn is_state_partially_or_fully_synced(&self) -> bool {
230 self.info.read().sync_info != SyncInfo::NoState
231 }
232
233 pub fn last_prev_batch(&self) -> Option<String> {
236 self.info.read().last_prev_batch.clone()
237 }
238
239 pub fn avatar_url(&self) -> Option<OwnedMxcUri> {
241 self.info.read().avatar_url().map(ToOwned::to_owned)
242 }
243
244 pub fn avatar_info(&self) -> Option<avatar::ImageInfo> {
246 self.info.read().avatar_info().map(ToOwned::to_owned)
247 }
248
249 pub fn canonical_alias(&self) -> Option<OwnedRoomAliasId> {
251 self.info.read().canonical_alias().map(ToOwned::to_owned)
252 }
253
254 pub fn alt_aliases(&self) -> Vec<OwnedRoomAliasId> {
256 self.info.read().alt_aliases().to_owned()
257 }
258
259 pub fn create_content(&self) -> Option<RoomCreateWithCreatorEventContent> {
269 Some(self.info.read().base_info.create.as_ref()?.content.clone())
270 }
271
272 #[instrument(skip_all, fields(room_id = ?self.room_id))]
276 pub async fn is_direct(&self) -> StoreResult<bool> {
277 match self.state() {
278 RoomState::Joined | RoomState::Left | RoomState::Banned => {
279 Ok(!self.info.read().base_info.dm_targets.is_empty())
280 }
281
282 RoomState::Invited => {
283 let member = self.get_member(self.own_user_id()).await?;
284
285 match member {
286 None => {
287 info!("RoomMember not found for the user's own id");
288 Ok(false)
289 }
290 Some(member) => match member.event.as_ref() {
291 MemberEvent::Sync(_) => {
292 warn!("Got MemberEvent::Sync in an invited room");
293 Ok(false)
294 }
295 MemberEvent::Stripped(event) => {
296 Ok(event.content.is_direct.unwrap_or(false))
297 }
298 },
299 }
300 }
301
302 RoomState::Knocked => Ok(false),
304 }
305 }
306
307 pub async fn compute_is_dm(&self, dm_room_definition: &DmRoomDefinition) -> StoreResult<bool> {
310 let is_direct = self.is_direct().await?;
311
312 match *dm_room_definition {
313 DmRoomDefinition::MatrixSpec => Ok(is_direct),
314 DmRoomDefinition::TwoMembers => {
315 if !is_direct {
316 return Ok(false);
317 }
318 let active_service_member_count =
319 self.update_active_service_members().await?.unwrap_or_default().len() as u64;
320 let has_at_most_two_members =
321 self.active_members_count().saturating_sub(active_service_member_count) <= 2;
322 Ok(has_at_most_two_members)
323 }
324 }
325 }
326
327 pub fn direct_targets(&self) -> HashSet<OwnedDirectUserIdentifier> {
336 self.info.read().base_info.dm_targets.clone()
337 }
338
339 pub fn direct_targets_length(&self) -> usize {
342 self.info.read().base_info.dm_targets.len()
343 }
344
345 pub fn guest_access(&self) -> GuestAccess {
347 self.info.read().guest_access().clone()
348 }
349
350 pub fn history_visibility(&self) -> Option<HistoryVisibility> {
352 self.info.read().history_visibility().cloned()
353 }
354
355 pub fn history_visibility_or_default(&self) -> HistoryVisibility {
358 self.info.read().history_visibility_or_default().clone()
359 }
360
361 pub fn is_public(&self) -> Option<bool> {
365 self.info.read().join_rule().map(|join_rule| matches!(join_rule, JoinRule::Public))
366 }
367
368 pub fn join_rule(&self) -> Option<JoinRule> {
370 self.info.read().join_rule().cloned()
371 }
372
373 pub fn max_power_level(&self) -> i64 {
378 self.info.read().base_info.max_power_level
379 }
380
381 pub fn retention(&self) -> Option<RoomRetentionEventContent> {
383 self.info.read().retention().cloned()
384 }
385
386 pub fn service_members(&self) -> Option<BTreeSet<OwnedUserId>> {
388 self.info.read().service_members().cloned()
389 }
390
391 pub async fn power_levels(&self) -> Result<RoomPowerLevels, Error> {
393 let power_levels_content = self
394 .store
395 .get_state_event_static::<RoomPowerLevelsEventContent>(self.room_id())
396 .await?
397 .ok_or(Error::InsufficientData)?
398 .deserialize()?;
399 let creators = self.creators().ok_or(Error::InsufficientData)?;
400 let rules = self.info.read().room_version_rules_or_default();
401
402 Ok(power_levels_content.power_levels(&rules.authorization, creators))
403 }
404
405 pub async fn power_levels_or_default(&self) -> RoomPowerLevels {
408 if let Ok(power_levels) = self.power_levels().await {
409 return power_levels;
410 }
411
412 let rules = self.info.read().room_version_rules_or_default();
414 RoomPowerLevels::new(
415 RoomPowerLevelsSource::None,
416 &rules.authorization,
417 self.creators().into_iter().flatten(),
418 )
419 }
420
421 pub fn name(&self) -> Option<String> {
426 self.info.read().name().map(ToOwned::to_owned)
427 }
428
429 pub fn topic(&self) -> Option<String> {
431 self.info.read().topic().map(ToOwned::to_owned)
432 }
433
434 pub fn update_cached_user_defined_notification_mode(&self, mode: RoomNotificationMode) {
440 self.info.update_if(|info| {
441 if info.cached_user_defined_notification_mode.as_ref() != Some(&mode) {
442 info.cached_user_defined_notification_mode = Some(mode);
443
444 true
445 } else {
446 false
447 }
448 });
449 }
450
451 pub fn cached_user_defined_notification_mode(&self) -> Option<RoomNotificationMode> {
456 self.info.read().cached_user_defined_notification_mode
457 }
458
459 pub fn clear_user_defined_notification_mode(&self) {
462 self.info.update_if(|info| {
463 if info.cached_user_defined_notification_mode.is_some() {
464 info.cached_user_defined_notification_mode = None;
465 true
466 } else {
467 false
468 }
469 })
470 }
471
472 pub async fn joined_user_ids(&self) -> StoreResult<Vec<OwnedUserId>> {
475 self.store.get_user_ids(self.room_id(), RoomMemberships::JOIN).await
476 }
477
478 #[cfg(feature = "unstable-msc4426")]
481 pub(crate) fn hero_user_ids(&self) -> Vec<OwnedUserId> {
482 self.info.read().heroes().iter().map(|hero| hero.user_id.clone()).collect()
483 }
484
485 #[cfg_attr(not(feature = "unstable-msc4426"), allow(clippy::unused_async))]
490 pub async fn heroes(&self) -> Vec<RoomHeroWithProfile> {
491 let heroes: Vec<RoomHero> = {
492 let guard = self.info.read();
493 let heroes = guard.heroes();
494
495 if let Some(service_members) = guard.service_members() {
496 heroes
497 .iter()
498 .filter(|hero| !service_members.contains(&hero.user_id))
499 .cloned()
500 .collect()
501 } else {
502 heroes.to_vec()
503 }
504 };
505
506 if heroes.is_empty() {
508 return Vec::new();
509 }
510
511 #[cfg(not(feature = "unstable-msc4426"))]
513 {
514 heroes.into_iter().map(RoomHeroWithProfile::from).collect()
515 }
516
517 #[cfg(feature = "unstable-msc4426")]
519 {
520 let user_ids = heroes.iter().map(|hero| hero.user_id.clone()).collect::<Vec<_>>();
521
522 let mut global_profiles =
523 self.store.get_global_profiles(&user_ids).await.unwrap_or_else(|error| {
524 tracing::warn!(?error, "Failed to load global profiles for room heroes");
525 Default::default()
526 });
527
528 heroes
529 .into_iter()
530 .map(|hero| {
531 let (status, call) = global_profiles
532 .remove(&*hero.user_id)
533 .map(|profile| {
534 (
535 profile.get_static::<Status>().ok().flatten(),
536 profile.get_static::<Call>().ok().flatten(),
537 )
538 })
539 .unwrap_or_default();
540
541 RoomHeroWithProfile {
542 user_id: hero.user_id,
543 display_name: hero.display_name,
544 avatar_url: hero.avatar_url,
545 status,
546 call,
547 }
548 })
549 .collect()
550 }
551 }
552
553 pub async fn load_user_receipt(
556 &self,
557 receipt_type: ReceiptType,
558 receipt_thread: &ReceiptThread,
559 user_id: &UserId,
560 ) -> StoreResult<Option<(OwnedEventId, Receipt)>> {
561 self.store
562 .get_user_room_receipt_event(self.room_id(), receipt_type, receipt_thread, user_id)
563 .await
564 }
565
566 pub async fn load_event_receipts(
570 &self,
571 receipt_type: ReceiptType,
572 receipt_thread: &ReceiptThread,
573 event_id: &EventId,
574 ) -> StoreResult<Vec<(OwnedUserId, Receipt)>> {
575 self.store
576 .get_event_room_receipt_events(self.room_id(), receipt_type, receipt_thread, event_id)
577 .await
578 }
579
580 pub fn is_marked_unread(&self) -> bool {
583 self.info.read().base_info.is_marked_unread
584 }
585
586 pub fn fully_read_event_id(&self) -> Option<OwnedEventId> {
589 self.info.read().fully_read_event_id().map(ToOwned::to_owned)
590 }
591
592 pub fn version(&self) -> Option<RoomVersionId> {
594 self.info.read().room_version().cloned()
595 }
596
597 pub fn recency_stamp(&self) -> Option<RoomRecencyStamp> {
601 self.info.read().recency_stamp
602 }
603
604 pub fn pinned_event_ids_stream(&self) -> impl Stream<Item = Vec<OwnedEventId>> + use<> {
607 self.info
608 .subscribe()
609 .map(|i| i.base_info.pinned_events.and_then(|c| c.pinned).unwrap_or_default())
610 }
611
612 pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
614 self.info.read().pinned_event_ids()
615 }
616
617 pub async fn update_active_service_members(&self) -> StoreResult<Option<Vec<RoomMember>>> {
621 if let Some(service_members) = self.service_members() {
622 let mut found = Vec::new();
623 for user_id in service_members {
624 match self.get_member(&user_id).await {
625 Ok(Some(member)) => {
626 if matches!(
628 member.membership(),
629 MembershipState::Join | MembershipState::Invite
630 ) {
631 found.push(member);
632 }
633 }
634 Ok(None) => (),
635 Err(error) => return Err(error),
636 }
637 }
638
639 trace!("Updating active service members ({}) in room {}", found.len(), self.room_id());
640
641 let new_active_service_member_count = found.len() as u64;
642 let current_active_service_member_count =
643 self.info.read().summary.active_service_members.unwrap_or_default();
644 if new_active_service_member_count != current_active_service_member_count {
645 self.update_and_save_room_info(|mut info| {
646 info.update_active_service_member_count(Some(new_active_service_member_count));
647 (info, RoomInfoNotableUpdateReasons::ACTIVE_SERVICE_MEMBERS)
648 })
649 .await?;
650 }
651
652 Ok(Some(found))
653 } else {
654 if self.info.read().summary.active_service_members.is_some() {
655 self.update_and_save_room_info(|mut info| {
656 info.update_active_service_member_count(None);
657 (info, RoomInfoNotableUpdateReasons::ACTIVE_SERVICE_MEMBERS)
658 })
659 .await?;
660 }
661 Ok(None)
662 }
663 }
664
665 #[instrument(skip_all, fields(room_id = ?self.room_id))]
669 pub async fn compute_joined_service_members(&self) -> StoreResult<Option<Vec<RoomMember>>> {
670 if !self.are_members_synced() {
671 trace!("Tried to compute joined service members in a room that is not synced");
672 return Ok(None);
673 }
674 if let Some(service_member_ids) = self.service_members() {
675 let mut ret = vec![];
676 for user_id in service_member_ids.iter() {
677 if let Some(member) = self.get_member(user_id).await.unwrap()
678 && matches!(member.membership(), MembershipState::Join)
679 {
680 trace!("Found a joined service member ({})", user_id);
681 ret.push(member);
682 } else {
683 trace!("Did not find a joined service member ({})", user_id);
684 }
685 }
686 trace!(
687 "Computed joined service members ({}) for service member count {}",
688 ret.len(),
689 service_member_ids.len()
690 );
691 Ok(Some(ret))
692 } else {
693 trace!("Tried to compute joined service members in a room that has no service members",);
694 Ok(None)
695 }
696 }
697
698 pub fn active_service_members_count(&self) -> Option<u64> {
701 self.info.read().summary.active_service_members
702 }
703}
704
705#[cfg(not(feature = "test-send-sync"))]
707unsafe impl Send for Room {}
708
709#[cfg(not(feature = "test-send-sync"))]
711unsafe impl Sync for Room {}
712
713#[cfg(feature = "test-send-sync")]
714#[test]
715fn test_send_sync_for_room() {
717 fn assert_send_sync<
718 T: matrix_sdk_common::SendOutsideWasm + matrix_sdk_common::SyncOutsideWasm,
719 >() {
720 }
721
722 assert_send_sync::<Room>();
723}
724
725#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
727pub(crate) enum AccountDataSource {
728 Stable,
730
731 #[default]
733 Unstable,
734}
735
736#[cfg(test)]
737mod tests {
738 use matrix_sdk_test::{
739 JoinedRoomBuilder, SyncResponseBuilder, async_test, event_factory::EventFactory,
740 };
741 use ruma::{room_id, user_id};
742 use serde_json::json;
743
744 use super::*;
745 use crate::test_utils::logged_in_base_client;
746
747 #[async_test]
748 async fn test_room_heroes_filters_out_service_members() {
749 let client = logged_in_base_client(None).await;
750 let user_id = &client.session_meta().unwrap().user_id;
751 let service_member_id = user_id!("@service:example.org");
752 let alice_id = user_id!("@alice:example.org");
753 let room_id = room_id!("!room:example.org");
754
755 let room = client.get_or_create_room(room_id, RoomState::Joined);
756
757 let mut sync_builder = SyncResponseBuilder::new();
759 let response = sync_builder
760 .add_joined_room(
761 JoinedRoomBuilder::new(room_id)
762 .set_room_summary(json!({
763 "m.joined_member_count": 3,
764 "m.invited_member_count": 0,
765 "m.heroes": [alice_id.to_owned(), service_member_id.to_owned()],
766 }))
767 .add_state_event(
768 EventFactory::new()
769 .sender(user_id)
770 .member_hints(BTreeSet::from([service_member_id.to_owned()])),
771 ),
772 )
773 .build_sync_response();
774
775 client.receive_sync_response(response).await.unwrap();
776
777 let heroes = room.heroes().await;
779 assert_eq!(heroes.len(), 1);
780 assert_eq!(heroes[0].user_id, alice_id);
781 }
782
783 #[async_test]
784 async fn test_human_member_ids_filters_out_service_members() {
785 let client = logged_in_base_client(None).await;
786 let user_id = &client.session_meta().unwrap().user_id;
787 let service_member_id = user_id!("@service:example.org");
788 let alice_id = user_id!("@alice:example.org");
789 let bob_id = user_id!("@bob:example.org");
790 let room_id = room_id!("!room:example.org");
791
792 let room = client.get_or_create_room(room_id, RoomState::Joined);
793 let factory = EventFactory::new().room(room_id);
794
795 let service_member_hint =
796 factory.member_hints(BTreeSet::from([service_member_id.to_owned()])).sender(user_id);
797 let alice_joins = factory.member(alice_id);
798 let service_member_joins = factory.member(service_member_id);
799 let bob_leaves = factory.member(bob_id).membership(MembershipState::Leave);
800
801 let mut sync_builder = SyncResponseBuilder::new();
802 let response = sync_builder
803 .add_joined_room(
804 JoinedRoomBuilder::new(room_id)
805 .add_state_event(service_member_hint)
806 .add_state_event(alice_joins)
807 .add_state_event(service_member_joins)
808 .add_state_event(bob_leaves),
809 )
810 .build_sync_response();
811
812 client.receive_sync_response(response).await.unwrap();
813
814 assert_eq!(
815 room.human_member_ids(RoomMemberships::ACTIVE).await.unwrap(),
816 vec![alice_id.to_owned()]
817 );
818 assert_eq!(
819 room.human_member_ids(RoomMemberships::empty())
820 .await
821 .unwrap()
822 .into_iter()
823 .collect::<BTreeSet<_>>(),
824 BTreeSet::from([alice_id.to_owned(), bob_id.to_owned()])
825 );
826 }
827
828 #[async_test]
829 async fn test_human_member_ids_without_member_hints() {
830 let client = logged_in_base_client(None).await;
831 let alice_id = user_id!("@alice:example.org");
832 let room_id = room_id!("!room:example.org");
833
834 let room = client.get_or_create_room(room_id, RoomState::Joined);
835 let factory = EventFactory::new().room(room_id);
836
837 let alice_joins = factory.member(alice_id);
838
839 let mut sync_builder = SyncResponseBuilder::new();
840 let response = sync_builder
841 .add_joined_room(JoinedRoomBuilder::new(room_id).add_state_event(alice_joins))
842 .build_sync_response();
843
844 client.receive_sync_response(response).await.unwrap();
845
846 assert_eq!(
847 room.human_member_ids(RoomMemberships::ACTIVE).await.unwrap(),
848 vec![alice_id.to_owned()]
849 );
850 }
851
852 #[cfg(feature = "unstable-msc4426")]
853 #[async_test]
854 async fn test_room_heroes_carry_global_profile() {
855 use ruma::{
856 SecondsSinceUnixEpoch,
857 profile::{
858 CallProfileField, ProfileFieldValue, StatusProfileField, UserProfileChanges,
859 UserProfileUpdate,
860 },
861 };
862
863 use crate::store::StateChanges;
864
865 let client = logged_in_base_client(None).await;
866 let alice_id = user_id!("@alice:example.org");
867 let room_id = room_id!("!room:example.org");
868
869 let room = client.get_or_create_room(room_id, RoomState::Joined);
870
871 let mut sync_builder = SyncResponseBuilder::new();
872 let response = sync_builder
873 .add_joined_room(JoinedRoomBuilder::new(room_id).set_room_summary(json!({
874 "m.joined_member_count": 2,
875 "m.invited_member_count": 0,
876 "m.heroes": [alice_id.to_owned()],
877 })))
878 .build_sync_response();
879 client.receive_sync_response(response).await.unwrap();
880
881 let heroes = room.heroes().await;
883 assert_eq!(heroes.len(), 1);
884 assert_eq!(heroes[0].user_id, alice_id);
885 assert!(heroes[0].status.is_none());
886 assert!(heroes[0].call.is_none());
887
888 let mut call = CallProfileField::new();
890 call.call_joined_ts = Some(SecondsSinceUnixEpoch(1_700_000_000u32.into()));
891 let mut changes = StateChanges::default();
892 changes.global_profiles.insert(alice_id.to_owned(), {
893 let mut profile_changes = UserProfileChanges::new();
894 profile_changes.insert_updated_value(ProfileFieldValue::Status(
895 StatusProfileField::new("Working".to_owned(), "💻".to_owned()),
896 ));
897 profile_changes.insert_updated_value(ProfileFieldValue::Call(call));
898 UserProfileUpdate::Updated(profile_changes)
899 });
900 client.state_store().save_changes(&changes).await.unwrap();
901
902 let heroes = room.heroes().await;
904 let hero = &heroes[0];
905 let status = hero.status.as_ref().expect("status is set");
906 assert_eq!(status.text, "Working");
907 assert_eq!(status.emoji, "💻");
908 assert_eq!(
909 hero.call.as_ref().expect("call is set").call_joined_ts,
910 Some(SecondsSinceUnixEpoch(1_700_000_000u32.into()))
911 );
912 }
913}