Add permalink detection and custom attributed string attributes (#536)

* Pass full attributed string to TimelineItems and let the UI level handle blockquote coalescing
* Add permalink detection and attribute embedding within the attributed string
This commit is contained in:
Stefan Ceriu
2023-02-06 16:59:32 +02:00
committed by GitHub
parent 2734a18217
commit f8e6179933
38 changed files with 287 additions and 171 deletions
@@ -72,6 +72,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
let mutableAttributedString = NSMutableAttributedString(attributedString: attributedString)
removeDefaultForegroundColor(mutableAttributedString)
addLinks(mutableAttributedString)
detectPermalinks(mutableAttributedString)
removeLinkColors(mutableAttributedString)
replaceMarkedBlockquotes(mutableAttributedString)
replaceMarkedCodeBlocks(mutableAttributedString)
@@ -80,28 +81,6 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
return try? AttributedString(mutableAttributedString, including: \.elementX)
}
func blockquoteCoalescedComponentsFrom(_ attributedString: AttributedString?) -> [AttributedStringBuilderComponent]? {
guard let attributedString else {
return nil
}
return attributedString.runs[\.blockquote].map { value, range in
var attributedString = AttributedString(attributedString[range])
// Remove trailing new lines if any
if attributedString.characters.last?.isNewline ?? false,
let range = attributedString.range(of: "\n", options: .backwards, locale: nil) {
attributedString.removeSubrange(range)
}
let isBlockquote = value != nil
/// This is a temporary workaround until replies are retrieved from the SDK.
let isReply = isBlockquote && attributedString.characters.starts(with: "In reply to @")
return AttributedStringBuilderComponent(attributedString: attributedString, isBlockquote: isBlockquote, isReply: isReply)
}
}
// MARK: - Private
private func replaceMarkedBlockquotes(_ attributedString: NSMutableAttributedString) {
@@ -115,7 +94,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
return
}
attributedString.addAttribute(.MXBlockquote, value: true, range: range)
attributedString.addAttribute(.MatrixBlockquote, value: true, range: range)
}
attributedString.enumerateAttribute(.backgroundColor, in: .init(location: 0, length: attributedString.length), options: []) { value, range, _ in
@@ -125,7 +104,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
}
attributedString.removeAttribute(.backgroundColor, range: range)
attributedString.addAttribute(.MXBlockquote, value: true, range: range)
attributedString.addAttribute(.MatrixBlockquote, value: true, range: range)
}
}
@@ -188,6 +167,27 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
}
}
private func detectPermalinks(_ attributedString: NSMutableAttributedString) {
attributedString.enumerateAttribute(.link, in: .init(location: 0, length: attributedString.length), options: []) { value, range, _ in
if value != nil {
if let url = value as? URL {
switch PermalinkBuilder.detectPermalink(in: url) {
case .userIdentifier(let identifier):
attributedString.addAttributes([.MatrixUserID: identifier], range: range)
case .roomIdentifier(let identifier):
attributedString.addAttributes([.MatrixRoomID: identifier], range: range)
case .roomAlias(let alias):
attributedString.addAttributes([.MatrixRoomAlias: alias], range: range)
case .event(let roomIdentifier, let eventIdentifier):
attributedString.addAttributes([.MatrixEventID: EventIDAttributeValue(roomID: roomIdentifier, eventID: eventIdentifier)], range: range)
case .none:
break
}
}
}
}
}
private func removeDefaultForegroundColor(_ attributedString: NSMutableAttributedString) {
attributedString.enumerateAttribute(.foregroundColor, in: .init(location: 0, length: attributedString.length), options: []) { value, range, _ in
if value as? UIColor == UIColor.black {
@@ -241,5 +241,9 @@ extension UIColor {
extension NSAttributedString.Key {
static let DTTextBlocks: NSAttributedString.Key = .init(rawValue: DTTextBlocksAttribute)
static let MXBlockquote: NSAttributedString.Key = .init(rawValue: BlockquoteAttribute.name)
static let MatrixBlockquote: NSAttributedString.Key = .init(rawValue: BlockquoteAttribute.name)
static let MatrixUserID: NSAttributedString.Key = .init(rawValue: UserIDAttribute.name)
static let MatrixRoomID: NSAttributedString.Key = .init(rawValue: RoomIDAttribute.name)
static let MatrixRoomAlias: NSAttributedString.Key = .init(rawValue: RoomAliasAttribute.name)
static let MatrixEventID: NSAttributedString.Key = .init(rawValue: EventIDAttribute.name)
}
@@ -26,6 +26,4 @@ protocol AttributedStringBuilderProtocol {
func fromPlain(_ string: String?) -> AttributedString?
func fromHTML(_ htmlString: String?) -> AttributedString?
func blockquoteCoalescedComponentsFrom(_ attributedString: AttributedString?) -> [AttributedStringBuilderComponent]?
}
@@ -21,10 +21,40 @@ enum BlockquoteAttribute: AttributedStringKey {
public static var name = "MXBlockquoteAttribute"
}
enum UserIDAttribute: AttributedStringKey {
typealias Value = String
public static var name = "MXUserIDAttribute"
}
enum RoomIDAttribute: AttributedStringKey {
typealias Value = String
public static var name = "MXRoomIDAttribute"
}
enum RoomAliasAttribute: AttributedStringKey {
typealias Value = String
public static var name = "MXRoomAliasAttribute"
}
struct EventIDAttributeValue: Hashable {
let roomID: String
let eventID: String
}
enum EventIDAttribute: AttributedStringKey {
typealias Value = EventIDAttributeValue
public static var name = "MXEventIDAttribute"
}
extension AttributeScopes {
struct ElementXAttributes: AttributeScope {
let blockquote: BlockquoteAttribute
let userID: UserIDAttribute
let roomID: RoomIDAttribute
let roomAlias: RoomAliasAttribute
let eventID: EventIDAttribute
let swiftUI: SwiftUIAttributes
let uiKit: UIKitAttributes
}