mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Adop the new SDK permalink detector and user builder
This commit is contained in:
committed by
Stefan Ceriu
parent
f8920d4d11
commit
fcc25dd441
@@ -17,12 +17,12 @@
|
||||
import DTCoreText
|
||||
import Foundation
|
||||
import LRUCache
|
||||
import MatrixRustSDK
|
||||
|
||||
struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
||||
private let cacheKey: String
|
||||
private let temporaryBlockquoteMarkingColor = UIColor.magenta
|
||||
private let temporaryCodeBlockMarkingColor = UIColor.cyan
|
||||
private let permalinkBaseURL: URL
|
||||
private let mentionBuilder: MentionBuilderProtocol
|
||||
|
||||
private static let defaultKey = "default"
|
||||
@@ -34,9 +34,8 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
||||
caches.removeAll()
|
||||
}
|
||||
|
||||
init(cacheKey: String = defaultKey, permalinkBaseURL: URL, mentionBuilder: MentionBuilderProtocol) {
|
||||
init(cacheKey: String = defaultKey, mentionBuilder: MentionBuilderProtocol) {
|
||||
self.cacheKey = cacheKey
|
||||
self.permalinkBaseURL = permalinkBaseURL
|
||||
self.mentionBuilder = mentionBuilder
|
||||
}
|
||||
|
||||
@@ -206,29 +205,45 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
||||
private func addLinksAndMentions(_ attributedString: NSMutableAttributedString) {
|
||||
let string = attributedString.string
|
||||
|
||||
var matches = MatrixEntityRegex.userIdentifierRegex.matches(in: string, options: []).map { TypedMatch(match: $0, type: .permalink(type: .userID)) }
|
||||
matches.append(contentsOf: MatrixEntityRegex.roomIdentifierRegex.matches(in: string, options: []).map { TypedMatch(match: $0, type: .permalink(type: .roomID)) })
|
||||
// Event identifiers and room aliases and identifiers detected in plain text are techincally incomplete
|
||||
// without via parameters and we won't bother detecting them
|
||||
|
||||
// As of right now we do not handle event id links in any way so there is no need to add them as links
|
||||
// matches.append(contentsOf: MatrixEntityRegex.eventIdentifierRegex.matches(in: string, options: []))
|
||||
var matches: [TextParsingMatch] = MatrixEntityRegex.userIdentifierRegex.matches(in: string, options: []).compactMap { match in
|
||||
guard let matchRange = Range(match.range, in: string) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let identifier = String(string[matchRange])
|
||||
|
||||
return TextParsingMatch(type: .userID(identifier: identifier), range: match.range)
|
||||
}
|
||||
|
||||
matches.append(contentsOf: MatrixEntityRegex.roomAliasRegex.matches(in: string, options: []).map { TypedMatch(match: $0, type: .permalink(type: .roomAlias)) })
|
||||
matches.append(contentsOf: MatrixEntityRegex.linkRegex.matches(in: string, options: []).compactMap { match in
|
||||
guard let matchRange = Range(match.range, in: string) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var link = String(string[matchRange])
|
||||
|
||||
if !link.contains("://") {
|
||||
link.insert(contentsOf: "https://", at: link.startIndex)
|
||||
}
|
||||
|
||||
return TextParsingMatch(type: .link(urlString: link), range: match.range)
|
||||
})
|
||||
|
||||
matches.append(contentsOf: MatrixEntityRegex.linkRegex.matches(in: string, options: []).map { TypedMatch(match: $0, type: .link) })
|
||||
|
||||
matches.append(contentsOf: MatrixEntityRegex.allUsersRegex.matches(in: attributedString.string, options: []).map { TypedMatch(match: $0, type: .atRoom) })
|
||||
matches.append(contentsOf: MatrixEntityRegex.allUsersRegex.matches(in: attributedString.string, options: []).map { match in
|
||||
TextParsingMatch(type: .atRoom, range: match.range)
|
||||
})
|
||||
|
||||
guard matches.count > 0 else {
|
||||
return
|
||||
}
|
||||
|
||||
// Sort the links by length so the longest one always takes priority
|
||||
matches.sorted { $0.match.range.length > $1.match.range.length }.forEach { [attributedString] typedMatch in
|
||||
guard let matchRange = Range(typedMatch.match.range, in: string) else {
|
||||
return
|
||||
}
|
||||
|
||||
matches.sorted { $0.range.length > $1.range.length }.forEach { [attributedString] match in
|
||||
var hasLink = false
|
||||
attributedString.enumerateAttribute(.link, in: typedMatch.match.range, options: []) { value, _, stop in
|
||||
attributedString.enumerateAttribute(.link, in: match.range, options: []) { value, _, stop in
|
||||
if value != nil {
|
||||
hasLink = true
|
||||
stop.pointee = true
|
||||
@@ -239,24 +254,12 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
||||
return
|
||||
}
|
||||
|
||||
switch typedMatch.type {
|
||||
switch match.type {
|
||||
case .atRoom:
|
||||
attributedString.addAttribute(.MatrixAllUsersMention, value: true, range: typedMatch.match.range)
|
||||
case let .permalink(type):
|
||||
let identifier = String(string[matchRange])
|
||||
|
||||
if let url = type.getPermalinkFrom(identifier: identifier, baseURL: permalinkBaseURL) {
|
||||
attributedString.addAttribute(.link, value: url, range: typedMatch.match.range)
|
||||
}
|
||||
case .link:
|
||||
var link = String(string[matchRange])
|
||||
|
||||
if !link.contains("://") {
|
||||
link.insert(contentsOf: "https://", at: link.startIndex)
|
||||
}
|
||||
|
||||
if let url = URL(string: link) {
|
||||
attributedString.addAttribute(.link, value: url, range: typedMatch.match.range)
|
||||
attributedString.addAttribute(.MatrixAllUsersMention, value: true, range: match.range)
|
||||
case .userID, .link:
|
||||
if let url = match.link {
|
||||
attributedString.addAttribute(.link, value: url, range: match.range)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -265,18 +268,18 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
||||
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, baseURL: permalinkBaseURL) {
|
||||
case .userIdentifier(let identifier):
|
||||
mentionBuilder.handleUserMention(for: attributedString, in: range, url: url, userID: identifier)
|
||||
case .roomIdentifier(let identifier):
|
||||
attributedString.addAttributes([.MatrixRoomID: identifier], range: range)
|
||||
if let url = value as? URL, let matrixEntity = parseMatrixEntityFrom(uri: url.absoluteString) {
|
||||
switch matrixEntity.id {
|
||||
case .user(let userID):
|
||||
mentionBuilder.handleUserMention(for: attributedString, in: range, url: url, userID: userID)
|
||||
case .room(let roomID):
|
||||
attributedString.addAttributes([.MatrixRoomID: roomID], 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
|
||||
case .eventOnRoomId(let roomID, let eventID):
|
||||
attributedString.addAttributes([.MatrixEventOnRoomID: EventOnRoomIDAttribute.Value(roomID: roomID, eventID: eventID)], range: range)
|
||||
case .eventOnRoomAlias(let alias, let eventID):
|
||||
attributedString.addAttributes([.MatrixEventOnRoomAlias: EventOnRoomAliasAttribute.Value(alias: alias, eventID: eventID)], range: range)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -347,7 +350,8 @@ extension NSAttributedString.Key {
|
||||
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)
|
||||
static let MatrixEventOnRoomID: NSAttributedString.Key = .init(rawValue: EventOnRoomIDAttribute.name)
|
||||
static let MatrixEventOnRoomAlias: NSAttributedString.Key = .init(rawValue: EventOnRoomAliasAttribute.name)
|
||||
static let MatrixAllUsersMention: NSAttributedString.Key = .init(rawValue: AllUsersMentionAttribute.name)
|
||||
}
|
||||
|
||||
@@ -356,30 +360,24 @@ protocol MentionBuilderProtocol {
|
||||
func handleAllUsersMention(for attributedString: NSMutableAttributedString, in range: NSRange)
|
||||
}
|
||||
|
||||
private struct TypedMatch {
|
||||
private struct TextParsingMatch {
|
||||
enum MatchType {
|
||||
case permalink(type: MentionType)
|
||||
case link
|
||||
case userID(identifier: String)
|
||||
case link(urlString: String)
|
||||
case atRoom
|
||||
}
|
||||
|
||||
enum MentionType {
|
||||
case roomAlias
|
||||
case roomID
|
||||
case userID
|
||||
|
||||
func getPermalinkFrom(identifier: String, baseURL: URL) -> URL? {
|
||||
switch self {
|
||||
case .roomAlias:
|
||||
return try? PermalinkBuilder.permalinkTo(roomAlias: identifier, baseURL: baseURL)
|
||||
case .roomID:
|
||||
return try? PermalinkBuilder.permalinkTo(roomIdentifier: identifier, baseURL: baseURL)
|
||||
case .userID:
|
||||
return try? PermalinkBuilder.permalinkTo(userIdentifier: identifier, baseURL: baseURL)
|
||||
}
|
||||
let type: MatchType
|
||||
let range: NSRange
|
||||
|
||||
var link: URL? {
|
||||
switch type {
|
||||
case .userID(let identifier):
|
||||
return try? URL(string: matrixToUserPermalink(userId: identifier))
|
||||
case .link(let urlString):
|
||||
return URL(string: urlString)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
let match: NSTextCheckingResult
|
||||
let type: MatchType
|
||||
}
|
||||
|
||||
@@ -36,21 +36,29 @@ enum RoomAliasAttribute: AttributedStringKey {
|
||||
static var name = "MXRoomAliasAttribute"
|
||||
}
|
||||
|
||||
enum EventOnRoomIDAttribute: AttributedStringKey {
|
||||
struct Value: Hashable {
|
||||
let roomID: String
|
||||
let eventID: String
|
||||
}
|
||||
|
||||
static var name = "MXEventOnRoomIDAttribute"
|
||||
}
|
||||
|
||||
enum EventOnRoomAliasAttribute: AttributedStringKey {
|
||||
struct Value: Hashable {
|
||||
let alias: String
|
||||
let eventID: String
|
||||
}
|
||||
|
||||
static var name = "MXEventOnRoomAliasAttribute"
|
||||
}
|
||||
|
||||
enum AllUsersMentionAttribute: AttributedStringKey {
|
||||
typealias Value = Bool
|
||||
static var name = "MXAllUsersMentionAttribute"
|
||||
}
|
||||
|
||||
struct EventIDAttributeValue: Hashable {
|
||||
let roomID: String
|
||||
let eventID: String
|
||||
}
|
||||
|
||||
enum EventIDAttribute: AttributedStringKey {
|
||||
typealias Value = EventIDAttributeValue
|
||||
static var name = "MXEventIDAttribute"
|
||||
}
|
||||
|
||||
// periphery: ignore - required to make NSAttributedString to AttributedString conversion even if not used directly
|
||||
extension AttributeScopes {
|
||||
struct ElementXAttributes: AttributeScope {
|
||||
@@ -59,7 +67,8 @@ extension AttributeScopes {
|
||||
let userID: UserIDAttribute
|
||||
let roomID: RoomIDAttribute
|
||||
let roomAlias: RoomAliasAttribute
|
||||
let eventID: EventIDAttribute
|
||||
let eventOnRoomID: EventOnRoomIDAttribute
|
||||
let eventOnRoomAlias: EventOnRoomAliasAttribute
|
||||
|
||||
let allUsersMention: AllUsersMentionAttribute
|
||||
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum PermalinkBuilderError: Error {
|
||||
case invalidUserIdentifier
|
||||
case invalidRoomIdentifier
|
||||
case invalidRoomAlias
|
||||
case invalidEventIdentifier
|
||||
case failedConstructingURL
|
||||
case failedAddingPercentEncoding
|
||||
}
|
||||
|
||||
enum PermalinkType: Equatable {
|
||||
case userIdentifier(String)
|
||||
case roomIdentifier(String)
|
||||
case roomAlias(String)
|
||||
case event(roomIdentifier: String, eventIdentifier: String)
|
||||
}
|
||||
|
||||
enum PermalinkBuilder {
|
||||
private static var uriComponentCharacterSet: CharacterSet = {
|
||||
var charset = CharacterSet.alphanumerics
|
||||
charset.insert(charactersIn: "-_.!~*'()")
|
||||
return charset
|
||||
}()
|
||||
|
||||
static func detectPermalink(in url: URL, baseURL: URL) -> PermalinkType? {
|
||||
guard url.absoluteString.hasPrefix(baseURL.absoluteString) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard var fragment = urlComponents.fragment else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if fragment.hasPrefix("/") {
|
||||
fragment = String(fragment.dropFirst(1))
|
||||
}
|
||||
|
||||
if let userIdentifierRange = MatrixEntityRegex.userIdentifierRegex.firstMatch(in: fragment)?.range {
|
||||
return .userIdentifier((fragment as NSString).substring(with: userIdentifierRange))
|
||||
}
|
||||
|
||||
if let roomAliasRange = MatrixEntityRegex.roomAliasRegex.firstMatch(in: fragment)?.range {
|
||||
return .roomAlias((fragment as NSString).substring(with: roomAliasRange))
|
||||
}
|
||||
|
||||
if let roomIdentifierRange = MatrixEntityRegex.roomIdentifierRegex.firstMatch(in: fragment)?.range {
|
||||
let roomIdentifier = (fragment as NSString).substring(with: roomIdentifierRange)
|
||||
|
||||
if let eventIdentifierRange = MatrixEntityRegex.eventIdentifierRegex.firstMatch(in: fragment)?.range {
|
||||
let eventIdentifier = (fragment as NSString).substring(with: eventIdentifierRange)
|
||||
return .event(roomIdentifier: roomIdentifier, eventIdentifier: eventIdentifier)
|
||||
}
|
||||
|
||||
return .roomIdentifier(roomIdentifier)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@available(*, deprecated, message: "Use a room's `matrixToPermalink` method instead")
|
||||
static func permalinkTo(userIdentifier: String, baseURL: URL) throws -> URL {
|
||||
guard MatrixEntityRegex.isMatrixUserIdentifier(userIdentifier) else {
|
||||
throw PermalinkBuilderError.invalidUserIdentifier
|
||||
}
|
||||
|
||||
let urlString = "\(baseURL)/#/\(userIdentifier)"
|
||||
|
||||
guard let url = URL(string: urlString) else {
|
||||
throw PermalinkBuilderError.failedConstructingURL
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
@available(*, deprecated, message: "Use a room's `matrixToPermalink` method instead")
|
||||
static func permalinkTo(roomIdentifier: String, baseURL: URL) throws -> URL {
|
||||
guard MatrixEntityRegex.isMatrixRoomIdentifier(roomIdentifier) else {
|
||||
throw PermalinkBuilderError.invalidRoomIdentifier
|
||||
}
|
||||
|
||||
return try permalinkTo(roomIdentifierOrAlias: roomIdentifier, baseURL: baseURL)
|
||||
}
|
||||
|
||||
@available(*, deprecated, message: "Use a room's `matrixToPermalink` method instead")
|
||||
static func permalinkTo(roomAlias: String, baseURL: URL) throws -> URL {
|
||||
guard MatrixEntityRegex.isMatrixRoomAlias(roomAlias) else {
|
||||
throw PermalinkBuilderError.invalidRoomAlias
|
||||
}
|
||||
|
||||
return try permalinkTo(roomIdentifierOrAlias: roomAlias, baseURL: baseURL)
|
||||
}
|
||||
|
||||
@available(*, deprecated, message: "Use a room's `matrixToEventPermalink` method instead")
|
||||
static func permalinkTo(eventIdentifier: String, roomIdentifier: String, baseURL: URL) throws -> URL {
|
||||
guard MatrixEntityRegex.isMatrixEventIdentifier(eventIdentifier) else {
|
||||
throw PermalinkBuilderError.invalidEventIdentifier
|
||||
}
|
||||
guard MatrixEntityRegex.isMatrixRoomIdentifier(roomIdentifier) else {
|
||||
throw PermalinkBuilderError.invalidRoomIdentifier
|
||||
}
|
||||
|
||||
guard let roomId = roomIdentifier.addingPercentEncoding(withAllowedCharacters: uriComponentCharacterSet),
|
||||
let eventId = eventIdentifier.addingPercentEncoding(withAllowedCharacters: uriComponentCharacterSet) else {
|
||||
throw PermalinkBuilderError.failedAddingPercentEncoding
|
||||
}
|
||||
|
||||
let urlString = "\(baseURL)/#/\(roomId)/\(eventId)"
|
||||
|
||||
guard let url = URL(string: urlString) else {
|
||||
throw PermalinkBuilderError.failedConstructingURL
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private static func permalinkTo(roomIdentifierOrAlias: String, baseURL: URL) throws -> URL {
|
||||
guard let identifier = roomIdentifierOrAlias.addingPercentEncoding(withAllowedCharacters: uriComponentCharacterSet) else {
|
||||
throw PermalinkBuilderError.failedAddingPercentEncoding
|
||||
}
|
||||
|
||||
let urlString = "\(baseURL)/#/\(identifier)"
|
||||
|
||||
guard let url = URL(string: urlString) else {
|
||||
throw PermalinkBuilderError.failedConstructingURL
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import MatrixRustSDK
|
||||
import SwiftUI
|
||||
|
||||
final class MessageTextView: UITextView, PillAttachmentViewProviderDelegate {
|
||||
@@ -129,8 +130,7 @@ struct MessageText: UIViewRepresentable {
|
||||
|
||||
final class Coordinator: NSObject, UITextViewDelegate {
|
||||
var openURLAction: OpenURLAction
|
||||
let permalinkBaseURL = ServiceLocator.shared.settings.permalinkBaseURL
|
||||
|
||||
|
||||
init(openURLAction: OpenURLAction) {
|
||||
self.openURLAction = openURLAction
|
||||
}
|
||||
@@ -146,7 +146,7 @@ struct MessageText: UIViewRepresentable {
|
||||
return false
|
||||
case .preview:
|
||||
// We don't want to show a URL preview for permalinks
|
||||
return PermalinkBuilder.detectPermalink(in: URL, baseURL: permalinkBaseURL) == nil
|
||||
return parseMatrixEntityFrom(uri: URL.absoluteString) == nil
|
||||
default:
|
||||
return true
|
||||
}
|
||||
@@ -157,7 +157,7 @@ struct MessageText: UIViewRepresentable {
|
||||
switch textItem.content {
|
||||
case let .link(url):
|
||||
// We don't want to show a URL preview for permalinks
|
||||
let isPermalink = PermalinkBuilder.detectPermalink(in: url, baseURL: permalinkBaseURL) != nil
|
||||
let isPermalink = parseMatrixEntityFrom(uri: url.absoluteString) != nil
|
||||
return .init(preview: isPermalink ? nil : .default, menu: defaultMenu)
|
||||
default:
|
||||
return nil
|
||||
@@ -197,7 +197,7 @@ struct MessageText_Previews: PreviewProvider, TestablePreview {
|
||||
|
||||
private static let htmlStringWithList = "<p>This is a list</p>\n<ul>\n<li>One</li>\n<li>Two</li>\n<li>And number 3</li>\n</ul>\n"
|
||||
|
||||
private static let attributedStringBuilder = AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL, mentionBuilder: MentionBuilder())
|
||||
private static let attributedStringBuilder = AttributedStringBuilder(mentionBuilder: MentionBuilder())
|
||||
|
||||
static var attachmentPreview: some View {
|
||||
MessageText(attributedString: attributedStringWithAttachment)
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import MatrixRustSDK
|
||||
import SwiftUI
|
||||
|
||||
struct MatrixUserShareLink<Label: View>: View {
|
||||
@@ -22,8 +23,7 @@ struct MatrixUserShareLink<Label: View>: View {
|
||||
|
||||
init(userID: String, @ViewBuilder label: () -> Label) {
|
||||
self.label = label()
|
||||
permalink = try? PermalinkBuilder.permalinkTo(userIdentifier: userID,
|
||||
baseURL: ServiceLocator.shared.settings.permalinkBaseURL)
|
||||
permalink = try? URL(string: matrixToUserPermalink(userId: userID))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
||||
Reference in New Issue
Block a user