1use std::fmt;
16
17use as_variant::as_variant;
18use regex::Regex;
19#[cfg(feature = "unstable-msc4426")]
20use ruma::profile::{CallProfileField, StatusProfileField};
21use ruma::{
22 OwnedMxcUri, OwnedUserId, RoomAliasId, UserId,
23 events::{SyncStateEvent, member_hints::MemberHintsEventContent},
24};
25use serde::{Deserialize, Serialize};
26use tracing::{debug, trace, warn};
27
28use super::{Room, RoomMemberships};
29use crate::{
30 RoomMember, RoomState, StateStore,
31 deserialized_responses::SyncOrStrippedState,
32 store::{Result as StoreResult, StateStoreExt},
33};
34
35impl Room {
36 pub async fn display_name(&self) -> StoreResult<RoomDisplayName> {
51 if let Some(name) = self.cached_display_name() {
52 Ok(name)
53 } else {
54 Ok(self.compute_display_name().await?.into_inner())
55 }
56 }
57
58 pub fn cached_display_name(&self) -> Option<RoomDisplayName> {
62 self.info.read().cached_display_name.clone()
63 }
64
65 pub fn compute_display_name_with_fields(
70 name: Option<String>,
71 canonical_alias: Option<&RoomAliasId>,
72 heroes: Vec<RoomHero>,
73 num_joined_members: u64,
74 ) -> RoomDisplayName {
75 let name = name.and_then(|name| (!name.is_empty()).then_some(name));
78
79 match (name, canonical_alias) {
80 (Some(name), _) => RoomDisplayName::Named(name.trim().to_owned()),
81 (None, Some(alias)) => RoomDisplayName::Aliased(alias.alias().trim().to_owned()),
82 (None, None) => {
83 let hero_display_names =
84 heroes.into_iter().filter_map(|hero| hero.display_name).collect::<Vec<_>>();
85
86 compute_display_name_from_heroes(
87 num_joined_members,
88 hero_display_names.iter().map(|name| name.as_str()).collect(),
89 )
90 }
91 }
92 }
93
94 pub(crate) async fn compute_display_name(&self) -> StoreResult<UpdatedRoomDisplayName> {
106 enum DisplayNameOrSummary {
107 Summary(RoomSummary),
108 DisplayName(RoomDisplayName),
109 }
110
111 let display_name_or_summary = {
112 let inner = self.info.read();
113
114 match (inner.name(), inner.canonical_alias()) {
115 (Some(name), _) => {
116 let name = RoomDisplayName::Named(name.trim().to_owned());
117 DisplayNameOrSummary::DisplayName(name)
118 }
119 (None, Some(alias)) => {
120 let name = RoomDisplayName::Aliased(alias.alias().trim().to_owned());
121 DisplayNameOrSummary::DisplayName(name)
122 }
123 (None, None) => DisplayNameOrSummary::Summary(inner.summary.clone()),
128 }
129 };
130
131 let display_name = match display_name_or_summary {
132 DisplayNameOrSummary::Summary(summary) => {
133 self.compute_display_name_from_summary(summary).await?
134 }
135 DisplayNameOrSummary::DisplayName(display_name) => display_name,
136 };
137
138 let mut updated = false;
140
141 self.info.update_if(|info| {
142 if info.cached_display_name.as_ref() != Some(&display_name) {
143 info.cached_display_name = Some(display_name.clone());
144 updated = true;
145
146 true
147 } else {
148 false
149 }
150 });
151
152 Ok(if updated {
153 UpdatedRoomDisplayName::New(display_name)
154 } else {
155 UpdatedRoomDisplayName::Same(display_name)
156 })
157 }
158
159 async fn compute_display_name_from_summary(
161 &self,
162 summary: RoomSummary,
163 ) -> StoreResult<RoomDisplayName> {
164 let computed_summary = if !summary.room_heroes.is_empty() {
165 self.extract_and_augment_summary(&summary).await?
166 } else {
167 self.compute_summary().await?
168 };
169
170 let ComputedSummary { heroes, num_service_members, num_joined_invited_guess } =
171 computed_summary;
172
173 let summary_member_count = (summary.joined_member_count + summary.invited_member_count)
174 .saturating_sub(num_service_members);
175
176 let num_joined_invited = if self.state() == RoomState::Invited {
177 heroes.len() as u64 + 1
180 } else if summary_member_count == 0 {
181 num_joined_invited_guess
182 } else {
183 summary_member_count
184 };
185
186 debug!(
187 room_id = ?self.room_id(),
188 own_user = ?self.own_user_id,
189 num_joined_invited,
190 heroes = ?heroes,
191 "Calculating name for a room based on heroes",
192 );
193
194 let display_name = compute_display_name_from_heroes(
195 num_joined_invited,
196 heroes.iter().map(|hero| hero.as_str()).collect(),
197 );
198
199 Ok(display_name)
200 }
201
202 async fn extract_and_augment_summary(
211 &self,
212 summary: &RoomSummary,
213 ) -> StoreResult<ComputedSummary> {
214 let heroes = &summary.room_heroes;
215
216 let mut names = Vec::with_capacity(heroes.len());
217 let own_user_id = self.own_user_id();
218 let member_hints = self.get_member_hints().await?;
219
220 let num_service_members = heroes
225 .iter()
226 .filter(|hero| member_hints.service_members.contains(&hero.user_id))
227 .count() as u64;
228
229 let heroes_filter = heroes_filter(own_user_id, &member_hints);
232 let heroes_filter = |hero: &&RoomHero| heroes_filter(&hero.user_id);
233
234 for hero in heroes.iter().filter(heroes_filter) {
235 if let Some(display_name) = &hero.display_name {
236 names.push(display_name.clone());
237 } else {
238 match self.get_member(&hero.user_id).await {
239 Ok(Some(member)) => {
240 names.push(member.name().to_owned());
241 }
242 Ok(None) => {
243 warn!(user_id = ?hero.user_id, "Ignoring hero, no member info");
244 }
245 Err(error) => {
246 warn!("Ignoring hero, error getting member: {error}");
247 }
248 }
249 }
250 }
251
252 let num_joined_invited_guess = summary.joined_member_count + summary.invited_member_count;
253
254 let num_joined_invited_guess = if num_joined_invited_guess == 0 {
257 let guess = self
258 .store
259 .get_user_ids(self.room_id(), RoomMemberships::JOIN | RoomMemberships::INVITE)
260 .await?
261 .len() as u64;
262
263 guess.saturating_sub(num_service_members)
264 } else {
265 num_joined_invited_guess
267 };
268
269 Ok(ComputedSummary { heroes: names, num_service_members, num_joined_invited_guess })
270 }
271
272 async fn compute_summary(&self) -> StoreResult<ComputedSummary> {
278 let member_hints = self.get_member_hints().await?;
279
280 let heroes_filter = heroes_filter(&self.own_user_id, &member_hints);
283 let heroes_filter = |u: &RoomMember| heroes_filter(u.user_id());
284
285 let mut members = self.members(RoomMemberships::JOIN | RoomMemberships::INVITE).await?;
286
287 let num_service_members = members
291 .iter()
292 .filter(|member| member_hints.service_members.contains(member.user_id()))
293 .count();
294
295 let num_joined_invited = members.len() - num_service_members;
302
303 if num_joined_invited == 0
304 || (num_joined_invited == 1 && members[0].user_id() == self.own_user_id)
305 {
306 members = self.members(RoomMemberships::LEAVE | RoomMemberships::BAN).await?;
308 }
309
310 members.sort_unstable_by(|lhs, rhs| lhs.name().cmp(rhs.name()));
312
313 let heroes = members
314 .into_iter()
315 .filter(heroes_filter)
316 .take(NUM_HEROES)
317 .map(|u| u.name().to_owned())
318 .collect();
319
320 trace!(
321 ?heroes,
322 num_joined_invited,
323 num_service_members,
324 "Computed a room summary since we didn't receive one."
325 );
326
327 let num_service_members = num_service_members as u64;
328 let num_joined_invited_guess = num_joined_invited as u64;
329
330 Ok(ComputedSummary { heroes, num_service_members, num_joined_invited_guess })
331 }
332
333 async fn get_member_hints(&self) -> StoreResult<MemberHintsEventContent> {
334 Ok(self
335 .store
336 .get_state_event_static::<MemberHintsEventContent>(self.room_id())
337 .await?
338 .and_then(|event| {
339 event
340 .deserialize()
341 .inspect_err(|e| warn!("Couldn't deserialize the member hints event: {e}"))
342 .ok()
343 })
344 .and_then(|event| as_variant!(event, SyncOrStrippedState::Sync(SyncStateEvent::Original(e)) => e.content))
345 .unwrap_or_default())
346 }
347}
348
349struct ComputedSummary {
356 heroes: Vec<String>,
359 num_service_members: u64,
361 num_joined_invited_guess: u64,
364}
365
366#[derive(Clone, Debug, Default, Serialize, Deserialize)]
369pub(crate) struct RoomSummary {
370 #[serde(default, skip_serializing_if = "Vec::is_empty")]
378 pub room_heroes: Vec<RoomHero>,
379 pub joined_member_count: u64,
381 pub invited_member_count: u64,
383 #[serde(default, skip_serializing_if = "Option::is_none")]
386 pub active_service_members: Option<u64>,
387}
388
389#[cfg(test)]
390impl RoomSummary {
391 pub(crate) fn heroes(&self) -> &[RoomHero] {
392 &self.room_heroes
393 }
394}
395
396#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
398pub struct RoomHero {
399 pub user_id: OwnedUserId,
401 pub display_name: Option<String>,
403 pub avatar_url: Option<OwnedMxcUri>,
405}
406
407#[derive(Clone, Debug, PartialEq)]
409pub struct RoomHeroWithProfile {
410 pub user_id: OwnedUserId,
412 pub display_name: Option<String>,
414 pub avatar_url: Option<OwnedMxcUri>,
416 #[cfg(feature = "unstable-msc4426")]
418 pub status: Option<StatusProfileField>,
419 #[cfg(feature = "unstable-msc4426")]
421 pub call: Option<CallProfileField>,
422}
423
424impl From<RoomHero> for RoomHeroWithProfile {
425 fn from(hero: RoomHero) -> Self {
426 Self {
427 user_id: hero.user_id,
428 display_name: hero.display_name,
429 avatar_url: hero.avatar_url,
430 #[cfg(feature = "unstable-msc4426")]
431 status: None,
432 #[cfg(feature = "unstable-msc4426")]
433 call: None,
434 }
435 }
436}
437
438impl From<&RoomHeroWithProfile> for RoomHero {
439 fn from(hero: &RoomHeroWithProfile) -> Self {
440 Self {
441 user_id: hero.user_id.clone(),
442 display_name: hero.display_name.clone(),
443 avatar_url: hero.avatar_url.clone(),
444 }
445 }
446}
447
448const NUM_HEROES: usize = 5;
455
456#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
459pub enum RoomDisplayName {
460 Named(String),
462 Aliased(String),
464 Calculated(String),
467 EmptyWas(String),
470 Empty,
472}
473
474pub(crate) enum UpdatedRoomDisplayName {
477 New(RoomDisplayName),
478 Same(RoomDisplayName),
479}
480
481impl UpdatedRoomDisplayName {
482 pub fn into_inner(self) -> RoomDisplayName {
484 match self {
485 UpdatedRoomDisplayName::New(room_display_name) => room_display_name,
486 UpdatedRoomDisplayName::Same(room_display_name) => room_display_name,
487 }
488 }
489}
490
491const WHITESPACE_REGEX: &str = r"\s+";
492const INVALID_SYMBOLS_REGEX: &str = r"[#,:\{\}\\]+";
493
494impl RoomDisplayName {
495 pub fn to_room_alias_name(&self) -> String {
498 let room_name = match self {
499 Self::Named(name) => name,
500 Self::Aliased(name) => name,
501 Self::Calculated(name) => name,
502 Self::EmptyWas(name) => name,
503 Self::Empty => "",
504 };
505
506 let whitespace_regex =
507 Regex::new(WHITESPACE_REGEX).expect("`WHITESPACE_REGEX` should be valid");
508 let symbol_regex =
509 Regex::new(INVALID_SYMBOLS_REGEX).expect("`INVALID_SYMBOLS_REGEX` should be valid");
510
511 let sanitised = whitespace_regex.replace_all(room_name, "-");
513 let sanitised =
515 String::from_iter(sanitised.chars().filter(|c| c.is_ascii() && !c.is_ascii_control()));
516 let sanitised = symbol_regex.replace_all(&sanitised, "");
518 sanitised.to_lowercase()
520 }
521}
522
523impl fmt::Display for RoomDisplayName {
524 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
525 match self {
526 RoomDisplayName::Named(s)
527 | RoomDisplayName::Calculated(s)
528 | RoomDisplayName::Aliased(s) => {
529 write!(f, "{s}")
530 }
531 RoomDisplayName::EmptyWas(s) => write!(f, "Empty Room (was {s})"),
532 RoomDisplayName::Empty => write!(f, "Empty Room"),
533 }
534 }
535}
536
537fn compute_display_name_from_heroes(
541 num_joined_invited: u64,
542 mut heroes: Vec<&str>,
543) -> RoomDisplayName {
544 let num_heroes = heroes.len() as u64;
545 let num_joined_invited_except_self = num_joined_invited.saturating_sub(1);
546
547 heroes.sort_unstable();
549
550 let names = if num_heroes == 0 && num_joined_invited > 1 {
551 format!("{num_joined_invited} people")
552 } else if num_heroes >= num_joined_invited_except_self {
553 heroes.join(", ")
554 } else if num_heroes < num_joined_invited_except_self && num_joined_invited > 1 {
555 format!("{}, and {} others", heroes.join(", "), (num_joined_invited - num_heroes))
558 } else {
559 "".to_owned()
560 };
561
562 if num_joined_invited <= 1 {
564 if names.is_empty() { RoomDisplayName::Empty } else { RoomDisplayName::EmptyWas(names) }
565 } else {
566 RoomDisplayName::Calculated(names)
567 }
568}
569
570fn heroes_filter<'a>(
576 own_user_id: &'a UserId,
577 member_hints: &'a MemberHintsEventContent,
578) -> impl Fn(&UserId) -> bool + use<'a> {
579 move |user_id| user_id != own_user_id && !member_hints.service_members.contains(user_id)
580}
581
582#[cfg(test)]
583mod tests {
584 use std::{collections::BTreeSet, sync::Arc};
585
586 use matrix_sdk_test::{async_test, event_factory::EventFactory};
587 use ruma::{
588 UserId,
589 api::client::sync::sync_events::v3::RoomSummary as RumaSummary,
590 assign,
591 events::{
592 StateEventType,
593 room::{
594 canonical_alias::{
595 PossiblyRedactedRoomCanonicalAliasEventContent, RoomCanonicalAliasEventContent,
596 },
597 member::{MembershipState, RoomMemberEventContent, StrippedRoomMemberEvent},
598 name::{PossiblyRedactedRoomNameEventContent, RoomNameEventContent},
599 },
600 },
601 owned_room_alias_id, owned_user_id, room_alias_id, room_id,
602 serde::Raw,
603 user_id,
604 };
605 use serde_json::json;
606
607 use super::{Room, RoomDisplayName, compute_display_name_from_heroes};
608 use crate::{
609 MinimalStateEvent, RoomHero, RoomState, StateChanges, StateStore,
610 store::{MemoryStore, SaveLockedStateStore},
611 };
612
613 fn make_room_test_helper(room_type: RoomState) -> (Arc<MemoryStore>, Room) {
614 let store = Arc::new(MemoryStore::new());
615 let user_id = user_id!("@me:example.org");
616 let room_id = room_id!("!test:localhost");
617 let (sender, _receiver) = tokio::sync::broadcast::channel(1);
618
619 (
620 store.clone(),
621 Room::new(user_id, SaveLockedStateStore::new(store), room_id, room_type, sender),
622 )
623 }
624
625 fn make_stripped_member_event(user_id: &UserId, name: &str) -> Raw<StrippedRoomMemberEvent> {
626 let ev_json = json!({
627 "type": "m.room.member",
628 "content": assign!(RoomMemberEventContent::new(MembershipState::Join), {
629 displayname: Some(name.to_owned())
630 }),
631 "sender": user_id,
632 "state_key": user_id,
633 });
634
635 Raw::new(&ev_json).unwrap().cast_unchecked()
636 }
637
638 fn make_canonical_alias_event() -> MinimalStateEvent<RoomCanonicalAliasEventContent> {
639 MinimalStateEvent {
640 content: assign!(PossiblyRedactedRoomCanonicalAliasEventContent::new(), {
641 alias: Some(owned_room_alias_id!("#test:example.com")),
642 }),
643 event_id: None,
644 }
645 }
646
647 fn make_name_event_with(name: &str) -> MinimalStateEvent<PossiblyRedactedRoomNameEventContent> {
648 MinimalStateEvent {
649 content: RoomNameEventContent::new(name.to_owned()).into(),
650 event_id: None,
651 }
652 }
653
654 fn make_name_event() -> MinimalStateEvent<PossiblyRedactedRoomNameEventContent> {
655 make_name_event_with("Test Room")
656 }
657
658 #[async_test]
659 async fn test_display_name_for_joined_room_is_empty_if_no_info() {
660 let (_, room) = make_room_test_helper(RoomState::Joined);
661 assert_eq!(room.compute_display_name().await.unwrap().into_inner(), RoomDisplayName::Empty);
662 }
663
664 #[test]
665 fn test_display_name_compute_fields_empty() {
666 assert_eq!(
667 Room::compute_display_name_with_fields(None, None, vec![], 0),
668 RoomDisplayName::Empty
669 );
670 }
671
672 #[async_test]
673 async fn test_display_name_for_joined_room_is_empty_if_name_empty() {
674 let (_, room) = make_room_test_helper(RoomState::Joined);
675 room.info.update(|info| info.base_info.name = Some(make_name_event_with("")));
676
677 assert_eq!(room.compute_display_name().await.unwrap().into_inner(), RoomDisplayName::Empty);
678 }
679
680 #[test]
681 fn test_display_name_compute_fields_empty_if_name_empty() {
682 assert_eq!(
683 Room::compute_display_name_with_fields(Some("".to_owned()), None, vec![], 0),
684 RoomDisplayName::Empty
685 );
686 }
687
688 #[async_test]
689 async fn test_display_name_for_joined_room_uses_canonical_alias_if_available() {
690 let (_, room) = make_room_test_helper(RoomState::Joined);
691 room.info
692 .update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
693 assert_eq!(
694 room.compute_display_name().await.unwrap().into_inner(),
695 RoomDisplayName::Aliased("test".to_owned())
696 );
697 }
698
699 #[test]
700 fn test_display_name_compute_fields_alias() {
701 assert_eq!(
702 Room::compute_display_name_with_fields(
703 None,
704 Some(room_alias_id!("#test:example.com")),
705 vec![],
706 0,
707 ),
708 RoomDisplayName::Aliased("test".to_owned())
709 );
710 }
711
712 #[async_test]
713 async fn test_display_name_for_joined_room_prefers_name_over_alias() {
714 let (_, room) = make_room_test_helper(RoomState::Joined);
715 room.info
716 .update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
717 assert_eq!(
718 room.compute_display_name().await.unwrap().into_inner(),
719 RoomDisplayName::Aliased("test".to_owned())
720 );
721 room.info.update(|info| info.base_info.name = Some(make_name_event()));
722 assert_eq!(
724 room.compute_display_name().await.unwrap().into_inner(),
725 RoomDisplayName::Named("Test Room".to_owned())
726 );
727 }
728
729 #[test]
730 fn test_display_name_compute_fields_name_over_alias() {
731 assert_eq!(
732 Room::compute_display_name_with_fields(
733 Some("Test Room".to_owned()),
734 Some(room_alias_id!("#test:example.com")),
735 vec![],
736 0
737 ),
738 RoomDisplayName::Named("Test Room".to_owned())
739 );
740 }
741
742 #[async_test]
743 async fn test_display_name_for_invited_room_is_empty_if_no_info() {
744 let (_, room) = make_room_test_helper(RoomState::Invited);
745 assert_eq!(room.compute_display_name().await.unwrap().into_inner(), RoomDisplayName::Empty);
746 }
747
748 #[async_test]
749 async fn test_display_name_for_invited_room_is_empty_if_room_name_empty() {
750 let (_, room) = make_room_test_helper(RoomState::Invited);
751
752 let room_name = make_name_event_with("");
753 room.info.update(|info| info.base_info.name = Some(room_name));
754
755 assert_eq!(room.compute_display_name().await.unwrap().into_inner(), RoomDisplayName::Empty);
756 }
757
758 #[async_test]
759 async fn test_display_name_for_invited_room_uses_canonical_alias_if_available() {
760 let (_, room) = make_room_test_helper(RoomState::Invited);
761 room.info
762 .update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
763 assert_eq!(
764 room.compute_display_name().await.unwrap().into_inner(),
765 RoomDisplayName::Aliased("test".to_owned())
766 );
767 }
768
769 #[async_test]
770 async fn test_display_name_for_invited_room_prefers_name_over_alias() {
771 let (_, room) = make_room_test_helper(RoomState::Invited);
772 room.info
773 .update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
774 assert_eq!(
775 room.compute_display_name().await.unwrap().into_inner(),
776 RoomDisplayName::Aliased("test".to_owned())
777 );
778 room.info.update(|info| info.base_info.name = Some(make_name_event()));
779 assert_eq!(
781 room.compute_display_name().await.unwrap().into_inner(),
782 RoomDisplayName::Named("Test Room".to_owned())
783 );
784 }
785
786 #[async_test]
787 async fn test_display_name_dm_invited() {
788 let (store, room) = make_room_test_helper(RoomState::Invited);
789 let room_id = room_id!("!test:localhost");
790 let matthew = user_id!("@matthew:example.org");
791 let me = user_id!("@me:example.org");
792 let mut changes = StateChanges::new("".to_owned());
793 let summary = assign!(RumaSummary::new(), {
794 heroes: vec![me.to_owned(), matthew.to_owned()],
795 });
796
797 changes.add_stripped_member(
798 room_id,
799 matthew,
800 make_stripped_member_event(matthew, "Matthew"),
801 );
802 changes.add_stripped_member(room_id, me, make_stripped_member_event(me, "Me"));
803 store.save_changes(&changes).await.unwrap();
804
805 room.info.update_if(|info| info.update_from_ruma_summary(&summary));
806 assert_eq!(
807 room.compute_display_name().await.unwrap().into_inner(),
808 RoomDisplayName::Calculated("Matthew".to_owned())
809 );
810 }
811
812 #[async_test]
813 async fn test_display_name_dm_invited_no_heroes() {
814 let (store, room) = make_room_test_helper(RoomState::Invited);
815 let room_id = room_id!("!test:localhost");
816 let matthew = user_id!("@matthew:example.org");
817 let me = user_id!("@me:example.org");
818 let mut changes = StateChanges::new("".to_owned());
819
820 changes.add_stripped_member(
821 room_id,
822 matthew,
823 make_stripped_member_event(matthew, "Matthew"),
824 );
825 changes.add_stripped_member(room_id, me, make_stripped_member_event(me, "Me"));
826 store.save_changes(&changes).await.unwrap();
827
828 assert_eq!(
829 room.compute_display_name().await.unwrap().into_inner(),
830 RoomDisplayName::Calculated("Matthew".to_owned())
831 );
832 }
833
834 #[async_test]
835 async fn test_display_name_dm_joined() {
836 let (store, room) = make_room_test_helper(RoomState::Joined);
837 let room_id = room_id!("!test:localhost");
838 let matthew = user_id!("@matthew:example.org");
839 let me = user_id!("@me:example.org");
840
841 let mut changes = StateChanges::new("".to_owned());
842 let summary = assign!(RumaSummary::new(), {
843 joined_member_count: Some(2u32.into()),
844 heroes: vec![me.to_owned(), matthew.to_owned()],
845 });
846
847 let f = EventFactory::new().room(room_id!("!test:localhost"));
848
849 let members = changes
850 .state
851 .entry(room_id.to_owned())
852 .or_default()
853 .entry(StateEventType::RoomMember)
854 .or_default();
855 members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into());
856 members.insert(me.into(), f.member(me).display_name("Me").into());
857
858 store.save_changes(&changes).await.unwrap();
859
860 room.info.update_if(|info| info.update_from_ruma_summary(&summary));
861 assert_eq!(
862 room.compute_display_name().await.unwrap().into_inner(),
863 RoomDisplayName::Calculated("Matthew".to_owned())
864 );
865 }
866
867 #[async_test]
868 async fn test_display_name_dm_joined_service_members() {
869 let (store, room) = make_room_test_helper(RoomState::Joined);
870 let room_id = room_id!("!test:localhost");
871
872 let matthew = user_id!("@sahasrhala:example.org");
873 let me = user_id!("@me:example.org");
874 let bot = user_id!("@bot:example.org");
875
876 let mut changes = StateChanges::new("".to_owned());
877 let summary = assign!(RumaSummary::new(), {
878 joined_member_count: Some(3u32.into()),
879 heroes: vec![me.to_owned(), matthew.to_owned(), bot.to_owned()],
880 });
881
882 let f = EventFactory::new().room(room_id!("!test:localhost"));
883
884 let members = changes
885 .state
886 .entry(room_id.to_owned())
887 .or_default()
888 .entry(StateEventType::RoomMember)
889 .or_default();
890 members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into());
891 members.insert(me.into(), f.member(me).display_name("Me").into());
892 members.insert(bot.into(), f.member(bot).display_name("Bot").into());
893
894 let member_hints_content =
895 f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into();
896 changes
897 .state
898 .entry(room_id.to_owned())
899 .or_default()
900 .entry(StateEventType::MemberHints)
901 .or_default()
902 .insert("".to_owned(), member_hints_content);
903
904 store.save_changes(&changes).await.unwrap();
905
906 room.info.update_if(|info| info.update_from_ruma_summary(&summary));
907 assert_eq!(
909 room.compute_display_name().await.unwrap().into_inner(),
910 RoomDisplayName::Calculated("Matthew".to_owned())
911 );
912 }
913
914 #[async_test]
915 async fn test_display_name_dm_joined_alone_with_service_members() {
916 let (store, room) = make_room_test_helper(RoomState::Joined);
917 let room_id = room_id!("!test:localhost");
918
919 let me = user_id!("@me:example.org");
920 let bot = user_id!("@bot:example.org");
921
922 let mut changes = StateChanges::new("".to_owned());
923 let summary = assign!(RumaSummary::new(), {
924 joined_member_count: Some(2u32.into()),
925 heroes: vec![me.to_owned(), bot.to_owned()],
926 });
927
928 let f = EventFactory::new().room(room_id!("!test:localhost"));
929
930 let members = changes
931 .state
932 .entry(room_id.to_owned())
933 .or_default()
934 .entry(StateEventType::RoomMember)
935 .or_default();
936 members.insert(me.into(), f.member(me).display_name("Me").into());
937 members.insert(bot.into(), f.member(bot).display_name("Bot").into());
938
939 let member_hints_content =
940 f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into();
941 changes
942 .state
943 .entry(room_id.to_owned())
944 .or_default()
945 .entry(StateEventType::MemberHints)
946 .or_default()
947 .insert("".to_owned(), member_hints_content);
948
949 store.save_changes(&changes).await.unwrap();
950
951 room.info.update_if(|info| info.update_from_ruma_summary(&summary));
952 assert_eq!(room.compute_display_name().await.unwrap().into_inner(), RoomDisplayName::Empty);
954 }
955
956 #[async_test]
957 async fn test_display_name_dm_joined_no_heroes() {
958 let (store, room) = make_room_test_helper(RoomState::Joined);
959 let room_id = room_id!("!test:localhost");
960 let matthew = user_id!("@matthew:example.org");
961 let me = user_id!("@me:example.org");
962 let mut changes = StateChanges::new("".to_owned());
963
964 let f = EventFactory::new().room(room_id!("!test:localhost"));
965
966 let members = changes
967 .state
968 .entry(room_id.to_owned())
969 .or_default()
970 .entry(StateEventType::RoomMember)
971 .or_default();
972 members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into());
973 members.insert(me.into(), f.member(me).display_name("Me").into());
974
975 store.save_changes(&changes).await.unwrap();
976
977 assert_eq!(
978 room.compute_display_name().await.unwrap().into_inner(),
979 RoomDisplayName::Calculated("Matthew".to_owned())
980 );
981 }
982
983 #[async_test]
984 async fn test_display_name_dm_joined_no_heroes_service_members() {
985 let (store, room) = make_room_test_helper(RoomState::Joined);
986 let room_id = room_id!("!test:localhost");
987
988 let matthew = user_id!("@matthew:example.org");
989 let me = user_id!("@me:example.org");
990 let bot = user_id!("@bot:example.org");
991
992 let mut changes = StateChanges::new("".to_owned());
993
994 let f = EventFactory::new().room(room_id!("!test:localhost"));
995
996 let members = changes
997 .state
998 .entry(room_id.to_owned())
999 .or_default()
1000 .entry(StateEventType::RoomMember)
1001 .or_default();
1002 members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into());
1003 members.insert(me.into(), f.member(me).display_name("Me").into());
1004 members.insert(bot.into(), f.member(bot).display_name("Bot").into());
1005
1006 let member_hints_content =
1007 f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into();
1008 changes
1009 .state
1010 .entry(room_id.to_owned())
1011 .or_default()
1012 .entry(StateEventType::MemberHints)
1013 .or_default()
1014 .insert("".to_owned(), member_hints_content);
1015
1016 store.save_changes(&changes).await.unwrap();
1017
1018 assert_eq!(
1019 room.compute_display_name().await.unwrap().into_inner(),
1020 RoomDisplayName::Calculated("Matthew".to_owned())
1021 );
1022 }
1023
1024 #[async_test]
1025 async fn test_display_name_deterministic() {
1026 let (store, room) = make_room_test_helper(RoomState::Joined);
1027
1028 let alice = user_id!("@alice:example.org");
1029 let bob = user_id!("@bob:example.org");
1030 let carol = user_id!("@carol:example.org");
1031 let denis = user_id!("@denis:example.org");
1032 let erica = user_id!("@erica:example.org");
1033 let fred = user_id!("@fred:example.org");
1034 let me = user_id!("@me:example.org");
1035
1036 let mut changes = StateChanges::new("".to_owned());
1037
1038 let f = EventFactory::new().room(room_id!("!test:localhost"));
1039
1040 {
1043 let members = changes
1044 .state
1045 .entry(room.room_id().to_owned())
1046 .or_default()
1047 .entry(StateEventType::RoomMember)
1048 .or_default();
1049 members.insert(carol.into(), f.member(carol).display_name("Carol").into());
1050 members.insert(bob.into(), f.member(bob).display_name("Bob").into());
1051 members.insert(fred.into(), f.member(fred).display_name("Fred").into());
1052 members.insert(me.into(), f.member(me).display_name("Me").into());
1053 store.save_changes(&changes).await.unwrap();
1054 }
1055
1056 {
1057 let members = changes
1058 .state
1059 .entry(room.room_id().to_owned())
1060 .or_default()
1061 .entry(StateEventType::RoomMember)
1062 .or_default();
1063 members.insert(alice.into(), f.member(alice).display_name("Alice").into());
1064 members.insert(erica.into(), f.member(erica).display_name("Erica").into());
1065 members.insert(denis.into(), f.member(denis).display_name("Denis").into());
1066 store.save_changes(&changes).await.unwrap();
1067 }
1068
1069 let summary = assign!(RumaSummary::new(), {
1070 joined_member_count: Some(7u32.into()),
1071 heroes: vec![denis.to_owned(), carol.to_owned(), bob.to_owned(), erica.to_owned()],
1072 });
1073 room.info.update_if(|info| info.update_from_ruma_summary(&summary));
1074
1075 assert_eq!(
1076 room.compute_display_name().await.unwrap().into_inner(),
1077 RoomDisplayName::Calculated("Bob, Carol, Denis, Erica, and 3 others".to_owned())
1078 );
1079 }
1080
1081 #[test]
1082 fn test_display_name_compute_fields_name_deterministic() {
1083 assert_eq!(
1084 Room::compute_display_name_with_fields(
1085 None,
1086 None,
1087 vec![
1088 RoomHero {
1089 user_id: owned_user_id!("@alice:example.org"),
1090 display_name: Some("Alice".to_owned()),
1091 avatar_url: None,
1092 },
1093 RoomHero {
1094 user_id: owned_user_id!("@bob:example.org"),
1095 display_name: Some("Bob".to_owned()),
1096 avatar_url: None,
1097 },
1098 RoomHero {
1099 user_id: owned_user_id!("@carol:example.org"),
1100 display_name: Some("Carol".to_owned()),
1101 avatar_url: None,
1102 },
1103 RoomHero {
1104 user_id: owned_user_id!("@denis:example.org"),
1105 display_name: Some("Denis".to_owned()),
1106 avatar_url: None,
1107 },
1108 RoomHero {
1109 user_id: owned_user_id!("@erica:example.org"),
1110 display_name: Some("Erica".to_owned()),
1111 avatar_url: None,
1112 },
1113 ],
1114 1234,
1115 ),
1116 RoomDisplayName::Calculated(
1117 "Alice, Bob, Carol, Denis, Erica, and 1229 others".to_owned()
1118 )
1119 );
1120 }
1121
1122 #[async_test]
1123 async fn test_display_name_deterministic_no_heroes() {
1124 let (store, room) = make_room_test_helper(RoomState::Joined);
1125
1126 let alice = user_id!("@alice:example.org");
1127 let bob = user_id!("@bob:example.org");
1128 let carol = user_id!("@carol:example.org");
1129 let denis = user_id!("@denis:example.org");
1130 let erica = user_id!("@erica:example.org");
1131 let fred = user_id!("@fred:example.org");
1132 let me = user_id!("@me:example.org");
1133
1134 let f = EventFactory::new().room(room_id!("!test:localhost"));
1135
1136 let mut changes = StateChanges::new("".to_owned());
1137
1138 {
1141 let members = changes
1142 .state
1143 .entry(room.room_id().to_owned())
1144 .or_default()
1145 .entry(StateEventType::RoomMember)
1146 .or_default();
1147 members.insert(carol.into(), f.member(carol).display_name("Carol").into());
1148 members.insert(bob.into(), f.member(bob).display_name("Bob").into());
1149 members.insert(fred.into(), f.member(fred).display_name("Fred").into());
1150 members.insert(me.into(), f.member(me).display_name("Me").into());
1151
1152 store.save_changes(&changes).await.unwrap();
1153 }
1154
1155 {
1156 let members = changes
1157 .state
1158 .entry(room.room_id().to_owned())
1159 .or_default()
1160 .entry(StateEventType::RoomMember)
1161 .or_default();
1162 members.insert(alice.into(), f.member(alice).display_name("Alice").into());
1163 members.insert(erica.into(), f.member(erica).display_name("Erica").into());
1164 members.insert(denis.into(), f.member(denis).display_name("Denis").into());
1165 store.save_changes(&changes).await.unwrap();
1166 }
1167
1168 assert_eq!(
1169 room.compute_display_name().await.unwrap().into_inner(),
1170 RoomDisplayName::Calculated("Alice, Bob, Carol, Denis, Erica, and 2 others".to_owned())
1171 );
1172 }
1173
1174 #[async_test]
1175 async fn test_display_name_dm_alone() {
1176 let (store, room) = make_room_test_helper(RoomState::Joined);
1177 let room_id = room_id!("!test:localhost");
1178 let matthew = user_id!("@matthew:example.org");
1179 let me = user_id!("@me:example.org");
1180 let mut changes = StateChanges::new("".to_owned());
1181 let summary = assign!(RumaSummary::new(), {
1182 joined_member_count: Some(1u32.into()),
1183 heroes: vec![me.to_owned(), matthew.to_owned()],
1184 });
1185
1186 let f = EventFactory::new().room(room_id!("!test:localhost"));
1187
1188 let members = changes
1189 .state
1190 .entry(room_id.to_owned())
1191 .or_default()
1192 .entry(StateEventType::RoomMember)
1193 .or_default();
1194 members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into());
1195 members.insert(me.into(), f.member(me).display_name("Me").into());
1196
1197 store.save_changes(&changes).await.unwrap();
1198
1199 room.info.update_if(|info| info.update_from_ruma_summary(&summary));
1200 assert_eq!(
1201 room.compute_display_name().await.unwrap().into_inner(),
1202 RoomDisplayName::EmptyWas("Matthew".to_owned())
1203 );
1204 }
1205
1206 #[test]
1207 fn test_calculate_room_name() {
1208 let mut actual = compute_display_name_from_heroes(2, vec!["a"]);
1209 assert_eq!(RoomDisplayName::Calculated("a".to_owned()), actual);
1210
1211 actual = compute_display_name_from_heroes(3, vec!["a", "b"]);
1212 assert_eq!(RoomDisplayName::Calculated("a, b".to_owned()), actual);
1213
1214 actual = compute_display_name_from_heroes(4, vec!["a", "b", "c"]);
1215 assert_eq!(RoomDisplayName::Calculated("a, b, c".to_owned()), actual);
1216
1217 actual = compute_display_name_from_heroes(5, vec!["a", "b", "c"]);
1218 assert_eq!(RoomDisplayName::Calculated("a, b, c, and 2 others".to_owned()), actual);
1219
1220 actual = compute_display_name_from_heroes(5, vec![]);
1221 assert_eq!(RoomDisplayName::Calculated("5 people".to_owned()), actual);
1222
1223 actual = compute_display_name_from_heroes(0, vec![]);
1224 assert_eq!(RoomDisplayName::Empty, actual);
1225
1226 actual = compute_display_name_from_heroes(1, vec![]);
1227 assert_eq!(RoomDisplayName::Empty, actual);
1228
1229 actual = compute_display_name_from_heroes(1, vec!["a"]);
1230 assert_eq!(RoomDisplayName::EmptyWas("a".to_owned()), actual);
1231
1232 actual = compute_display_name_from_heroes(1, vec!["a", "b"]);
1233 assert_eq!(RoomDisplayName::EmptyWas("a, b".to_owned()), actual);
1234
1235 actual = compute_display_name_from_heroes(1, vec!["a", "b", "c"]);
1236 assert_eq!(RoomDisplayName::EmptyWas("a, b, c".to_owned()), actual);
1237 }
1238
1239 #[test]
1240 fn test_room_alias_from_room_display_name_lowercases() {
1241 assert_eq!(
1242 "roomalias",
1243 RoomDisplayName::Named("RoomAlias".to_owned()).to_room_alias_name()
1244 );
1245 }
1246
1247 #[test]
1248 fn test_room_alias_from_room_display_name_removes_whitespace() {
1249 assert_eq!(
1250 "room-alias",
1251 RoomDisplayName::Named("Room Alias".to_owned()).to_room_alias_name()
1252 );
1253 }
1254
1255 #[test]
1256 fn test_room_alias_from_room_display_name_removes_non_ascii_symbols() {
1257 assert_eq!(
1258 "roomalias",
1259 RoomDisplayName::Named("Room±Alias√".to_owned()).to_room_alias_name()
1260 );
1261 }
1262
1263 #[test]
1264 fn test_room_alias_from_room_display_name_removes_invalid_ascii_symbols() {
1265 assert_eq!(
1266 "roomalias",
1267 RoomDisplayName::Named("#Room,{Alias}:".to_owned()).to_room_alias_name()
1268 );
1269 }
1270}