TM-SGNL-iOS/SignalUI/Views/BodyRanges/Mention.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

287 lines
11 KiB
Swift

//
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
@objcMembers
public class Mention: NSObject {
public static let mentionPrefix = MessageBodyRanges.mentionPrefix
public static let mentionPrefixLength = (mentionPrefix as NSString).length
public static let attributeKey = NSAttributedString.Key.mention
// Each mention has a uniqueID so we can differentiate
// two mentions for the same address that are side-by-side
public let uniqueId = UUID().uuidString
public let address: SignalServiceAddress
public let style: Style
@objc(MentionStyle)
public enum Style: Int {
case incoming
case outgoing
case composingAttachment
case quotedReply
case longMessageView
case groupReply
public static var composing: Self = .incoming
}
public let text: String
public var length: Int { (text as NSString).length }
public class func withSneakyTransaction(address: SignalServiceAddress, style: Style) -> Mention {
databaseStorage.read { transaction in
Mention(address: address, style: style, transaction: transaction.unwrapGrdbRead)
}
}
public convenience init(address: SignalServiceAddress, style: Style, transaction: GRDBReadTransaction) {
let displayName = Self.contactsManager.displayName(
for: address,
transaction: transaction.asAnyRead
)
self.init(
address: address,
style: style,
text: Self.mentionPrefix + displayName
)
}
private init(address: SignalServiceAddress, style: Style, text: String) {
self.address = address
self.style = style
self.text = text
}
public var attributedString: NSAttributedString { NSAttributedString(string: text, attributes: attributes) }
public var attributes: [NSAttributedString.Key: Any] {
var attributes: [NSAttributedString.Key: Any] = [
.mention: self,
.font: UIFont.dynamicTypeBody
]
switch style {
case .incoming:
attributes[.backgroundColor] = Theme.isDarkThemeEnabled ? UIColor.ows_gray60 : UIColor.ows_gray20
attributes[.foregroundColor] = ConversationStyle.bubbleTextColorIncoming
case .outgoing:
attributes[.backgroundColor] = UIColor(white: 0, alpha: 0.25)
attributes[.foregroundColor] = ConversationStyle.bubbleTextColorOutgoing
case .composingAttachment:
attributes[.backgroundColor] = UIColor.ows_gray75
attributes[.foregroundColor] = Theme.darkThemePrimaryColor
case .quotedReply:
attributes[.backgroundColor] = nil
attributes[.foregroundColor] = Theme.primaryTextColor
case .longMessageView:
attributes[.backgroundColor] = Theme.isDarkThemeEnabled ? UIColor.ows_signalBlueDark : UIColor.ows_blackAlpha20
attributes[.foregroundColor] = Theme.primaryTextColor
case .groupReply:
attributes[.backgroundColor] = UIColor.ows_gray60
attributes[.foregroundColor] = UIColor.ows_gray05
}
return attributes
}
override public func isEqual(_ object: Any?) -> Bool {
guard let other = object as? Mention else { return false }
return other.uniqueId == uniqueId
}
override public var hash: Int { uniqueId.hashValue }
public class func threadAllowsMentionSend(_ thread: TSThread) -> Bool {
guard let groupThread = thread as? TSGroupThread else { return false }
return groupThread.groupModel.groupsVersion == .V2
}
@objc(refreshAttributedInMutableAttributedString:)
public class func refreshAttributes(in mutableAttributedString: NSMutableAttributedString) {
mutableAttributedString.enumerateMentionsAndStyles { mention, _, subrange, _ in
guard let mention = mention else { return }
mutableAttributedString.addAttributes(mention.attributes, range: subrange)
}
}
@objc(updateWithStyle:inMutableAttributedString:)
public class func updateWithStyle(_ style: Style, in mutableAttributedString: NSMutableAttributedString) {
mutableAttributedString.enumerateMentionsAndStyles { mention, _, subrange, _ in
guard let mention = mention else { return }
let restyledMention = Mention(address: mention.address, style: style, text: mention.text)
mutableAttributedString.addAttributes(restyledMention.attributes, range: subrange)
}
}
}
extension NSAttributedString.Key {
public static let mention = NSAttributedString.Key("Mention")
}
extension MessageBody {
public convenience init(attributedString: NSAttributedString) {
var mentions = [NSRange: UUID]()
var styles = [(NSRange, MessageBodyRanges.Style)]()
let filteredAttributedString = attributedString.filterForDisplay
let mutableAttributedString = NSMutableAttributedString(attributedString: filteredAttributedString)
mutableAttributedString.enumerateMentionsAndStyles { mention, style, subrange, _ in
if let mention {
// This string may not be a full mention, for example we may
// have copied a string that only selects part of a mention.
// We only want to treat it as a mention if we have the full
// thing.
guard subrange.length == mention.length else { return }
mutableAttributedString.replaceCharacters(in: subrange, with: Self.mentionPlaceholder)
let placeholderRange = NSRange(
location: subrange.location,
length: (Self.mentionPlaceholder as NSString).length
)
mentions[placeholderRange] = mention.address.uuid
}
if let style {
styles.append((subrange, style))
}
}
self.init(text: mutableAttributedString.string, ranges: .init(mentions: mentions, styles: styles))
}
public func textValue(style: Mention.Style,
attributes: [NSAttributedString.Key: Any],
shouldResolveAddress: (SignalServiceAddress) -> Bool,
transaction: GRDBReadTransaction) -> CVTextValue {
ranges.textValue(text: text,
style: style,
attributes: attributes,
shouldResolveAddress: shouldResolveAddress,
transaction: transaction)
}
@objc
public func attributedBody(
style: Mention.Style,
attributes: [NSAttributedString.Key: Any],
shouldResolveAddress: (SignalServiceAddress) -> Bool,
transaction: GRDBReadTransaction
) -> NSAttributedString {
return ranges.attributedBody(
text: text,
style: style,
attributes: attributes,
shouldResolveAddress: shouldResolveAddress,
transaction: transaction
)
}
}
extension MessageBodyRanges {
public func textValue(text: String,
style: Mention.Style,
attributes: [NSAttributedString.Key: Any],
shouldResolveAddress: (SignalServiceAddress) -> Bool,
transaction: GRDBReadTransaction) -> CVTextValue {
guard hasRanges || !attributes.isEmpty else {
return .text(text: text)
}
let attributedText = attributedBody(text: text,
style: style,
attributes: attributes,
shouldResolveAddress: shouldResolveAddress,
transaction: transaction)
return .attributedText(attributedText: attributedText)
}
/// Note: this method does _not_ apply styles; instead it just sets the `.owsStyle`
/// attribute for any styles, and it is up to callers to apply those styles using
/// `MessageBodyRanges.applyStyleAttributes` with the appropriate font and themed color.
@objc
public func attributedBody(
text: String,
style: Mention.Style,
attributes: [NSAttributedString.Key: Any],
shouldResolveAddress: (SignalServiceAddress) -> Bool,
transaction: GRDBReadTransaction
) -> NSAttributedString {
guard hasRanges else { return NSAttributedString(string: text, attributes: attributes) }
var mentions = [UUID: Mention]()
for (_, uuid) in orderedMentions {
mentions[uuid] = mentions[uuid] ?? Mention(
address: SignalServiceAddress(uuid: uuid),
style: style,
transaction: transaction
)
}
let mutableText = NSMutableAttributedString(string: text, attributes: attributes)
self.hydrateMentionsAndSetStyleAttributes(
on: mutableText,
hydrator: { mentionUuid in
guard let mention = mentions[mentionUuid] else {
return .preserveMention
}
if shouldResolveAddress(mention.address) {
return .hydrateAttributed(mention.attributedString, alreadyIncludesPrefix: true)
} else {
return .hydrate(mention.text, alreadyIncludesPrefix: true)
}
}
)
return NSAttributedString(attributedString: mutableText)
}
}
extension NSAttributedString {
public func enumerateMentionsAndStyles(
in range: NSRange? = nil,
handler: (Mention?, MessageBodyRanges.Style?, NSRange, UnsafeMutablePointer<ObjCBool>) -> Void
) {
enumerateAttributes(
in: range ?? NSRange(location: 0, length: length),
options: []
) { attributes, range, stop in
let mention = attributes[.mention] as? Mention
let style = attributes[.owsStyle] as? MessageBodyRanges.Style
handler(mention, style, range, stop)
}
}
// This is private because it's *only* safe to use on mention/style attributed strings.
fileprivate var filterForDisplay: NSAttributedString {
guard length > 0 else { return self }
if string.ows_stripped().isEmpty { return NSAttributedString(string: "") }
let mutableString = NSMutableAttributedString(attributedString: self)
// Filter each non-mention, non-style substring
mutableString.enumerateMentionsAndStyles { mention, style, subrange, _ in
guard mention == nil && style == nil else { return }
let string = mutableString.attributedSubstring(from: subrange).string
let attributes = mutableString.attributes(at: subrange.location, effectiveRange: nil)
mutableString.replaceCharacters(
in: subrange,
with: NSAttributedString(string: string.filterSubstringForDisplay(), attributes: attributes)
)
}
// Strip the resulting string
mutableString.ows_strip()
return NSAttributedString(attributedString: mutableString)
}
}