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