From 29dfba140de597a7d80b23e9eaa9cee34dee8890 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 12 Jan 2026 15:46:42 +0200 Subject: [PATCH] Fix #4946 - Linkification strips trailing brackets from URLs --- ElementX/Sources/Other/Extensions/CharacterSet.swift | 6 ++++++ ElementX/Sources/Other/Extensions/String.swift | 7 ++++--- UnitTests/Sources/AttributedStringBuilderTests.swift | 11 ++++++----- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/ElementX/Sources/Other/Extensions/CharacterSet.swift b/ElementX/Sources/Other/Extensions/CharacterSet.swift index 5195a5746..516997b23 100644 --- a/ElementX/Sources/Other/Extensions/CharacterSet.swift +++ b/ElementX/Sources/Other/Extensions/CharacterSet.swift @@ -30,4 +30,10 @@ extension CharacterSet { static let matrixUserIDAllowedCharacters = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyz0123456789._=-/@:") static let roomAliasAllowedCharacters = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyz0123456789!$&‘()*+/;=?@[]-._:#") + + static let punctuationWithoutClosingBracketCharacters: CharacterSet = { + var baseSet: CharacterSet = .punctuationCharacters + baseSet.remove(")") + return baseSet + }() } diff --git a/ElementX/Sources/Other/Extensions/String.swift b/ElementX/Sources/Other/Extensions/String.swift index 8ccc51e3b..47a700d5d 100644 --- a/ElementX/Sources/Other/Extensions/String.swift +++ b/ElementX/Sources/Other/Extensions/String.swift @@ -125,10 +125,11 @@ extension String { link.insert(contentsOf: "https://", at: link.startIndex) } - // Don't include punctuation characters at the end of links - // e.g `https://element.io/blog:` <- which is a valid link but the wrong place + // Don't include punctuation characters at the end of links but keep + // closing brackets as per https://github.com/element-hq/element-x-ios/issues/4946 + // e.g `https://element.io/blog:` which is a valid link but the wrong place while !link.isEmpty, - link.rangeOfCharacter(from: .punctuationCharacters, options: .backwards)?.upperBound == link.endIndex { + link.rangeOfCharacter(from: .punctuationWithoutClosingBracketCharacters, options: .backwards)?.upperBound == link.endIndex { link = String(link.dropLast()) } diff --git a/UnitTests/Sources/AttributedStringBuilderTests.swift b/UnitTests/Sources/AttributedStringBuilderTests.swift index 405fa057a..660e3335d 100644 --- a/UnitTests/Sources/AttributedStringBuilderTests.swift +++ b/UnitTests/Sources/AttributedStringBuilderTests.swift @@ -80,7 +80,7 @@ class AttributedStringBuilderTests: XCTestCase { } func testPunctuationAtTheEndOfPlainStringLinks() { - let plainString = "This text contains a https://www.matrix.org:;., link." + let plainString = "Most punctuation marks are removed https://www.matrix.org:;., but closing brackets are kept https://example.com/(test)" guard let attributedString = attributedStringBuilder.fromPlain(plainString) else { XCTFail("Could not build the attributed string") @@ -89,11 +89,12 @@ class AttributedStringBuilderTests: XCTestCase { XCTAssertEqual(String(attributedString.characters), plainString) - XCTAssertEqual(attributedString.runs.count, 3) + XCTAssertEqual(attributedString.runs.count, 4) - let link = attributedString.runs.first { $0.link != nil }?.link - - XCTAssertEqual(link?.host, "www.matrix.org") + let firstLink = attributedString.runs.first { $0.link != nil }?.link + XCTAssertEqual(firstLink, "https://www.matrix.org") + let secondLink = attributedString.runs.last { $0.link != nil }?.link + XCTAssertEqual(secondLink, "https://example.com/(test)") } func testLinkDefaultScheme() {