diff --git a/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift b/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift index 40daeb689..bf4f5e772 100644 --- a/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift +++ b/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift @@ -176,31 +176,29 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { private func addLinksAndMentions(_ attributedString: NSMutableAttributedString) { let string = attributedString.string - var matches = MatrixEntityRegex.userIdentifierRegex.matches(in: string, options: []) - matches.append(contentsOf: MatrixEntityRegex.roomIdentifierRegex.matches(in: string, options: [])) + 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)) }) + // 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: [])) - matches.append(contentsOf: MatrixEntityRegex.roomAliasRegex.matches(in: string, options: [])) - let linkMatches = MatrixEntityRegex.linkRegex.matches(in: string, options: []) - matches.append(contentsOf: linkMatches) + matches.append(contentsOf: MatrixEntityRegex.roomAliasRegex.matches(in: string, options: []).map { TypedMatch(match: $0, type: .permalink(type: .roomAlias)) }) - let allUserMentionsMatches = MatrixEntityRegex.allUsersRegex.matches(in: attributedString.string, options: []) - matches.append(contentsOf: allUserMentionsMatches) + matches.append(contentsOf: MatrixEntityRegex.linkRegex.matches(in: string, options: []).map { TypedMatch(match: $0, type: .link) }) - let allUsersMentionsCount = allUserMentionsMatches.count + matches.append(contentsOf: MatrixEntityRegex.allUsersRegex.matches(in: attributedString.string, options: []).map { TypedMatch(match: $0, type: .atRoom) }) guard matches.count > 0 else { return } // Sort the links by length so the longest one always takes priority - matches.sorted { $0.range.length > $1.range.length }.enumerated().forEach { offset, match in - guard let matchRange = Range(match.range, in: string) else { + 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 } var hasLink = false - attributedString.enumerateAttribute(.link, in: match.range, options: []) { value, _, stop in + attributedString.enumerateAttribute(.link, in: typedMatch.match.range, options: []) { value, _, stop in if value != nil { hasLink = true stop.pointee = true @@ -211,17 +209,24 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { return } - if offset > matches.count - allUsersMentionsCount - 1 { - attributedString.addAttribute(.MatrixAllUsersMention, value: true, range: match.range) - } else { + switch typedMatch.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 linkMatches.contains(match), !link.contains("://") { + if !link.contains("://") { link.insert(contentsOf: "https://", at: link.startIndex) } if let url = URL(string: link) { - attributedString.addAttribute(.link, value: url, range: match.range) + attributedString.addAttribute(.link, value: url, range: typedMatch.match.range) } } } @@ -320,3 +325,31 @@ protocol MentionBuilderProtocol { func handleUserMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, userID: String) func handleAllUsersMention(for attributedString: NSMutableAttributedString, in range: NSRange) } + +private struct TypedMatch { + enum MatchType { + case permalink(type: MentionType) + case link + 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 match: NSTextCheckingResult + let type: MatchType +} diff --git a/UnitTests/Sources/AttributedStringBuilderTests.swift b/UnitTests/Sources/AttributedStringBuilderTests.swift index 303cf4baa..3a6d76925 100644 --- a/UnitTests/Sources/AttributedStringBuilderTests.swift +++ b/UnitTests/Sources/AttributedStringBuilderTests.swift @@ -18,8 +18,9 @@ import XCTest class AttributedStringBuilderTests: XCTestCase { - let attributedStringBuilder = AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL, mentionBuilder: MentionBuilder(mentionsEnabled: true)) - let maxHeaderPointSize = ceil(UIFont.preferredFont(forTextStyle: .body).pointSize * 1.2) + private let permalinkBaseURL = ServiceLocator.shared.settings.permalinkBaseURL + private lazy var attributedStringBuilder = AttributedStringBuilder(permalinkBaseURL: permalinkBaseURL, mentionBuilder: MentionBuilder(mentionsEnabled: true)) + private let maxHeaderPointSize = ceil(UIFont.preferredFont(forTextStyle: .body).pointSize * 1.2) func testRenderHTMLStringWithHeaders() { let h1HTMLString = "

Large Heading

" @@ -191,22 +192,25 @@ class AttributedStringBuilderTests: XCTestCase { func testUserIdLink() { let userId = "@user:matrix.org" let string = "The user is \(userId)." - checkLinkIn(attributedString: attributedStringBuilder.fromHTML(string), expectedLink: userId, expectedRuns: 3) - checkLinkIn(attributedString: attributedStringBuilder.fromPlain(string), expectedLink: userId, expectedRuns: 3) + let expectedLink = "\(permalinkBaseURL)/#/\(userId)" + checkLinkIn(attributedString: attributedStringBuilder.fromHTML(string), expectedLink: expectedLink, expectedRuns: 3) + checkLinkIn(attributedString: attributedStringBuilder.fromPlain(string), expectedLink: expectedLink, expectedRuns: 3) } func testRoomAliasLink() { let roomAlias = "#matrix:matrix.org" let string = "The room alias is \(roomAlias)." - checkLinkIn(attributedString: attributedStringBuilder.fromHTML(string), expectedLink: roomAlias, expectedRuns: 3) - checkLinkIn(attributedString: attributedStringBuilder.fromPlain(string), expectedLink: roomAlias, expectedRuns: 3) + let expectedLink = "https://matrix.to/#/%23matrix%3Amatrix.org" + checkLinkIn(attributedString: attributedStringBuilder.fromHTML(string), expectedLink: expectedLink, expectedRuns: 3) + checkLinkIn(attributedString: attributedStringBuilder.fromPlain(string), expectedLink: expectedLink, expectedRuns: 3) } func testRoomIdLink() { let roomId = "!roomidentifier:matrix.org" let string = "The room is \(roomId)." - checkLinkIn(attributedString: attributedStringBuilder.fromHTML(string), expectedLink: roomId, expectedRuns: 3) - checkLinkIn(attributedString: attributedStringBuilder.fromPlain(string), expectedLink: roomId, expectedRuns: 3) + let expectedLink = "https://matrix.to/#/!roomidentifier%3Amatrix.org" + checkLinkIn(attributedString: attributedStringBuilder.fromHTML(string), expectedLink: expectedLink, expectedRuns: 3) + checkLinkIn(attributedString: attributedStringBuilder.fromPlain(string), expectedLink: expectedLink, expectedRuns: 3) } // As of right now we do not handle event id links in any way so there is no need to add them as links