mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-06 04:18:31 +00:00
Handle tap on user mentions (#1850)
* user mention routing implemented * more tests * better naming * fixed a test
This commit is contained in:
@@ -158,6 +158,8 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
|
|||||||
} else {
|
} else {
|
||||||
navigationRootCoordinator.setSheetCoordinator(GenericCallLinkCoordinator(parameters: .init(url: url)))
|
navigationRootCoordinator.setSheetCoordinator(GenericCallLinkCoordinator(parameters: .init(url: url)))
|
||||||
}
|
}
|
||||||
|
case .roomMemberDetails:
|
||||||
|
userSessionFlowCoordinator?.handleAppRoute(route, animated: true)
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ enum AppRoute: Equatable {
|
|||||||
case roomList
|
case roomList
|
||||||
case room(roomID: String)
|
case room(roomID: String)
|
||||||
case roomDetails(roomID: String)
|
case roomDetails(roomID: String)
|
||||||
|
case roomMemberDetails(userID: String)
|
||||||
case invites
|
case invites
|
||||||
case genericCallLink(url: URL)
|
case genericCallLink(url: URL)
|
||||||
}
|
}
|
||||||
@@ -30,6 +31,7 @@ struct AppRouteURLParser {
|
|||||||
|
|
||||||
init(appSettings: AppSettings) {
|
init(appSettings: AppSettings) {
|
||||||
urlParsers = [
|
urlParsers = [
|
||||||
|
MatrixPermalinkParser(appSettings: appSettings),
|
||||||
OIDCCallbackURLParser(appSettings: appSettings),
|
OIDCCallbackURLParser(appSettings: appSettings),
|
||||||
ElementCallURLParser()
|
ElementCallURLParser()
|
||||||
]
|
]
|
||||||
@@ -105,3 +107,17 @@ struct ElementCallURLParser: URLParser {
|
|||||||
return .genericCallLink(url: url)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -81,6 +81,17 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
stateMachine.tryEvent(.presentRoomDetails(roomID: roomID), userInfo: EventUserInfo(animated: animated))
|
stateMachine.tryEvent(.presentRoomDetails(roomID: roomID), userInfo: EventUserInfo(animated: animated))
|
||||||
case .roomList:
|
case .roomList:
|
||||||
stateMachine.tryEvent(.dismissRoom, userInfo: EventUserInfo(animated: animated))
|
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:
|
case .invites:
|
||||||
break
|
break
|
||||||
case .genericCallLink, .oidcCallback:
|
case .genericCallLink, .oidcCallback:
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
|
||||||
switch appRoute {
|
switch appRoute {
|
||||||
case .room, .roomDetails, .roomList:
|
case .room, .roomDetails, .roomList, .roomMemberDetails:
|
||||||
self.roomFlowCoordinator.handleAppRoute(appRoute, animated: animated)
|
self.roomFlowCoordinator.handleAppRoute(appRoute, animated: animated)
|
||||||
case .invites:
|
case .invites:
|
||||||
if UIDevice.current.isPhone {
|
if UIDevice.current.isPhone {
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
|||||||
|
|
||||||
let mutableAttributedString = NSMutableAttributedString(string: string)
|
let mutableAttributedString = NSMutableAttributedString(string: string)
|
||||||
addLinks(mutableAttributedString)
|
addLinks(mutableAttributedString)
|
||||||
|
addAllUsersMention(mutableAttributedString)
|
||||||
detectPermalinks(mutableAttributedString)
|
detectPermalinks(mutableAttributedString)
|
||||||
removeLinkColors(mutableAttributedString)
|
removeLinkColors(mutableAttributedString)
|
||||||
|
|
||||||
@@ -110,6 +111,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
|||||||
let mutableAttributedString = NSMutableAttributedString(attributedString: attributedString)
|
let mutableAttributedString = NSMutableAttributedString(attributedString: attributedString)
|
||||||
removeDefaultForegroundColor(mutableAttributedString)
|
removeDefaultForegroundColor(mutableAttributedString)
|
||||||
addLinks(mutableAttributedString)
|
addLinks(mutableAttributedString)
|
||||||
|
addAllUsersMention(mutableAttributedString)
|
||||||
replaceMarkedBlockquotes(mutableAttributedString)
|
replaceMarkedBlockquotes(mutableAttributedString)
|
||||||
replaceMarkedCodeBlocks(mutableAttributedString)
|
replaceMarkedCodeBlocks(mutableAttributedString)
|
||||||
detectPermalinks(mutableAttributedString)
|
detectPermalinks(mutableAttributedString)
|
||||||
@@ -184,7 +186,10 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
|||||||
|
|
||||||
let linkMatches = MatrixEntityRegex.linkRegex.matches(in: string, options: [])
|
let linkMatches = MatrixEntityRegex.linkRegex.matches(in: string, options: [])
|
||||||
matches.append(contentsOf: linkMatches)
|
matches.append(contentsOf: linkMatches)
|
||||||
if matches.count > 0 {
|
|
||||||
|
guard matches.count > 0 else {
|
||||||
|
return
|
||||||
|
}
|
||||||
// Sort the links by length so the longest one always takes priority
|
// Sort the links by length so the longest one always takes priority
|
||||||
matches.sorted { $0.range.length > $1.range.length }.forEach { match in
|
matches.sorted { $0.range.length > $1.range.length }.forEach { match in
|
||||||
guard let matchRange = Range(match.range, in: string) else {
|
guard let matchRange = Range(match.range, in: string) else {
|
||||||
@@ -215,7 +220,8 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
if attributedString.attribute(.link, at: 0, longestEffectiveRange: nil, in: match.range) == nil {
|
||||||
attributedString.addAttribute(.MatrixAllUsersMention, value: true, range: match.range)
|
attributedString.addAttribute(.MatrixAllUsersMention, value: true, range: match.range)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,13 +37,13 @@ struct MentionBuilder: MentionBuilderProtocol {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var attributesToAdd: [NSAttributedString.Key: Any] = [.link: url, .MatrixUserID: userID]
|
var attachmentAttributes: [NSAttributedString.Key: Any] = [.link: url, .MatrixUserID: userID]
|
||||||
if let blockquote {
|
if let blockquote {
|
||||||
// mentions can be in blockquotes, so if the replaced string was in one, we keep the attribute
|
// 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)
|
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)
|
attributedString.replaceCharacters(in: range, with: attachmentString)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,13 +61,13 @@ struct MentionBuilder: MentionBuilderProtocol {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var attributesToAdd: [NSAttributedString.Key: Any] = [:]
|
var attachmentAttributes: [NSAttributedString.Key: Any] = [:]
|
||||||
if let blockquote {
|
if let blockquote {
|
||||||
// mentions can be in blockquotes, so if the replaced string was in one, we keep the attribute
|
// 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)
|
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)
|
attributedString.replaceCharacters(in: range, with: attachmentString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,4 +108,16 @@ class AppRouteURLParserTests: XCTestCase {
|
|||||||
// Then the route shouldn't be considered valid and should be ignored.
|
// Then the route shouldn't be considered valid and should be ignored.
|
||||||
XCTAssertEqual(route, nil)
|
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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -417,8 +417,40 @@ class AttributedStringBuilderTests: XCTestCase {
|
|||||||
let string = "https://matrix.to/#/@test:matrix.org"
|
let string = "https://matrix.to/#/@test:matrix.org"
|
||||||
let attributedStringFromHTML = attributedStringBuilder.fromHTML(string)
|
let attributedStringFromHTML = attributedStringBuilder.fromHTML(string)
|
||||||
XCTAssertNotNil(attributedStringFromHTML?.attachment)
|
XCTAssertNotNil(attributedStringFromHTML?.attachment)
|
||||||
|
XCTAssertNotNil(attributedStringFromHTML?.link)
|
||||||
let attributedStringFromPlain = attributedStringBuilder.fromPlain(string)
|
let attributedStringFromPlain = attributedStringBuilder.fromPlain(string)
|
||||||
XCTAssertNotNil(attributedStringFromPlain?.attachment)
|
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() {
|
func testAllUsersMentionAttachment() {
|
||||||
@@ -464,6 +496,13 @@ class AttributedStringBuilderTests: XCTestCase {
|
|||||||
XCTAssertNil(attributedStringFromHTML?.link)
|
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() {
|
func testAllUsersIsIgnoredInCode() {
|
||||||
let htmlString = "<pre><code>test @room test</code></pre>"
|
let htmlString = "<pre><code>test @room test</code></pre>"
|
||||||
let attributedStringFromHTML = attributedStringBuilder.fromHTML(htmlString)
|
let attributedStringFromHTML = attributedStringBuilder.fromHTML(htmlString)
|
||||||
@@ -471,6 +510,52 @@ class AttributedStringBuilderTests: XCTestCase {
|
|||||||
XCTAssertNil(attributedStringFromHTML?.attachment)
|
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
|
// MARK: - Private
|
||||||
|
|
||||||
private func checkLinkIn(attributedString: AttributedString?, expectedLink: String, expectedRuns: Int) {
|
private func checkLinkIn(attributedString: AttributedString?, expectedLink: String, expectedRuns: Int) {
|
||||||
@@ -489,7 +574,7 @@ class AttributedStringBuilderTests: XCTestCase {
|
|||||||
XCTFail("Couldn't find expected value.")
|
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 {
|
guard let attributedString else {
|
||||||
XCTFail("Could not build the attributed string")
|
XCTFail("Could not build the attributed string")
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user