mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 10:33:45 +00:00
Render Room and Message Pills (#3809)
* added a way to render the room and the message pills, but is WIP * permalinks now get converted into pills! * fixed an issue where room address mentions were not adding a URL properly but a string * updated tests * c * Revert "c" This reverts commit 5c80252fa23dba7e4d44f2a07fbf1e9500e37c82. * updated tests * more tests * created APIs to get a specific RoomSummary given the id or the alias * small mention builder improvement * pr suggestions
This commit is contained in:
@@ -9,13 +9,20 @@ import Foundation
|
||||
import UIKit
|
||||
|
||||
struct MentionBuilder: MentionBuilderProtocol {
|
||||
func handleUserMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, userID: String, userDisplayName: String?) {
|
||||
let attributes = attributedString.attributes(at: 0, longestEffectiveRange: nil, in: range)
|
||||
let font = attributes[.font] as? UIFont ?? .preferredFont(forTextStyle: .body)
|
||||
let blockquote = attributes[.MatrixBlockquote]
|
||||
let foregroundColor = attributes[.foregroundColor] as? UIColor ?? .compound.textPrimary
|
||||
struct AttributesToRestore {
|
||||
let font: UIFont
|
||||
let blockquote: Bool?
|
||||
let foregroundColor: UIColor
|
||||
}
|
||||
|
||||
func handleUserMention(for attributedString: NSMutableAttributedString,
|
||||
in range: NSRange,
|
||||
url: URL,
|
||||
userID: String,
|
||||
userDisplayName: String?) {
|
||||
let attributesToRestore = getAttributesToRestore(for: attributedString, in: range)
|
||||
|
||||
let attachmentData = PillTextAttachmentData(type: .user(userID: userID), font: font)
|
||||
let attachmentData = PillTextAttachmentData(type: .user(userID: userID), font: attributesToRestore.font)
|
||||
guard let attachment = PillTextAttachment(attachmentData: attachmentData) else {
|
||||
attributedString.addAttribute(.MatrixUserID, value: userID, range: range)
|
||||
|
||||
@@ -26,34 +33,151 @@ struct MentionBuilder: MentionBuilderProtocol {
|
||||
return
|
||||
}
|
||||
|
||||
var attachmentAttributes: [NSAttributedString.Key: Any] = [.link: url, .MatrixUserID: userID, .font: font, .foregroundColor: foregroundColor]
|
||||
if let blockquote {
|
||||
// mentions can be in blockquotes, so if the replaced string was in one, we keep the attribute
|
||||
attachmentAttributes[.MatrixBlockquote] = blockquote
|
||||
}
|
||||
let attachmentString = NSMutableAttributedString(attachment: attachment)
|
||||
attachmentString.addAttributes(attachmentAttributes, range: NSRange(location: 0, length: attachmentString.length))
|
||||
attributedString.replaceCharacters(in: range, with: attachmentString)
|
||||
var attachmentAttributes: [NSAttributedString.Key: Any] = [.link: url,
|
||||
.MatrixUserID: userID,
|
||||
.font: attributesToRestore.font,
|
||||
.foregroundColor: attributesToRestore.foregroundColor]
|
||||
attachmentAttributes.addBlockquoteIfNeeded(attributesToRestore.blockquote)
|
||||
attachmentAttributes.addMatrixUsernameIfNeeded(userDisplayName)
|
||||
|
||||
setPillAttachment(attachment: attachment,
|
||||
attributedString: attributedString,
|
||||
in: range,
|
||||
with: attachmentAttributes)
|
||||
}
|
||||
|
||||
func handleAllUsersMention(for attributedString: NSMutableAttributedString, in range: NSRange) {
|
||||
let attributes = attributedString.attributes(at: 0, longestEffectiveRange: nil, in: range)
|
||||
let font = attributes[.font] as? UIFont ?? .preferredFont(forTextStyle: .body)
|
||||
let blockquote = attributes[.MatrixBlockquote]
|
||||
let foregroundColor = attributes[.foregroundColor] as? UIColor ?? .compound.textPrimary
|
||||
|
||||
let attachmentData = PillTextAttachmentData(type: .allUsers, font: font)
|
||||
let attributesToRestore = getAttributesToRestore(for: attributedString, in: range)
|
||||
|
||||
let attachmentData = PillTextAttachmentData(type: .allUsers, font: attributesToRestore.font)
|
||||
guard let attachment = PillTextAttachment(attachmentData: attachmentData) else {
|
||||
return
|
||||
}
|
||||
|
||||
var attachmentAttributes: [NSAttributedString.Key: Any] = [.font: font, .MatrixAllUsersMention: true, .foregroundColor: foregroundColor]
|
||||
if let blockquote {
|
||||
// mentions can be in blockquotes, so if the replaced string was in one, we keep the attribute
|
||||
attachmentAttributes[.MatrixBlockquote] = blockquote
|
||||
var attachmentAttributes: [NSAttributedString.Key: Any] = [.font: attributesToRestore.font,
|
||||
.MatrixAllUsersMention: true,
|
||||
.foregroundColor: attributesToRestore.foregroundColor]
|
||||
attachmentAttributes.addBlockquoteIfNeeded(attributesToRestore.blockquote)
|
||||
|
||||
setPillAttachment(attachment: attachment,
|
||||
attributedString: attributedString,
|
||||
in: range,
|
||||
with: attachmentAttributes)
|
||||
}
|
||||
|
||||
func handleRoomIDMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, roomID: String) {
|
||||
let attributesToRestore = getAttributesToRestore(for: attributedString, in: range)
|
||||
|
||||
let attachmentData = PillTextAttachmentData(type: .roomID(roomID), font: attributesToRestore.font)
|
||||
guard let attachment = PillTextAttachment(attachmentData: attachmentData) else {
|
||||
attributedString.addAttribute(.MatrixRoomID, value: roomID, range: range)
|
||||
return
|
||||
}
|
||||
|
||||
var attachmentAttributes: [NSAttributedString.Key: Any] = [.link: url,
|
||||
.MatrixRoomID: roomID,
|
||||
.font: attributesToRestore.font,
|
||||
.foregroundColor: attributesToRestore.foregroundColor]
|
||||
attachmentAttributes.addBlockquoteIfNeeded(attributesToRestore.blockquote)
|
||||
|
||||
setPillAttachment(attachment: attachment,
|
||||
attributedString: attributedString,
|
||||
in: range,
|
||||
with: attachmentAttributes)
|
||||
}
|
||||
|
||||
func handleRoomAliasMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, roomAlias: String) {
|
||||
let attributesToRestore = getAttributesToRestore(for: attributedString, in: range)
|
||||
|
||||
let attachmentData = PillTextAttachmentData(type: .roomAlias(roomAlias), font: attributesToRestore.font)
|
||||
guard let attachment = PillTextAttachment(attachmentData: attachmentData) else {
|
||||
attributedString.addAttribute(.MatrixRoomAlias, value: roomAlias, range: range)
|
||||
return
|
||||
}
|
||||
|
||||
var attachmentAttributes: [NSAttributedString.Key: Any] = [.link: url,
|
||||
.MatrixRoomAlias: roomAlias,
|
||||
.font: attributesToRestore.font,
|
||||
.foregroundColor: attributesToRestore.foregroundColor]
|
||||
attachmentAttributes.addBlockquoteIfNeeded(attributesToRestore.blockquote)
|
||||
|
||||
setPillAttachment(attachment: attachment,
|
||||
attributedString: attributedString,
|
||||
in: range,
|
||||
with: attachmentAttributes)
|
||||
}
|
||||
|
||||
func handleEventOnRoomAliasMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, eventID: String, roomAlias: String) {
|
||||
let attributesToRestore = getAttributesToRestore(for: attributedString, in: range)
|
||||
|
||||
let attachmentData = PillTextAttachmentData(type: .event(room: .roomAlias(roomAlias)), font: attributesToRestore.font)
|
||||
guard let attachment = PillTextAttachment(attachmentData: attachmentData) else {
|
||||
attributedString.addAttribute(.MatrixEventOnRoomAlias, value: EventOnRoomAliasAttribute.Value(alias: roomAlias, eventID: eventID), range: range)
|
||||
return
|
||||
}
|
||||
|
||||
var attachmentAttributes: [NSAttributedString.Key: Any] = [.link: url,
|
||||
.MatrixEventOnRoomAlias: EventOnRoomAliasAttribute.Value(alias: roomAlias, eventID: eventID),
|
||||
.font: attributesToRestore.font,
|
||||
.foregroundColor: attributesToRestore.foregroundColor]
|
||||
attachmentAttributes.addBlockquoteIfNeeded(attributesToRestore.blockquote)
|
||||
|
||||
setPillAttachment(attachment: attachment,
|
||||
attributedString: attributedString,
|
||||
in: range,
|
||||
with: attachmentAttributes)
|
||||
}
|
||||
|
||||
func handleEventOnRoomIDMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, eventID: String, roomID: String) {
|
||||
let attributesToRestore = getAttributesToRestore(for: attributedString, in: range)
|
||||
|
||||
let attachmentData = PillTextAttachmentData(type: .event(room: .roomID(roomID)), font: attributesToRestore.font)
|
||||
guard let attachment = PillTextAttachment(attachmentData: attachmentData) else {
|
||||
attributedString.addAttribute(.MatrixEventOnRoomID, value: EventOnRoomIDAttribute.Value(roomID: roomID, eventID: eventID), range: range)
|
||||
return
|
||||
}
|
||||
|
||||
var attachmentAttributes: [NSAttributedString.Key: Any] = [.link: url,
|
||||
.MatrixEventOnRoomID: EventOnRoomIDAttribute.Value(roomID: roomID, eventID: eventID),
|
||||
.font: attributesToRestore.font,
|
||||
.foregroundColor: attributesToRestore.foregroundColor]
|
||||
attachmentAttributes.addBlockquoteIfNeeded(attributesToRestore.blockquote)
|
||||
|
||||
setPillAttachment(attachment: attachment,
|
||||
attributedString: attributedString,
|
||||
in: range,
|
||||
with: attachmentAttributes)
|
||||
}
|
||||
|
||||
private func getAttributesToRestore(for attributedString: NSMutableAttributedString, in range: NSRange) -> AttributesToRestore {
|
||||
let attributes = attributedString.attributes(at: 0, longestEffectiveRange: nil, in: range)
|
||||
let font = attributes[.font] as? UIFont ?? .preferredFont(forTextStyle: .body)
|
||||
let blockquote = attributes[.MatrixBlockquote] as? Bool
|
||||
let foregroundColor = attributes[.foregroundColor] as? UIColor ?? .compound.textPrimary
|
||||
|
||||
return AttributesToRestore(font: font, blockquote: blockquote, foregroundColor: foregroundColor)
|
||||
}
|
||||
|
||||
private func setPillAttachment(attachment: PillTextAttachment,
|
||||
attributedString: NSMutableAttributedString,
|
||||
in range: NSRange,
|
||||
with attributes: [NSAttributedString.Key: Any]) {
|
||||
let attachmentString = NSMutableAttributedString(attachment: attachment)
|
||||
attachmentString.addAttributes(attachmentAttributes, range: NSRange(location: 0, length: attachmentString.length))
|
||||
attachmentString.addAttributes(attributes, range: NSRange(location: 0, length: attachmentString.length))
|
||||
attributedString.replaceCharacters(in: range, with: attachmentString)
|
||||
}
|
||||
}
|
||||
|
||||
private extension Dictionary where Key == NSAttributedString.Key, Value == Any {
|
||||
mutating func addBlockquoteIfNeeded(_ value: Bool?) {
|
||||
if let value {
|
||||
self[.MatrixBlockquote] = value
|
||||
}
|
||||
}
|
||||
|
||||
mutating func addMatrixUsernameIfNeeded(_ value: String?) {
|
||||
if let value {
|
||||
self[.MatrixUserDisplayName] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ final class PillAttachmentViewProvider: NSTextAttachmentViewProvider, NSSecureCo
|
||||
let mediaProvider: MediaProviderProtocol?
|
||||
if ProcessInfo.isXcodePreview || ProcessInfo.isRunningTests {
|
||||
// The mock viewModel simulates the loading logic for testing purposes
|
||||
context = PillContext.mock(type: .loadUser(isOwn: false))
|
||||
context = PillContext.mock(viewState: .mention(isOwnMention: false, displayText: "Alice"), delay: .seconds(2))
|
||||
mediaProvider = MediaProviderMock(configuration: .init())
|
||||
} else if let timelineContext = delegate?.timelineContext {
|
||||
context = PillContext(timelineContext: timelineContext, data: pillData)
|
||||
|
||||
@@ -10,75 +10,77 @@ import Foundation
|
||||
|
||||
@MainActor
|
||||
final class PillContext: ObservableObject {
|
||||
struct PillViewState: Equatable {
|
||||
let isOwnMention: Bool
|
||||
let displayText: String
|
||||
}
|
||||
@Published var viewState: PillViewState = .undefined
|
||||
|
||||
@Published private(set) var viewState: PillViewState
|
||||
|
||||
private var cancellable: AnyCancellable?
|
||||
let data: PillTextAttachmentData
|
||||
var cancellable: AnyCancellable?
|
||||
|
||||
init(timelineContext: TimelineViewModel.Context, data: PillTextAttachmentData) {
|
||||
switch data.type {
|
||||
case let .user(id):
|
||||
let isOwnMention = id == timelineContext.viewState.ownUserID
|
||||
if let profile = timelineContext.viewState.members[id] {
|
||||
var name = id
|
||||
if let displayName = profile.displayName {
|
||||
name = "@\(displayName)"
|
||||
}
|
||||
viewState = PillViewState(isOwnMention: isOwnMention, displayText: name)
|
||||
} else {
|
||||
viewState = PillViewState(isOwnMention: isOwnMention, displayText: id)
|
||||
cancellable = timelineContext.$viewState.sink { [weak self] viewState in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let profile = viewState.members[id] {
|
||||
var name = id
|
||||
if let displayName = profile.displayName {
|
||||
name = "@\(displayName)"
|
||||
}
|
||||
self.viewState = PillViewState(isOwnMention: isOwnMention, displayText: name)
|
||||
cancellable = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
case .allUsers:
|
||||
viewState = PillViewState(isOwnMention: true, displayText: PillConstants.atRoom)
|
||||
}
|
||||
self.data = data
|
||||
timelineContext.viewState.pillContextUpdater?(self)
|
||||
}
|
||||
}
|
||||
|
||||
extension PillContext {
|
||||
enum MockType {
|
||||
case loadUser(isOwn: Bool)
|
||||
case loadedUser(isOwn: Bool)
|
||||
case allUsers
|
||||
static func mock(viewState: PillViewState, delay: Duration? = nil) -> PillContext {
|
||||
// This is just for previews so the internal data doesn't really matter
|
||||
let viewModel = PillContext(timelineContext: TimelineViewModel.mock.context, data: PillTextAttachmentData(type: .allUsers, font: .preferredFont(forTextStyle: .body)))
|
||||
if let delay {
|
||||
viewModel.viewState = .mention(isOwnMention: false, displayText: "placeholder")
|
||||
Task {
|
||||
try? await Task.sleep(for: delay)
|
||||
viewModel.viewState = viewState
|
||||
}
|
||||
} else {
|
||||
viewModel.viewState = viewState
|
||||
}
|
||||
return viewModel
|
||||
}
|
||||
}
|
||||
|
||||
enum PillViewState: Equatable {
|
||||
enum PillImage: Equatable {
|
||||
case link
|
||||
case roomAvatar(RoomAvatar)
|
||||
}
|
||||
|
||||
static func mock(type: MockType) -> PillContext {
|
||||
let testID = "@test:test.com"
|
||||
let pillType: PillType
|
||||
switch type {
|
||||
case .loadUser(let isOwn):
|
||||
pillType = .user(userID: testID)
|
||||
let viewModel = PillContext(timelineContext: TimelineViewModel.mock.context, data: PillTextAttachmentData(type: pillType, font: .preferredFont(forTextStyle: .body)))
|
||||
viewModel.viewState = PillViewState(isOwnMention: isOwn, displayText: testID)
|
||||
Task {
|
||||
try? await Task.sleep(for: .seconds(2))
|
||||
viewModel.viewState = PillViewState(isOwnMention: isOwn, displayText: "@Test Long Display Text")
|
||||
}
|
||||
return viewModel
|
||||
case .loadedUser(let isOwn):
|
||||
pillType = .user(userID: "@test:test.com")
|
||||
let viewModel = PillContext(timelineContext: TimelineViewModel.mock.context, data: PillTextAttachmentData(type: pillType, font: .preferredFont(forTextStyle: .body)))
|
||||
viewModel.viewState = PillViewState(isOwnMention: isOwn, displayText: "@Very Very Long Test Display Text")
|
||||
return viewModel
|
||||
case .allUsers:
|
||||
pillType = .allUsers
|
||||
return PillContext(timelineContext: TimelineViewModel.mock.context, data: PillTextAttachmentData(type: pillType, font: .preferredFont(forTextStyle: .body)))
|
||||
case mention(isOwnMention: Bool, displayText: String)
|
||||
case reference(avatar: PillImage, displayText: String)
|
||||
case undefined
|
||||
|
||||
var isOwnMention: Bool {
|
||||
switch self {
|
||||
case .mention(let isOwnMention, _):
|
||||
return isOwnMention
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var displayText: String {
|
||||
switch self {
|
||||
case .mention(_, let displayText), .reference(_, let displayText):
|
||||
return displayText
|
||||
case .undefined:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
var isUndefined: Bool {
|
||||
switch self {
|
||||
case .undefined:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var image: PillImage? {
|
||||
switch self {
|
||||
case .reference(let avatar, _):
|
||||
return avatar
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,21 @@ import Foundation
|
||||
import UIKit
|
||||
|
||||
enum PillType: Codable, Equatable {
|
||||
enum EventRoom: Codable, Equatable {
|
||||
case roomAlias(String)
|
||||
case roomID(String)
|
||||
|
||||
var value: String {
|
||||
switch self {
|
||||
case .roomAlias(let value), .roomID(let value):
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case event(room: EventRoom)
|
||||
case roomAlias(String)
|
||||
case roomID(String)
|
||||
/// A pill that mentions a user
|
||||
case user(userID: String)
|
||||
/// A pill that mentions all users in a room
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Compound
|
||||
import SwiftUI
|
||||
|
||||
struct PillView: View {
|
||||
@@ -22,18 +23,42 @@ struct PillView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Text(context.viewState.displayText)
|
||||
.font(.compound.bodyLGSemibold)
|
||||
.foregroundColor(textColor)
|
||||
.lineLimit(1)
|
||||
.padding(.leading, 4)
|
||||
.padding(.trailing, 6)
|
||||
.padding(.vertical, 1)
|
||||
.background { Capsule().foregroundColor(backgroundColor) }
|
||||
mainContent
|
||||
.onChange(of: context.viewState.displayText) {
|
||||
didChangeText()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var mainContent: some View {
|
||||
HStack(spacing: 4) {
|
||||
image
|
||||
Text(context.viewState.displayText)
|
||||
.font(.compound.bodyLGSemibold)
|
||||
.foregroundColor(textColor)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.padding(.leading, 4)
|
||||
.padding(.trailing, 6)
|
||||
.padding(.vertical, 1)
|
||||
.background { Capsule().foregroundColor(backgroundColor) }
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var image: some View {
|
||||
if let image = context.viewState.image {
|
||||
switch image {
|
||||
case .link:
|
||||
CompoundIcon(\.link, size: .custom(12), relativeTo: .compound.bodyLGSemibold)
|
||||
.padding(2)
|
||||
.foregroundStyle(.compound.bgCanvasDefault)
|
||||
.background(.compound.textLinkExternal)
|
||||
.clipShape(Circle())
|
||||
case .roomAvatar(let avatar):
|
||||
RoomAvatarImage(avatar: avatar, avatarSize: .custom(16), mediaProvider: mediaProvider)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PillView_Previews: PreviewProvider, TestablePreview {
|
||||
@@ -41,24 +66,27 @@ struct PillView_Previews: PreviewProvider, TestablePreview {
|
||||
|
||||
static var previews: some View {
|
||||
PillView(mediaProvider: mockMediaProvider,
|
||||
context: PillContext.mock(type: .loadUser(isOwn: false))) { }
|
||||
context: PillContext.mock(viewState: .mention(isOwnMention: false,
|
||||
displayText: "@Alice"))) { }
|
||||
.frame(maxWidth: PillConstants.mockMaxWidth)
|
||||
.previewDisplayName("Loading")
|
||||
.previewDisplayName("User")
|
||||
PillView(mediaProvider: mockMediaProvider,
|
||||
context: PillContext.mock(type: .loadUser(isOwn: true))) { }
|
||||
context: PillContext.mock(viewState: .mention(isOwnMention: false,
|
||||
displayText: "@Alice but with a very very long name"))) { }
|
||||
.frame(maxWidth: PillConstants.mockMaxWidth)
|
||||
.previewDisplayName("Loading Own")
|
||||
.previewDisplayName("User with a long name")
|
||||
PillView(mediaProvider: mockMediaProvider,
|
||||
context: PillContext.mock(type: .loadedUser(isOwn: false))) { }
|
||||
context: PillContext.mock(viewState: .mention(isOwnMention: true,
|
||||
displayText: "@Alice"))) { }
|
||||
.frame(maxWidth: PillConstants.mockMaxWidth)
|
||||
.previewDisplayName("Loaded Long")
|
||||
.previewDisplayName("Own user")
|
||||
PillView(mediaProvider: mockMediaProvider,
|
||||
context: PillContext.mock(type: .loadedUser(isOwn: true))) { }
|
||||
context: PillContext.mock(viewState: .reference(avatar: .roomAvatar(.room(id: "roomID", name: "Room", avatarURL: nil)), displayText: "Room"))) { }
|
||||
.frame(maxWidth: PillConstants.mockMaxWidth)
|
||||
.previewDisplayName("Loaded Long Own")
|
||||
.previewDisplayName("Room")
|
||||
PillView(mediaProvider: mockMediaProvider,
|
||||
context: PillContext.mock(type: .allUsers)) { }
|
||||
context: PillContext.mock(viewState: .reference(avatar: .link, displayText: L10n.screenRoomEventPill("Room")))) { }
|
||||
.frame(maxWidth: PillConstants.mockMaxWidth)
|
||||
.previewDisplayName("All Users")
|
||||
.previewDisplayName("Message link")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,12 @@ import Foundation
|
||||
|
||||
// In the future we might use this to do some customisation in what is plain text used to represent mentions.
|
||||
struct PlainMentionBuilder: MentionBuilderProtocol {
|
||||
func handleEventOnRoomAliasMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, eventID: String, roomAlias: String) { }
|
||||
|
||||
func handleEventOnRoomIDMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, eventID: String, roomID: String) { }
|
||||
|
||||
func handleRoomAliasMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, roomAlias: String) { }
|
||||
|
||||
func handleAllUsersMention(for attributedString: NSMutableAttributedString, in range: NSRange) { }
|
||||
|
||||
func handleUserMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, userID: String, userDisplayName: String?) {
|
||||
@@ -17,4 +23,6 @@ struct PlainMentionBuilder: MentionBuilderProtocol {
|
||||
}
|
||||
attributedString.insert(NSAttributedString(string: "@"), at: range.location)
|
||||
}
|
||||
|
||||
func handleRoomIDMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, roomID: String) { }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user