diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomEventStringBuilder.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomEventStringBuilder.swift index 5c85e309a..d3b69beb7 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomEventStringBuilder.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomEventStringBuilder.swift @@ -17,9 +17,7 @@ struct RoomEventStringBuilder { func buildAttributedString(for eventItemProxy: EventTimelineItemProxy) -> AttributedString? { let sender = eventItemProxy.sender let isOutgoing = eventItemProxy.isOwn - let displayName = if isOutgoing { - L10n.commonYou - } else if shouldDisambiguateDisplayNames { + let displayName = if shouldDisambiguateDisplayNames { sender.disambiguatedDisplayName ?? sender.id } else { sender.displayName ?? sender.id @@ -29,14 +27,14 @@ struct RoomEventStringBuilder { case .msgLike(let messageLikeContent): switch messageLikeContent.kind { case .message(let messageContent): - return messageEventStringBuilder.buildAttributedString(for: messageContent.msgType, senderDisplayName: displayName) + return messageEventStringBuilder.buildAttributedString(for: messageContent.msgType, senderDisplayName: displayName, isOutgoing: isOutgoing) case .sticker: if messageEventStringBuilder.destination == .pinnedEvent { var string = AttributedString(L10n.commonSticker) string.bold() return string } - return prefix(L10n.commonSticker, with: displayName) + return prefix(L10n.commonSticker, with: displayName, isOutgoing: isOutgoing) case .poll(let question, _, _, _, _, _, _): if messageEventStringBuilder.destination == .pinnedEvent { let questionPlaceholder = "{question}" @@ -46,9 +44,9 @@ struct RoomEventStringBuilder { finalString.replace(questionPlaceholder, with: normalString) return finalString } - return prefix(L10n.commonPollSummary(question), with: displayName) + return prefix(L10n.commonPollSummary(question), with: displayName, isOutgoing: isOutgoing) case .redacted: - return prefix(L10n.commonMessageRemoved, with: displayName) + return prefix(L10n.commonMessageRemoved, with: displayName, isOutgoing: isOutgoing) case .unableToDecrypt(let encryptedMessage): let errorMessage = switch encryptedMessage { case .megolmV1AesSha2(_, .sentBeforeWeJoined): L10n.commonUnableToDecryptNoAccess @@ -56,10 +54,10 @@ struct RoomEventStringBuilder { case .megolmV1AesSha2(_, .unknownDevice), .megolmV1AesSha2(_, .unsignedDevice): L10n.commonUnableToDecryptInsecureDevice default: L10n.commonWaitingForDecryptionKey } - return prefix(errorMessage, with: displayName) + return prefix(errorMessage, with: displayName, isOutgoing: isOutgoing) } case .failedToParseMessageLike, .failedToParseState: - return prefix(L10n.commonUnsupportedEvent, with: displayName) + return prefix(L10n.commonUnsupportedEvent, with: displayName, isOutgoing: isOutgoing) case .state(_, let state): return stateEventStringBuilder .buildString(for: state, sender: sender, isOutgoing: isOutgoing) @@ -78,19 +76,19 @@ struct RoomEventStringBuilder { memberIsYou: isOutgoing) .map(AttributedString.init) case .callInvite: - return prefix(L10n.commonUnsupportedCall, with: displayName) + return prefix(L10n.commonUnsupportedCall, with: displayName, isOutgoing: isOutgoing) case .callNotify: - return prefix(L10n.commonCallStarted, with: displayName) + return prefix(L10n.commonCallStarted, with: displayName, isOutgoing: isOutgoing) } } - private func prefix(_ eventSummary: String, with senderDisplayName: String) -> AttributedString { + private func prefix(_ eventSummary: String, with senderDisplayName: String, isOutgoing: Bool) -> AttributedString { guard shouldPrefixSenderName else { return AttributedString(eventSummary) } let attributedEventSummary = AttributedString(eventSummary.trimmingCharacters(in: .whitespacesAndNewlines)) - var attributedSenderDisplayName = AttributedString(senderDisplayName) + var attributedSenderDisplayName = AttributedString(isOutgoing ? L10n.commonYou : senderDisplayName) attributedSenderDisplayName.bold() // Don't include the message body in the markdown otherwise it makes tappable links. diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomMessageEventStringBuilder.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomMessageEventStringBuilder.swift index 62276d576..680c40043 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomMessageEventStringBuilder.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomMessageEventStringBuilder.swift @@ -24,7 +24,7 @@ struct RoomMessageEventStringBuilder { let attributedStringBuilder: AttributedStringBuilderProtocol let destination: Destination - func buildAttributedString(for messageType: MessageType, senderDisplayName: String) -> AttributedString { + func buildAttributedString(for messageType: MessageType, senderDisplayName: String, isOutgoing: Bool) -> AttributedString { let message: AttributedString switch messageType { case .emote(content: let content): @@ -69,7 +69,7 @@ struct RoomMessageEventStringBuilder { } if destination == .roomList { - return prefix(message, with: senderDisplayName) + return prefix(message, with: isOutgoing ? L10n.commonYou : senderDisplayName) } else { return message } diff --git a/NSE/Sources/NotificationContentBuilder.swift b/NSE/Sources/NotificationContentBuilder.swift index 8d629d82a..cb0e2608e 100644 --- a/NSE/Sources/NotificationContentBuilder.swift +++ b/NSE/Sources/NotificationContentBuilder.swift @@ -104,7 +104,7 @@ struct NotificationContentBuilder { var notification = try await processCommonRoomMessage(notificationItem: notificationItem, mediaProvider: mediaProvider) let displayName = notificationItem.senderDisplayName ?? notificationItem.roomDisplayName - notification.body = String(messageEventStringBuilder.buildAttributedString(for: messageType, senderDisplayName: displayName).characters) + notification.body = String(messageEventStringBuilder.buildAttributedString(for: messageType, senderDisplayName: displayName, isOutgoing: false).characters) guard settings.timelineMediaVisibility == .always || (settings.timelineMediaVisibility == .privateOnly && notificationItem.isRoomPrivate) diff --git a/UnitTests/Sources/RoomEventStringBuilderTests.swift b/UnitTests/Sources/RoomEventStringBuilderTests.swift index e6fee3cdb..ec38e06b8 100644 --- a/UnitTests/Sources/RoomEventStringBuilderTests.swift +++ b/UnitTests/Sources/RoomEventStringBuilderTests.swift @@ -26,43 +26,93 @@ class RoomEventStringBuilderTests: XCTestCase { } func testSenderPrefix() { - let ownMessageString = stringBuilder.buildAttributedString(for: makeTextMessageItem(senderID: ownUserID, senderDisplayName: "Alice")) - XCTAssertEqual(ownMessageString?.string, "You: Hello, World!", - "Your own messages should be prefixed with 'You'") + let ownMessageString = stringBuilder.buildAttributedString(for: makeMessageItem(senderID: ownUserID, senderDisplayName: "Alice")) + XCTAssertEqual(ownMessageString?.string, "You: Hello, World!", "Your own messages should be prefixed with 'You'") - let otherMessageString = stringBuilder.buildAttributedString(for: makeTextMessageItem(senderID: "@bob:matrix.org", senderDisplayName: "Bob")) - XCTAssertEqual(otherMessageString?.string, "Bob: Hello, World!", - "Everyone else's messages should be prefixed with their display name.") + let otherMessageString = stringBuilder.buildAttributedString(for: makeMessageItem(senderID: "@bob:matrix.org", senderDisplayName: "Bob")) + XCTAssertEqual(otherMessageString?.string, "Bob: Hello, World!", "Everyone else's messages should be prefixed with their display name.") - let ambiguousMessageString = stringBuilder.buildAttributedString(for: makeTextMessageItem(senderID: "@charlie:matrix.org", - senderDisplayName: "Charlie", - senderDisplayNameAmbiguous: true)) + let ambiguousMessageString = stringBuilder.buildAttributedString(for: makeMessageItem(senderID: "@charlie:matrix.org", + senderDisplayName: "Charlie", + senderDisplayNameAmbiguous: true)) XCTAssertEqual(ambiguousMessageString?.string, "Charlie (@charlie:matrix.org): Hello, World!", "Messages from senders with ambiguous display names should include their user ID in the prefix.") + + let ownEmoteString = stringBuilder.buildAttributedString(for: makeMessageItem(senderID: ownUserID, + senderDisplayName: "Alice", + type: .emote, + message: "laughs")) + XCTAssertEqual(ownEmoteString?.string, "* Alice laughs", "Your own emotes shouldn't contain 'You'") + + let otherEmoteString = stringBuilder.buildAttributedString(for: makeMessageItem(senderID: "@bob:matrix.org", + senderDisplayName: "Bob", + type: .emote, + message: "sighs")) + XCTAssertEqual(otherEmoteString?.string, "* Bob sighs", "Everyone else's emotes should contain their display name.") + + let ownPollString = stringBuilder.buildAttributedString(for: makePollItem(senderID: ownUserID, senderDisplayName: "Alice")) + XCTAssertEqual(ownPollString?.string, "You: Poll: Which is better?", "Your own polls should be prefixed with 'You'") + + let otherPollString = stringBuilder.buildAttributedString(for: makePollItem(senderID: "@bob:matrix.org", senderDisplayName: "Bob")) + XCTAssertEqual(otherPollString?.string, "Bob: Poll: Which is better?", "Everyone else's polls should be prefixed with their display name.") } // MARK: - Helpers - private func makeTextMessageItem(senderID: String, - senderDisplayName: String? = nil, - senderDisplayNameAmbiguous: Bool = false, - message: String = "Hello, World!") -> EventTimelineItemProxy { + private enum MockMessageType { case textMessage, emote } + + private func makeMessageItem(senderID: String, + senderDisplayName: String? = nil, + senderDisplayNameAmbiguous: Bool = false, + type: MockMessageType = .textMessage, + message: String = "Hello, World!") -> EventTimelineItemProxy { + let content = switch type { + case .textMessage: makeTextContent(message: message) + case .emote: makeEmoteContent(message: message) + } + + return .init(item: .init(configuration: .init(eventID: "1234", + sender: senderID, + senderProfile: .ready(displayName: senderDisplayName, displayNameAmbiguous: senderDisplayNameAmbiguous, avatarUrl: nil), + isOwn: senderID == ownUserID, + content: .msgLike(content: .init(kind: .message(content: .init(msgType: content, + body: message, + isEdited: false, + mentions: nil)), + reactions: [], + inReplyTo: nil, + threadRoot: nil, + threadSummary: nil)))), + uniqueID: .init("0")) + } + + private func makeTextContent(message: String) -> MessageType { + .text(content: .init(body: message, formatted: nil)) + } + + private func makeEmoteContent(message: String) -> MessageType { + .emote(content: .init(body: message, formatted: nil)) + } + + private func makePollItem(senderID: String, + senderDisplayName: String? = nil, + senderDisplayNameAmbiguous: Bool = false, + question: String = "Which is better?") -> EventTimelineItemProxy { .init(item: .init(configuration: .init(eventID: "1234", sender: senderID, senderProfile: .ready(displayName: senderDisplayName, displayNameAmbiguous: senderDisplayNameAmbiguous, avatarUrl: nil), isOwn: senderID == ownUserID, - content: .msgLike(content: .init(kind: .message(content: .init(msgType: makeTextContent(message: message), - body: message, - isEdited: false, - mentions: nil)), + content: .msgLike(content: .init(kind: .poll(question: question, + kind: .disclosed, + maxSelections: 1, + answers: [], + votes: [:], + endTime: nil, + hasBeenEdited: false), reactions: [], inReplyTo: nil, threadRoot: nil, threadSummary: nil)))), uniqueID: .init("0")) } - - private func makeTextContent(message: String) -> MessageType { - .text(content: .init(body: message, formatted: nil)) - } }