diff --git a/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift b/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift index 49b03d5a7..ae4fbbc4c 100644 --- a/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift +++ b/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift @@ -149,7 +149,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { // Event identifiers and room aliases and identifiers detected in plain text are techincally incomplete // without via parameters and we won't bother detecting them - var matches: [TextParsingMatch] = MatrixEntityRegex.userIdentifierRegex.matches(in: string, options: []).compactMap { match in + var matches: [TextParsingMatch] = MatrixEntityRegex.userIdentifierRegex.matches(in: string).compactMap { match in guard let matchRange = Range(match.range, in: string) else { return nil } @@ -159,7 +159,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { return TextParsingMatch(type: .userID(identifier: identifier), range: match.range) } - matches.append(contentsOf: MatrixEntityRegex.roomAliasRegex.matches(in: string, options: []).compactMap { match in + matches.append(contentsOf: MatrixEntityRegex.roomAliasRegex.matches(in: string).compactMap { match in guard let matchRange = Range(match.range, in: string) else { return nil } @@ -169,7 +169,17 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { return TextParsingMatch(type: .roomAlias(alias: alias), range: match.range) }) - matches.append(contentsOf: MatrixEntityRegex.linkRegex.matches(in: string, options: []).compactMap { match in + matches.append(contentsOf: MatrixEntityRegex.uriRegex.matches(in: string).compactMap { match in + guard let matchRange = Range(match.range, in: string) else { + return nil + } + + let uri = String(string[matchRange]) + + return TextParsingMatch(type: .matrixURI(uri: uri), range: match.range) + }) + + matches.append(contentsOf: MatrixEntityRegex.linkRegex.matches(in: string).compactMap { match in guard let matchRange = Range(match.range, in: string) else { return nil } @@ -183,7 +193,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { return TextParsingMatch(type: .link(urlString: link), range: match.range) }) - matches.append(contentsOf: MatrixEntityRegex.allUsersRegex.matches(in: attributedString.string, options: []).map { match in + matches.append(contentsOf: MatrixEntityRegex.allUsersRegex.matches(in: attributedString.string).map { match in TextParsingMatch(type: .atRoom, range: match.range) }) @@ -217,6 +227,10 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { if let url = try? matrixToRoomAliasPermalink(roomAlias: alias) { attributedString.addAttribute(.link, value: url, range: match.range) } + case .matrixURI(let uri): + if let url = URL(string: uri) { + attributedString.addAttribute(.link, value: url, range: match.range) + } case .userID, .link: if let url = match.link { attributedString.addAttribute(.link, value: url, range: match.range) @@ -357,6 +371,7 @@ private struct TextParsingMatch { enum MatchType { case userID(identifier: String) case roomAlias(alias: String) + case matrixURI(uri: String) case link(urlString: String) case atRoom } diff --git a/ElementX/Sources/Other/MatrixEntityRegex.swift b/ElementX/Sources/Other/MatrixEntityRegex.swift index a5c503265..3959a88d6 100644 --- a/ElementX/Sources/Other/MatrixEntityRegex.swift +++ b/ElementX/Sources/Other/MatrixEntityRegex.swift @@ -22,6 +22,7 @@ enum MatrixEntityRegex: String { case homeserver case userID case roomAlias + case uri case allUsers var rawValue: String { @@ -32,6 +33,8 @@ enum MatrixEntityRegex: String { return "@[\\x21-\\x39\\x3B-\\x7F]+:" + MatrixEntityRegex.homeserver.rawValue case .roomAlias: return "#[A-Z0-9._%#@=+-]+:" + MatrixEntityRegex.homeserver.rawValue + case .uri: + return "matrix:(r|u|roomid)\\/[A-Z0-9\\-._~:/?#\\[\\]@!$&'()*+,;=%]*(?:\\?[A-Z0-9\\-._~:/?#\\[\\]@!$&'()*+,;=%]*)?" case .allUsers: return PillConstants.atRoom } @@ -41,6 +44,7 @@ enum MatrixEntityRegex: String { static var homeserverRegex = try! NSRegularExpression(pattern: MatrixEntityRegex.homeserver.rawValue, options: .caseInsensitive) static var userIdentifierRegex = try! NSRegularExpression(pattern: MatrixEntityRegex.userID.rawValue, options: .caseInsensitive) static var roomAliasRegex = try! NSRegularExpression(pattern: MatrixEntityRegex.roomAlias.rawValue, options: .caseInsensitive) + static var uriRegex = try! NSRegularExpression(pattern: MatrixEntityRegex.uri.rawValue, options: .caseInsensitive) static var allUsersRegex = try! NSRegularExpression(pattern: MatrixEntityRegex.allUsers.rawValue) static var linkRegex = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) // swiftlint:enable force_try @@ -69,6 +73,14 @@ enum MatrixEntityRegex: String { return match.range.length == alias.count } + static func isMatrixURI(_ uri: String) -> Bool { + guard let match = uriRegex.firstMatch(in: uri) else { + return false + } + + return match.range.length == uri.count + } + static func containsMatrixAllUsers(_ string: String) -> Bool { guard allUsersRegex.firstMatch(in: string) != nil else { return false diff --git a/UnitTests/Sources/AttributedStringBuilderTests.swift b/UnitTests/Sources/AttributedStringBuilderTests.swift index cf5916e69..dee091088 100644 --- a/UnitTests/Sources/AttributedStringBuilderTests.swift +++ b/UnitTests/Sources/AttributedStringBuilderTests.swift @@ -188,6 +188,12 @@ class AttributedStringBuilderTests: XCTestCase { checkLinkIn(attributedString: attributedStringBuilder.fromPlain(string), expectedLink: string, expectedRuns: 1) } + func testMatrixURI() { + let string = "matrix:roomid/hello:matrix.org/e/world?via=matrix.org" + checkLinkIn(attributedString: attributedStringBuilder.fromHTML(string), expectedLink: string, expectedRuns: 1) + checkLinkIn(attributedString: attributedStringBuilder.fromPlain(string), expectedLink: string, expectedRuns: 1) + } + func testUserIDLink() { let userID = "@user:matrix.org" let string = "The user is \(userID)." diff --git a/UnitTests/Sources/MatrixEntityRegexTests.swift b/UnitTests/Sources/MatrixEntityRegexTests.swift index ed2bef9af..175dcd073 100644 --- a/UnitTests/Sources/MatrixEntityRegexTests.swift +++ b/UnitTests/Sources/MatrixEntityRegexTests.swift @@ -39,6 +39,38 @@ class MatrixEntityRegexTests: XCTestCase { XCTAssertFalse(MatrixEntityRegex.isMatrixRoomAlias("#element-ios.matrix.org")) } + func testMatrixURI() { + // Users + XCTAssertTrue(MatrixEntityRegex.isMatrixURI("matrix:u/alice:example.org")) + XCTAssertTrue(MatrixEntityRegex.isMatrixURI("matrix:u/alice:example.org?action=chat")) + + // Room ID + XCTAssertTrue(MatrixEntityRegex.isMatrixURI("matrix:roomid/somewhere:example.org")) + XCTAssertTrue(MatrixEntityRegex.isMatrixURI("matrix:roomid/my-room:example.com?via=elsewhere.ca")) + XCTAssertTrue(MatrixEntityRegex.isMatrixURI("matrix:roomid/123_room:chat.myserver.net?via=elsewhere.ca&via=other.org")) + + // Room Alias + XCTAssertTrue(MatrixEntityRegex.isMatrixURI("matrix:r/general:matrix.org")) + XCTAssertTrue(MatrixEntityRegex.isMatrixURI("matrix:r/123_room:chat.myserver.net")) + + // Event + XCTAssertTrue(MatrixEntityRegex.isMatrixURI("matrix:roomid/somewhere:example.org/e/event")) + XCTAssertTrue(MatrixEntityRegex.isMatrixURI("matrix:roomid/my-room:example.com/e/message?via=elsewhere.ca")) + XCTAssertTrue(MatrixEntityRegex.isMatrixURI("matrix:roomid/123_room:chat.myserver.net/e/1234?via=elsewhere.ca&via=other.org")) + + // Inline + let string = "Hello matrix:u/alice:example.org how are you?" + XCTAssertFalse(MatrixEntityRegex.isMatrixURI("Hello matrix:u/alice:example.org how are you?")) + XCTAssertEqual(MatrixEntityRegex.uriRegex.matches(in: string).count, 1) + + // Invalid + XCTAssertFalse(MatrixEntityRegex.isMatrixURI("matrix://@alice:example.org")) + XCTAssertFalse(MatrixEntityRegex.isMatrixURI("matrix://!somewhere:example.org")) + XCTAssertFalse(MatrixEntityRegex.isMatrixURI("matrix://#general:matrix.org")) + XCTAssertFalse(MatrixEntityRegex.isMatrixURI("matrix:event/somewhere:example.org/e/event")) + XCTAssertFalse(MatrixEntityRegex.isMatrixURI("matrix:e/somewhere:example.org/e/event")) + } + func testAllUsers() { XCTAssertTrue(MatrixEntityRegex.containsMatrixAllUsers("@room")) XCTAssertTrue(MatrixEntityRegex.containsMatrixAllUsers("a@rooma")) diff --git a/changelog.d/2802.bugfix b/changelog.d/2802.bugfix new file mode 100644 index 000000000..c53ed3d19 --- /dev/null +++ b/changelog.d/2802.bugfix @@ -0,0 +1 @@ +Render matrix URIs as links so they can be tapped. \ No newline at end of file