use disambiguated display name for avatar change events

This commit is contained in:
Mauro Romito
2026-09-08 10:45:50 +02:00
committed by Mauro
parent 15627e4512
commit 098edab1b4
4 changed files with 38 additions and 16 deletions
@@ -76,7 +76,8 @@ nonisolated struct RoomEventStringBuilder {
previousDisplayName: prevDisplayName, previousDisplayName: prevDisplayName,
avatarURLString: avatarUrl, avatarURLString: avatarUrl,
previousAvatarURLString: prevAvatarUrl, previousAvatarURLString: prevAvatarUrl,
member: sender.id, memberID: sender.id,
memberDisplayName: sender.disambiguatedDisplayName ?? sender.id,
memberIsYou: isOutgoing) memberIsYou: isOutgoing)
.map(AttributedString.init) .map(AttributedString.init)
case .callInvite: case .callInvite:
@@ -87,26 +87,39 @@ nonisolated struct RoomStateEventStringBuilder {
L10n.stateEventRoomInviteYou(senderDisplayName) L10n.stateEventRoomInviteYou(senderDisplayName)
} }
func buildProfileChangeString(displayName: String?, previousDisplayName: String?, /// - Parameters:
avatarURLString: String?, previousAvatarURLString: String?, /// - displayName: The new display name, only set by the SDK when the display name changed.
member: String, memberIsYou: Bool) -> String? { /// - previousDisplayName: The old display name, only set by the SDK when the display name changed.
/// - avatarURLString: The new avatar URL, only set by the SDK when the avatar changed.
/// - previousAvatarURLString: The old avatar URL, only set by the SDK when the avatar changed.
/// - memberID: The member's user ID, used in the display name change strings.
/// - memberDisplayName: The member's disambiguated display name, used when only the avatar changed.
/// - memberIsYou: Whether the member is the current user, which selects the "You" variant of the strings.
func buildProfileChangeString(displayName: String?,
previousDisplayName: String?,
avatarURLString: String?,
previousAvatarURLString: String?,
memberID: String,
memberDisplayName: String,
memberIsYou: Bool) -> String? {
let displayNameChanged = displayName != previousDisplayName let displayNameChanged = displayName != previousDisplayName
let avatarChanged = avatarURLString != previousAvatarURLString let avatarChanged = avatarURLString != previousAvatarURLString
switch (displayNameChanged, avatarChanged, memberIsYou) { switch (displayNameChanged, avatarChanged, memberIsYou) {
case (true, false, false): case (true, false, false):
if let displayName, let previousDisplayName { if let displayName, let previousDisplayName {
return L10n.stateEventDisplayNameChangedFrom(member, previousDisplayName, displayName) return L10n.stateEventDisplayNameChangedFrom(memberID, previousDisplayName, displayName)
} else if let displayName { } else if let displayName {
return L10n.stateEventDisplayNameSet(member, displayName) return L10n.stateEventDisplayNameSet(memberID, displayName)
} else if let previousDisplayName { } else if let previousDisplayName {
return L10n.stateEventDisplayNameRemoved(member, previousDisplayName) return L10n.stateEventDisplayNameRemoved(memberID, previousDisplayName)
} else { } else {
MXLog.error("The display name changed from nil to nil, filtering the item.") MXLog.error("The display name changed from nil to nil, filtering the item.")
return nil return nil
} }
case (false, true, false): case (false, true, false):
return L10n.stateEventAvatarUrlChanged(displayName ?? member) // The SDK only sets displayName when it changed, so it's nil here and we need the member's current name.
return L10n.stateEventAvatarUrlChanged(memberDisplayName)
case (true, false, true): case (true, false, true):
if let displayName, let previousDisplayName { if let displayName, let previousDisplayName {
return L10n.stateEventDisplayNameChangedFromByYou(previousDisplayName, displayName) return L10n.stateEventDisplayNameChangedFromByYou(previousDisplayName, displayName)
@@ -122,9 +135,13 @@ nonisolated struct RoomStateEventStringBuilder {
return L10n.stateEventAvatarUrlChangedByYou return L10n.stateEventAvatarUrlChangedByYou
case (true, true, _): case (true, true, _):
// When both have changed, get the string for the display name and tack on that the avatar changed too. // When both have changed, get the string for the display name and tack on that the avatar changed too.
guard let string = buildProfileChangeString(displayName: displayName, previousDisplayName: previousDisplayName, guard let string = buildProfileChangeString(displayName: displayName,
avatarURLString: nil, previousAvatarURLString: nil, previousDisplayName: previousDisplayName,
member: member, memberIsYou: memberIsYou) else { return nil } avatarURLString: nil,
previousAvatarURLString: nil,
memberID: memberID,
memberDisplayName: memberDisplayName,
memberIsYou: memberIsYou) else { return nil }
return string + "\n" + L10n.stateEventAvatarChangedToo return string + "\n" + L10n.stateEventAvatarChangedToo
case (false, false, _): case (false, false, _):
MXLog.error("Nothing changed, shouldn't be possible. Filtering the item.") MXLog.error("Nothing changed, shouldn't be possible. Filtering the item.")
@@ -948,7 +948,8 @@ nonisolated struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
previousDisplayName: previousDisplayName, previousDisplayName: previousDisplayName,
avatarURLString: avatarURLString, avatarURLString: avatarURLString,
previousAvatarURLString: previousAvatarURLString, previousAvatarURLString: previousAvatarURLString,
member: eventItemProxy.sender.id, memberID: eventItemProxy.sender.id,
memberDisplayName: eventItemProxy.sender.disambiguatedDisplayName ?? eventItemProxy.sender.id,
memberIsYou: isOutgoing) else { return nil } memberIsYou: isOutgoing) else { return nil }
return buildStateTimelineItem(for: eventItemProxy, text: text, isOutgoing: isOutgoing) return buildStateTimelineItem(for: eventItemProxy, text: text, isOutgoing: isOutgoing)
} }
@@ -47,7 +47,8 @@ struct RoomStateEventStringBuilderTests {
previousDisplayName: oldName, previousDisplayName: oldName,
avatarURLString: nil, avatarURLString: nil,
previousAvatarURLString: nil, previousAvatarURLString: nil,
member: sender.id, memberID: sender.id,
memberDisplayName: sender.disambiguatedDisplayName ?? sender.id,
memberIsYou: sender.id == userID) memberIsYou: sender.id == userID)
#expect(string == expectedString) #expect(string == expectedString)
} }
@@ -76,12 +77,14 @@ struct RoomStateEventStringBuilderTests {
private func validateAvatarChange(senderID: String, senderName: String? = nil, private func validateAvatarChange(senderID: String, senderName: String? = nil,
oldAvatarURL: String?, newAvatarURL: String?, oldAvatarURL: String?, newAvatarURL: String?,
expectedString: String) { expectedString: String) {
// The SDK only includes the display name when it changed, so it is nil for an avatar-only change.
let sender = TimelineItemSender(id: senderID, displayName: senderName) let sender = TimelineItemSender(id: senderID, displayName: senderName)
let string = stringBuilder.buildProfileChangeString(displayName: senderName, let string = stringBuilder.buildProfileChangeString(displayName: nil,
previousDisplayName: senderName, previousDisplayName: nil,
avatarURLString: newAvatarURL, avatarURLString: newAvatarURL,
previousAvatarURLString: oldAvatarURL, previousAvatarURLString: oldAvatarURL,
member: sender.id, memberID: sender.id,
memberDisplayName: sender.disambiguatedDisplayName ?? sender.id,
memberIsYou: sender.id == userID) memberIsYou: sender.id == userID)
#expect(string == expectedString) #expect(string == expectedString)
} }