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