Correctly handle disambiguation for exiting members, refactor and test.
parent
765487dd9f
commit
5bd3c49afc
|
@ -147,6 +147,12 @@ pub struct Tombstone {
|
||||||
replacement: RoomId,
|
replacement: RoomId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
enum MemberDirection {
|
||||||
|
Entering,
|
||||||
|
Exiting,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||||
#[cfg_attr(test, derive(Clone))]
|
#[cfg_attr(test, derive(Clone))]
|
||||||
/// A Matrix room.
|
/// A Matrix room.
|
||||||
|
@ -304,8 +310,11 @@ impl Room {
|
||||||
|
|
||||||
/// Return the display name of the room.
|
/// Return the display name of the room.
|
||||||
pub fn display_name(&self) -> String {
|
pub fn display_name(&self) -> String {
|
||||||
self.room_name
|
self.room_name.calculate_name(
|
||||||
.calculate_name(&self.own_user_id, &self.invited_members, &self.joined_members)
|
&self.own_user_id,
|
||||||
|
&self.invited_members,
|
||||||
|
&self.joined_members,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Is the room a encrypted room.
|
/// Is the room a encrypted room.
|
||||||
|
@ -377,10 +386,12 @@ impl Room {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Perform display name disambiguations, if necessary.
|
// Perform display name disambiguations, if necessary.
|
||||||
if let Some(disambiguations) = self.disambiguate_member(&new_member, true) {
|
let disambiguations = self.disambiguation_updates(&new_member, MemberDirection::Entering);
|
||||||
for (id, name) in disambiguations.into_iter() {
|
for (id, name) in disambiguations.into_iter() {
|
||||||
self.disambiguated_display_names.insert(id, name);
|
match name {
|
||||||
}
|
None => self.disambiguated_display_names.remove(&id),
|
||||||
|
Some(name) => self.disambiguated_display_names.insert(id, name),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
|
@ -392,6 +403,16 @@ impl Room {
|
||||||
fn remove_member(&mut self, event: &MemberEvent) -> bool {
|
fn remove_member(&mut self, event: &MemberEvent) -> bool {
|
||||||
let leaving_member = RoomMember::new(event);
|
let leaving_member = RoomMember::new(event);
|
||||||
|
|
||||||
|
// Perform display name disambiguations, if necessary.
|
||||||
|
let disambiguations =
|
||||||
|
self.disambiguation_updates(&leaving_member, MemberDirection::Exiting);
|
||||||
|
for (id, name) in disambiguations.into_iter() {
|
||||||
|
match name {
|
||||||
|
None => self.disambiguated_display_names.remove(&id),
|
||||||
|
Some(name) => self.disambiguated_display_names.insert(id, name),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if self.joined_members.contains_key(&leaving_member.user_id) {
|
if self.joined_members.contains_key(&leaving_member.user_id) {
|
||||||
self.joined_members.remove(&leaving_member.user_id);
|
self.joined_members.remove(&leaving_member.user_id);
|
||||||
true
|
true
|
||||||
|
@ -403,25 +424,18 @@ impl Room {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Given a room member, generate a map of member display name disambiguations required in
|
/// Given a room `member`, return the list of members which have the same display name.
|
||||||
/// order to make everyone's display name unique.
|
|
||||||
///
|
///
|
||||||
/// The `inclusive` parameter controls whether the member which we are disambiguating should
|
/// The `inclusive` parameter controls whether the passed member should be included in the
|
||||||
/// be considered a member of the room or not.
|
/// list or not.
|
||||||
///
|
fn shares_displayname_with(&self, member: &RoomMember, inclusive: bool) -> Vec<UserId> {
|
||||||
/// Returns either the map of disambiguations or None if no disambiguations are necessary.
|
|
||||||
fn disambiguate_member(
|
|
||||||
&self,
|
|
||||||
member: &RoomMember,
|
|
||||||
inclusive: bool,
|
|
||||||
) -> Option<HashMap<UserId, String>> {
|
|
||||||
let members = self
|
let members = self
|
||||||
.invited_members
|
.invited_members
|
||||||
.iter()
|
.iter()
|
||||||
.chain(self.joined_members.iter());
|
.chain(self.joined_members.iter());
|
||||||
|
|
||||||
// Find all other users that share the same display name as the joining user.
|
// Find all other users that share the same display name as the joining user.
|
||||||
let users_with_same_name: Vec<UserId> = members
|
members
|
||||||
.filter(|(_, existing_member)| {
|
.filter(|(_, existing_member)| {
|
||||||
member
|
member
|
||||||
.display_name
|
.display_name
|
||||||
|
@ -438,45 +452,74 @@ impl Room {
|
||||||
.filter(|(id, _)| inclusive || **id != member.user_id)
|
.filter(|(id, _)| inclusive || **id != member.user_id)
|
||||||
.map(|(id, _)| id)
|
.map(|(id, _)| id)
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect();
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
// There is at least one other user with the same display name.
|
/// Given a room member, generate a map of all display name disambiguations which are necessary
|
||||||
if !users_with_same_name.is_empty() {
|
/// in order to make that member's display name unique.
|
||||||
if users_with_same_name.len() == 1 {
|
///
|
||||||
// The ambiguous set is now of size 1, so we can revert to simply using the display
|
/// The `inclusive` parameter controls whether or not the member for which we are
|
||||||
// name for this user.
|
/// disambiguating should be considered a current member of the room.
|
||||||
Some(
|
///
|
||||||
users_with_same_name
|
/// Returns a map from MXID to disambiguated name.
|
||||||
|
fn member_disambiguations(
|
||||||
|
&self,
|
||||||
|
member: &RoomMember,
|
||||||
|
inclusive: bool,
|
||||||
|
) -> HashMap<UserId, String> {
|
||||||
|
let users_with_same_name = self.shares_displayname_with(member, inclusive);
|
||||||
|
let disambiguate_with = |members: Vec<UserId>, f: fn(&RoomMember) -> String| {
|
||||||
|
members
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|id| {
|
.filter_map(|id| {
|
||||||
self.joined_members
|
self.joined_members
|
||||||
.get(&id)
|
.get(&id)
|
||||||
.or_else(|| self.invited_members.get(&id))
|
.or_else(|| self.invited_members.get(&id))
|
||||||
.map(|m| m.name())
|
.map(f)
|
||||||
.map(|m| (id, m))
|
.map(|m| (id, m))
|
||||||
})
|
})
|
||||||
.collect::<HashMap<UserId, String>>(),
|
.collect::<HashMap<UserId, String>>()
|
||||||
)
|
};
|
||||||
|
|
||||||
|
match users_with_same_name.len() {
|
||||||
|
0 => HashMap::new(),
|
||||||
|
1 => disambiguate_with(users_with_same_name, |m: &RoomMember| m.name()),
|
||||||
|
_ => disambiguate_with(users_with_same_name, |m: &RoomMember| m.unique_name()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate disambiguation updates needed when a room member either enters or exits.
|
||||||
|
fn disambiguation_updates(
|
||||||
|
&self,
|
||||||
|
member: &RoomMember,
|
||||||
|
when: MemberDirection,
|
||||||
|
) -> HashMap<UserId, Option<String>> {
|
||||||
|
let before;
|
||||||
|
let after;
|
||||||
|
|
||||||
|
match when {
|
||||||
|
MemberDirection::Entering => {
|
||||||
|
before = self.member_disambiguations(member, false);
|
||||||
|
after = self.member_disambiguations(member, true);
|
||||||
|
}
|
||||||
|
MemberDirection::Exiting => {
|
||||||
|
before = self.member_disambiguations(member, true);
|
||||||
|
after = self.member_disambiguations(member, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut res = before;
|
||||||
|
res.extend(after.clone());
|
||||||
|
|
||||||
|
res.into_iter()
|
||||||
|
.map(|(user_id, name)| {
|
||||||
|
if !after.contains_key(&user_id) {
|
||||||
|
(user_id, None)
|
||||||
} else {
|
} else {
|
||||||
// Disambiguate everyone in the list by using "DISPLAY_NAME (MXID)" as their
|
(user_id, Some(name))
|
||||||
// display name.
|
}
|
||||||
Some(
|
|
||||||
users_with_same_name
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|id| {
|
|
||||||
self.joined_members
|
|
||||||
.get(&id)
|
|
||||||
.or_else(|| self.invited_members.get(&id))
|
|
||||||
.map(|m| m.unique_name())
|
|
||||||
.map(|m| (id, m))
|
|
||||||
})
|
})
|
||||||
.collect::<HashMap<UserId, String>>(),
|
.collect()
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Nothing to disambiguate.
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add to the list of `RoomAliasId`s.
|
/// Add to the list of `RoomAliasId`s.
|
||||||
|
@ -550,7 +593,9 @@ impl Room {
|
||||||
// TODO: This would not be handled correctly as all the MemberEvents have the `prev_content`
|
// TODO: This would not be handled correctly as all the MemberEvents have the `prev_content`
|
||||||
// inside of `unsigned` field.
|
// inside of `unsigned` field.
|
||||||
match event.membership_change() {
|
match event.membership_change() {
|
||||||
Invited | Joined => self.add_member(event),
|
Invited | Joined => {
|
||||||
|
self.add_member(event)
|
||||||
|
}
|
||||||
Kicked | Banned | KickedAndBanned | InvitationRejected | Left => {
|
Kicked | Banned | KickedAndBanned | InvitationRejected | Left => {
|
||||||
self.remove_member(event)
|
self.remove_member(event)
|
||||||
}
|
}
|
||||||
|
@ -804,7 +849,8 @@ mod test {
|
||||||
let client = get_client().await;
|
let client = get_client().await;
|
||||||
let room_id = get_room_id();
|
let room_id = get_room_id();
|
||||||
let user_id1 = UserId::try_from("@example:localhost").unwrap();
|
let user_id1 = UserId::try_from("@example:localhost").unwrap();
|
||||||
let user_id2 = UserId::try_from("@someone_else:localhost").unwrap();
|
let user_id2 = UserId::try_from("@example2:localhost").unwrap();
|
||||||
|
let user_id3 = UserId::try_from("@example3:localhost").unwrap();
|
||||||
|
|
||||||
let member2_join_event = serde_json::json!({
|
let member2_join_event = serde_json::json!({
|
||||||
"content": {
|
"content": {
|
||||||
|
@ -815,17 +861,86 @@ mod test {
|
||||||
"event_id": "$16345217l517tabbz:localhost",
|
"event_id": "$16345217l517tabbz:localhost",
|
||||||
"membership": "join",
|
"membership": "join",
|
||||||
"origin_server_ts": 1455123234,
|
"origin_server_ts": 1455123234,
|
||||||
"sender": "@someone_else:localhost",
|
"sender": format!("{}", user_id2),
|
||||||
"state_key": "@someone_else:localhost",
|
"state_key": format!("{}", user_id2),
|
||||||
"type": "m.room.member",
|
"type": "m.room.member",
|
||||||
"unsigned": {
|
|
||||||
"age": 1989321234,
|
|
||||||
"replaces_state": "$1622a2311315tkjoA:localhost",
|
|
||||||
"prev_content": {
|
"prev_content": {
|
||||||
"avatar_url": null,
|
"avatar_url": null,
|
||||||
"displayname": "example",
|
"displayname": "example",
|
||||||
"membership": "invite"
|
"membership": "invite"
|
||||||
|
},
|
||||||
|
"unsigned": {
|
||||||
|
"age": 1989321234,
|
||||||
|
"replaces_state": "$1622a2311315tkjoA:localhost"
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let member2_leave_event = serde_json::json!({
|
||||||
|
"content": {
|
||||||
|
"avatar_url": null,
|
||||||
|
"displayname": "example",
|
||||||
|
"membership": "leave"
|
||||||
|
},
|
||||||
|
"event_id": "$263452333l22bggbz:localhost",
|
||||||
|
"membership": "leave",
|
||||||
|
"origin_server_ts": 1455123228,
|
||||||
|
"sender": format!("{}", user_id2),
|
||||||
|
"state_key": format!("{}", user_id2),
|
||||||
|
"type": "m.room.member",
|
||||||
|
"prev_content": {
|
||||||
|
"avatar_url": null,
|
||||||
|
"displayname": "example",
|
||||||
|
"membership": "join"
|
||||||
|
},
|
||||||
|
"unsigned": {
|
||||||
|
"age": 1989321221,
|
||||||
|
"replaces_state": "$16345217l517tabbz:localhost"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let member3_join_event = serde_json::json!({
|
||||||
|
"content": {
|
||||||
|
"avatar_url": null,
|
||||||
|
"displayname": "example",
|
||||||
|
"membership": "join"
|
||||||
|
},
|
||||||
|
"event_id": "$16845287981ktggba:localhost",
|
||||||
|
"membership": "join",
|
||||||
|
"origin_server_ts": 1455123244,
|
||||||
|
"sender": format!("{}", user_id3),
|
||||||
|
"state_key": format!("{}", user_id3),
|
||||||
|
"type": "m.room.member",
|
||||||
|
"prev_content": {
|
||||||
|
"avatar_url": null,
|
||||||
|
"displayname": "example",
|
||||||
|
"membership": "invite"
|
||||||
|
},
|
||||||
|
"unsigned": {
|
||||||
|
"age": 1989321254,
|
||||||
|
"replaces_state": "$1622l2323445kabrA:localhost"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let member3_leave_event = serde_json::json!({
|
||||||
|
"content": {
|
||||||
|
"avatar_url": null,
|
||||||
|
"displayname": "example",
|
||||||
|
"membership": "leave"
|
||||||
|
},
|
||||||
|
"event_id": "$11121987981abfgr:localhost",
|
||||||
|
"membership": "leave",
|
||||||
|
"origin_server_ts": 1455123230,
|
||||||
|
"sender": format!("{}", user_id3),
|
||||||
|
"state_key": format!("{}", user_id3),
|
||||||
|
"type": "m.room.member",
|
||||||
|
"prev_content": {
|
||||||
|
"avatar_url": null,
|
||||||
|
"displayname": "example",
|
||||||
|
"membership": "join"
|
||||||
|
},
|
||||||
|
"unsigned": {
|
||||||
|
"age": 1989321244,
|
||||||
|
"replaces_state": "$16845287981ktggba:localhost"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -839,6 +954,18 @@ mod test {
|
||||||
.add_custom_joined_event(&room_id, member2_join_event, RoomEvent::RoomMember)
|
.add_custom_joined_event(&room_id, member2_join_event, RoomEvent::RoomMember)
|
||||||
.build_sync_response();
|
.build_sync_response();
|
||||||
|
|
||||||
|
let mut member3_join_sync_response = event_builder
|
||||||
|
.add_custom_joined_event(&room_id, member3_join_event, RoomEvent::RoomMember)
|
||||||
|
.build_sync_response();
|
||||||
|
|
||||||
|
let mut member2_leave_sync_response = event_builder
|
||||||
|
.add_custom_joined_event(&room_id, member2_leave_event, RoomEvent::RoomMember)
|
||||||
|
.build_sync_response();
|
||||||
|
|
||||||
|
let mut member3_leave_sync_response = event_builder
|
||||||
|
.add_custom_joined_event(&room_id, member3_leave_event, RoomEvent::RoomMember)
|
||||||
|
.build_sync_response();
|
||||||
|
|
||||||
// First member with display name "example" joins
|
// First member with display name "example" joins
|
||||||
client
|
client
|
||||||
.receive_sync_response(&mut member1_join_sync_response)
|
.receive_sync_response(&mut member1_join_sync_response)
|
||||||
|
@ -854,21 +981,46 @@ mod test {
|
||||||
assert_eq!("example", display_name1);
|
assert_eq!("example", display_name1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Second member with display name "example" joins
|
// Second and third member with display name "example" join
|
||||||
client
|
client
|
||||||
.receive_sync_response(&mut member2_join_sync_response)
|
.receive_sync_response(&mut member2_join_sync_response)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
client
|
||||||
|
.receive_sync_response(&mut member3_join_sync_response)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// Both of their display names are now disambiguated with MXIDs
|
// All of their display names are now disambiguated with MXIDs
|
||||||
{
|
{
|
||||||
let room = client.get_joined_room(&room_id).await.unwrap();
|
let room = client.get_joined_room(&room_id).await.unwrap();
|
||||||
let room = room.read().await;
|
let room = room.read().await;
|
||||||
let display_name1 = room.member_display_name(&user_id1);
|
let display_name1 = room.member_display_name(&user_id1);
|
||||||
let display_name2 = room.member_display_name(&user_id2);
|
let display_name2 = room.member_display_name(&user_id2);
|
||||||
|
let display_name3 = room.member_display_name(&user_id3);
|
||||||
|
|
||||||
assert_eq!(format!("example ({})", user_id1), display_name1);
|
assert_eq!(format!("example ({})", user_id1), display_name1);
|
||||||
assert_eq!(format!("example ({})", user_id2), display_name2);
|
assert_eq!(format!("example ({})", user_id2), display_name2);
|
||||||
|
assert_eq!(format!("example ({})", user_id3), display_name3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second and third member leave. The first's display name is now just "example" again.
|
||||||
|
client
|
||||||
|
.receive_sync_response(&mut member2_leave_sync_response)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
client
|
||||||
|
.receive_sync_response(&mut member3_leave_sync_response)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
{
|
||||||
|
let room = client.get_joined_room(&room_id).await.unwrap();
|
||||||
|
let room = room.read().await;
|
||||||
|
|
||||||
|
let display_name1 = room.member_display_name(&user_id1);
|
||||||
|
|
||||||
|
assert_eq!("example", display_name1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue