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