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,
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}
382
383#[cfg(test)]
384impl RoomSummary {
385 pub(crate) fn heroes(&self) -> &[RoomHero] {
386 &self.room_heroes
387 }
388}
389
390#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
392pub struct RoomHero {
393 pub user_id: OwnedUserId,
395 pub display_name: Option<String>,
397 pub avatar_url: Option<OwnedMxcUri>,
399}
400
401const NUM_HEROES: usize = 5;
408
409#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
412pub enum RoomDisplayName {
413 Named(String),
415 Aliased(String),
417 Calculated(String),
420 EmptyWas(String),
423 Empty,
425}
426
427pub(crate) enum UpdatedRoomDisplayName {
430 New(RoomDisplayName),
431 Same(RoomDisplayName),
432}
433
434impl UpdatedRoomDisplayName {
435 pub fn into_inner(self) -> RoomDisplayName {
437 match self {
438 UpdatedRoomDisplayName::New(room_display_name) => room_display_name,
439 UpdatedRoomDisplayName::Same(room_display_name) => room_display_name,
440 }
441 }
442}
443
444const WHITESPACE_REGEX: &str = r"\s+";
445const INVALID_SYMBOLS_REGEX: &str = r"[#,:\{\}\\]+";
446
447impl RoomDisplayName {
448 pub fn to_room_alias_name(&self) -> String {
451 let room_name = match self {
452 Self::Named(name) => name,
453 Self::Aliased(name) => name,
454 Self::Calculated(name) => name,
455 Self::EmptyWas(name) => name,
456 Self::Empty => "",
457 };
458
459 let whitespace_regex =
460 Regex::new(WHITESPACE_REGEX).expect("`WHITESPACE_REGEX` should be valid");
461 let symbol_regex =
462 Regex::new(INVALID_SYMBOLS_REGEX).expect("`INVALID_SYMBOLS_REGEX` should be valid");
463
464 let sanitised = whitespace_regex.replace_all(room_name, "-");
466 let sanitised =
468 String::from_iter(sanitised.chars().filter(|c| c.is_ascii() && !c.is_ascii_control()));
469 let sanitised = symbol_regex.replace_all(&sanitised, "");
471 sanitised.to_lowercase()
473 }
474}
475
476impl fmt::Display for RoomDisplayName {
477 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
478 match self {
479 RoomDisplayName::Named(s)
480 | RoomDisplayName::Calculated(s)
481 | RoomDisplayName::Aliased(s) => {
482 write!(f, "{s}")
483 }
484 RoomDisplayName::EmptyWas(s) => write!(f, "Empty Room (was {s})"),
485 RoomDisplayName::Empty => write!(f, "Empty Room"),
486 }
487 }
488}
489
490fn compute_display_name_from_heroes(
494 num_joined_invited: u64,
495 mut heroes: Vec<&str>,
496) -> RoomDisplayName {
497 let num_heroes = heroes.len() as u64;
498 let num_joined_invited_except_self = num_joined_invited.saturating_sub(1);
499
500 heroes.sort_unstable();
502
503 let names = if num_heroes == 0 && num_joined_invited > 1 {
504 format!("{num_joined_invited} people")
505 } else if num_heroes >= num_joined_invited_except_self {
506 heroes.join(", ")
507 } else if num_heroes < num_joined_invited_except_self && num_joined_invited > 1 {
508 format!("{}, and {} others", heroes.join(", "), (num_joined_invited - num_heroes))
511 } else {
512 "".to_owned()
513 };
514
515 if num_joined_invited <= 1 {
517 if names.is_empty() { RoomDisplayName::Empty } else { RoomDisplayName::EmptyWas(names) }
518 } else {
519 RoomDisplayName::Calculated(names)
520 }
521}
522
523fn heroes_filter<'a>(
529 own_user_id: &'a UserId,
530 member_hints: &'a MemberHintsEventContent,
531) -> impl Fn(&UserId) -> bool + use<'a> {
532 move |user_id| user_id != own_user_id && !member_hints.service_members.contains(user_id)
533}
534
535#[cfg(test)]
536mod tests {
537 use std::{collections::BTreeSet, sync::Arc};
538
539 use matrix_sdk_test::{async_test, event_factory::EventFactory};
540 use ruma::{
541 UserId,
542 api::client::sync::sync_events::v3::RoomSummary as RumaSummary,
543 assign,
544 events::{
545 StateEventType,
546 room::{
547 canonical_alias::{
548 PossiblyRedactedRoomCanonicalAliasEventContent, RoomCanonicalAliasEventContent,
549 },
550 member::{MembershipState, RoomMemberEventContent, StrippedRoomMemberEvent},
551 name::{PossiblyRedactedRoomNameEventContent, RoomNameEventContent},
552 },
553 },
554 owned_room_alias_id, owned_user_id, room_alias_id, room_id,
555 serde::Raw,
556 user_id,
557 };
558 use serde_json::json;
559
560 use super::{Room, RoomDisplayName, compute_display_name_from_heroes};
561 use crate::{
562 MinimalStateEvent, RoomHero, RoomState, StateChanges, StateStore, store::MemoryStore,
563 };
564
565 fn make_room_test_helper(room_type: RoomState) -> (Arc<MemoryStore>, Room) {
566 let store = Arc::new(MemoryStore::new());
567 let user_id = user_id!("@me:example.org");
568 let room_id = room_id!("!test:localhost");
569 let (sender, _receiver) = tokio::sync::broadcast::channel(1);
570
571 (store.clone(), Room::new(user_id, store, room_id, room_type, sender))
572 }
573
574 fn make_stripped_member_event(user_id: &UserId, name: &str) -> Raw<StrippedRoomMemberEvent> {
575 let ev_json = json!({
576 "type": "m.room.member",
577 "content": assign!(RoomMemberEventContent::new(MembershipState::Join), {
578 displayname: Some(name.to_owned())
579 }),
580 "sender": user_id,
581 "state_key": user_id,
582 });
583
584 Raw::new(&ev_json).unwrap().cast_unchecked()
585 }
586
587 fn make_canonical_alias_event() -> MinimalStateEvent<RoomCanonicalAliasEventContent> {
588 MinimalStateEvent {
589 content: assign!(PossiblyRedactedRoomCanonicalAliasEventContent::new(), {
590 alias: Some(owned_room_alias_id!("#test:example.com")),
591 }),
592 event_id: None,
593 }
594 }
595
596 fn make_name_event_with(name: &str) -> MinimalStateEvent<PossiblyRedactedRoomNameEventContent> {
597 MinimalStateEvent {
598 content: RoomNameEventContent::new(name.to_owned()).into(),
599 event_id: None,
600 }
601 }
602
603 fn make_name_event() -> MinimalStateEvent<PossiblyRedactedRoomNameEventContent> {
604 make_name_event_with("Test Room")
605 }
606
607 #[async_test]
608 async fn test_display_name_for_joined_room_is_empty_if_no_info() {
609 let (_, room) = make_room_test_helper(RoomState::Joined);
610 assert_eq!(room.compute_display_name().await.unwrap().into_inner(), RoomDisplayName::Empty);
611 }
612
613 #[test]
614 fn test_display_name_compute_fields_empty() {
615 assert_eq!(
616 Room::compute_display_name_with_fields(None, None, vec![], 0),
617 RoomDisplayName::Empty
618 );
619 }
620
621 #[async_test]
622 async fn test_display_name_for_joined_room_is_empty_if_name_empty() {
623 let (_, room) = make_room_test_helper(RoomState::Joined);
624 room.info.update(|info| info.base_info.name = Some(make_name_event_with("")));
625
626 assert_eq!(room.compute_display_name().await.unwrap().into_inner(), RoomDisplayName::Empty);
627 }
628
629 #[test]
630 fn test_display_name_compute_fields_empty_if_name_empty() {
631 assert_eq!(
632 Room::compute_display_name_with_fields(Some("".to_owned()), None, vec![], 0),
633 RoomDisplayName::Empty
634 );
635 }
636
637 #[async_test]
638 async fn test_display_name_for_joined_room_uses_canonical_alias_if_available() {
639 let (_, room) = make_room_test_helper(RoomState::Joined);
640 room.info
641 .update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
642 assert_eq!(
643 room.compute_display_name().await.unwrap().into_inner(),
644 RoomDisplayName::Aliased("test".to_owned())
645 );
646 }
647
648 #[test]
649 fn test_display_name_compute_fields_alias() {
650 assert_eq!(
651 Room::compute_display_name_with_fields(
652 None,
653 Some(room_alias_id!("#test:example.com")),
654 vec![],
655 0,
656 ),
657 RoomDisplayName::Aliased("test".to_owned())
658 );
659 }
660
661 #[async_test]
662 async fn test_display_name_for_joined_room_prefers_name_over_alias() {
663 let (_, room) = make_room_test_helper(RoomState::Joined);
664 room.info
665 .update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
666 assert_eq!(
667 room.compute_display_name().await.unwrap().into_inner(),
668 RoomDisplayName::Aliased("test".to_owned())
669 );
670 room.info.update(|info| info.base_info.name = Some(make_name_event()));
671 assert_eq!(
673 room.compute_display_name().await.unwrap().into_inner(),
674 RoomDisplayName::Named("Test Room".to_owned())
675 );
676 }
677
678 #[test]
679 fn test_display_name_compute_fields_name_over_alias() {
680 assert_eq!(
681 Room::compute_display_name_with_fields(
682 Some("Test Room".to_owned()),
683 Some(room_alias_id!("#test:example.com")),
684 vec![],
685 0
686 ),
687 RoomDisplayName::Named("Test Room".to_owned())
688 );
689 }
690
691 #[async_test]
692 async fn test_display_name_for_invited_room_is_empty_if_no_info() {
693 let (_, room) = make_room_test_helper(RoomState::Invited);
694 assert_eq!(room.compute_display_name().await.unwrap().into_inner(), RoomDisplayName::Empty);
695 }
696
697 #[async_test]
698 async fn test_display_name_for_invited_room_is_empty_if_room_name_empty() {
699 let (_, room) = make_room_test_helper(RoomState::Invited);
700
701 let room_name = make_name_event_with("");
702 room.info.update(|info| info.base_info.name = Some(room_name));
703
704 assert_eq!(room.compute_display_name().await.unwrap().into_inner(), RoomDisplayName::Empty);
705 }
706
707 #[async_test]
708 async fn test_display_name_for_invited_room_uses_canonical_alias_if_available() {
709 let (_, room) = make_room_test_helper(RoomState::Invited);
710 room.info
711 .update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
712 assert_eq!(
713 room.compute_display_name().await.unwrap().into_inner(),
714 RoomDisplayName::Aliased("test".to_owned())
715 );
716 }
717
718 #[async_test]
719 async fn test_display_name_for_invited_room_prefers_name_over_alias() {
720 let (_, room) = make_room_test_helper(RoomState::Invited);
721 room.info
722 .update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
723 assert_eq!(
724 room.compute_display_name().await.unwrap().into_inner(),
725 RoomDisplayName::Aliased("test".to_owned())
726 );
727 room.info.update(|info| info.base_info.name = Some(make_name_event()));
728 assert_eq!(
730 room.compute_display_name().await.unwrap().into_inner(),
731 RoomDisplayName::Named("Test Room".to_owned())
732 );
733 }
734
735 #[async_test]
736 async fn test_display_name_dm_invited() {
737 let (store, room) = make_room_test_helper(RoomState::Invited);
738 let room_id = room_id!("!test:localhost");
739 let matthew = user_id!("@matthew:example.org");
740 let me = user_id!("@me:example.org");
741 let mut changes = StateChanges::new("".to_owned());
742 let summary = assign!(RumaSummary::new(), {
743 heroes: vec![me.to_owned(), matthew.to_owned()],
744 });
745
746 changes.add_stripped_member(
747 room_id,
748 matthew,
749 make_stripped_member_event(matthew, "Matthew"),
750 );
751 changes.add_stripped_member(room_id, me, make_stripped_member_event(me, "Me"));
752 store.save_changes(&changes).await.unwrap();
753
754 room.info.update_if(|info| info.update_from_ruma_summary(&summary));
755 assert_eq!(
756 room.compute_display_name().await.unwrap().into_inner(),
757 RoomDisplayName::Calculated("Matthew".to_owned())
758 );
759 }
760
761 #[async_test]
762 async fn test_display_name_dm_invited_no_heroes() {
763 let (store, room) = make_room_test_helper(RoomState::Invited);
764 let room_id = room_id!("!test:localhost");
765 let matthew = user_id!("@matthew:example.org");
766 let me = user_id!("@me:example.org");
767 let mut changes = StateChanges::new("".to_owned());
768
769 changes.add_stripped_member(
770 room_id,
771 matthew,
772 make_stripped_member_event(matthew, "Matthew"),
773 );
774 changes.add_stripped_member(room_id, me, make_stripped_member_event(me, "Me"));
775 store.save_changes(&changes).await.unwrap();
776
777 assert_eq!(
778 room.compute_display_name().await.unwrap().into_inner(),
779 RoomDisplayName::Calculated("Matthew".to_owned())
780 );
781 }
782
783 #[async_test]
784 async fn test_display_name_dm_joined() {
785 let (store, room) = make_room_test_helper(RoomState::Joined);
786 let room_id = room_id!("!test:localhost");
787 let matthew = user_id!("@matthew:example.org");
788 let me = user_id!("@me:example.org");
789
790 let mut changes = StateChanges::new("".to_owned());
791 let summary = assign!(RumaSummary::new(), {
792 joined_member_count: Some(2u32.into()),
793 heroes: vec![me.to_owned(), matthew.to_owned()],
794 });
795
796 let f = EventFactory::new().room(room_id!("!test:localhost"));
797
798 let members = changes
799 .state
800 .entry(room_id.to_owned())
801 .or_default()
802 .entry(StateEventType::RoomMember)
803 .or_default();
804 members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into());
805 members.insert(me.into(), f.member(me).display_name("Me").into());
806
807 store.save_changes(&changes).await.unwrap();
808
809 room.info.update_if(|info| info.update_from_ruma_summary(&summary));
810 assert_eq!(
811 room.compute_display_name().await.unwrap().into_inner(),
812 RoomDisplayName::Calculated("Matthew".to_owned())
813 );
814 }
815
816 #[async_test]
817 async fn test_display_name_dm_joined_service_members() {
818 let (store, room) = make_room_test_helper(RoomState::Joined);
819 let room_id = room_id!("!test:localhost");
820
821 let matthew = user_id!("@sahasrhala:example.org");
822 let me = user_id!("@me:example.org");
823 let bot = user_id!("@bot:example.org");
824
825 let mut changes = StateChanges::new("".to_owned());
826 let summary = assign!(RumaSummary::new(), {
827 joined_member_count: Some(3u32.into()),
828 heroes: vec![me.to_owned(), matthew.to_owned(), bot.to_owned()],
829 });
830
831 let f = EventFactory::new().room(room_id!("!test:localhost"));
832
833 let members = changes
834 .state
835 .entry(room_id.to_owned())
836 .or_default()
837 .entry(StateEventType::RoomMember)
838 .or_default();
839 members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into());
840 members.insert(me.into(), f.member(me).display_name("Me").into());
841 members.insert(bot.into(), f.member(bot).display_name("Bot").into());
842
843 let member_hints_content =
844 f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into();
845 changes
846 .state
847 .entry(room_id.to_owned())
848 .or_default()
849 .entry(StateEventType::MemberHints)
850 .or_default()
851 .insert("".to_owned(), member_hints_content);
852
853 store.save_changes(&changes).await.unwrap();
854
855 room.info.update_if(|info| info.update_from_ruma_summary(&summary));
856 assert_eq!(
858 room.compute_display_name().await.unwrap().into_inner(),
859 RoomDisplayName::Calculated("Matthew".to_owned())
860 );
861 }
862
863 #[async_test]
864 async fn test_display_name_dm_joined_alone_with_service_members() {
865 let (store, room) = make_room_test_helper(RoomState::Joined);
866 let room_id = room_id!("!test:localhost");
867
868 let me = user_id!("@me:example.org");
869 let bot = user_id!("@bot:example.org");
870
871 let mut changes = StateChanges::new("".to_owned());
872 let summary = assign!(RumaSummary::new(), {
873 joined_member_count: Some(2u32.into()),
874 heroes: vec![me.to_owned(), bot.to_owned()],
875 });
876
877 let f = EventFactory::new().room(room_id!("!test:localhost"));
878
879 let members = changes
880 .state
881 .entry(room_id.to_owned())
882 .or_default()
883 .entry(StateEventType::RoomMember)
884 .or_default();
885 members.insert(me.into(), f.member(me).display_name("Me").into());
886 members.insert(bot.into(), f.member(bot).display_name("Bot").into());
887
888 let member_hints_content =
889 f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into();
890 changes
891 .state
892 .entry(room_id.to_owned())
893 .or_default()
894 .entry(StateEventType::MemberHints)
895 .or_default()
896 .insert("".to_owned(), member_hints_content);
897
898 store.save_changes(&changes).await.unwrap();
899
900 room.info.update_if(|info| info.update_from_ruma_summary(&summary));
901 assert_eq!(room.compute_display_name().await.unwrap().into_inner(), RoomDisplayName::Empty);
903 }
904
905 #[async_test]
906 async fn test_display_name_dm_joined_no_heroes() {
907 let (store, room) = make_room_test_helper(RoomState::Joined);
908 let room_id = room_id!("!test:localhost");
909 let matthew = user_id!("@matthew:example.org");
910 let me = user_id!("@me:example.org");
911 let mut changes = StateChanges::new("".to_owned());
912
913 let f = EventFactory::new().room(room_id!("!test:localhost"));
914
915 let members = changes
916 .state
917 .entry(room_id.to_owned())
918 .or_default()
919 .entry(StateEventType::RoomMember)
920 .or_default();
921 members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into());
922 members.insert(me.into(), f.member(me).display_name("Me").into());
923
924 store.save_changes(&changes).await.unwrap();
925
926 assert_eq!(
927 room.compute_display_name().await.unwrap().into_inner(),
928 RoomDisplayName::Calculated("Matthew".to_owned())
929 );
930 }
931
932 #[async_test]
933 async fn test_display_name_dm_joined_no_heroes_service_members() {
934 let (store, room) = make_room_test_helper(RoomState::Joined);
935 let room_id = room_id!("!test:localhost");
936
937 let matthew = user_id!("@matthew:example.org");
938 let me = user_id!("@me:example.org");
939 let bot = user_id!("@bot:example.org");
940
941 let mut changes = StateChanges::new("".to_owned());
942
943 let f = EventFactory::new().room(room_id!("!test:localhost"));
944
945 let members = changes
946 .state
947 .entry(room_id.to_owned())
948 .or_default()
949 .entry(StateEventType::RoomMember)
950 .or_default();
951 members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into());
952 members.insert(me.into(), f.member(me).display_name("Me").into());
953 members.insert(bot.into(), f.member(bot).display_name("Bot").into());
954
955 let member_hints_content =
956 f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into();
957 changes
958 .state
959 .entry(room_id.to_owned())
960 .or_default()
961 .entry(StateEventType::MemberHints)
962 .or_default()
963 .insert("".to_owned(), member_hints_content);
964
965 store.save_changes(&changes).await.unwrap();
966
967 assert_eq!(
968 room.compute_display_name().await.unwrap().into_inner(),
969 RoomDisplayName::Calculated("Matthew".to_owned())
970 );
971 }
972
973 #[async_test]
974 async fn test_display_name_deterministic() {
975 let (store, room) = make_room_test_helper(RoomState::Joined);
976
977 let alice = user_id!("@alice:example.org");
978 let bob = user_id!("@bob:example.org");
979 let carol = user_id!("@carol:example.org");
980 let denis = user_id!("@denis:example.org");
981 let erica = user_id!("@erica:example.org");
982 let fred = user_id!("@fred:example.org");
983 let me = user_id!("@me:example.org");
984
985 let mut changes = StateChanges::new("".to_owned());
986
987 let f = EventFactory::new().room(room_id!("!test:localhost"));
988
989 {
992 let members = changes
993 .state
994 .entry(room.room_id().to_owned())
995 .or_default()
996 .entry(StateEventType::RoomMember)
997 .or_default();
998 members.insert(carol.into(), f.member(carol).display_name("Carol").into());
999 members.insert(bob.into(), f.member(bob).display_name("Bob").into());
1000 members.insert(fred.into(), f.member(fred).display_name("Fred").into());
1001 members.insert(me.into(), f.member(me).display_name("Me").into());
1002 store.save_changes(&changes).await.unwrap();
1003 }
1004
1005 {
1006 let members = changes
1007 .state
1008 .entry(room.room_id().to_owned())
1009 .or_default()
1010 .entry(StateEventType::RoomMember)
1011 .or_default();
1012 members.insert(alice.into(), f.member(alice).display_name("Alice").into());
1013 members.insert(erica.into(), f.member(erica).display_name("Erica").into());
1014 members.insert(denis.into(), f.member(denis).display_name("Denis").into());
1015 store.save_changes(&changes).await.unwrap();
1016 }
1017
1018 let summary = assign!(RumaSummary::new(), {
1019 joined_member_count: Some(7u32.into()),
1020 heroes: vec![denis.to_owned(), carol.to_owned(), bob.to_owned(), erica.to_owned()],
1021 });
1022 room.info.update_if(|info| info.update_from_ruma_summary(&summary));
1023
1024 assert_eq!(
1025 room.compute_display_name().await.unwrap().into_inner(),
1026 RoomDisplayName::Calculated("Bob, Carol, Denis, Erica, and 3 others".to_owned())
1027 );
1028 }
1029
1030 #[test]
1031 fn test_display_name_compute_fields_name_deterministic() {
1032 assert_eq!(
1033 Room::compute_display_name_with_fields(
1034 None,
1035 None,
1036 vec![
1037 RoomHero {
1038 user_id: owned_user_id!("@alice:example.org"),
1039 display_name: Some("Alice".to_owned()),
1040 avatar_url: None,
1041 },
1042 RoomHero {
1043 user_id: owned_user_id!("@bob:example.org"),
1044 display_name: Some("Bob".to_owned()),
1045 avatar_url: None,
1046 },
1047 RoomHero {
1048 user_id: owned_user_id!("@carol:example.org"),
1049 display_name: Some("Carol".to_owned()),
1050 avatar_url: None,
1051 },
1052 RoomHero {
1053 user_id: owned_user_id!("@denis:example.org"),
1054 display_name: Some("Denis".to_owned()),
1055 avatar_url: None,
1056 },
1057 RoomHero {
1058 user_id: owned_user_id!("@erica:example.org"),
1059 display_name: Some("Erica".to_owned()),
1060 avatar_url: None,
1061 },
1062 ],
1063 1234,
1064 ),
1065 RoomDisplayName::Calculated(
1066 "Alice, Bob, Carol, Denis, Erica, and 1229 others".to_owned()
1067 )
1068 );
1069 }
1070
1071 #[async_test]
1072 async fn test_display_name_deterministic_no_heroes() {
1073 let (store, room) = make_room_test_helper(RoomState::Joined);
1074
1075 let alice = user_id!("@alice:example.org");
1076 let bob = user_id!("@bob:example.org");
1077 let carol = user_id!("@carol:example.org");
1078 let denis = user_id!("@denis:example.org");
1079 let erica = user_id!("@erica:example.org");
1080 let fred = user_id!("@fred:example.org");
1081 let me = user_id!("@me:example.org");
1082
1083 let f = EventFactory::new().room(room_id!("!test:localhost"));
1084
1085 let mut changes = StateChanges::new("".to_owned());
1086
1087 {
1090 let members = changes
1091 .state
1092 .entry(room.room_id().to_owned())
1093 .or_default()
1094 .entry(StateEventType::RoomMember)
1095 .or_default();
1096 members.insert(carol.into(), f.member(carol).display_name("Carol").into());
1097 members.insert(bob.into(), f.member(bob).display_name("Bob").into());
1098 members.insert(fred.into(), f.member(fred).display_name("Fred").into());
1099 members.insert(me.into(), f.member(me).display_name("Me").into());
1100
1101 store.save_changes(&changes).await.unwrap();
1102 }
1103
1104 {
1105 let members = changes
1106 .state
1107 .entry(room.room_id().to_owned())
1108 .or_default()
1109 .entry(StateEventType::RoomMember)
1110 .or_default();
1111 members.insert(alice.into(), f.member(alice).display_name("Alice").into());
1112 members.insert(erica.into(), f.member(erica).display_name("Erica").into());
1113 members.insert(denis.into(), f.member(denis).display_name("Denis").into());
1114 store.save_changes(&changes).await.unwrap();
1115 }
1116
1117 assert_eq!(
1118 room.compute_display_name().await.unwrap().into_inner(),
1119 RoomDisplayName::Calculated("Alice, Bob, Carol, Denis, Erica, and 2 others".to_owned())
1120 );
1121 }
1122
1123 #[async_test]
1124 async fn test_display_name_dm_alone() {
1125 let (store, room) = make_room_test_helper(RoomState::Joined);
1126 let room_id = room_id!("!test:localhost");
1127 let matthew = user_id!("@matthew:example.org");
1128 let me = user_id!("@me:example.org");
1129 let mut changes = StateChanges::new("".to_owned());
1130 let summary = assign!(RumaSummary::new(), {
1131 joined_member_count: Some(1u32.into()),
1132 heroes: vec![me.to_owned(), matthew.to_owned()],
1133 });
1134
1135 let f = EventFactory::new().room(room_id!("!test:localhost"));
1136
1137 let members = changes
1138 .state
1139 .entry(room_id.to_owned())
1140 .or_default()
1141 .entry(StateEventType::RoomMember)
1142 .or_default();
1143 members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into());
1144 members.insert(me.into(), f.member(me).display_name("Me").into());
1145
1146 store.save_changes(&changes).await.unwrap();
1147
1148 room.info.update_if(|info| info.update_from_ruma_summary(&summary));
1149 assert_eq!(
1150 room.compute_display_name().await.unwrap().into_inner(),
1151 RoomDisplayName::EmptyWas("Matthew".to_owned())
1152 );
1153 }
1154
1155 #[test]
1156 fn test_calculate_room_name() {
1157 let mut actual = compute_display_name_from_heroes(2, vec!["a"]);
1158 assert_eq!(RoomDisplayName::Calculated("a".to_owned()), actual);
1159
1160 actual = compute_display_name_from_heroes(3, vec!["a", "b"]);
1161 assert_eq!(RoomDisplayName::Calculated("a, b".to_owned()), actual);
1162
1163 actual = compute_display_name_from_heroes(4, vec!["a", "b", "c"]);
1164 assert_eq!(RoomDisplayName::Calculated("a, b, c".to_owned()), actual);
1165
1166 actual = compute_display_name_from_heroes(5, vec!["a", "b", "c"]);
1167 assert_eq!(RoomDisplayName::Calculated("a, b, c, and 2 others".to_owned()), actual);
1168
1169 actual = compute_display_name_from_heroes(5, vec![]);
1170 assert_eq!(RoomDisplayName::Calculated("5 people".to_owned()), actual);
1171
1172 actual = compute_display_name_from_heroes(0, vec![]);
1173 assert_eq!(RoomDisplayName::Empty, actual);
1174
1175 actual = compute_display_name_from_heroes(1, vec![]);
1176 assert_eq!(RoomDisplayName::Empty, actual);
1177
1178 actual = compute_display_name_from_heroes(1, vec!["a"]);
1179 assert_eq!(RoomDisplayName::EmptyWas("a".to_owned()), actual);
1180
1181 actual = compute_display_name_from_heroes(1, vec!["a", "b"]);
1182 assert_eq!(RoomDisplayName::EmptyWas("a, b".to_owned()), actual);
1183
1184 actual = compute_display_name_from_heroes(1, vec!["a", "b", "c"]);
1185 assert_eq!(RoomDisplayName::EmptyWas("a, b, c".to_owned()), actual);
1186 }
1187
1188 #[test]
1189 fn test_room_alias_from_room_display_name_lowercases() {
1190 assert_eq!(
1191 "roomalias",
1192 RoomDisplayName::Named("RoomAlias".to_owned()).to_room_alias_name()
1193 );
1194 }
1195
1196 #[test]
1197 fn test_room_alias_from_room_display_name_removes_whitespace() {
1198 assert_eq!(
1199 "room-alias",
1200 RoomDisplayName::Named("Room Alias".to_owned()).to_room_alias_name()
1201 );
1202 }
1203
1204 #[test]
1205 fn test_room_alias_from_room_display_name_removes_non_ascii_symbols() {
1206 assert_eq!(
1207 "roomalias",
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_invalid_ascii_symbols() {
1214 assert_eq!(
1215 "roomalias",
1216 RoomDisplayName::Named("#Room,{Alias}:".to_owned()).to_room_alias_name()
1217 );
1218 }
1219}