1use std::fmt;
16
17use as_variant::as_variant;
18use regex::Regex;
19use ruma::{
20 OwnedMxcUri, OwnedUserId, 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.inner.read().cached_display_name.clone()
61 }
62
63 pub(crate) async fn compute_display_name(&self) -> StoreResult<UpdatedRoomDisplayName> {
75 enum DisplayNameOrSummary {
76 Summary(RoomSummary),
77 DisplayName(RoomDisplayName),
78 }
79
80 let display_name_or_summary = {
81 let inner = self.inner.read();
82
83 match (inner.name(), inner.canonical_alias()) {
84 (Some(name), _) => {
85 let name = RoomDisplayName::Named(name.trim().to_owned());
86 DisplayNameOrSummary::DisplayName(name)
87 }
88 (None, Some(alias)) => {
89 let name = RoomDisplayName::Aliased(alias.alias().trim().to_owned());
90 DisplayNameOrSummary::DisplayName(name)
91 }
92 (None, None) => DisplayNameOrSummary::Summary(inner.summary.clone()),
97 }
98 };
99
100 let display_name = match display_name_or_summary {
101 DisplayNameOrSummary::Summary(summary) => {
102 self.compute_display_name_from_summary(summary).await?
103 }
104 DisplayNameOrSummary::DisplayName(display_name) => display_name,
105 };
106
107 let mut updated = false;
109
110 self.inner.update_if(|info| {
111 if info.cached_display_name.as_ref() != Some(&display_name) {
112 info.cached_display_name = Some(display_name.clone());
113 updated = true;
114
115 true
116 } else {
117 false
118 }
119 });
120
121 Ok(if updated {
122 UpdatedRoomDisplayName::New(display_name)
123 } else {
124 UpdatedRoomDisplayName::Same(display_name)
125 })
126 }
127
128 async fn compute_display_name_from_summary(
130 &self,
131 summary: RoomSummary,
132 ) -> StoreResult<RoomDisplayName> {
133 let computed_summary = if !summary.room_heroes.is_empty() {
134 self.extract_and_augment_summary(&summary).await?
135 } else {
136 self.compute_summary().await?
137 };
138
139 let ComputedSummary { heroes, num_service_members, num_joined_invited_guess } =
140 computed_summary;
141
142 let summary_member_count = (summary.joined_member_count + summary.invited_member_count)
143 .saturating_sub(num_service_members);
144
145 let num_joined_invited = if self.state() == RoomState::Invited {
146 heroes.len() as u64 + 1
149 } else if summary_member_count == 0 {
150 num_joined_invited_guess
151 } else {
152 summary_member_count
153 };
154
155 debug!(
156 room_id = ?self.room_id(),
157 own_user = ?self.own_user_id,
158 num_joined_invited,
159 heroes = ?heroes,
160 "Calculating name for a room based on heroes",
161 );
162
163 let display_name = compute_display_name_from_heroes(
164 num_joined_invited,
165 heroes.iter().map(|hero| hero.as_str()).collect(),
166 );
167
168 Ok(display_name)
169 }
170
171 async fn extract_and_augment_summary(
180 &self,
181 summary: &RoomSummary,
182 ) -> StoreResult<ComputedSummary> {
183 let heroes = &summary.room_heroes;
184
185 let mut names = Vec::with_capacity(heroes.len());
186 let own_user_id = self.own_user_id();
187 let member_hints = self.get_member_hints().await?;
188
189 let num_service_members = heroes
194 .iter()
195 .filter(|hero| member_hints.service_members.contains(&hero.user_id))
196 .count() as u64;
197
198 let heroes_filter = heroes_filter(own_user_id, &member_hints);
201 let heroes_filter = |hero: &&RoomHero| heroes_filter(&hero.user_id);
202
203 for hero in heroes.iter().filter(heroes_filter) {
204 if let Some(display_name) = &hero.display_name {
205 names.push(display_name.clone());
206 } else {
207 match self.get_member(&hero.user_id).await {
208 Ok(Some(member)) => {
209 names.push(member.name().to_owned());
210 }
211 Ok(None) => {
212 warn!(user_id = ?hero.user_id, "Ignoring hero, no member info");
213 }
214 Err(error) => {
215 warn!("Ignoring hero, error getting member: {error}");
216 }
217 }
218 }
219 }
220
221 let num_joined_invited_guess = summary.joined_member_count + summary.invited_member_count;
222
223 let num_joined_invited_guess = if num_joined_invited_guess == 0 {
226 let guess = self
227 .store
228 .get_user_ids(self.room_id(), RoomMemberships::JOIN | RoomMemberships::INVITE)
229 .await?
230 .len() as u64;
231
232 guess.saturating_sub(num_service_members)
233 } else {
234 num_joined_invited_guess
236 };
237
238 Ok(ComputedSummary { heroes: names, num_service_members, num_joined_invited_guess })
239 }
240
241 async fn compute_summary(&self) -> StoreResult<ComputedSummary> {
247 let member_hints = self.get_member_hints().await?;
248
249 let heroes_filter = heroes_filter(&self.own_user_id, &member_hints);
252 let heroes_filter = |u: &RoomMember| heroes_filter(u.user_id());
253
254 let mut members = self.members(RoomMemberships::JOIN | RoomMemberships::INVITE).await?;
255
256 let num_service_members = members
260 .iter()
261 .filter(|member| member_hints.service_members.contains(member.user_id()))
262 .count();
263
264 let num_joined_invited = members.len() - num_service_members;
271
272 if num_joined_invited == 0
273 || (num_joined_invited == 1 && members[0].user_id() == self.own_user_id)
274 {
275 members = self.members(RoomMemberships::LEAVE | RoomMemberships::BAN).await?;
277 }
278
279 members.sort_unstable_by(|lhs, rhs| lhs.name().cmp(rhs.name()));
281
282 let heroes = members
283 .into_iter()
284 .filter(heroes_filter)
285 .take(NUM_HEROES)
286 .map(|u| u.name().to_owned())
287 .collect();
288
289 trace!(
290 ?heroes,
291 num_joined_invited,
292 num_service_members,
293 "Computed a room summary since we didn't receive one."
294 );
295
296 let num_service_members = num_service_members as u64;
297 let num_joined_invited_guess = num_joined_invited as u64;
298
299 Ok(ComputedSummary { heroes, num_service_members, num_joined_invited_guess })
300 }
301
302 async fn get_member_hints(&self) -> StoreResult<MemberHintsEventContent> {
303 Ok(self
304 .store
305 .get_state_event_static::<MemberHintsEventContent>(self.room_id())
306 .await?
307 .and_then(|event| {
308 event
309 .deserialize()
310 .inspect_err(|e| warn!("Couldn't deserialize the member hints event: {e}"))
311 .ok()
312 })
313 .and_then(|event| as_variant!(event, SyncOrStrippedState::Sync(SyncStateEvent::Original(e)) => e.content))
314 .unwrap_or_default())
315 }
316}
317
318struct ComputedSummary {
325 heroes: Vec<String>,
328 num_service_members: u64,
330 num_joined_invited_guess: u64,
333}
334
335#[derive(Clone, Debug, Default, Serialize, Deserialize)]
338pub(crate) struct RoomSummary {
339 #[serde(default, skip_serializing_if = "Vec::is_empty")]
347 pub room_heroes: Vec<RoomHero>,
348 pub joined_member_count: u64,
350 pub invited_member_count: u64,
352}
353
354#[cfg(test)]
355impl RoomSummary {
356 pub(crate) fn heroes(&self) -> &[RoomHero] {
357 &self.room_heroes
358 }
359}
360
361#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
363pub struct RoomHero {
364 pub user_id: OwnedUserId,
366 pub display_name: Option<String>,
368 pub avatar_url: Option<OwnedMxcUri>,
370}
371
372const NUM_HEROES: usize = 5;
379
380#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
383pub enum RoomDisplayName {
384 Named(String),
386 Aliased(String),
388 Calculated(String),
391 EmptyWas(String),
394 Empty,
396}
397
398pub(crate) enum UpdatedRoomDisplayName {
401 New(RoomDisplayName),
402 Same(RoomDisplayName),
403}
404
405impl UpdatedRoomDisplayName {
406 pub fn into_inner(self) -> RoomDisplayName {
408 match self {
409 UpdatedRoomDisplayName::New(room_display_name) => room_display_name,
410 UpdatedRoomDisplayName::Same(room_display_name) => room_display_name,
411 }
412 }
413}
414
415const WHITESPACE_REGEX: &str = r"\s+";
416const INVALID_SYMBOLS_REGEX: &str = r"[#,:\{\}\\]+";
417
418impl RoomDisplayName {
419 pub fn to_room_alias_name(&self) -> String {
422 let room_name = match self {
423 Self::Named(name) => name,
424 Self::Aliased(name) => name,
425 Self::Calculated(name) => name,
426 Self::EmptyWas(name) => name,
427 Self::Empty => "",
428 };
429
430 let whitespace_regex =
431 Regex::new(WHITESPACE_REGEX).expect("`WHITESPACE_REGEX` should be valid");
432 let symbol_regex =
433 Regex::new(INVALID_SYMBOLS_REGEX).expect("`INVALID_SYMBOLS_REGEX` should be valid");
434
435 let sanitised = whitespace_regex.replace_all(room_name, "-");
437 let sanitised =
439 String::from_iter(sanitised.chars().filter(|c| c.is_ascii() && !c.is_ascii_control()));
440 let sanitised = symbol_regex.replace_all(&sanitised, "");
442 sanitised.to_lowercase()
444 }
445}
446
447impl fmt::Display for RoomDisplayName {
448 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
449 match self {
450 RoomDisplayName::Named(s)
451 | RoomDisplayName::Calculated(s)
452 | RoomDisplayName::Aliased(s) => {
453 write!(f, "{s}")
454 }
455 RoomDisplayName::EmptyWas(s) => write!(f, "Empty Room (was {s})"),
456 RoomDisplayName::Empty => write!(f, "Empty Room"),
457 }
458 }
459}
460
461fn compute_display_name_from_heroes(
465 num_joined_invited: u64,
466 mut heroes: Vec<&str>,
467) -> RoomDisplayName {
468 let num_heroes = heroes.len() as u64;
469 let num_joined_invited_except_self = num_joined_invited.saturating_sub(1);
470
471 heroes.sort_unstable();
473
474 let names = if num_heroes == 0 && num_joined_invited > 1 {
475 format!("{num_joined_invited} people")
476 } else if num_heroes >= num_joined_invited_except_self {
477 heroes.join(", ")
478 } else if num_heroes < num_joined_invited_except_self && num_joined_invited > 1 {
479 format!("{}, and {} others", heroes.join(", "), (num_joined_invited - num_heroes))
482 } else {
483 "".to_owned()
484 };
485
486 if num_joined_invited <= 1 {
488 if names.is_empty() { RoomDisplayName::Empty } else { RoomDisplayName::EmptyWas(names) }
489 } else {
490 RoomDisplayName::Calculated(names)
491 }
492}
493
494fn heroes_filter<'a>(
500 own_user_id: &'a UserId,
501 member_hints: &'a MemberHintsEventContent,
502) -> impl Fn(&UserId) -> bool + use<'a> {
503 move |user_id| user_id != own_user_id && !member_hints.service_members.contains(user_id)
504}
505
506#[cfg(test)]
507mod tests {
508 use std::{collections::BTreeSet, sync::Arc};
509
510 use matrix_sdk_test::{async_test, event_factory::EventFactory};
511 use ruma::{
512 UserId,
513 api::client::sync::sync_events::v3::RoomSummary as RumaSummary,
514 assign,
515 events::{
516 StateEventType,
517 room::{
518 canonical_alias::RoomCanonicalAliasEventContent,
519 member::{MembershipState, RoomMemberEventContent, StrippedRoomMemberEvent},
520 name::RoomNameEventContent,
521 },
522 },
523 room_alias_id, room_id,
524 serde::Raw,
525 user_id,
526 };
527 use serde_json::json;
528
529 use super::{Room, RoomDisplayName, compute_display_name_from_heroes};
530 use crate::{
531 MinimalStateEvent, OriginalMinimalStateEvent, RoomState, StateChanges, StateStore,
532 store::MemoryStore,
533 };
534
535 fn make_room_test_helper(room_type: RoomState) -> (Arc<MemoryStore>, Room) {
536 let store = Arc::new(MemoryStore::new());
537 let user_id = user_id!("@me:example.org");
538 let room_id = room_id!("!test:localhost");
539 let (sender, _receiver) = tokio::sync::broadcast::channel(1);
540
541 (store.clone(), Room::new(user_id, store, room_id, room_type, sender))
542 }
543
544 fn make_stripped_member_event(user_id: &UserId, name: &str) -> Raw<StrippedRoomMemberEvent> {
545 let ev_json = json!({
546 "type": "m.room.member",
547 "content": assign!(RoomMemberEventContent::new(MembershipState::Join), {
548 displayname: Some(name.to_owned())
549 }),
550 "sender": user_id,
551 "state_key": user_id,
552 });
553
554 Raw::new(&ev_json).unwrap().cast_unchecked()
555 }
556
557 fn make_canonical_alias_event() -> MinimalStateEvent<RoomCanonicalAliasEventContent> {
558 MinimalStateEvent::Original(OriginalMinimalStateEvent {
559 content: assign!(RoomCanonicalAliasEventContent::new(), {
560 alias: Some(room_alias_id!("#test:example.com").to_owned()),
561 }),
562 event_id: None,
563 })
564 }
565
566 fn make_name_event() -> MinimalStateEvent<RoomNameEventContent> {
567 MinimalStateEvent::Original(OriginalMinimalStateEvent {
568 content: RoomNameEventContent::new("Test Room".to_owned()),
569 event_id: None,
570 })
571 }
572
573 #[async_test]
574 async fn test_display_name_for_joined_room_is_empty_if_no_info() {
575 let (_, room) = make_room_test_helper(RoomState::Joined);
576 assert_eq!(room.compute_display_name().await.unwrap().into_inner(), RoomDisplayName::Empty);
577 }
578
579 #[async_test]
580 async fn test_display_name_for_joined_room_uses_canonical_alias_if_available() {
581 let (_, room) = make_room_test_helper(RoomState::Joined);
582 room.inner
583 .update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
584 assert_eq!(
585 room.compute_display_name().await.unwrap().into_inner(),
586 RoomDisplayName::Aliased("test".to_owned())
587 );
588 }
589
590 #[async_test]
591 async fn test_display_name_for_joined_room_prefers_name_over_alias() {
592 let (_, room) = make_room_test_helper(RoomState::Joined);
593 room.inner
594 .update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
595 assert_eq!(
596 room.compute_display_name().await.unwrap().into_inner(),
597 RoomDisplayName::Aliased("test".to_owned())
598 );
599 room.inner.update(|info| info.base_info.name = Some(make_name_event()));
600 assert_eq!(
602 room.compute_display_name().await.unwrap().into_inner(),
603 RoomDisplayName::Named("Test Room".to_owned())
604 );
605 }
606
607 #[async_test]
608 async fn test_display_name_for_invited_room_is_empty_if_no_info() {
609 let (_, room) = make_room_test_helper(RoomState::Invited);
610 assert_eq!(room.compute_display_name().await.unwrap().into_inner(), RoomDisplayName::Empty);
611 }
612
613 #[async_test]
614 async fn test_display_name_for_invited_room_is_empty_if_room_name_empty() {
615 let (_, room) = make_room_test_helper(RoomState::Invited);
616
617 let room_name = MinimalStateEvent::Original(OriginalMinimalStateEvent {
618 content: RoomNameEventContent::new(String::new()),
619 event_id: None,
620 });
621 room.inner.update(|info| info.base_info.name = Some(room_name));
622
623 assert_eq!(room.compute_display_name().await.unwrap().into_inner(), RoomDisplayName::Empty);
624 }
625
626 #[async_test]
627 async fn test_display_name_for_invited_room_uses_canonical_alias_if_available() {
628 let (_, room) = make_room_test_helper(RoomState::Invited);
629 room.inner
630 .update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
631 assert_eq!(
632 room.compute_display_name().await.unwrap().into_inner(),
633 RoomDisplayName::Aliased("test".to_owned())
634 );
635 }
636
637 #[async_test]
638 async fn test_display_name_for_invited_room_prefers_name_over_alias() {
639 let (_, room) = make_room_test_helper(RoomState::Invited);
640 room.inner
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 room.inner.update(|info| info.base_info.name = Some(make_name_event()));
647 assert_eq!(
649 room.compute_display_name().await.unwrap().into_inner(),
650 RoomDisplayName::Named("Test Room".to_owned())
651 );
652 }
653
654 #[async_test]
655 async fn test_display_name_dm_invited() {
656 let (store, room) = make_room_test_helper(RoomState::Invited);
657 let room_id = room_id!("!test:localhost");
658 let matthew = user_id!("@matthew:example.org");
659 let me = user_id!("@me:example.org");
660 let mut changes = StateChanges::new("".to_owned());
661 let summary = assign!(RumaSummary::new(), {
662 heroes: vec![me.to_owned(), matthew.to_owned()],
663 });
664
665 changes.add_stripped_member(
666 room_id,
667 matthew,
668 make_stripped_member_event(matthew, "Matthew"),
669 );
670 changes.add_stripped_member(room_id, me, make_stripped_member_event(me, "Me"));
671 store.save_changes(&changes).await.unwrap();
672
673 room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
674 assert_eq!(
675 room.compute_display_name().await.unwrap().into_inner(),
676 RoomDisplayName::Calculated("Matthew".to_owned())
677 );
678 }
679
680 #[async_test]
681 async fn test_display_name_dm_invited_no_heroes() {
682 let (store, room) = make_room_test_helper(RoomState::Invited);
683 let room_id = room_id!("!test:localhost");
684 let matthew = user_id!("@matthew:example.org");
685 let me = user_id!("@me:example.org");
686 let mut changes = StateChanges::new("".to_owned());
687
688 changes.add_stripped_member(
689 room_id,
690 matthew,
691 make_stripped_member_event(matthew, "Matthew"),
692 );
693 changes.add_stripped_member(room_id, me, make_stripped_member_event(me, "Me"));
694 store.save_changes(&changes).await.unwrap();
695
696 assert_eq!(
697 room.compute_display_name().await.unwrap().into_inner(),
698 RoomDisplayName::Calculated("Matthew".to_owned())
699 );
700 }
701
702 #[async_test]
703 async fn test_display_name_dm_joined() {
704 let (store, room) = make_room_test_helper(RoomState::Joined);
705 let room_id = room_id!("!test:localhost");
706 let matthew = user_id!("@matthew:example.org");
707 let me = user_id!("@me:example.org");
708
709 let mut changes = StateChanges::new("".to_owned());
710 let summary = assign!(RumaSummary::new(), {
711 joined_member_count: Some(2u32.into()),
712 heroes: vec![me.to_owned(), matthew.to_owned()],
713 });
714
715 let f = EventFactory::new().room(room_id!("!test:localhost"));
716
717 let members = changes
718 .state
719 .entry(room_id.to_owned())
720 .or_default()
721 .entry(StateEventType::RoomMember)
722 .or_default();
723 members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
724 members.insert(me.into(), f.member(me).display_name("Me").into_raw());
725
726 store.save_changes(&changes).await.unwrap();
727
728 room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
729 assert_eq!(
730 room.compute_display_name().await.unwrap().into_inner(),
731 RoomDisplayName::Calculated("Matthew".to_owned())
732 );
733 }
734
735 #[async_test]
736 async fn test_display_name_dm_joined_service_members() {
737 let (store, room) = make_room_test_helper(RoomState::Joined);
738 let room_id = room_id!("!test:localhost");
739
740 let matthew = user_id!("@sahasrhala:example.org");
741 let me = user_id!("@me:example.org");
742 let bot = user_id!("@bot:example.org");
743
744 let mut changes = StateChanges::new("".to_owned());
745 let summary = assign!(RumaSummary::new(), {
746 joined_member_count: Some(3u32.into()),
747 heroes: vec![me.to_owned(), matthew.to_owned(), bot.to_owned()],
748 });
749
750 let f = EventFactory::new().room(room_id!("!test:localhost"));
751
752 let members = changes
753 .state
754 .entry(room_id.to_owned())
755 .or_default()
756 .entry(StateEventType::RoomMember)
757 .or_default();
758 members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
759 members.insert(me.into(), f.member(me).display_name("Me").into_raw());
760 members.insert(bot.into(), f.member(bot).display_name("Bot").into_raw());
761
762 let member_hints_content =
763 f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into_raw();
764 changes
765 .state
766 .entry(room_id.to_owned())
767 .or_default()
768 .entry(StateEventType::MemberHints)
769 .or_default()
770 .insert("".to_owned(), member_hints_content);
771
772 store.save_changes(&changes).await.unwrap();
773
774 room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
775 assert_eq!(
777 room.compute_display_name().await.unwrap().into_inner(),
778 RoomDisplayName::Calculated("Matthew".to_owned())
779 );
780 }
781
782 #[async_test]
783 async fn test_display_name_dm_joined_alone_with_service_members() {
784 let (store, room) = make_room_test_helper(RoomState::Joined);
785 let room_id = room_id!("!test:localhost");
786
787 let me = user_id!("@me:example.org");
788 let bot = user_id!("@bot: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(), bot.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(me.into(), f.member(me).display_name("Me").into_raw());
805 members.insert(bot.into(), f.member(bot).display_name("Bot").into_raw());
806
807 let member_hints_content =
808 f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into_raw();
809 changes
810 .state
811 .entry(room_id.to_owned())
812 .or_default()
813 .entry(StateEventType::MemberHints)
814 .or_default()
815 .insert("".to_owned(), member_hints_content);
816
817 store.save_changes(&changes).await.unwrap();
818
819 room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
820 assert_eq!(room.compute_display_name().await.unwrap().into_inner(), RoomDisplayName::Empty);
822 }
823
824 #[async_test]
825 async fn test_display_name_dm_joined_no_heroes() {
826 let (store, room) = make_room_test_helper(RoomState::Joined);
827 let room_id = room_id!("!test:localhost");
828 let matthew = user_id!("@matthew:example.org");
829 let me = user_id!("@me:example.org");
830 let mut changes = StateChanges::new("".to_owned());
831
832 let f = EventFactory::new().room(room_id!("!test:localhost"));
833
834 let members = changes
835 .state
836 .entry(room_id.to_owned())
837 .or_default()
838 .entry(StateEventType::RoomMember)
839 .or_default();
840 members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
841 members.insert(me.into(), f.member(me).display_name("Me").into_raw());
842
843 store.save_changes(&changes).await.unwrap();
844
845 assert_eq!(
846 room.compute_display_name().await.unwrap().into_inner(),
847 RoomDisplayName::Calculated("Matthew".to_owned())
848 );
849 }
850
851 #[async_test]
852 async fn test_display_name_dm_joined_no_heroes_service_members() {
853 let (store, room) = make_room_test_helper(RoomState::Joined);
854 let room_id = room_id!("!test:localhost");
855
856 let matthew = user_id!("@matthew:example.org");
857 let me = user_id!("@me:example.org");
858 let bot = user_id!("@bot:example.org");
859
860 let mut changes = StateChanges::new("".to_owned());
861
862 let f = EventFactory::new().room(room_id!("!test:localhost"));
863
864 let members = changes
865 .state
866 .entry(room_id.to_owned())
867 .or_default()
868 .entry(StateEventType::RoomMember)
869 .or_default();
870 members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
871 members.insert(me.into(), f.member(me).display_name("Me").into_raw());
872 members.insert(bot.into(), f.member(bot).display_name("Bot").into_raw());
873
874 let member_hints_content =
875 f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into_raw();
876 changes
877 .state
878 .entry(room_id.to_owned())
879 .or_default()
880 .entry(StateEventType::MemberHints)
881 .or_default()
882 .insert("".to_owned(), member_hints_content);
883
884 store.save_changes(&changes).await.unwrap();
885
886 assert_eq!(
887 room.compute_display_name().await.unwrap().into_inner(),
888 RoomDisplayName::Calculated("Matthew".to_owned())
889 );
890 }
891
892 #[async_test]
893 async fn test_display_name_deterministic() {
894 let (store, room) = make_room_test_helper(RoomState::Joined);
895
896 let alice = user_id!("@alice:example.org");
897 let bob = user_id!("@bob:example.org");
898 let carol = user_id!("@carol:example.org");
899 let denis = user_id!("@denis:example.org");
900 let erica = user_id!("@erica:example.org");
901 let fred = user_id!("@fred:example.org");
902 let me = user_id!("@me:example.org");
903
904 let mut changes = StateChanges::new("".to_owned());
905
906 let f = EventFactory::new().room(room_id!("!test:localhost"));
907
908 {
911 let members = changes
912 .state
913 .entry(room.room_id().to_owned())
914 .or_default()
915 .entry(StateEventType::RoomMember)
916 .or_default();
917 members.insert(carol.into(), f.member(carol).display_name("Carol").into_raw());
918 members.insert(bob.into(), f.member(bob).display_name("Bob").into_raw());
919 members.insert(fred.into(), f.member(fred).display_name("Fred").into_raw());
920 members.insert(me.into(), f.member(me).display_name("Me").into_raw());
921 store.save_changes(&changes).await.unwrap();
922 }
923
924 {
925 let members = changes
926 .state
927 .entry(room.room_id().to_owned())
928 .or_default()
929 .entry(StateEventType::RoomMember)
930 .or_default();
931 members.insert(alice.into(), f.member(alice).display_name("Alice").into_raw());
932 members.insert(erica.into(), f.member(erica).display_name("Erica").into_raw());
933 members.insert(denis.into(), f.member(denis).display_name("Denis").into_raw());
934 store.save_changes(&changes).await.unwrap();
935 }
936
937 let summary = assign!(RumaSummary::new(), {
938 joined_member_count: Some(7u32.into()),
939 heroes: vec![denis.to_owned(), carol.to_owned(), bob.to_owned(), erica.to_owned()],
940 });
941 room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
942
943 assert_eq!(
944 room.compute_display_name().await.unwrap().into_inner(),
945 RoomDisplayName::Calculated("Bob, Carol, Denis, Erica, and 3 others".to_owned())
946 );
947 }
948
949 #[async_test]
950 async fn test_display_name_deterministic_no_heroes() {
951 let (store, room) = make_room_test_helper(RoomState::Joined);
952
953 let alice = user_id!("@alice:example.org");
954 let bob = user_id!("@bob:example.org");
955 let carol = user_id!("@carol:example.org");
956 let denis = user_id!("@denis:example.org");
957 let erica = user_id!("@erica:example.org");
958 let fred = user_id!("@fred:example.org");
959 let me = user_id!("@me:example.org");
960
961 let f = EventFactory::new().room(room_id!("!test:localhost"));
962
963 let mut changes = StateChanges::new("".to_owned());
964
965 {
968 let members = changes
969 .state
970 .entry(room.room_id().to_owned())
971 .or_default()
972 .entry(StateEventType::RoomMember)
973 .or_default();
974 members.insert(carol.into(), f.member(carol).display_name("Carol").into_raw());
975 members.insert(bob.into(), f.member(bob).display_name("Bob").into_raw());
976 members.insert(fred.into(), f.member(fred).display_name("Fred").into_raw());
977 members.insert(me.into(), f.member(me).display_name("Me").into_raw());
978
979 store.save_changes(&changes).await.unwrap();
980 }
981
982 {
983 let members = changes
984 .state
985 .entry(room.room_id().to_owned())
986 .or_default()
987 .entry(StateEventType::RoomMember)
988 .or_default();
989 members.insert(alice.into(), f.member(alice).display_name("Alice").into_raw());
990 members.insert(erica.into(), f.member(erica).display_name("Erica").into_raw());
991 members.insert(denis.into(), f.member(denis).display_name("Denis").into_raw());
992 store.save_changes(&changes).await.unwrap();
993 }
994
995 assert_eq!(
996 room.compute_display_name().await.unwrap().into_inner(),
997 RoomDisplayName::Calculated("Alice, Bob, Carol, Denis, Erica, and 2 others".to_owned())
998 );
999 }
1000
1001 #[async_test]
1002 async fn test_display_name_dm_alone() {
1003 let (store, room) = make_room_test_helper(RoomState::Joined);
1004 let room_id = room_id!("!test:localhost");
1005 let matthew = user_id!("@matthew:example.org");
1006 let me = user_id!("@me:example.org");
1007 let mut changes = StateChanges::new("".to_owned());
1008 let summary = assign!(RumaSummary::new(), {
1009 joined_member_count: Some(1u32.into()),
1010 heroes: vec![me.to_owned(), matthew.to_owned()],
1011 });
1012
1013 let f = EventFactory::new().room(room_id!("!test:localhost"));
1014
1015 let members = changes
1016 .state
1017 .entry(room_id.to_owned())
1018 .or_default()
1019 .entry(StateEventType::RoomMember)
1020 .or_default();
1021 members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
1022 members.insert(me.into(), f.member(me).display_name("Me").into_raw());
1023
1024 store.save_changes(&changes).await.unwrap();
1025
1026 room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
1027 assert_eq!(
1028 room.compute_display_name().await.unwrap().into_inner(),
1029 RoomDisplayName::EmptyWas("Matthew".to_owned())
1030 );
1031 }
1032
1033 #[test]
1034 fn test_calculate_room_name() {
1035 let mut actual = compute_display_name_from_heroes(2, vec!["a"]);
1036 assert_eq!(RoomDisplayName::Calculated("a".to_owned()), actual);
1037
1038 actual = compute_display_name_from_heroes(3, vec!["a", "b"]);
1039 assert_eq!(RoomDisplayName::Calculated("a, b".to_owned()), actual);
1040
1041 actual = compute_display_name_from_heroes(4, vec!["a", "b", "c"]);
1042 assert_eq!(RoomDisplayName::Calculated("a, b, c".to_owned()), actual);
1043
1044 actual = compute_display_name_from_heroes(5, vec!["a", "b", "c"]);
1045 assert_eq!(RoomDisplayName::Calculated("a, b, c, and 2 others".to_owned()), actual);
1046
1047 actual = compute_display_name_from_heroes(5, vec![]);
1048 assert_eq!(RoomDisplayName::Calculated("5 people".to_owned()), actual);
1049
1050 actual = compute_display_name_from_heroes(0, vec![]);
1051 assert_eq!(RoomDisplayName::Empty, actual);
1052
1053 actual = compute_display_name_from_heroes(1, vec![]);
1054 assert_eq!(RoomDisplayName::Empty, actual);
1055
1056 actual = compute_display_name_from_heroes(1, vec!["a"]);
1057 assert_eq!(RoomDisplayName::EmptyWas("a".to_owned()), actual);
1058
1059 actual = compute_display_name_from_heroes(1, vec!["a", "b"]);
1060 assert_eq!(RoomDisplayName::EmptyWas("a, b".to_owned()), actual);
1061
1062 actual = compute_display_name_from_heroes(1, vec!["a", "b", "c"]);
1063 assert_eq!(RoomDisplayName::EmptyWas("a, b, c".to_owned()), actual);
1064 }
1065
1066 #[test]
1067 fn test_room_alias_from_room_display_name_lowercases() {
1068 assert_eq!(
1069 "roomalias",
1070 RoomDisplayName::Named("RoomAlias".to_owned()).to_room_alias_name()
1071 );
1072 }
1073
1074 #[test]
1075 fn test_room_alias_from_room_display_name_removes_whitespace() {
1076 assert_eq!(
1077 "room-alias",
1078 RoomDisplayName::Named("Room Alias".to_owned()).to_room_alias_name()
1079 );
1080 }
1081
1082 #[test]
1083 fn test_room_alias_from_room_display_name_removes_non_ascii_symbols() {
1084 assert_eq!(
1085 "roomalias",
1086 RoomDisplayName::Named("Room±Alias√".to_owned()).to_room_alias_name()
1087 );
1088 }
1089
1090 #[test]
1091 fn test_room_alias_from_room_display_name_removes_invalid_ascii_symbols() {
1092 assert_eq!(
1093 "roomalias",
1094 RoomDisplayName::Named("#Room,{Alias}:".to_owned()).to_room_alias_name()
1095 );
1096 }
1097}