116 lines
6.4 KiB
Swift
116 lines
6.4 KiB
Swift
//
|
|
// Copyright 2025 Element Creations Ltd.
|
|
// Copyright 2023-2025 New Vector Ltd.
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
// Please see LICENSE files in the repository root for full details.
|
|
//
|
|
|
|
import Foundation
|
|
import MatrixRustSDK
|
|
|
|
struct RoomEventStringBuilder {
|
|
let stateEventStringBuilder: RoomStateEventStringBuilder
|
|
let messageEventStringBuilder: RoomMessageEventStringBuilder
|
|
let shouldPrefixSenderName: Bool
|
|
|
|
func buildAttributedString(for eventItemProxy: EventTimelineItemProxy) -> AttributedString? {
|
|
buildAttributedString(for: eventItemProxy.content,
|
|
sender: eventItemProxy.sender,
|
|
isOutgoing: eventItemProxy.isOwn)
|
|
}
|
|
|
|
func buildAttributedString(for content: TimelineItemContent, sender: TimelineItemSender, isOutgoing: Bool) -> AttributedString? {
|
|
let displayName = sender.disambiguatedDisplayName ?? sender.id
|
|
|
|
switch content {
|
|
case .msgLike(let messageLikeContent):
|
|
switch messageLikeContent.kind {
|
|
case .message(let messageContent):
|
|
return messageEventStringBuilder.buildAttributedString(for: messageContent.msgType, senderDisplayName: displayName, isOutgoing: isOutgoing)
|
|
case .sticker:
|
|
if messageEventStringBuilder.style == .typeBolded {
|
|
var string = AttributedString(L10n.commonSticker)
|
|
string.bold()
|
|
return string
|
|
}
|
|
return prefix(L10n.commonSticker, with: displayName, isOutgoing: isOutgoing)
|
|
case .poll(let question, _, _, _, _, _, _):
|
|
if messageEventStringBuilder.style == .typeBolded {
|
|
let questionPlaceholder = "{question}"
|
|
var finalString = AttributedString(L10n.commonPollSummary(questionPlaceholder))
|
|
finalString.bold()
|
|
let normalString = AttributedString(question)
|
|
finalString.replace(questionPlaceholder, with: normalString)
|
|
return finalString
|
|
}
|
|
return prefix(L10n.commonPollSummary(question), with: displayName, isOutgoing: isOutgoing)
|
|
case .redacted:
|
|
return prefix(L10n.commonMessageRemoved, with: displayName, isOutgoing: isOutgoing)
|
|
case .unableToDecrypt(let encryptedMessage):
|
|
let errorMessage = switch encryptedMessage {
|
|
case .megolmV1AesSha2(_, .sentBeforeWeJoined): L10n.commonUnableToDecryptNoAccess
|
|
case .megolmV1AesSha2(_, .verificationViolation): L10n.commonUnableToDecryptVerificationViolation
|
|
case .megolmV1AesSha2(_, .unknownDevice), .megolmV1AesSha2(_, .unsignedDevice): L10n.commonUnableToDecryptInsecureDevice
|
|
default: L10n.commonWaitingForDecryptionKey
|
|
}
|
|
return prefix(errorMessage, with: displayName, isOutgoing: isOutgoing)
|
|
case .liveLocation:
|
|
return messageEventStringBuilder.buildAttributedStringForLiveLocation(senderDisplayName: displayName, isOutgoing: isOutgoing)
|
|
case .other:
|
|
return nil // We shouldn't receive these without asking for custom event types.
|
|
}
|
|
case .failedToParseMessageLike, .failedToParseState:
|
|
return prefix(L10n.commonUnsupportedEvent, with: displayName, isOutgoing: isOutgoing)
|
|
case .state(_, let state):
|
|
return stateEventStringBuilder
|
|
.buildString(for: state, sender: sender, isOutgoing: isOutgoing)
|
|
.map(AttributedString.init)
|
|
case .roomMembership(let userID, let displayName, let change, let reason):
|
|
return stateEventStringBuilder
|
|
.buildString(for: change, reason: reason, memberUserID: userID, memberDisplayName: displayName, sender: sender, isOutgoing: isOutgoing)
|
|
.map(AttributedString.init)
|
|
case .profileChange(let displayName, let prevDisplayName, let avatarUrl, let prevAvatarUrl):
|
|
return stateEventStringBuilder
|
|
.buildProfileChangeString(displayName: displayName,
|
|
previousDisplayName: prevDisplayName,
|
|
avatarURLString: avatarUrl,
|
|
previousAvatarURLString: prevAvatarUrl,
|
|
member: sender.id,
|
|
memberIsYou: isOutgoing)
|
|
.map(AttributedString.init)
|
|
case .callInvite:
|
|
return prefix(L10n.commonUnsupportedCall, with: displayName, isOutgoing: isOutgoing)
|
|
case .rtcNotification:
|
|
return prefix(L10n.commonCallStarted, with: displayName, isOutgoing: isOutgoing)
|
|
}
|
|
}
|
|
|
|
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(isOutgoing ? L10n.commonYou : senderDisplayName)
|
|
attributedSenderDisplayName.bold()
|
|
|
|
// Don't include the message body in the markdown otherwise it makes tappable links.
|
|
return attributedSenderDisplayName + ": " + attributedEventSummary
|
|
}
|
|
|
|
static func pinnedEventStringBuilder(userID: String) -> Self {
|
|
RoomEventStringBuilder(stateEventStringBuilder: .init(userID: userID),
|
|
messageEventStringBuilder: .init(attributedStringBuilder: AttributedStringBuilder(cacheKey: "pinnedEvents", mentionBuilder: PlainMentionBuilder()),
|
|
style: .typeBolded),
|
|
shouldPrefixSenderName: false)
|
|
}
|
|
|
|
static func threadListEventStringBuilder(userID: String) -> Self {
|
|
RoomEventStringBuilder(stateEventStringBuilder: .init(userID: userID),
|
|
messageEventStringBuilder: .init(attributedStringBuilder: AttributedStringBuilder(cacheKey: "threadList", mentionBuilder: PlainMentionBuilder()),
|
|
style: .plain),
|
|
shouldPrefixSenderName: false)
|
|
}
|
|
}
|