Handle tap on user mentions (#1850)

* user mention routing implemented

* more tests

* better naming

* fixed a test
This commit is contained in:
Mauro
2023-10-03 10:11:05 +02:00
committed by GitHub
parent 1c23afba98
commit bceebff6cb
8 changed files with 169 additions and 37 deletions

View File

@@ -158,6 +158,8 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
} else {
navigationRootCoordinator.setSheetCoordinator(GenericCallLinkCoordinator(parameters: .init(url: url)))
}
case .roomMemberDetails:
userSessionFlowCoordinator?.handleAppRoute(route, animated: true)
default:
break
}

View File

@@ -21,6 +21,7 @@ enum AppRoute: Equatable {
case roomList
case room(roomID: String)
case roomDetails(roomID: String)
case roomMemberDetails(userID: String)
case invites
case genericCallLink(url: URL)
}
@@ -30,6 +31,7 @@ struct AppRouteURLParser {
init(appSettings: AppSettings) {
urlParsers = [
MatrixPermalinkParser(appSettings: appSettings),
OIDCCallbackURLParser(appSettings: appSettings),
ElementCallURLParser()
]
@@ -105,3 +107,17 @@ struct ElementCallURLParser: URLParser {
return .genericCallLink(url: url)
}
}
struct MatrixPermalinkParser: URLParser {
let appSettings: AppSettings
func route(from url: URL) -> AppRoute? {
switch PermalinkBuilder.detectPermalink(in: url, baseURL: appSettings.permalinkBaseURL) {
case .userIdentifier(let userID):
return .roomMemberDetails(userID: userID)
// Other cases will be handled in the future
default:
return nil
}
}
}

View File

@@ -81,6 +81,17 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
stateMachine.tryEvent(.presentRoomDetails(roomID: roomID), userInfo: EventUserInfo(animated: animated))
case .roomList:
stateMachine.tryEvent(.dismissRoom, userInfo: EventUserInfo(animated: animated))
case .roomMemberDetails(let userID):
Task {
switch await roomProxy?.getMember(userID: userID) {
case .success(let member):
stateMachine.tryEvent(.presentRoomMemberDetails(member: .init(value: member)))
case .failure(let error):
MXLog.error("[RoomFlowCoordinator] Failed to get member: \(error)")
case .none:
MXLog.error("[RoomFlowCoordinator] Failed to get member: RoomProxy is nil")
}
}
case .invites:
break
case .genericCallLink, .oidcCallback:

View File

@@ -115,7 +115,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
guard let self else { return }
switch appRoute {
case .room, .roomDetails, .roomList:
case .room, .roomDetails, .roomList, .roomMemberDetails:
self.roomFlowCoordinator.handleAppRoute(appRoute, animated: animated)
case .invites:
if UIDevice.current.isPhone {

View File

@@ -57,6 +57,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
let mutableAttributedString = NSMutableAttributedString(string: string)
addLinks(mutableAttributedString)
addAllUsersMention(mutableAttributedString)
detectPermalinks(mutableAttributedString)
removeLinkColors(mutableAttributedString)
@@ -110,6 +111,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
let mutableAttributedString = NSMutableAttributedString(attributedString: attributedString)
removeDefaultForegroundColor(mutableAttributedString)
addLinks(mutableAttributedString)
addAllUsersMention(mutableAttributedString)
replaceMarkedBlockquotes(mutableAttributedString)
replaceMarkedCodeBlocks(mutableAttributedString)
detectPermalinks(mutableAttributedString)
@@ -184,38 +186,42 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
let linkMatches = MatrixEntityRegex.linkRegex.matches(in: string, options: [])
matches.append(contentsOf: linkMatches)
if matches.count > 0 {
// Sort the links by length so the longest one always takes priority
matches.sorted { $0.range.length > $1.range.length }.forEach { match in
guard let matchRange = Range(match.range, in: string) else {
return
}
var hasLink = false
attributedString.enumerateAttribute(.link, in: match.range, options: []) { value, _, stop in
if value != nil {
hasLink = true
stop.pointee = true
}
}
if hasLink {
return
}
var link = String(string[matchRange])
if linkMatches.contains(match), !link.contains("://") {
link.insert(contentsOf: "https://", at: link.startIndex)
}
if let url = URL(string: link) {
attributedString.addAttribute(.link, value: url, range: match.range)
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 }.forEach { match in
guard let matchRange = Range(match.range, in: string) else {
return
}
var hasLink = false
attributedString.enumerateAttribute(.link, in: match.range, options: []) { value, _, stop in
if value != nil {
hasLink = true
stop.pointee = true
}
}
if hasLink {
return
}
var link = String(string[matchRange])
if linkMatches.contains(match), !link.contains("://") {
link.insert(contentsOf: "https://", at: link.startIndex)
}
if let url = URL(string: link) {
attributedString.addAttribute(.link, value: url, range: match.range)
}
}
MatrixEntityRegex.allUsersRegex.matches(in: string, options: []).forEach { match in
}
private func addAllUsersMention(_ attributedString: NSMutableAttributedString) {
MatrixEntityRegex.allUsersRegex.matches(in: attributedString.string, options: []).forEach { match in
if attributedString.attribute(.link, at: 0, longestEffectiveRange: nil, in: match.range) == nil {
attributedString.addAttribute(.MatrixAllUsersMention, value: true, range: match.range)
}

View File

@@ -37,13 +37,13 @@ struct MentionBuilder: MentionBuilderProtocol {
return
}
var attributesToAdd: [NSAttributedString.Key: Any] = [.link: url, .MatrixUserID: userID]
var attachmentAttributes: [NSAttributedString.Key: Any] = [.link: url, .MatrixUserID: userID]
if let blockquote {
// mentions can be in blockquotes, so if the replaced string was in one, we keep the attribute
attributesToAdd[.MatrixBlockquote] = blockquote
attachmentAttributes[.MatrixBlockquote] = blockquote
}
let attachmentString = NSMutableAttributedString(attachment: attachment)
attachmentString.addAttributes(attributes, range: NSRange(location: 0, length: attachmentString.length))
attachmentString.addAttributes(attachmentAttributes, range: NSRange(location: 0, length: attachmentString.length))
attributedString.replaceCharacters(in: range, with: attachmentString)
}
@@ -61,13 +61,13 @@ struct MentionBuilder: MentionBuilderProtocol {
return
}
var attributesToAdd: [NSAttributedString.Key: Any] = [:]
var attachmentAttributes: [NSAttributedString.Key: Any] = [:]
if let blockquote {
// mentions can be in blockquotes, so if the replaced string was in one, we keep the attribute
attributesToAdd[.MatrixBlockquote] = blockquote
attachmentAttributes[.MatrixBlockquote] = blockquote
}
let attachmentString = NSMutableAttributedString(attachment: attachment)
attachmentString.addAttributes(attributes, range: NSRange(location: 0, length: attachmentString.length))
attachmentString.addAttributes(attachmentAttributes, range: NSRange(location: 0, length: attachmentString.length))
attributedString.replaceCharacters(in: range, with: attachmentString)
}
}

View File

@@ -108,4 +108,16 @@ class AppRouteURLParserTests: XCTestCase {
// Then the route shouldn't be considered valid and should be ignored.
XCTAssertEqual(route, nil)
}
func testMatrixUserURL() {
let userID = "@test:matrix.org"
guard let url = URL(string: "\(appSettings.permalinkBaseURL)/#/\(userID)") else {
XCTFail("Invalid url")
return
}
let route = appRouteURLParser.route(from: url)
XCTAssertEqual(route, .roomMemberDetails(userID: userID))
}
}

View File

@@ -417,8 +417,40 @@ class AttributedStringBuilderTests: XCTestCase {
let string = "https://matrix.to/#/@test:matrix.org"
let attributedStringFromHTML = attributedStringBuilder.fromHTML(string)
XCTAssertNotNil(attributedStringFromHTML?.attachment)
XCTAssertNotNil(attributedStringFromHTML?.link)
let attributedStringFromPlain = attributedStringBuilder.fromPlain(string)
XCTAssertNotNil(attributedStringFromPlain?.attachment)
XCTAssertNotNil(attributedStringFromHTML?.link)
}
func testUserMentionAtachmentInBlockQuotes() {
let link = "https://matrix.to/#/@test:matrix.org"
let string = "<blockquote>hello \(link) how are you?</blockquote>"
guard let attributedStringFromHTML = attributedStringBuilder.fromHTML(string) else {
XCTFail("Attributed string is nil")
return
}
for run in attributedStringFromHTML.runs {
XCTAssertNotNil(run.blockquote)
}
checkAttachment(attributedString: attributedStringFromHTML, expectedRuns: 3)
checkLinkIn(attributedString: attributedStringFromHTML, expectedLink: link, expectedRuns: 3)
}
func testAllUsersMentionAtachmentInBlockQuotes() {
let string = "<blockquote>hello @room how are you?</blockquote>"
guard let attributedStringFromHTML = attributedStringBuilder.fromHTML(string) else {
XCTFail("Attributed string is nil")
return
}
for run in attributedStringFromHTML.runs {
XCTAssertNotNil(run.blockquote)
}
checkAttachment(attributedString: attributedStringFromHTML, expectedRuns: 3)
}
func testAllUsersMentionAttachment() {
@@ -464,12 +496,65 @@ class AttributedStringBuilderTests: XCTestCase {
XCTAssertNil(attributedStringFromHTML?.link)
}
func testUserMentionIsIgnoredInCode() {
let htmlString = "<pre><code>test https://matrix.org/#/@test:matrix.org test</code></pre>"
let attributedStringFromHTML = attributedStringBuilder.fromHTML(htmlString)
XCTAssert(attributedStringFromHTML?.runs.count == 1)
XCTAssertNil(attributedStringFromHTML?.attachment)
}
func testAllUsersIsIgnoredInCode() {
let htmlString = "<pre><code>test @room test</code></pre>"
let attributedStringFromHTML = attributedStringBuilder.fromHTML(htmlString)
XCTAssert(attributedStringFromHTML?.runs.count == 1)
XCTAssertNil(attributedStringFromHTML?.attachment)
}
func testMultipleMentions() {
guard let url = URL(string: "https://matrix.to/#/@test:matrix.org") else {
XCTFail("Invalid url")
return
}
let string = "Hello @room, but especially hello to you \(url)"
guard let attributedStringFromHTML = attributedStringBuilder.fromHTML(string) else {
XCTFail("Attributed string is nil")
return
}
var foundAttachments = 0
var foundLink: URL?
for run in attributedStringFromHTML.runs {
if run.attachment != nil {
foundAttachments += 1
}
if let link = run.link {
foundLink = link
}
}
XCTAssertEqual(foundLink, url)
XCTAssertEqual(foundAttachments, 2)
guard let attributedStringFromPlain = attributedStringBuilder.fromPlain(string) else {
XCTFail("Attributed string is nil")
return
}
foundAttachments = 0
foundLink = nil
for run in attributedStringFromPlain.runs {
if run.attachment != nil {
foundAttachments += 1
}
if let link = run.link {
foundLink = link
}
}
XCTAssertEqual(foundLink, url)
XCTAssertEqual(foundAttachments, 2)
}
// MARK: - Private
@@ -489,7 +574,7 @@ class AttributedStringBuilderTests: XCTestCase {
XCTFail("Couldn't find expected value.")
}
private func checkAttachment(attributedString: AttributedString?, expectedRuns: Int) {
private func checkAttachment(attributedString: AttributedString?, expectedRuns: Int, expectedAttachments: Int = 1) {
guard let attributedString else {
XCTFail("Could not build the attributed string")
return