mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-05-02 06:06:57 +02:00
Enable user mentions in Rich Text Editor
This commit is contained in:
@@ -25,13 +25,18 @@ import UIKit
|
||||
avatarLeading: 2.0,
|
||||
avatarSideLength: 16.0,
|
||||
itemSpacing: 4)
|
||||
private weak var messageTextView: MXKMessageTextView?
|
||||
private weak var pillViewFlusher: PillViewFlusher?
|
||||
|
||||
// MARK: - Override
|
||||
override init(textAttachment: NSTextAttachment, parentView: UIView?, textLayoutManager: NSTextLayoutManager?, location: NSTextLocation) {
|
||||
super.init(textAttachment: textAttachment, parentView: parentView, textLayoutManager: textLayoutManager, location: location)
|
||||
|
||||
self.messageTextView = parentView?.superview as? MXKMessageTextView
|
||||
// Try to register a flusher for the pills.
|
||||
if let pillViewFlusher = parentView?.superview as? PillViewFlusher {
|
||||
self.pillViewFlusher = pillViewFlusher
|
||||
} else {
|
||||
MXLog.debug("[PillAttachmentViewProvider]: no handler found, pills will not be flushed properly")
|
||||
}
|
||||
}
|
||||
|
||||
override func loadView() {
|
||||
@@ -55,6 +60,6 @@ import UIKit
|
||||
mediaManager: mainSession?.mediaManager,
|
||||
andPillData: pillData)
|
||||
view = pillView
|
||||
messageTextView?.registerPillView(pillView)
|
||||
pillViewFlusher?.registerPillView(pillView)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// Copyright 2023 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import WysiwygComposer
|
||||
|
||||
/// Defines behaviour for an object that is able to manage views created
|
||||
/// by a `NSTextAttachmentViewProvider`. This can be implemented
|
||||
/// by an `UITextView` that would keep track of views in order to
|
||||
/// (internally) clear them when required (e.g. when setting a new attributed text).
|
||||
///
|
||||
/// Note: It is necessary to clear views manually due to a bug in iOS. See `MXKMessageTextView`.
|
||||
@available(iOS 15.0, *)
|
||||
protocol PillViewFlusher: AnyObject {
|
||||
/// Register a pill view that has been added through `NSTextAttachmentViewProvider`.
|
||||
/// Should be called within the `loadView` function in order to clear the pills properly on text updates.
|
||||
///
|
||||
/// - Parameter pillView: View to register.
|
||||
func registerPillView(_ pillView: UIView)
|
||||
}
|
||||
|
||||
@available(iOS 15.0, *)
|
||||
extension MXKMessageTextView: PillViewFlusher { }
|
||||
|
||||
@available(iOS 15.0, *)
|
||||
extension WysiwygTextView: PillViewFlusher { }
|
||||
@@ -74,6 +74,48 @@ class PillsFormatter: NSObject {
|
||||
return newAttr
|
||||
}
|
||||
|
||||
/// Insert text attachments for pills inside given attributed string containing markdown.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - markdownString: An attributed string with markdown formatting
|
||||
/// - roomState: The current room state
|
||||
/// - Returns: A new attributed string with pills.
|
||||
static func insertPills(in markdownString: NSAttributedString, roomState: MXRoomState) -> NSAttributedString {
|
||||
// Create a regexp that detects markdown links.
|
||||
let pattern = "\\[([^\\]]+)\\]\\(([^\\)\"\\s]+)(?:\\s+\"(.*)\")?\\)"
|
||||
guard let regExp = try? NSRegularExpression(pattern: pattern) else { return markdownString }
|
||||
|
||||
let matches = regExp.matches(in: markdownString.string,
|
||||
range: .init(location: 0, length: markdownString.length))
|
||||
|
||||
// If we have some matches, replace permalinks by a pill version.
|
||||
let mutable = NSMutableAttributedString(attributedString: markdownString)
|
||||
for match in matches.reversed() {
|
||||
// Range at 2 is the URL, no need to care about the other parts because
|
||||
// we are retrieving the most recent display name from the room state.
|
||||
let urlRange = match.range(at: 2)
|
||||
var url = markdownString.attributedSubstring(from: urlRange).string
|
||||
|
||||
// Note: a valid markdown link can be written with
|
||||
// enclosing <..>, remove them for userId detection.
|
||||
if url.first == "<" && url.last == ">" {
|
||||
url = String(url[url.index(after: url.startIndex)...url.index(url.endIndex, offsetBy: -2)])
|
||||
}
|
||||
|
||||
// If we find a user matching the link, replace the
|
||||
// entire range of the match with a mention pill.
|
||||
if let userId = userIdFromPermalink(url),
|
||||
let roomMember = roomMember(withUserId: userId,
|
||||
roomState: roomState,
|
||||
andLatestRoomState: nil) {
|
||||
let attachmentString = mentionPill(withRoomMember: roomMember, isHighlighted: false, font: UIFont.systemFont(ofSize: 14))
|
||||
mutable.replaceCharacters(in: match.range, with: attachmentString)
|
||||
}
|
||||
}
|
||||
|
||||
return mutable
|
||||
}
|
||||
|
||||
/// Creates a string with all pills of given attributed string replaced by display names.
|
||||
///
|
||||
/// - Parameters:
|
||||
@@ -160,7 +202,6 @@ class PillsFormatter: NSObject {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Private Methods
|
||||
@@ -175,4 +216,30 @@ extension PillsFormatter {
|
||||
}
|
||||
return string
|
||||
}
|
||||
|
||||
/// Extract user id from given permalink
|
||||
/// - Parameter permalink: the permalink
|
||||
/// - Returns: userId, if any
|
||||
static func userIdFromPermalink(_ permalink: String) -> String? {
|
||||
let baseUrl: String
|
||||
if let clientBaseUrl = BuildSettings.clientPermalinkBaseUrl {
|
||||
baseUrl = String(format: "%@/#/user/", clientBaseUrl)
|
||||
} else {
|
||||
baseUrl = String(format: "%@/#/", kMXMatrixDotToUrl)
|
||||
}
|
||||
return permalink.starts(with: baseUrl) ? String(permalink.dropFirst(baseUrl.count)) : nil
|
||||
}
|
||||
|
||||
/// Retrieve the latest available `MXRoomMember` from given data.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - userId: the id of the user
|
||||
/// - roomState: room state for message
|
||||
/// - latestRoomState: latest room state of the room containing this message
|
||||
/// - Returns: the room member, if available
|
||||
static func roomMember(withUserId userId: String,
|
||||
roomState: MXRoomState,
|
||||
andLatestRoomState latestRoomState: MXRoomState?) -> MXRoomMember? {
|
||||
return latestRoomState?.members.member(withUserId: userId) ?? roomState.members.member(withUserId: userId)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user