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