1use std::fmt;
16
17use as_variant::as_variant;
18use regex::Regex;
19use ruma::{
20 events::{member_hints::MemberHintsEventContent, SyncStateEvent},
21 OwnedMxcUri, OwnedUserId, UserId,
22};
23use serde::{Deserialize, Serialize};
24use tracing::{debug, trace, warn};
25
26use super::{Room, RoomMemberships};
27use crate::{
28 deserialized_responses::SyncOrStrippedState,
29 store::{Result as StoreResult, StateStoreExt},
30 RoomMember, RoomState,
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() {
489 RoomDisplayName::Empty
490 } else {
491 RoomDisplayName::EmptyWas(names)
492 }
493 } else {
494 RoomDisplayName::Calculated(names)
495 }
496}
497
498fn heroes_filter<'a>(
504 own_user_id: &'a UserId,
505 member_hints: &'a MemberHintsEventContent,
506) -> impl Fn(&UserId) -> bool + use<'a> {
507 move |user_id| user_id != own_user_id && !member_hints.service_members.contains(user_id)
508}
509
510#[cfg(test)]
511mod tests {
512 use std::{collections::BTreeSet, sync::Arc};
513
514 use matrix_sdk_test::{async_test, event_factory::EventFactory};
515 use ruma::{
516 api::client::sync::sync_events::v3::RoomSummary as RumaSummary,
517 assign,
518 events::{
519 room::{
520 canonical_alias::RoomCanonicalAliasEventContent,
521 member::{MembershipState, RoomMemberEventContent, StrippedRoomMemberEvent},
522 name::RoomNameEventContent,
523 },
524 StateEventType,
525 },
526 room_alias_id, room_id,
527 serde::Raw,
528 user_id, UserId,
529 };
530 use serde_json::json;
531
532 use super::{compute_display_name_from_heroes, Room, RoomDisplayName};
533 use crate::{
534 store::MemoryStore, MinimalStateEvent, OriginalMinimalStateEvent, RoomState, StateChanges,
535 StateStore,
536 };
537
538 fn make_room_test_helper(room_type: RoomState) -> (Arc<MemoryStore>, Room) {
539 let store = Arc::new(MemoryStore::new());
540 let user_id = user_id!("@me:example.org");
541 let room_id = room_id!("!test:localhost");
542 let (sender, _receiver) = tokio::sync::broadcast::channel(1);
543
544 (store.clone(), Room::new(user_id, store, room_id, room_type, sender))
545 }
546
547 fn make_stripped_member_event(user_id: &UserId, name: &str) -> Raw<StrippedRoomMemberEvent> {
548 let ev_json = json!({
549 "type": "m.room.member",
550 "content": assign!(RoomMemberEventContent::new(MembershipState::Join), {
551 displayname: Some(name.to_owned())
552 }),
553 "sender": user_id,
554 "state_key": user_id,
555 });
556
557 Raw::new(&ev_json).unwrap().cast()
558 }
559
560 fn make_canonical_alias_event() -> MinimalStateEvent<RoomCanonicalAliasEventContent> {
561 MinimalStateEvent::Original(OriginalMinimalStateEvent {
562 content: assign!(RoomCanonicalAliasEventContent::new(), {
563 alias: Some(room_alias_id!("#test:example.com").to_owned()),
564 }),
565 event_id: None,
566 })
567 }
568
569 fn make_name_event() -> MinimalStateEvent<RoomNameEventContent> {
570 MinimalStateEvent::Original(OriginalMinimalStateEvent {
571 content: RoomNameEventContent::new("Test Room".to_owned()),
572 event_id: None,
573 })
574 }
575
576 #[async_test]
577 async fn test_display_name_for_joined_room_is_empty_if_no_info() {
578 let (_, room) = make_room_test_helper(RoomState::Joined);
579 assert_eq!(room.compute_display_name().await.unwrap().into_inner(), RoomDisplayName::Empty);
580 }
581
582 #[async_test]
583 async fn test_display_name_for_joined_room_uses_canonical_alias_if_available() {
584 let (_, room) = make_room_test_helper(RoomState::Joined);
585 room.inner
586 .update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
587 assert_eq!(
588 room.compute_display_name().await.unwrap().into_inner(),
589 RoomDisplayName::Aliased("test".to_owned())
590 );
591 }
592
593 #[async_test]
594 async fn test_display_name_for_joined_room_prefers_name_over_alias() {
595 let (_, room) = make_room_test_helper(RoomState::Joined);
596 room.inner
597 .update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
598 assert_eq!(
599 room.compute_display_name().await.unwrap().into_inner(),
600 RoomDisplayName::Aliased("test".to_owned())
601 );
602 room.inner.update(|info| info.base_info.name = Some(make_name_event()));
603 assert_eq!(
605 room.compute_display_name().await.unwrap().into_inner(),
606 RoomDisplayName::Named("Test Room".to_owned())
607 );
608 }
609
610 #[async_test]
611 async fn test_display_name_for_invited_room_is_empty_if_no_info() {
612 let (_, room) = make_room_test_helper(RoomState::Invited);
613 assert_eq!(room.compute_display_name().await.unwrap().into_inner(), RoomDisplayName::Empty);
614 }
615
616 #[async_test]
617 async fn test_display_name_for_invited_room_is_empty_if_room_name_empty() {
618 let (_, room) = make_room_test_helper(RoomState::Invited);
619
620 let room_name = MinimalStateEvent::Original(OriginalMinimalStateEvent {
621 content: RoomNameEventContent::new(String::new()),
622 event_id: None,
623 });
624 room.inner.update(|info| info.base_info.name = Some(room_name));
625
626 assert_eq!(room.compute_display_name().await.unwrap().into_inner(), RoomDisplayName::Empty);
627 }
628
629 #[async_test]
630 async fn test_display_name_for_invited_room_uses_canonical_alias_if_available() {
631 let (_, room) = make_room_test_helper(RoomState::Invited);
632 room.inner
633 .update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
634 assert_eq!(
635 room.compute_display_name().await.unwrap().into_inner(),
636 RoomDisplayName::Aliased("test".to_owned())
637 );
638 }
639
640 #[async_test]
641 async fn test_display_name_for_invited_room_prefers_name_over_alias() {
642 let (_, room) = make_room_test_helper(RoomState::Invited);
643 room.inner
644 .update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
645 assert_eq!(
646 room.compute_display_name().await.unwrap().into_inner(),
647 RoomDisplayName::Aliased("test".to_owned())
648 );
649 room.inner.update(|info| info.base_info.name = Some(make_name_event()));
650 assert_eq!(
652 room.compute_display_name().await.unwrap().into_inner(),
653 RoomDisplayName::Named("Test Room".to_owned())
654 );
655 }
656
657 #[async_test]
658 async fn test_display_name_dm_invited() {
659 let (store, room) = make_room_test_helper(RoomState::Invited);
660 let room_id = room_id!("!test:localhost");
661 let matthew = user_id!("@matthew:example.org");
662 let me = user_id!("@me:example.org");
663 let mut changes = StateChanges::new("".to_owned());
664 let summary = assign!(RumaSummary::new(), {
665 heroes: vec![me.to_owned(), matthew.to_owned()],
666 });
667
668 changes.add_stripped_member(
669 room_id,
670 matthew,
671 make_stripped_member_event(matthew, "Matthew"),
672 );
673 changes.add_stripped_member(room_id, me, make_stripped_member_event(me, "Me"));
674 store.save_changes(&changes).await.unwrap();
675
676 room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
677 assert_eq!(
678 room.compute_display_name().await.unwrap().into_inner(),
679 RoomDisplayName::Calculated("Matthew".to_owned())
680 );
681 }
682
683 #[async_test]
684 async fn test_display_name_dm_invited_no_heroes() {
685 let (store, room) = make_room_test_helper(RoomState::Invited);
686 let room_id = room_id!("!test:localhost");
687 let matthew = user_id!("@matthew:example.org");
688 let me = user_id!("@me:example.org");
689 let mut changes = StateChanges::new("".to_owned());
690
691 changes.add_stripped_member(
692 room_id,
693 matthew,
694 make_stripped_member_event(matthew, "Matthew"),
695 );
696 changes.add_stripped_member(room_id, me, make_stripped_member_event(me, "Me"));
697 store.save_changes(&changes).await.unwrap();
698
699 assert_eq!(
700 room.compute_display_name().await.unwrap().into_inner(),
701 RoomDisplayName::Calculated("Matthew".to_owned())
702 );
703 }
704
705 #[async_test]
706 async fn test_display_name_dm_joined() {
707 let (store, room) = make_room_test_helper(RoomState::Joined);
708 let room_id = room_id!("!test:localhost");
709 let matthew = user_id!("@matthew:example.org");
710 let me = user_id!("@me:example.org");
711
712 let mut changes = StateChanges::new("".to_owned());
713 let summary = assign!(RumaSummary::new(), {
714 joined_member_count: Some(2u32.into()),
715 heroes: vec![me.to_owned(), matthew.to_owned()],
716 });
717
718 let f = EventFactory::new().room(room_id!("!test:localhost"));
719
720 let members = changes
721 .state
722 .entry(room_id.to_owned())
723 .or_default()
724 .entry(StateEventType::RoomMember)
725 .or_default();
726 members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
727 members.insert(me.into(), f.member(me).display_name("Me").into_raw());
728
729 store.save_changes(&changes).await.unwrap();
730
731 room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
732 assert_eq!(
733 room.compute_display_name().await.unwrap().into_inner(),
734 RoomDisplayName::Calculated("Matthew".to_owned())
735 );
736 }
737
738 #[async_test]
739 async fn test_display_name_dm_joined_service_members() {
740 let (store, room) = make_room_test_helper(RoomState::Joined);
741 let room_id = room_id!("!test:localhost");
742
743 let matthew = user_id!("@sahasrhala:example.org");
744 let me = user_id!("@me:example.org");
745 let bot = user_id!("@bot:example.org");
746
747 let mut changes = StateChanges::new("".to_owned());
748 let summary = assign!(RumaSummary::new(), {
749 joined_member_count: Some(3u32.into()),
750 heroes: vec![me.to_owned(), matthew.to_owned(), bot.to_owned()],
751 });
752
753 let f = EventFactory::new().room(room_id!("!test:localhost"));
754
755 let members = changes
756 .state
757 .entry(room_id.to_owned())
758 .or_default()
759 .entry(StateEventType::RoomMember)
760 .or_default();
761 members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
762 members.insert(me.into(), f.member(me).display_name("Me").into_raw());
763 members.insert(bot.into(), f.member(bot).display_name("Bot").into_raw());
764
765 let member_hints_content =
766 f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into_raw();
767 changes
768 .state
769 .entry(room_id.to_owned())
770 .or_default()
771 .entry(StateEventType::MemberHints)
772 .or_default()
773 .insert("".to_owned(), member_hints_content);
774
775 store.save_changes(&changes).await.unwrap();
776
777 room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
778 assert_eq!(
780 room.compute_display_name().await.unwrap().into_inner(),
781 RoomDisplayName::Calculated("Matthew".to_owned())
782 );
783 }
784
785 #[async_test]
786 async fn test_display_name_dm_joined_alone_with_service_members() {
787 let (store, room) = make_room_test_helper(RoomState::Joined);
788 let room_id = room_id!("!test:localhost");
789
790 let me = user_id!("@me:example.org");
791 let bot = user_id!("@bot:example.org");
792
793 let mut changes = StateChanges::new("".to_owned());
794 let summary = assign!(RumaSummary::new(), {
795 joined_member_count: Some(2u32.into()),
796 heroes: vec![me.to_owned(), bot.to_owned()],
797 });
798
799 let f = EventFactory::new().room(room_id!("!test:localhost"));
800
801 let members = changes
802 .state
803 .entry(room_id.to_owned())
804 .or_default()
805 .entry(StateEventType::RoomMember)
806 .or_default();
807 members.insert(me.into(), f.member(me).display_name("Me").into_raw());
808 members.insert(bot.into(), f.member(bot).display_name("Bot").into_raw());
809
810 let member_hints_content =
811 f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into_raw();
812 changes
813 .state
814 .entry(room_id.to_owned())
815 .or_default()
816 .entry(StateEventType::MemberHints)
817 .or_default()
818 .insert("".to_owned(), member_hints_content);
819
820 store.save_changes(&changes).await.unwrap();
821
822 room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
823 assert_eq!(room.compute_display_name().await.unwrap().into_inner(), RoomDisplayName::Empty);
825 }
826
827 #[async_test]
828 async fn test_display_name_dm_joined_no_heroes() {
829 let (store, room) = make_room_test_helper(RoomState::Joined);
830 let room_id = room_id!("!test:localhost");
831 let matthew = user_id!("@matthew:example.org");
832 let me = user_id!("@me:example.org");
833 let mut changes = StateChanges::new("".to_owned());
834
835 let f = EventFactory::new().room(room_id!("!test:localhost"));
836
837 let members = changes
838 .state
839 .entry(room_id.to_owned())
840 .or_default()
841 .entry(StateEventType::RoomMember)
842 .or_default();
843 members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
844 members.insert(me.into(), f.member(me).display_name("Me").into_raw());
845
846 store.save_changes(&changes).await.unwrap();
847
848 assert_eq!(
849 room.compute_display_name().await.unwrap().into_inner(),
850 RoomDisplayName::Calculated("Matthew".to_owned())
851 );
852 }
853
854 #[async_test]
855 async fn test_display_name_dm_joined_no_heroes_service_members() {
856 let (store, room) = make_room_test_helper(RoomState::Joined);
857 let room_id = room_id!("!test:localhost");
858
859 let matthew = user_id!("@matthew:example.org");
860 let me = user_id!("@me:example.org");
861 let bot = user_id!("@bot:example.org");
862
863 let mut changes = StateChanges::new("".to_owned());
864
865 let f = EventFactory::new().room(room_id!("!test:localhost"));
866
867 let members = changes
868 .state
869 .entry(room_id.to_owned())
870 .or_default()
871 .entry(StateEventType::RoomMember)
872 .or_default();
873 members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
874 members.insert(me.into(), f.member(me).display_name("Me").into_raw());
875 members.insert(bot.into(), f.member(bot).display_name("Bot").into_raw());
876
877 let member_hints_content =
878 f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into_raw();
879 changes
880 .state
881 .entry(room_id.to_owned())
882 .or_default()
883 .entry(StateEventType::MemberHints)
884 .or_default()
885 .insert("".to_owned(), member_hints_content);
886
887 store.save_changes(&changes).await.unwrap();
888
889 assert_eq!(
890 room.compute_display_name().await.unwrap().into_inner(),
891 RoomDisplayName::Calculated("Matthew".to_owned())
892 );
893 }
894
895 #[async_test]
896 async fn test_display_name_deterministic() {
897 let (store, room) = make_room_test_helper(RoomState::Joined);
898
899 let alice = user_id!("@alice:example.org");
900 let bob = user_id!("@bob:example.org");
901 let carol = user_id!("@carol:example.org");
902 let denis = user_id!("@denis:example.org");
903 let erica = user_id!("@erica:example.org");
904 let fred = user_id!("@fred:example.org");
905 let me = user_id!("@me:example.org");
906
907 let mut changes = StateChanges::new("".to_owned());
908
909 let f = EventFactory::new().room(room_id!("!test:localhost"));
910
911 {
914 let members = changes
915 .state
916 .entry(room.room_id().to_owned())
917 .or_default()
918 .entry(StateEventType::RoomMember)
919 .or_default();
920 members.insert(carol.into(), f.member(carol).display_name("Carol").into_raw());
921 members.insert(bob.into(), f.member(bob).display_name("Bob").into_raw());
922 members.insert(fred.into(), f.member(fred).display_name("Fred").into_raw());
923 members.insert(me.into(), f.member(me).display_name("Me").into_raw());
924 store.save_changes(&changes).await.unwrap();
925 }
926
927 {
928 let members = changes
929 .state
930 .entry(room.room_id().to_owned())
931 .or_default()
932 .entry(StateEventType::RoomMember)
933 .or_default();
934 members.insert(alice.into(), f.member(alice).display_name("Alice").into_raw());
935 members.insert(erica.into(), f.member(erica).display_name("Erica").into_raw());
936 members.insert(denis.into(), f.member(denis).display_name("Denis").into_raw());
937 store.save_changes(&changes).await.unwrap();
938 }
939
940 let summary = assign!(RumaSummary::new(), {
941 joined_member_count: Some(7u32.into()),
942 heroes: vec![denis.to_owned(), carol.to_owned(), bob.to_owned(), erica.to_owned()],
943 });
944 room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
945
946 assert_eq!(
947 room.compute_display_name().await.unwrap().into_inner(),
948 RoomDisplayName::Calculated("Bob, Carol, Denis, Erica, and 3 others".to_owned())
949 );
950 }
951
952 #[async_test]
953 async fn test_display_name_deterministic_no_heroes() {
954 let (store, room) = make_room_test_helper(RoomState::Joined);
955
956 let alice = user_id!("@alice:example.org");
957 let bob = user_id!("@bob:example.org");
958 let carol = user_id!("@carol:example.org");
959 let denis = user_id!("@denis:example.org");
960 let erica = user_id!("@erica:example.org");
961 let fred = user_id!("@fred:example.org");
962 let me = user_id!("@me:example.org");
963
964 let f = EventFactory::new().room(room_id!("!test:localhost"));
965
966 let mut changes = StateChanges::new("".to_owned());
967
968 {
971 let members = changes
972 .state
973 .entry(room.room_id().to_owned())
974 .or_default()
975 .entry(StateEventType::RoomMember)
976 .or_default();
977 members.insert(carol.into(), f.member(carol).display_name("Carol").into_raw());
978 members.insert(bob.into(), f.member(bob).display_name("Bob").into_raw());
979 members.insert(fred.into(), f.member(fred).display_name("Fred").into_raw());
980 members.insert(me.into(), f.member(me).display_name("Me").into_raw());
981
982 store.save_changes(&changes).await.unwrap();
983 }
984
985 {
986 let members = changes
987 .state
988 .entry(room.room_id().to_owned())
989 .or_default()
990 .entry(StateEventType::RoomMember)
991 .or_default();
992 members.insert(alice.into(), f.member(alice).display_name("Alice").into_raw());
993 members.insert(erica.into(), f.member(erica).display_name("Erica").into_raw());
994 members.insert(denis.into(), f.member(denis).display_name("Denis").into_raw());
995 store.save_changes(&changes).await.unwrap();
996 }
997
998 assert_eq!(
999 room.compute_display_name().await.unwrap().into_inner(),
1000 RoomDisplayName::Calculated("Alice, Bob, Carol, Denis, Erica, and 2 others".to_owned())
1001 );
1002 }
1003
1004 #[async_test]
1005 async fn test_display_name_dm_alone() {
1006 let (store, room) = make_room_test_helper(RoomState::Joined);
1007 let room_id = room_id!("!test:localhost");
1008 let matthew = user_id!("@matthew:example.org");
1009 let me = user_id!("@me:example.org");
1010 let mut changes = StateChanges::new("".to_owned());
1011 let summary = assign!(RumaSummary::new(), {
1012 joined_member_count: Some(1u32.into()),
1013 heroes: vec![me.to_owned(), matthew.to_owned()],
1014 });
1015
1016 let f = EventFactory::new().room(room_id!("!test:localhost"));
1017
1018 let members = changes
1019 .state
1020 .entry(room_id.to_owned())
1021 .or_default()
1022 .entry(StateEventType::RoomMember)
1023 .or_default();
1024 members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
1025 members.insert(me.into(), f.member(me).display_name("Me").into_raw());
1026
1027 store.save_changes(&changes).await.unwrap();
1028
1029 room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
1030 assert_eq!(
1031 room.compute_display_name().await.unwrap().into_inner(),
1032 RoomDisplayName::EmptyWas("Matthew".to_owned())
1033 );
1034 }
1035
1036 #[test]
1037 fn test_calculate_room_name() {
1038 let mut actual = compute_display_name_from_heroes(2, vec!["a"]);
1039 assert_eq!(RoomDisplayName::Calculated("a".to_owned()), actual);
1040
1041 actual = compute_display_name_from_heroes(3, vec!["a", "b"]);
1042 assert_eq!(RoomDisplayName::Calculated("a, b".to_owned()), actual);
1043
1044 actual = compute_display_name_from_heroes(4, vec!["a", "b", "c"]);
1045 assert_eq!(RoomDisplayName::Calculated("a, b, c".to_owned()), actual);
1046
1047 actual = compute_display_name_from_heroes(5, vec!["a", "b", "c"]);
1048 assert_eq!(RoomDisplayName::Calculated("a, b, c, and 2 others".to_owned()), actual);
1049
1050 actual = compute_display_name_from_heroes(5, vec![]);
1051 assert_eq!(RoomDisplayName::Calculated("5 people".to_owned()), actual);
1052
1053 actual = compute_display_name_from_heroes(0, vec![]);
1054 assert_eq!(RoomDisplayName::Empty, actual);
1055
1056 actual = compute_display_name_from_heroes(1, vec![]);
1057 assert_eq!(RoomDisplayName::Empty, actual);
1058
1059 actual = compute_display_name_from_heroes(1, vec!["a"]);
1060 assert_eq!(RoomDisplayName::EmptyWas("a".to_owned()), actual);
1061
1062 actual = compute_display_name_from_heroes(1, vec!["a", "b"]);
1063 assert_eq!(RoomDisplayName::EmptyWas("a, b".to_owned()), actual);
1064
1065 actual = compute_display_name_from_heroes(1, vec!["a", "b", "c"]);
1066 assert_eq!(RoomDisplayName::EmptyWas("a, b, c".to_owned()), actual);
1067 }
1068
1069 #[test]
1070 fn test_room_alias_from_room_display_name_lowercases() {
1071 assert_eq!(
1072 "roomalias",
1073 RoomDisplayName::Named("RoomAlias".to_owned()).to_room_alias_name()
1074 );
1075 }
1076
1077 #[test]
1078 fn test_room_alias_from_room_display_name_removes_whitespace() {
1079 assert_eq!(
1080 "room-alias",
1081 RoomDisplayName::Named("Room Alias".to_owned()).to_room_alias_name()
1082 );
1083 }
1084
1085 #[test]
1086 fn test_room_alias_from_room_display_name_removes_non_ascii_symbols() {
1087 assert_eq!(
1088 "roomalias",
1089 RoomDisplayName::Named("Room±Alias√".to_owned()).to_room_alias_name()
1090 );
1091 }
1092
1093 #[test]
1094 fn test_room_alias_from_room_display_name_removes_invalid_ascii_symbols() {
1095 assert_eq!(
1096 "roomalias",
1097 RoomDisplayName::Named("#Room,{Alias}:".to_owned()).to_room_alias_name()
1098 );
1099 }
1100}