Fixes #2706 - Use disambiguated display names for timeline item senders and state event bodies. (#2739)

This commit is contained in:
Stefan Ceriu
2024-04-25 14:53:18 +03:00
committed by GitHub
parent 6661c16154
commit 8d66572cc9
7 changed files with 84 additions and 50 deletions
@@ -78,11 +78,19 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
if shouldShowSenderDetails { if shouldShowSenderDetails {
HStack(alignment: .top, spacing: 4) { HStack(alignment: .top, spacing: 4) {
TimelineSenderAvatarView(timelineItem: timelineItem) TimelineSenderAvatarView(timelineItem: timelineItem)
Text(timelineItem.sender.displayName ?? timelineItem.sender.id) HStack(alignment: .center, spacing: 4) {
.font(.compound.bodySMSemibold) Text(timelineItem.sender.displayName ?? timelineItem.sender.id)
.foregroundColor(.compound.decorativeColor(for: timelineItem.sender.id).text) .font(.compound.bodySMSemibold)
.lineLimit(1) .foregroundColor(.compound.decorativeColor(for: timelineItem.sender.id).text)
.scaledPadding(.vertical, 3)
if timelineItem.sender.displayName != nil, timelineItem.sender.isDisplayNameAmbiguous {
Text(timelineItem.sender.id)
.font(.compound.bodyXS)
.foregroundColor(.compound.textSecondary)
}
}
.lineLimit(1)
.scaledPadding(.vertical, 3)
} }
// sender info are read inside the `TimelineAccessibilityModifier` // sender info are read inside the `TimelineAccessibilityModifier`
.accessibilityHidden(true) .accessibilityHidden(true)
@@ -712,8 +712,9 @@ class ClientProxy: ClientProxyProtocol {
let roomMessageEventStringBuilder = RoomMessageEventStringBuilder(attributedStringBuilder: AttributedStringBuilder(cacheKey: "roomList", let roomMessageEventStringBuilder = RoomMessageEventStringBuilder(attributedStringBuilder: AttributedStringBuilder(cacheKey: "roomList",
mentionBuilder: PlainMentionBuilder())) mentionBuilder: PlainMentionBuilder()))
let eventStringBuilder = RoomEventStringBuilder(stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID), let eventStringBuilder = RoomEventStringBuilder(stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID, shouldDisambiguateDisplayNames: false),
messageEventStringBuilder: roomMessageEventStringBuilder) messageEventStringBuilder: roomMessageEventStringBuilder,
shouldDisambiguateDisplayNames: false)
roomSummaryProvider = RoomSummaryProvider(roomListService: roomListService, roomSummaryProvider = RoomSummaryProvider(roomListService: roomListService,
eventStringBuilder: eventStringBuilder, eventStringBuilder: eventStringBuilder,
@@ -18,36 +18,35 @@ import Foundation
import MatrixRustSDK import MatrixRustSDK
struct RoomEventStringBuilder { struct RoomEventStringBuilder {
private let stateEventStringBuilder: RoomStateEventStringBuilder let stateEventStringBuilder: RoomStateEventStringBuilder
private let messageEventStringBuilder: RoomMessageEventStringBuilder let messageEventStringBuilder: RoomMessageEventStringBuilder
let shouldDisambiguateDisplayNames: Bool
init(stateEventStringBuilder: RoomStateEventStringBuilder, messageEventStringBuilder: RoomMessageEventStringBuilder) {
self.stateEventStringBuilder = stateEventStringBuilder
self.messageEventStringBuilder = messageEventStringBuilder
}
func buildAttributedString(for eventItemProxy: EventTimelineItemProxy) -> AttributedString? { func buildAttributedString(for eventItemProxy: EventTimelineItemProxy) -> AttributedString? {
let sender = eventItemProxy.sender let sender = eventItemProxy.sender
let isOutgoing = eventItemProxy.isOwn let isOutgoing = eventItemProxy.isOwn
let displayName = if shouldDisambiguateDisplayNames {
let senderDisplayName = sender.displayName ?? sender.id sender.disambiguatedDisplayName ?? sender.id
} else {
sender.displayName ?? sender.id
}
switch eventItemProxy.content.kind() { switch eventItemProxy.content.kind() {
case .unableToDecrypt: case .unableToDecrypt:
return prefix(L10n.commonDecryptionError, with: senderDisplayName) return prefix(L10n.commonDecryptionError, with: displayName)
case .redactedMessage: case .redactedMessage:
return prefix(L10n.commonMessageRemoved, with: senderDisplayName) return prefix(L10n.commonMessageRemoved, with: displayName)
case .sticker: case .sticker:
return prefix(L10n.commonSticker, with: senderDisplayName) return prefix(L10n.commonSticker, with: displayName)
case .failedToParseMessageLike, .failedToParseState: case .failedToParseMessageLike, .failedToParseState:
return prefix(L10n.commonUnsupportedEvent, with: senderDisplayName) return prefix(L10n.commonUnsupportedEvent, with: displayName)
case .message: case .message:
guard let messageContent = eventItemProxy.content.asMessage() else { guard let messageContent = eventItemProxy.content.asMessage() else {
fatalError("Invalid message timeline item: \(eventItemProxy)") fatalError("Invalid message timeline item: \(eventItemProxy)")
} }
let messageType = messageContent.msgtype() let messageType = messageContent.msgtype()
return messageEventStringBuilder.buildAttributedString(for: messageType, senderDisplayName: senderDisplayName, prefixWithSenderName: true) return messageEventStringBuilder.buildAttributedString(for: messageType, senderDisplayName: displayName, prefixWithSenderName: true)
case .state(_, let state): case .state(_, let state):
return stateEventStringBuilder return stateEventStringBuilder
.buildString(for: state, sender: sender, isOutgoing: isOutgoing) .buildString(for: state, sender: sender, isOutgoing: isOutgoing)
@@ -66,9 +65,9 @@ struct RoomEventStringBuilder {
memberIsYou: isOutgoing) memberIsYou: isOutgoing)
.map(AttributedString.init) .map(AttributedString.init)
case .poll(let question, _, _, _, _, _, _): case .poll(let question, _, _, _, _, _, _):
return prefix(L10n.commonPollSummary(question), with: senderDisplayName) return prefix(L10n.commonPollSummary(question), with: displayName)
case .callInvite: case .callInvite:
return prefix(L10n.commonCallInvite, with: senderDisplayName) return prefix(L10n.commonCallInvite, with: displayName)
} }
} }
@@ -100,13 +100,15 @@ class EventTimelineItemProxy {
let profile = item.senderProfile() let profile = item.senderProfile()
switch profile { switch profile {
case let .ready(displayName, _, avatarUrl): case let .ready(displayName, isDisplayNameAmbiguous, avatarUrl):
return .init(id: item.sender(), return .init(id: item.sender(),
displayName: displayName, displayName: displayName,
isDisplayNameAmbiguous: isDisplayNameAmbiguous,
avatarURL: avatarUrl.flatMap(URL.init(string:))) avatarURL: avatarUrl.flatMap(URL.init(string:)))
default: default:
return .init(id: item.sender(), return .init(id: item.sender(),
displayName: nil, displayName: nil,
isDisplayNameAmbiguous: false,
avatarURL: nil) avatarURL: nil)
} }
}() }()
@@ -21,11 +21,21 @@ struct TimelineItemSender: Identifiable, Hashable {
let id: String let id: String
let displayName: String? let displayName: String?
let isDisplayNameAmbiguous: Bool
let avatarURL: URL? let avatarURL: URL?
init(id: String, displayName: String? = nil, avatarURL: URL? = nil) { init(id: String, displayName: String? = nil, isDisplayNameAmbiguous: Bool = false, avatarURL: URL? = nil) {
self.id = id self.id = id
self.displayName = displayName self.displayName = displayName
self.isDisplayNameAmbiguous = isDisplayNameAmbiguous
self.avatarURL = avatarURL self.avatarURL = avatarURL
} }
var disambiguatedDisplayName: String? {
guard let displayName else {
return nil
}
return isDisplayNameAmbiguous ? "\(displayName) (\(id))" : displayName
}
} }
@@ -19,55 +19,63 @@ import UIKit
struct RoomStateEventStringBuilder { struct RoomStateEventStringBuilder {
let userID: String let userID: String
var shouldDisambiguateDisplayNames = true
func buildString(for change: MembershipChange?, member: String, sender: TimelineItemSender, isOutgoing: Bool) -> String? { func buildString(for change: MembershipChange?,
member: String,
sender: TimelineItemSender,
isOutgoing: Bool) -> String? {
guard let change else { guard let change else {
MXLog.verbose("Filtering timeline item for membership change that is nil") MXLog.verbose("Filtering timeline item for membership change that is nil")
return nil return nil
} }
let senderName = sender.displayName ?? sender.id
let senderIsYou = isOutgoing let senderIsYou = isOutgoing
let memberIsYou = member == userID let memberIsYou = member == userID
let displayName = if shouldDisambiguateDisplayNames {
sender.disambiguatedDisplayName ?? sender.id
} else {
sender.displayName ?? sender.id
}
switch change { switch change {
case .joined: case .joined:
return memberIsYou ? L10n.stateEventRoomJoinByYou : L10n.stateEventRoomJoin(member) return memberIsYou ? L10n.stateEventRoomJoinByYou : L10n.stateEventRoomJoin(displayName)
case .left: case .left:
return memberIsYou ? L10n.stateEventRoomLeaveByYou : L10n.stateEventRoomLeave(member) return memberIsYou ? L10n.stateEventRoomLeaveByYou : L10n.stateEventRoomLeave(displayName)
case .banned, .kickedAndBanned: case .banned, .kickedAndBanned:
return senderIsYou ? L10n.stateEventRoomBanByYou(member) : L10n.stateEventRoomBan(senderName, member) return senderIsYou ? L10n.stateEventRoomBanByYou(member) : L10n.stateEventRoomBan(displayName, member)
case .unbanned: case .unbanned:
return senderIsYou ? L10n.stateEventRoomUnbanByYou(member) : L10n.stateEventRoomUnban(senderName, member) return senderIsYou ? L10n.stateEventRoomUnbanByYou(member) : L10n.stateEventRoomUnban(displayName, member)
case .kicked: case .kicked:
return senderIsYou ? L10n.stateEventRoomRemoveByYou(member) : L10n.stateEventRoomRemove(senderName, member) return senderIsYou ? L10n.stateEventRoomRemoveByYou(member) : L10n.stateEventRoomRemove(displayName, member)
case .invited: case .invited:
if senderIsYou { if senderIsYou {
return L10n.stateEventRoomInviteByYou(member) return L10n.stateEventRoomInviteByYou(member)
} else if memberIsYou { } else if memberIsYou {
return L10n.stateEventRoomInviteYou(senderName) return L10n.stateEventRoomInviteYou(displayName)
} else { } else {
return L10n.stateEventRoomInvite(senderName, member) return L10n.stateEventRoomInvite(displayName, member)
} }
case .invitationAccepted: case .invitationAccepted:
return memberIsYou ? L10n.stateEventRoomInviteAcceptedByYou : L10n.stateEventRoomInviteAccepted(member) return memberIsYou ? L10n.stateEventRoomInviteAcceptedByYou : L10n.stateEventRoomInviteAccepted(displayName)
case .invitationRejected: case .invitationRejected:
return memberIsYou ? L10n.stateEventRoomRejectByYou : L10n.stateEventRoomReject(member) return memberIsYou ? L10n.stateEventRoomRejectByYou : L10n.stateEventRoomReject(displayName)
case .invitationRevoked: case .invitationRevoked:
return senderIsYou ? L10n.stateEventRoomThirdPartyRevokedInviteByYou(member) : L10n.stateEventRoomThirdPartyRevokedInvite(sender, member) return senderIsYou ? L10n.stateEventRoomThirdPartyRevokedInviteByYou(member) : L10n.stateEventRoomThirdPartyRevokedInvite(displayName, member)
case .knocked: case .knocked:
return memberIsYou ? L10n.stateEventRoomKnockByYou : L10n.stateEventRoomKnock(member) return memberIsYou ? L10n.stateEventRoomKnockByYou : L10n.stateEventRoomKnock(member)
case .knockAccepted: case .knockAccepted:
return senderIsYou ? L10n.stateEventRoomKnockAcceptedByYou(senderName) : L10n.stateEventRoomKnockAccepted(senderName, member) return senderIsYou ? L10n.stateEventRoomKnockAcceptedByYou(displayName) : L10n.stateEventRoomKnockAccepted(displayName, member)
case .knockRetracted: case .knockRetracted:
return memberIsYou ? L10n.stateEventRoomKnockRetractedByYou : L10n.stateEventRoomKnockRetracted(member) return memberIsYou ? L10n.stateEventRoomKnockRetractedByYou : L10n.stateEventRoomKnockRetracted(member)
case .knockDenied: case .knockDenied:
if senderIsYou { if senderIsYou {
return L10n.stateEventRoomKnockDeniedByYou(member) return L10n.stateEventRoomKnockDeniedByYou(member)
} else if memberIsYou { } else if memberIsYou {
return L10n.stateEventRoomKnockDeniedYou(senderName) return L10n.stateEventRoomKnockDeniedYou(displayName)
} else { } else {
return L10n.stateEventRoomKnockDenied(senderName, member) return L10n.stateEventRoomKnockDenied(displayName, member)
} }
case .none, .error, .notImplemented: // Not useful information for the user. case .none, .error, .notImplemented: // Not useful information for the user.
MXLog.verbose("Filtering timeline item for membership change: \(change)") MXLog.verbose("Filtering timeline item for membership change: \(change)")
@@ -122,30 +130,34 @@ struct RoomStateEventStringBuilder {
} }
func buildString(for state: OtherState, sender: TimelineItemSender, isOutgoing: Bool) -> String? { func buildString(for state: OtherState, sender: TimelineItemSender, isOutgoing: Bool) -> String? {
let senderName = sender.displayName ?? sender.id let displayName = if shouldDisambiguateDisplayNames {
sender.disambiguatedDisplayName ?? sender.id
} else {
sender.displayName ?? sender.id
}
switch state { switch state {
case .roomAvatar(let url): case .roomAvatar(let url):
switch (url, isOutgoing) { switch (url, isOutgoing) {
case (.some, false): case (.some, false):
return L10n.stateEventRoomAvatarChanged(senderName) return L10n.stateEventRoomAvatarChanged(displayName)
case (nil, false): case (nil, false):
return L10n.stateEventRoomAvatarRemoved(senderName) return L10n.stateEventRoomAvatarRemoved(displayName)
case (.some, true): case (.some, true):
return L10n.stateEventRoomAvatarChangedByYou return L10n.stateEventRoomAvatarChangedByYou
case (nil, true): case (nil, true):
return L10n.stateEventRoomAvatarRemovedByYou return L10n.stateEventRoomAvatarRemovedByYou
} }
case .roomCreate: case .roomCreate:
return isOutgoing ? L10n.stateEventRoomCreatedByYou : L10n.stateEventRoomCreated(senderName) return isOutgoing ? L10n.stateEventRoomCreatedByYou : L10n.stateEventRoomCreated(displayName)
case .roomEncryption: case .roomEncryption:
return L10n.commonEncryptionEnabled return L10n.commonEncryptionEnabled
case .roomName(let name): case .roomName(let name):
switch (name, isOutgoing) { switch (name, isOutgoing) {
case (.some(let name), false): case (.some(let name), false):
return L10n.stateEventRoomNameChanged(senderName, name) return L10n.stateEventRoomNameChanged(displayName, name)
case (nil, false): case (nil, false):
return L10n.stateEventRoomNameRemoved(senderName) return L10n.stateEventRoomNameRemoved(displayName)
case (.some(let name), true): case (.some(let name), true):
return L10n.stateEventRoomNameChangedByYou(name) return L10n.stateEventRoomNameChangedByYou(name)
case (nil, true): case (nil, true):
@@ -160,14 +172,14 @@ struct RoomStateEventStringBuilder {
if isOutgoing { if isOutgoing {
return L10n.stateEventRoomThirdPartyInviteByYou(displayName) return L10n.stateEventRoomThirdPartyInviteByYou(displayName)
} else { } else {
return L10n.stateEventRoomThirdPartyInvite(senderName, displayName) return L10n.stateEventRoomThirdPartyInvite(displayName, displayName)
} }
case .roomTopic(let topic): case .roomTopic(let topic):
switch (topic, isOutgoing) { switch (topic, isOutgoing) {
case (.some(let topic), false): case (.some(let topic), false):
return L10n.stateEventRoomTopicChanged(senderName, topic) return L10n.stateEventRoomTopicChanged(displayName, topic)
case (nil, false): case (nil, false):
return L10n.stateEventRoomTopicRemoved(senderName) return L10n.stateEventRoomTopicRemoved(displayName)
case (.some(let name), true): case (.some(let name), true):
return L10n.stateEventRoomTopicChangedByYou(name) return L10n.stateEventRoomTopicChangedByYou(name)
case (nil, true): case (nil, true):
@@ -626,13 +626,15 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
case let .ready(timelineItem, senderID, senderProfile): case let .ready(timelineItem, senderID, senderProfile):
let sender: TimelineItemSender let sender: TimelineItemSender
switch senderProfile { switch senderProfile {
case let .ready(displayName, _, avatarUrl): case let .ready(displayName, isDisplayNameAmbiguous, avatarUrl):
sender = TimelineItemSender(id: senderID, sender = TimelineItemSender(id: senderID,
displayName: displayName, displayName: displayName,
isDisplayNameAmbiguous: isDisplayNameAmbiguous,
avatarURL: avatarUrl.flatMap(URL.init(string:))) avatarURL: avatarUrl.flatMap(URL.init(string:)))
default: default:
sender = TimelineItemSender(id: senderID, sender = TimelineItemSender(id: senderID,
displayName: nil, displayName: nil,
isDisplayNameAmbiguous: false,
avatarURL: nil) avatarURL: nil)
} }