From b5316efb4f7015adbbb12e27dad1ebd25d063150 Mon Sep 17 00:00:00 2001 From: multi prise Date: Mon, 13 Oct 2025 22:20:32 +0200 Subject: [PATCH 1/9] Verify membership when test if name is ambiguous --- crates/matrix-sdk-base/src/room/members.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk-base/src/room/members.rs b/crates/matrix-sdk-base/src/room/members.rs index 6884c678050..1f12141b8c4 100644 --- a/crates/matrix-sdk-base/src/room/members.rs +++ b/crates/matrix-sdk-base/src/room/members.rs @@ -215,9 +215,10 @@ impl RoomMember { room_info; let display_name = event.display_name(); - let display_name_ambiguous = users_display_names - .get(&display_name) - .is_some_and(|s| is_display_name_ambiguous(&display_name, s)); + let membership = event.membership(); + let display_name_ambiguous = users_display_names.get(&display_name).is_some_and(|s| { + is_display_name_ambiguous(&display_name, s) && *membership != MembershipState::Leave + }); let is_ignored = ignored_users.as_ref().is_some_and(|s| s.contains(event.user_id())); Self { From 932cea51226bc230e37cdc35da26c28b4c41f911 Mon Sep 17 00:00:00 2001 From: multi prise Date: Tue, 14 Oct 2025 10:29:24 +0200 Subject: [PATCH 2/9] Following the spec more precisely --- crates/matrix-sdk-base/src/room/members.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk-base/src/room/members.rs b/crates/matrix-sdk-base/src/room/members.rs index 1f12141b8c4..aa774f59443 100644 --- a/crates/matrix-sdk-base/src/room/members.rs +++ b/crates/matrix-sdk-base/src/room/members.rs @@ -217,7 +217,8 @@ impl RoomMember { let display_name = event.display_name(); let membership = event.membership(); let display_name_ambiguous = users_display_names.get(&display_name).is_some_and(|s| { - is_display_name_ambiguous(&display_name, s) && *membership != MembershipState::Leave + is_display_name_ambiguous(&display_name, s) + && (*membership == MembershipState::Join || *membership == MembershipState::Invite) }); let is_ignored = ignored_users.as_ref().is_some_and(|s| s.contains(event.user_id())); From 79beb6c2c8e8e08c50a90350342c1dca0174a772 Mon Sep 17 00:00:00 2001 From: multi prise Date: Tue, 14 Oct 2025 15:54:20 +0200 Subject: [PATCH 3/9] Separate both conditions for readability --- crates/matrix-sdk-base/src/room/members.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk-base/src/room/members.rs b/crates/matrix-sdk-base/src/room/members.rs index aa774f59443..e4f2b47bd5d 100644 --- a/crates/matrix-sdk-base/src/room/members.rs +++ b/crates/matrix-sdk-base/src/room/members.rs @@ -217,8 +217,10 @@ impl RoomMember { let display_name = event.display_name(); let membership = event.membership(); let display_name_ambiguous = users_display_names.get(&display_name).is_some_and(|s| { - is_display_name_ambiguous(&display_name, s) - && (*membership == MembershipState::Join || *membership == MembershipState::Invite) + if !is_display_name_ambiguous(&display_name, s) { + return false; + } + matches!(*membership, MembershipState::Join | MembershipState::Invite) }); let is_ignored = ignored_users.as_ref().is_some_and(|s| s.contains(event.user_id())); From 7cd077d8df27785dd12883c1c6deb0c448bd9f79 Mon Sep 17 00:00:00 2001 From: multi prise Date: Tue, 14 Oct 2025 20:48:11 +0200 Subject: [PATCH 4/9] Non working tests --- crates/matrix-sdk-base/src/room/members.rs | 91 ++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/crates/matrix-sdk-base/src/room/members.rs b/crates/matrix-sdk-base/src/room/members.rs index e4f2b47bd5d..6bc5a056af1 100644 --- a/crates/matrix-sdk-base/src/room/members.rs +++ b/crates/matrix-sdk-base/src/room/members.rs @@ -482,10 +482,26 @@ pub fn normalize_power_level(power_level: Int, max_power_level: i64) -> Int { #[cfg(test)] mod tests { + use std::sync::Arc; + use proptest::prelude::*; + use matrix_sdk_test::{async_test, event_factory::EventFactory}; + use ruma::{room_id, user_id}; + use super::*; + use crate::{RoomState, StateChanges, StateStore, store::MemoryStore}; + + fn make_room_test_helper(room_type: RoomState) -> (Arc, Room) { + let store = Arc::new(MemoryStore::new()); + let user_id = user_id!("@me:example.org"); + let room_id = room_id!("!test:localhost"); + let (sender, _receiver) = tokio::sync::broadcast::channel(1); + + (store.clone(), Room::new(user_id, store, room_id, room_type, sender)) + } + prop_compose! { fn arb_int()(id in any::()) -> Int { id.try_into().unwrap_or_default() @@ -530,4 +546,79 @@ mod tests { assert!(normalized >= 0); assert!(normalized <= 100); } + + #[async_test] + async fn test_room_member_from_parts() { + let (store, room) = make_room_test_helper(RoomState::Joined); + + let carol = user_id!("@carol:example.org"); + let denis = user_id!("@denis:example.org"); + let erica = user_id!("@erica:example.org"); + let fred = user_id!("@fred:example.org"); + let fredo = user_id!("@fredo:example.org"); + let bob = user_id!("@bob:example.org"); + let julie = user_id!("@julie:example.org"); + let me = user_id!("@me:example.org"); + let mewto = user_id!("@mewto:example.org"); + + let mut changes = StateChanges::new("".to_owned()); + + let f = EventFactory::new().room(room_id!("!test:localhost")); + + { + let members = changes + .state + .entry(room.room_id().to_owned()) + .or_default() + .entry(StateEventType::RoomMember) + .or_default(); + members.insert(carol.into(), f.member(carol).display_name("Carol").into()); + + members.insert(fred.into(), f.member(fred).display_name("Fred").into()); + members.insert( + fredo.into(), + f.member(fredo).display_name("Fred").membership(MembershipState::Knock).into(), + ); + members.insert( + denis.into(), + f.member(denis).display_name("Fred").membership(MembershipState::Leave).into(), + ); + members.insert( + erica.into(), + f.member(erica).display_name("Fred").membership(MembershipState::Ban).into(), + ); + + members.insert( + bob.into(), + f.member(bob).display_name("Bob").membership(MembershipState::Invite).into(), + ); + members.insert(julie.into(), f.member(me).display_name("Bob").into()); + + members.insert(me.into(), f.member(me).display_name("Me").into()); + members.insert(mewto.into(), f.member(mewto).display_name("Me").into()); + + store.save_changes(&changes).await.unwrap(); + } + + assert!(!room.get_member(carol).await.unwrap().expect("Carol user").name_ambiguous()); + + assert!(!room.get_member(fred).await.unwrap().expect("Fred user").name_ambiguous()); + assert!(!room.get_member(fredo).await.unwrap().expect("Fredo user").name_ambiguous()); + assert!(!room.get_member(denis).await.unwrap().expect("Denis user").name_ambiguous()); + assert!(!room.get_member(erica).await.unwrap().expect("Erica user").name_ambiguous()); + + assert!(!room.get_member(bob).await.unwrap().expect("Bob user").name_ambiguous()); + + assert!(!room.get_member(julie).await.unwrap().expect("Julie user").name_ambiguous()); + assert!(!room.get_member(bob).await.unwrap().expect("Bob user").name_ambiguous()); + + assert_eq!(room.get_member(me).await.unwrap().expect("Me user").display_name(), Some("Me")); + assert_eq!( + room.get_member(mewto).await.unwrap().expect("Mewto user").display_name(), + Some("Me") + ); + + assert!(room.get_member(me).await.unwrap().expect("Me user").name_ambiguous()); + assert!(room.get_member(mewto).await.unwrap().expect("Mewto user").name_ambiguous()); + } } From 48f8549bdb81b09bdfae24338b905e689a0ed49e Mon Sep 17 00:00:00 2001 From: multi prise Date: Wed, 15 Oct 2025 16:41:32 +0200 Subject: [PATCH 5/9] Setting the map_ambiguity's changes key in the tests --- crates/matrix-sdk-base/src/room/members.rs | 27 ++++++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/crates/matrix-sdk-base/src/room/members.rs b/crates/matrix-sdk-base/src/room/members.rs index 6bc5a056af1..3f415ae461c 100644 --- a/crates/matrix-sdk-base/src/room/members.rs +++ b/crates/matrix-sdk-base/src/room/members.rs @@ -216,6 +216,8 @@ impl RoomMember { let display_name = event.display_name(); let membership = event.membership(); + + println!("{:?} {:?}", users_display_names, &display_name); let display_name_ambiguous = users_display_names.get(&display_name).is_some_and(|s| { if !is_display_name_ambiguous(&display_name, s) { return false; @@ -572,30 +574,50 @@ mod tests { .or_default() .entry(StateEventType::RoomMember) .or_default(); + + let ambiguity_maps = changes + .ambiguity_maps + .entry(room.room_id().to_owned()) + .or_default(); + + + let display_name = DisplayName::new("Carol"); members.insert(carol.into(), f.member(carol).display_name("Carol").into()); + ambiguity_maps.entry(display_name).or_default().insert(carol.to_owned()); + let display_name = DisplayName::new("Fred"); members.insert(fred.into(), f.member(fred).display_name("Fred").into()); + ambiguity_maps.entry(display_name.clone()).or_default().insert(fred.to_owned()); members.insert( fredo.into(), f.member(fredo).display_name("Fred").membership(MembershipState::Knock).into(), ); + ambiguity_maps.entry(display_name.clone()).or_default().insert(fredo.to_owned()); members.insert( denis.into(), f.member(denis).display_name("Fred").membership(MembershipState::Leave).into(), ); + ambiguity_maps.entry(display_name.clone()).or_default().insert(erica.to_owned()); members.insert( erica.into(), f.member(erica).display_name("Fred").membership(MembershipState::Ban).into(), ); + let display_name = DisplayName::new("Bob"); members.insert( bob.into(), f.member(bob).display_name("Bob").membership(MembershipState::Invite).into(), ); + ambiguity_maps.entry(display_name.clone()).or_default().insert(bob.to_owned()); members.insert(julie.into(), f.member(me).display_name("Bob").into()); + ambiguity_maps.entry(display_name.clone()).or_default().insert(julie.to_owned()); + let display_name = DisplayName::new("Me"); members.insert(me.into(), f.member(me).display_name("Me").into()); + ambiguity_maps.entry(display_name.clone()).or_default().insert(me.to_owned()); members.insert(mewto.into(), f.member(mewto).display_name("Me").into()); + ambiguity_maps.entry(display_name.clone()).or_default().insert(mewto.to_owned()); + store.save_changes(&changes).await.unwrap(); } @@ -612,11 +634,6 @@ mod tests { assert!(!room.get_member(julie).await.unwrap().expect("Julie user").name_ambiguous()); assert!(!room.get_member(bob).await.unwrap().expect("Bob user").name_ambiguous()); - assert_eq!(room.get_member(me).await.unwrap().expect("Me user").display_name(), Some("Me")); - assert_eq!( - room.get_member(mewto).await.unwrap().expect("Mewto user").display_name(), - Some("Me") - ); assert!(room.get_member(me).await.unwrap().expect("Me user").name_ambiguous()); assert!(room.get_member(mewto).await.unwrap().expect("Mewto user").name_ambiguous()); From cd9e7e8a3152594b37765480291de1b53d4d6189 Mon Sep 17 00:00:00 2001 From: multi prise Date: Thu, 16 Oct 2025 14:27:16 +0200 Subject: [PATCH 6/9] Solved the membership check by creating an active_users variable in MemberRoomInfo --- crates/matrix-sdk-base/src/room/members.rs | 28 ++++++++++++---------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/crates/matrix-sdk-base/src/room/members.rs b/crates/matrix-sdk-base/src/room/members.rs index 3f415ae461c..2dc881ef9ed 100644 --- a/crates/matrix-sdk-base/src/room/members.rs +++ b/crates/matrix-sdk-base/src/room/members.rs @@ -179,10 +179,13 @@ impl Room { .transpose()? .map(|e| e.content.ignored_users.into_keys().collect()); + let active_users = self.store.get_user_ids(self.room_id(), RoomMemberships::ACTIVE).await?; + Ok(MemberRoomInfo { power_levels: power_levels.into(), max_power_level, users_display_names, + active_users, ignored_users, }) } @@ -211,18 +214,23 @@ impl RoomMember { presence: Option, room_info: &MemberRoomInfo<'_>, ) -> Self { - let MemberRoomInfo { power_levels, max_power_level, users_display_names, ignored_users } = - room_info; + let MemberRoomInfo { + power_levels, + max_power_level, + users_display_names, + ignored_users, + active_users, + } = room_info; let display_name = event.display_name(); - let membership = event.membership(); - println!("{:?} {:?}", users_display_names, &display_name); let display_name_ambiguous = users_display_names.get(&display_name).is_some_and(|s| { + // s.filter(|n| ) if !is_display_name_ambiguous(&display_name, s) { return false; } - matches!(*membership, MembershipState::Join | MembershipState::Invite) + //We check of many active_users with the same surname exist + active_users.iter().filter(|u| s.contains(*u)).count() > 1 }); let is_ignored = ignored_users.as_ref().is_some_and(|s| s.contains(event.user_id())); @@ -396,6 +404,7 @@ pub(crate) struct MemberRoomInfo<'a> { pub(crate) max_power_level: i64, pub(crate) users_display_names: HashMap<&'a DisplayName, BTreeSet>, pub(crate) ignored_users: Option>, + pub(crate) active_users: Vec, } /// The kind of room member updates that just happened. @@ -574,12 +583,9 @@ mod tests { .or_default() .entry(StateEventType::RoomMember) .or_default(); - - let ambiguity_maps = changes - .ambiguity_maps - .entry(room.room_id().to_owned()) - .or_default(); + let ambiguity_maps = + changes.ambiguity_maps.entry(room.room_id().to_owned()).or_default(); let display_name = DisplayName::new("Carol"); members.insert(carol.into(), f.member(carol).display_name("Carol").into()); @@ -618,7 +624,6 @@ mod tests { members.insert(mewto.into(), f.member(mewto).display_name("Me").into()); ambiguity_maps.entry(display_name.clone()).or_default().insert(mewto.to_owned()); - store.save_changes(&changes).await.unwrap(); } @@ -634,7 +639,6 @@ mod tests { assert!(!room.get_member(julie).await.unwrap().expect("Julie user").name_ambiguous()); assert!(!room.get_member(bob).await.unwrap().expect("Bob user").name_ambiguous()); - assert!(room.get_member(me).await.unwrap().expect("Me user").name_ambiguous()); assert!(room.get_member(mewto).await.unwrap().expect("Mewto user").name_ambiguous()); } From 2892f6064f042845735d5e17ed0269eaae3229c7 Mon Sep 17 00:00:00 2001 From: multi prise Date: Thu, 16 Oct 2025 14:34:32 +0200 Subject: [PATCH 7/9] Formating imports --- crates/matrix-sdk-base/src/room/members.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/matrix-sdk-base/src/room/members.rs b/crates/matrix-sdk-base/src/room/members.rs index 2dc881ef9ed..e63e3835042 100644 --- a/crates/matrix-sdk-base/src/room/members.rs +++ b/crates/matrix-sdk-base/src/room/members.rs @@ -495,13 +495,11 @@ pub fn normalize_power_level(power_level: Int, max_power_level: i64) -> Int { mod tests { use std::sync::Arc; - use proptest::prelude::*; - use matrix_sdk_test::{async_test, event_factory::EventFactory}; + use proptest::prelude::*; use ruma::{room_id, user_id}; use super::*; - use crate::{RoomState, StateChanges, StateStore, store::MemoryStore}; fn make_room_test_helper(room_type: RoomState) -> (Arc, Room) { From 4ca85024f5ef45143307abb718eb8ea74d34e699 Mon Sep 17 00:00:00 2001 From: multi prise Date: Tue, 6 Jan 2026 18:37:03 +0100 Subject: [PATCH 8/9] Add new get_active_users_with_display_name trait --- crates/matrix-sdk-base/src/store/traits.rs | 24 ++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/crates/matrix-sdk-base/src/store/traits.rs b/crates/matrix-sdk-base/src/store/traits.rs index fe0eed45d20..faa94d5bbd5 100644 --- a/crates/matrix-sdk-base/src/store/traits.rs +++ b/crates/matrix-sdk-base/src/store/traits.rs @@ -233,6 +233,30 @@ pub trait StateStore: AsyncTraitDeps { display_names: &'a [DisplayName], ) -> Result>, Self::Error>; + /// Get an event out of the account data store. + /// + /// # Arguments + /// + /// * `event_type` - The event type of the account data event. + async fn get_active_users_with_display_name( + &self, + room_id: &RoomId, + display_name: &DisplayName, + ) -> Result, Self::Error>; + + /// Get all the active users that use the given display names in the given room. + /// + /// # Arguments + /// + /// * `room_id` - The ID of the room to fetch the display names for. + /// + /// * `display_names` - The display names that the users use. + async fn get_active_users_with_display_names<'a>( + &self, + room_id: &RoomId, + display_names: &'a [DisplayName], + ) -> Result>, Self::Error>; + /// Get an event out of the account data store. /// /// # Arguments From b84b451144f02f48e9d28952dc0206d0bc4038cf Mon Sep 17 00:00:00 2001 From: multi prise Date: Tue, 6 Jan 2026 18:37:28 +0100 Subject: [PATCH 9/9] Add new get_active_users_with_display_name functions to sqlite store --- crates/matrix-sdk-sqlite/src/state_store.rs | 96 ++++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk-sqlite/src/state_store.rs b/crates/matrix-sdk-sqlite/src/state_store.rs index cca55782817..55a5a631bff 100644 --- a/crates/matrix-sdk-sqlite/src/state_store.rs +++ b/crates/matrix-sdk-sqlite/src/state_store.rs @@ -996,6 +996,30 @@ trait SqliteObjectStateStoreExt: SqliteAsyncConnExt { }) .await } + + async fn get_active_display_names( + &self, + room_id: Key, + names: Vec, + ) -> Result, Vec)>> { + let names_length = names.len(); + + self.chunk_large_query_over(names, Some(names_length), move |txn, names| { + let sql_params = repeat_vars(names.len()); + let sql = format!( + "SELECT name, data FROM display_name WHERE room_id = ? AND name IN ({sql_params}) AND membership = ?" + ); + + let params = rusqlite::params_from_iter(iter::once(room_id.clone()).chain(names).chain(iter::once(RoomMemberships::ACTIVE))); + + Ok(txn + .prepare(&sql)? + .query(params)? + .mapped(|row| Ok((row.get(0)?, row.get(1)?))) + .collect::>()?) + }) + .await + } async fn get_user_receipt( &self, @@ -1652,7 +1676,7 @@ impl StateStore for SqliteStateStore { .transpose()? .unwrap_or_default()) } - + async fn get_users_with_display_names<'a>( &self, room_id: &RoomId, @@ -1700,6 +1724,76 @@ impl StateStore for SqliteStateStore { Ok(result) } + async fn get_active_users_with_display_name( + &self, + room_id: &RoomId, + display_name: &DisplayName, + ) -> Result> { + let room_id = self.encode_key(keys::DISPLAY_NAME, room_id); + let names = vec![self.encode_key( + keys::DISPLAY_NAME, + display_name.as_normalized_str().unwrap_or_else(|| display_name.as_raw_str()), + )]; + + Ok(self + .read() + .await? + .get_active_display_names(room_id, names) + .await? + .into_iter() + .next() + .map(|(_, data)| self.deserialize_json(&data)) + .transpose()? + .unwrap_or_default()) + } + + async fn get_active_users_with_display_names<'a>( + &self, + room_id: &RoomId, + display_names: &'a [DisplayName], + ) -> Result>> { + let mut result = HashMap::new(); + + if display_names.is_empty() { + return Ok(result); + } + + let room_id = self.encode_key(keys::DISPLAY_NAME, room_id); + let mut names_map = display_names + .iter() + .flat_map(|display_name| { + // We encode the display name as the `raw_str()` and the normalized string. + // + // This is for compatibility reasons since: + // 1. Previously "Alice" and "alice" were considered to be distinct display + // names, while we now consider them to be the same so we need to merge the + // previously distinct buckets of user IDs. + // 2. We can't do a migration to merge the previously distinct buckets of user + // IDs since the display names itself are hashed before they are persisted + // in the store. + let raw = + (self.encode_key(keys::DISPLAY_NAME, display_name.as_raw_str()), display_name); + let normalized = display_name.as_normalized_str().map(|normalized| { + (self.encode_key(keys::DISPLAY_NAME, normalized), display_name) + }); + + iter::once(raw).chain(normalized) + }) + .collect::>(); + let names = names_map.keys().cloned().collect(); + + for (name, data) in self.read().await?.get_active_display_names(room_id, names).await?.into_iter() + { + let display_name = + names_map.remove(name.as_slice()).expect("returned display names were requested"); + let user_ids: BTreeSet<_> = self.deserialize_json(&data)?; + + result.entry(display_name).or_insert_with(BTreeSet::new).extend(user_ids); + } + + Ok(result) + } + async fn get_account_data_event( &self, event_type: GlobalAccountDataEventType,