mirror of
https://github.com/element-hq/element-x-ios.git
synced 2025-03-10 05:27:11 +00:00
Room mentioning in the composer (#3868)
* refactored the suggestion item structure to scale with the room pill * Implemented a way for the rooms to appear in the suggestions view for the RTE, however I need to add the pills to the composer and the compatibility with the plain text composer * small code correction * fix * fixed a bug where the suggestion wasn't returning the right suggestion type and the suggestion text properly * implementation done! also updated some tests, but we need more of them * updated toolbar view model tests * updated tests * updated preview tests * renamed the Avatars case for the suggestions
This commit is contained in:
parent
2d77a1b10b
commit
5f59e867b5
@ -695,7 +695,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
mediaProvider: userSession.mediaProvider)
|
||||
self.timelineController = timelineController
|
||||
|
||||
let completionSuggestionService = CompletionSuggestionService(roomProxy: roomProxy)
|
||||
let completionSuggestionService = CompletionSuggestionService(roomProxy: roomProxy,
|
||||
roomListPublisher: userSession.clientProxy.staticRoomSummaryProvider?.roomListPublisher.eraseToAnyPublisher() ?? Empty().replaceEmpty(with: []).eraseToAnyPublisher())
|
||||
let composerDraftService = ComposerDraftService(roomProxy: roomProxy, timelineItemfactory: timelineItemFactory)
|
||||
|
||||
let parameters = RoomScreenCoordinatorParameters(clientProxy: userSession.clientProxy,
|
||||
|
@ -197,7 +197,7 @@ extension Array where Element == RoomSummary {
|
||||
unreadMentionsCount: 0,
|
||||
unreadNotificationsCount: 0,
|
||||
notificationMode: .mute,
|
||||
canonicalAlias: nil,
|
||||
canonicalAlias: "#prelude-foundation:matrix.org",
|
||||
alternativeAliases: [],
|
||||
hasOngoingCall: true,
|
||||
isMarkedUnread: false,
|
||||
|
@ -70,7 +70,7 @@ enum UserAvatarSizeOnScreen {
|
||||
case readReceipt
|
||||
case readReceiptSheet
|
||||
case editUserDetails
|
||||
case suggestions
|
||||
case completionSuggestions
|
||||
case blockedUsers
|
||||
case knockingUsersBannerStack
|
||||
case knockingUserBanner
|
||||
@ -89,7 +89,7 @@ enum UserAvatarSizeOnScreen {
|
||||
return 32
|
||||
case .home:
|
||||
return 32
|
||||
case .suggestions:
|
||||
case .completionSuggestions:
|
||||
return 32
|
||||
case .blockedUsers:
|
||||
return 32
|
||||
@ -133,6 +133,7 @@ enum RoomAvatarSizeOnScreen {
|
||||
case notificationSettings
|
||||
case roomDirectorySearch
|
||||
case joinRoom
|
||||
case completionSuggestions
|
||||
|
||||
var value: CGFloat {
|
||||
switch self {
|
||||
@ -142,6 +143,8 @@ enum RoomAvatarSizeOnScreen {
|
||||
return 32
|
||||
case .roomDirectorySearch:
|
||||
return 32
|
||||
case .completionSuggestions:
|
||||
return 32
|
||||
case .messageForwarding:
|
||||
return 36
|
||||
case .globalSearch:
|
||||
|
@ -285,7 +285,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
||||
case .room(let roomID):
|
||||
mentionBuilder.handleRoomIDMention(for: attributedString, in: range, url: url, roomID: roomID)
|
||||
case .roomAlias(let alias):
|
||||
mentionBuilder.handleRoomAliasMention(for: attributedString, in: range, url: url, roomAlias: alias)
|
||||
mentionBuilder.handleRoomAliasMention(for: attributedString, in: range, url: url, roomAlias: alias, roomDisplayName: nil)
|
||||
case .eventOnRoomId(let roomID, let eventID):
|
||||
mentionBuilder.handleEventOnRoomIDMention(for: attributedString, in: range, url: url, eventID: eventID, roomID: roomID)
|
||||
case .eventOnRoomAlias(let alias, let eventID):
|
||||
@ -356,6 +356,7 @@ extension NSAttributedString.Key {
|
||||
static let MatrixBlockquote: NSAttributedString.Key = .init(rawValue: BlockquoteAttribute.name)
|
||||
static let MatrixUserID: NSAttributedString.Key = .init(rawValue: UserIDAttribute.name)
|
||||
static let MatrixUserDisplayName: NSAttributedString.Key = .init(rawValue: UserDisplayNameAttribute.name)
|
||||
static let MatrixRoomDisplayName: NSAttributedString.Key = .init(rawValue: RoomDisplayNameAttribute.name)
|
||||
static let MatrixRoomID: NSAttributedString.Key = .init(rawValue: RoomIDAttribute.name)
|
||||
static let MatrixRoomAlias: NSAttributedString.Key = .init(rawValue: RoomAliasAttribute.name)
|
||||
static let MatrixEventOnRoomID: NSAttributedString.Key = .init(rawValue: EventOnRoomIDAttribute.name)
|
||||
@ -366,7 +367,7 @@ extension NSAttributedString.Key {
|
||||
protocol MentionBuilderProtocol {
|
||||
func handleUserMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, userID: String, userDisplayName: String?)
|
||||
func handleRoomIDMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, roomID: String)
|
||||
func handleRoomAliasMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, roomAlias: String)
|
||||
func handleRoomAliasMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, roomAlias: String, roomDisplayName: String?)
|
||||
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 handleAllUsersMention(for attributedString: NSMutableAttributedString, in range: NSRange)
|
||||
|
@ -24,6 +24,11 @@ enum UserDisplayNameAttribute: AttributedStringKey {
|
||||
static var name = "MXUserDisplayNameAttribute"
|
||||
}
|
||||
|
||||
enum RoomDisplayNameAttribute: AttributedStringKey {
|
||||
typealias Value = String
|
||||
static var name = "MXRoomDisplayNameAttribute"
|
||||
}
|
||||
|
||||
enum RoomIDAttribute: AttributedStringKey {
|
||||
typealias Value = String
|
||||
static var name = "MXRoomIDAttribute"
|
||||
@ -64,6 +69,7 @@ extension AttributeScopes {
|
||||
|
||||
let userID: UserIDAttribute
|
||||
let userDisplayName: UserDisplayNameAttribute
|
||||
let roomDisplayName: RoomDisplayNameAttribute
|
||||
let roomID: RoomIDAttribute
|
||||
let roomAlias: RoomAliasAttribute
|
||||
let eventOnRoomID: EventOnRoomIDAttribute
|
||||
|
@ -79,19 +79,22 @@ struct MentionBuilder: MentionBuilderProtocol {
|
||||
.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) {
|
||||
func handleRoomAliasMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, roomAlias: String, roomDisplayName: 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)
|
||||
if let roomDisplayName {
|
||||
attributedString.addAttribute(.MatrixRoomDisplayName, value: roomDisplayName, range: range)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -100,6 +103,7 @@ struct MentionBuilder: MentionBuilderProtocol {
|
||||
.font: attributesToRestore.font,
|
||||
.foregroundColor: attributesToRestore.foregroundColor]
|
||||
attachmentAttributes.addBlockquoteIfNeeded(attributesToRestore.blockquote)
|
||||
attachmentAttributes.addMatrixRoomNameIfNeeded(roomDisplayName)
|
||||
|
||||
setPillAttachment(attachment: attachment,
|
||||
attributedString: attributedString,
|
||||
@ -180,4 +184,10 @@ private extension Dictionary where Key == NSAttributedString.Key, Value == Any {
|
||||
self[.MatrixUserDisplayName] = value
|
||||
}
|
||||
}
|
||||
|
||||
mutating func addMatrixRoomNameIfNeeded(_ value: String?) {
|
||||
if let value {
|
||||
self[.MatrixRoomDisplayName] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ struct PlainMentionBuilder: MentionBuilderProtocol {
|
||||
|
||||
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 handleRoomAliasMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, roomAlias: String, roomDisplayName: String?) { }
|
||||
|
||||
func handleAllUsersMention(for attributedString: NSMutableAttributedString, in range: NSRange) { }
|
||||
|
||||
|
@ -9,7 +9,10 @@ import Combine
|
||||
import Foundation
|
||||
|
||||
private enum SuggestionTriggerRegex {
|
||||
static let at = /@\w+/
|
||||
static let atOrHash = /[@#]\w*/
|
||||
|
||||
static let at: Character = "@"
|
||||
static let hash: Character = "#"
|
||||
}
|
||||
|
||||
final class CompletionSuggestionService: CompletionSuggestionServiceProtocol {
|
||||
@ -19,12 +22,13 @@ final class CompletionSuggestionService: CompletionSuggestionServiceProtocol {
|
||||
|
||||
private let suggestionTriggerSubject = CurrentValueSubject<SuggestionTrigger?, Never>(nil)
|
||||
|
||||
init(roomProxy: JoinedRoomProxyProtocol) {
|
||||
init(roomProxy: JoinedRoomProxyProtocol,
|
||||
roomListPublisher: AnyPublisher<[RoomSummary], Never>) {
|
||||
self.roomProxy = roomProxy
|
||||
|
||||
suggestionsPublisher = suggestionTriggerSubject
|
||||
.combineLatest(roomProxy.membersPublisher)
|
||||
.map { [weak self, ownUserID = roomProxy.ownUserID] suggestionTrigger, members -> [SuggestionItem] in
|
||||
.combineLatest(roomProxy.membersPublisher, roomListPublisher)
|
||||
.map { [weak self, ownUserID = roomProxy.ownUserID] suggestionTrigger, members, roomSummaries -> [SuggestionItem] in
|
||||
guard let self,
|
||||
let suggestionTrigger else {
|
||||
return []
|
||||
@ -32,32 +36,9 @@ final class CompletionSuggestionService: CompletionSuggestionServiceProtocol {
|
||||
|
||||
switch suggestionTrigger.type {
|
||||
case .user:
|
||||
var membersSuggestion = members
|
||||
.compactMap { member -> SuggestionItem? in
|
||||
guard member.userID != ownUserID,
|
||||
member.membership == .join,
|
||||
Self.shouldIncludeMember(userID: member.userID, displayName: member.displayName, searchText: suggestionTrigger.text) else {
|
||||
return nil
|
||||
}
|
||||
return SuggestionItem.user(item: .init(id: member.userID,
|
||||
displayName: member.displayName,
|
||||
avatarURL: member.avatarURL,
|
||||
range: suggestionTrigger.range,
|
||||
rawSuggestionText: suggestionTrigger.text))
|
||||
}
|
||||
|
||||
if self.canMentionAllUsers,
|
||||
!self.roomProxy.isDirectOneToOneRoom,
|
||||
Self.shouldIncludeMember(userID: PillConstants.atRoom, displayName: PillConstants.everyone, searchText: suggestionTrigger.text) {
|
||||
membersSuggestion
|
||||
.insert(SuggestionItem.allUsers(item: .init(id: PillConstants.atRoom,
|
||||
displayName: PillConstants.everyone,
|
||||
avatarURL: self.roomProxy.infoPublisher.value.avatarURL,
|
||||
range: suggestionTrigger.range,
|
||||
rawSuggestionText: suggestionTrigger.text)), at: 0)
|
||||
}
|
||||
|
||||
return membersSuggestion
|
||||
return membersSuggestions(suggestionTrigger: suggestionTrigger, members: members, ownUserID: ownUserID)
|
||||
case .room:
|
||||
return roomSuggestions(suggestionTrigger: suggestionTrigger, roomSummaries: roomSummaries)
|
||||
}
|
||||
}
|
||||
// We only debounce if the suggestion is nil
|
||||
@ -85,25 +66,71 @@ final class CompletionSuggestionService: CompletionSuggestionServiceProtocol {
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func detectTriggerInText(_ text: String, selectedRange: NSRange) -> SuggestionTrigger? {
|
||||
let matches = text.matches(of: SuggestionTriggerRegex.at)
|
||||
let match = matches
|
||||
.first { matchResult in
|
||||
let lowerBound = matchResult.range.lowerBound.utf16Offset(in: matchResult.base)
|
||||
let upperBound = matchResult.range.upperBound.utf16Offset(in: matchResult.base)
|
||||
return selectedRange.location >= lowerBound
|
||||
&& selectedRange.location <= upperBound
|
||||
&& selectedRange.length <= upperBound - lowerBound
|
||||
private func membersSuggestions(suggestionTrigger: SuggestionTrigger,
|
||||
members: [RoomMemberProxyProtocol],
|
||||
ownUserID: String) -> [SuggestionItem] {
|
||||
var membersSuggestion = members
|
||||
.compactMap { member -> SuggestionItem? in
|
||||
guard member.userID != ownUserID,
|
||||
member.membership == .join,
|
||||
Self.shouldIncludeMember(userID: member.userID, displayName: member.displayName, searchText: suggestionTrigger.text) else {
|
||||
return nil
|
||||
}
|
||||
return .init(suggestionType: .user(.init(id: member.userID, displayName: member.displayName, avatarURL: member.avatarURL)), range: suggestionTrigger.range, rawSuggestionText: suggestionTrigger.text)
|
||||
}
|
||||
|
||||
if canMentionAllUsers,
|
||||
!roomProxy.isDirectOneToOneRoom,
|
||||
Self.shouldIncludeMember(userID: PillConstants.atRoom, displayName: PillConstants.everyone, searchText: suggestionTrigger.text) {
|
||||
membersSuggestion
|
||||
.insert(SuggestionItem(suggestionType: .allUsers(roomProxy.details.avatar), range: suggestionTrigger.range, rawSuggestionText: suggestionTrigger.text), at: 0)
|
||||
}
|
||||
|
||||
return membersSuggestion
|
||||
}
|
||||
|
||||
private func roomSuggestions(suggestionTrigger: SuggestionTrigger,
|
||||
roomSummaries: [RoomSummary]) -> [SuggestionItem] {
|
||||
roomSummaries
|
||||
.compactMap { roomSummary -> SuggestionItem? in
|
||||
guard let canonicalAlias = roomSummary.canonicalAlias,
|
||||
Self.shouldIncludeRoom(roomName: roomSummary.name, roomAlias: canonicalAlias, searchText: suggestionTrigger.text) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return .init(suggestionType: .room(.init(id: roomSummary.id,
|
||||
canonicalAlias: canonicalAlias,
|
||||
name: roomSummary.name,
|
||||
avatar: roomSummary.avatar)),
|
||||
range: suggestionTrigger.range, rawSuggestionText: suggestionTrigger.text)
|
||||
}
|
||||
}
|
||||
|
||||
private func detectTriggerInText(_ text: String, selectedRange: NSRange) -> SuggestionTrigger? {
|
||||
let matches = text.matches(of: SuggestionTriggerRegex.atOrHash)
|
||||
let match = matches.first { matchResult in
|
||||
let lowerBound = matchResult.range.lowerBound.utf16Offset(in: matchResult.base)
|
||||
let upperBound = matchResult.range.upperBound.utf16Offset(in: matchResult.base)
|
||||
return selectedRange.location >= lowerBound
|
||||
&& selectedRange.location <= upperBound
|
||||
&& selectedRange.length <= upperBound - lowerBound
|
||||
}
|
||||
|
||||
guard let match else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
var suggestionText = String(text[match.range])
|
||||
suggestionText.removeFirst()
|
||||
let firstChar = suggestionText.removeFirst()
|
||||
|
||||
return .init(type: .user, text: suggestionText, range: NSRange(match.range, in: text))
|
||||
switch firstChar {
|
||||
case SuggestionTriggerRegex.at:
|
||||
return .init(type: .user, text: suggestionText, range: NSRange(match.range, in: text))
|
||||
case SuggestionTriggerRegex.hash:
|
||||
return .init(type: .room, text: suggestionText, range: NSRange(match.range, in: text))
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private static func shouldIncludeMember(userID: String, displayName: String?, searchText: String) -> Bool {
|
||||
@ -122,4 +149,12 @@ final class CompletionSuggestionService: CompletionSuggestionServiceProtocol {
|
||||
|
||||
return containedInUserID || containedInDisplayName
|
||||
}
|
||||
|
||||
private static func shouldIncludeRoom(roomName: String, roomAlias: String, searchText: String) -> Bool {
|
||||
// If the search text is empty give back all the results
|
||||
guard !searchText.isEmpty else {
|
||||
return true
|
||||
}
|
||||
return roomName.localizedStandardContains(searchText.lowercased()) || roomAlias.localizedStandardContains(searchText.lowercased())
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import WysiwygComposer
|
||||
struct SuggestionTrigger: Equatable {
|
||||
enum SuggestionType: Equatable {
|
||||
case user
|
||||
case room
|
||||
}
|
||||
|
||||
let type: SuggestionType
|
||||
@ -19,33 +20,62 @@ struct SuggestionTrigger: Equatable {
|
||||
let range: NSRange
|
||||
}
|
||||
|
||||
enum SuggestionItem: Identifiable, Equatable {
|
||||
case user(item: MentionSuggestionItem)
|
||||
case allUsers(item: MentionSuggestionItem)
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case .user(let user):
|
||||
return user.id
|
||||
case .allUsers:
|
||||
return PillConstants.atRoom
|
||||
}
|
||||
struct SuggestionItem: Identifiable, Equatable {
|
||||
enum SuggestionType: Equatable {
|
||||
case user(User)
|
||||
case allUsers(RoomAvatar)
|
||||
case room(Room)
|
||||
}
|
||||
|
||||
var range: NSRange {
|
||||
switch self {
|
||||
case .user(let item), .allUsers(let item):
|
||||
return item.range
|
||||
}
|
||||
struct User: Equatable {
|
||||
let id: String
|
||||
let displayName: String?
|
||||
let avatarURL: URL?
|
||||
}
|
||||
}
|
||||
|
||||
struct MentionSuggestionItem: Identifiable, Equatable {
|
||||
let id: String
|
||||
let displayName: String?
|
||||
let avatarURL: URL?
|
||||
|
||||
struct Room: Equatable {
|
||||
let id: String
|
||||
let canonicalAlias: String
|
||||
let name: String
|
||||
let avatar: RoomAvatar
|
||||
}
|
||||
|
||||
let suggestionType: SuggestionType
|
||||
let range: NSRange
|
||||
let rawSuggestionText: String
|
||||
|
||||
var id: String {
|
||||
switch suggestionType {
|
||||
case .user(let user):
|
||||
user.id
|
||||
case .allUsers:
|
||||
PillConstants.atRoom
|
||||
case .room(let room):
|
||||
room.id
|
||||
}
|
||||
}
|
||||
|
||||
var displayName: String {
|
||||
switch suggestionType {
|
||||
case .allUsers:
|
||||
return PillConstants.everyone
|
||||
case .user(let user):
|
||||
return user.displayName ?? user.id
|
||||
case .room(let room):
|
||||
return room.name
|
||||
}
|
||||
}
|
||||
|
||||
var subtitle: String? {
|
||||
switch suggestionType {
|
||||
case .allUsers:
|
||||
return nil
|
||||
case .user(let user):
|
||||
return user.displayName == nil ? nil : user.id
|
||||
case .room(let room):
|
||||
return room.canonicalAlias
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sourcery: AutoMockable
|
||||
@ -62,6 +92,8 @@ extension WysiwygComposer.SuggestionPattern {
|
||||
switch key {
|
||||
case .at:
|
||||
return SuggestionTrigger(type: .user, text: text, range: .init(location: Int(start), length: Int(end)))
|
||||
case .hash:
|
||||
return SuggestionTrigger(type: .room, text: text, range: .init(location: Int(start), length: Int(end)))
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
@ -378,22 +378,23 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
|
||||
shouldMakeAnotherPass = false
|
||||
attributedString.enumerateAttribute(.link, in: .init(location: 0, length: attributedString.length), options: []) { value, range, stop in
|
||||
guard let value else { return }
|
||||
|
||||
shouldMakeAnotherPass = true
|
||||
|
||||
// Remove the attribute so it doesn't get inherited by the new string
|
||||
attributedString.removeAttribute(.link, range: range)
|
||||
|
||||
guard let userID = attributedString.attribute(.MatrixUserID, at: range.location, effectiveRange: nil) as? String else {
|
||||
if let userID = attributedString.attribute(.MatrixUserID, at: range.location, effectiveRange: nil) as? String {
|
||||
let displayName = attributedString.attribute(.MatrixUserDisplayName, at: range.location, effectiveRange: nil)
|
||||
attributedString.replaceCharacters(in: range, with: "[\(displayName ?? userID)](\(value))")
|
||||
userIDs.insert(userID)
|
||||
stop.pointee = true
|
||||
} else if let roomAlias = attributedString.attribute(.MatrixRoomAlias, at: range.location, effectiveRange: nil) as? String {
|
||||
let displayName = attributedString.attribute(.MatrixRoomDisplayName, at: range.location, effectiveRange: nil)
|
||||
attributedString.replaceCharacters(in: range, with: "[\(displayName ?? roomAlias)](\(value))")
|
||||
stop.pointee = true
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
let displayName = attributedString.attribute(.MatrixUserDisplayName, at: range.location, effectiveRange: nil)
|
||||
|
||||
attributedString.replaceCharacters(in: range, with: "[\(displayName ?? userID)](\(value))")
|
||||
userIDs.insert(userID)
|
||||
|
||||
stop.pointee = true
|
||||
}
|
||||
} while shouldMakeAnotherPass
|
||||
|
||||
@ -473,23 +474,24 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
|
||||
}
|
||||
|
||||
private func handleSuggestion(_ suggestion: SuggestionItem) {
|
||||
switch suggestion {
|
||||
case let .user(item):
|
||||
guard let url = try? URL(string: matrixToUserPermalink(userId: item.id)) else {
|
||||
switch suggestion.suggestionType {
|
||||
case let .user(user):
|
||||
guard let url = try? URL(string: matrixToUserPermalink(userId: user.id)) else {
|
||||
MXLog.error("Could not build user permalink")
|
||||
return
|
||||
}
|
||||
|
||||
if context.composerFormattingEnabled {
|
||||
wysiwygViewModel.setMention(url: url.absoluteString, name: item.id, mentionType: .user)
|
||||
wysiwygViewModel.setMention(url: url.absoluteString, name: user.id, mentionType: .user)
|
||||
} else {
|
||||
let attributedString = NSMutableAttributedString(attributedString: state.bindings.plainComposerText)
|
||||
mentionBuilder.handleUserMention(for: attributedString, in: suggestion.range, url: url, userID: item.id, userDisplayName: item.displayName)
|
||||
mentionBuilder.handleUserMention(for: attributedString, in: suggestion.range, url: url, userID: user.id, userDisplayName: user.displayName)
|
||||
state.bindings.plainComposerText = attributedString
|
||||
let newSelectedRange = NSRange(location: state.bindings.selectedRange.location - item.rawSuggestionText.count, length: 0)
|
||||
|
||||
let newSelectedRange = NSRange(location: state.bindings.selectedRange.location - suggestion.rawSuggestionText.count, length: 0)
|
||||
state.bindings.selectedRange = newSelectedRange
|
||||
}
|
||||
case let .allUsers(item):
|
||||
case .allUsers:
|
||||
if context.composerFormattingEnabled {
|
||||
wysiwygViewModel.setAtRoomMention()
|
||||
} else {
|
||||
@ -497,7 +499,23 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
|
||||
mentionBuilder.handleAllUsersMention(for: attributedString, in: suggestion.range)
|
||||
state.bindings.plainComposerText = attributedString
|
||||
|
||||
let newSelectedRange = NSRange(location: state.bindings.selectedRange.location - item.rawSuggestionText.count, length: 0)
|
||||
let newSelectedRange = NSRange(location: state.bindings.selectedRange.location - suggestion.rawSuggestionText.count, length: 0)
|
||||
state.bindings.selectedRange = newSelectedRange
|
||||
}
|
||||
case let .room(room):
|
||||
guard let url = try? URL(string: matrixToRoomAliasPermalink(roomAlias: room.canonicalAlias)) else {
|
||||
MXLog.error("Could not build alias permalink")
|
||||
return
|
||||
}
|
||||
|
||||
if context.composerFormattingEnabled {
|
||||
wysiwygViewModel.setMention(url: url.absoluteString, name: room.name, mentionType: .room)
|
||||
} else {
|
||||
let attributedString = NSMutableAttributedString(attributedString: state.bindings.plainComposerText)
|
||||
mentionBuilder.handleRoomAliasMention(for: attributedString, in: suggestion.range, url: url, roomAlias: room.canonicalAlias, roomDisplayName: room.name)
|
||||
state.bindings.plainComposerText = attributedString
|
||||
|
||||
let newSelectedRange = NSRange(location: state.bindings.selectedRange.location - suggestion.rawSuggestionText.count, length: 0)
|
||||
state.bindings.selectedRange = newSelectedRange
|
||||
}
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ struct CompletionSuggestionView: View {
|
||||
EmptyView()
|
||||
} else {
|
||||
ZStack {
|
||||
MentionSuggestionItemView(mediaProvider: nil, item: .init(id: "", displayName: nil, avatarURL: nil, range: .init(), rawSuggestionText: ""))
|
||||
MentionSuggestionItemView(mediaProvider: nil, item: .init(suggestionType: .user(.init(id: "", displayName: nil, avatarURL: nil)), range: .init(), rawSuggestionText: ""))
|
||||
.readFrame($prototypeListItemFrame)
|
||||
.hidden()
|
||||
if showBackgroundShadow {
|
||||
@ -52,10 +52,7 @@ struct CompletionSuggestionView: View {
|
||||
Button {
|
||||
onTap(item)
|
||||
} label: {
|
||||
switch item {
|
||||
case .user(let mention), .allUsers(let mention):
|
||||
MentionSuggestionItemView(mediaProvider: mediaProvider, item: mention)
|
||||
}
|
||||
MentionSuggestionItemView(mediaProvider: mediaProvider, item: item)
|
||||
}
|
||||
.modifier(ListItemPaddingModifier(isFirst: items.first?.id == item.id))
|
||||
.listRowInsets(.init(top: 0, leading: Constants.leadingPadding, bottom: 0, trailing: 0))
|
||||
@ -110,16 +107,17 @@ private struct BackgroundView<Content: View>: View {
|
||||
|
||||
struct CompletionSuggestion_Previews: PreviewProvider, TestablePreview {
|
||||
static let multipleItems: [SuggestionItem] = (0...10).map { index in
|
||||
SuggestionItem.user(item: MentionSuggestionItem(id: "\(index)", displayName: "\(index)", avatarURL: nil, range: .init(), rawSuggestionText: ""))
|
||||
.init(suggestionType: .user(.init(id: "\(index)", displayName: "\(index)", avatarURL: nil)), range: .init(), rawSuggestionText: "")
|
||||
}
|
||||
|
||||
static var previews: some View {
|
||||
// Putting them is VStack allows the preview to work properly in tests
|
||||
VStack(spacing: 8) {
|
||||
CompletionSuggestionView(mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
items: [.user(item: MentionSuggestionItem(id: "@user_mention_1:matrix.org", displayName: "User 1", avatarURL: nil, range: .init(), rawSuggestionText: "")),
|
||||
.user(item: MentionSuggestionItem(id: "@user_mention_2:matrix.org", displayName: "User 2", avatarURL: .mockMXCUserAvatar, range: .init(), rawSuggestionText: ""))]) { _ in }
|
||||
items: [.init(suggestionType: .user(.init(id: "@user_mention_1:matrix.org", displayName: "User 1", avatarURL: nil)), range: .init(), rawSuggestionText: ""),
|
||||
.init(suggestionType: .user(.init(id: "@user_mention_2:matrix.org", displayName: "User 2", avatarURL: .mockMXCUserAvatar)), range: .init(), rawSuggestionText: "")]) { _ in }
|
||||
}
|
||||
|
||||
VStack(spacing: 8) {
|
||||
CompletionSuggestionView(mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
items: multipleItems) { _ in }
|
||||
|
@ -321,8 +321,11 @@ struct ComposerToolbar_Previews: PreviewProvider, TestablePreview {
|
||||
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
composerDraftService: ComposerDraftServiceMock())
|
||||
static let suggestions: [SuggestionItem] = [.user(item: MentionSuggestionItem(id: "@user_mention_1:matrix.org", displayName: "User 1", avatarURL: nil, range: .init(), rawSuggestionText: "")),
|
||||
.user(item: MentionSuggestionItem(id: "@user_mention_2:matrix.org", displayName: "User 2", avatarURL: .mockMXCUserAvatar, range: .init(), rawSuggestionText: ""))]
|
||||
|
||||
static let suggestions: [SuggestionItem] = [
|
||||
.init(suggestionType: .user(.init(id: "@user_mention_1:matrix.org", displayName: "User 1", avatarURL: nil)), range: .init(), rawSuggestionText: ""),
|
||||
.init(suggestionType: .user(.init(id: "@user_mention_2:matrix.org", displayName: "User 2", avatarURL: .mockMXCUserAvatar)), range: .init(), rawSuggestionText: "")
|
||||
]
|
||||
|
||||
static var previews: some View {
|
||||
ComposerToolbar.mock(focused: true)
|
||||
|
@ -9,22 +9,25 @@ import SwiftUI
|
||||
|
||||
struct MentionSuggestionItemView: View {
|
||||
let mediaProvider: MediaProviderProtocol?
|
||||
let item: MentionSuggestionItem
|
||||
let item: SuggestionItem
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center, spacing: 16) {
|
||||
LoadableAvatarImage(url: item.avatarURL,
|
||||
name: item.displayName,
|
||||
contentID: item.id,
|
||||
avatarSize: .user(on: .suggestions),
|
||||
mediaProvider: mediaProvider)
|
||||
switch item.suggestionType {
|
||||
case .user(let user):
|
||||
LoadableAvatarImage(url: user.avatarURL, name: user.displayName, contentID: user.id, avatarSize: .user(on: .completionSuggestions), mediaProvider: mediaProvider)
|
||||
case .allUsers(let avatar):
|
||||
RoomAvatarImage(avatar: avatar, avatarSize: .room(on: .completionSuggestions), mediaProvider: mediaProvider)
|
||||
case .room(let room):
|
||||
RoomAvatarImage(avatar: room.avatar, avatarSize: .room(on: .completionSuggestions), mediaProvider: mediaProvider)
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text(item.displayName ?? item.id)
|
||||
Text(item.displayName)
|
||||
.font(.compound.bodyLG)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
.lineLimit(1)
|
||||
if item.displayName != nil {
|
||||
Text(item.id)
|
||||
if let subtitle = item.subtitle {
|
||||
Text(subtitle)
|
||||
.font(.compound.bodySM)
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
.lineLimit(1)
|
||||
@ -38,7 +41,20 @@ struct MentionSuggestionItemView_Previews: PreviewProvider, TestablePreview {
|
||||
static let mockMediaProvider = MediaProviderMock(configuration: .init())
|
||||
|
||||
static var previews: some View {
|
||||
MentionSuggestionItemView(mediaProvider: mockMediaProvider, item: .init(id: "test", displayName: "Test", avatarURL: .mockMXCUserAvatar, range: .init(), rawSuggestionText: ""))
|
||||
MentionSuggestionItemView(mediaProvider: mockMediaProvider, item: .init(id: "test2", displayName: nil, avatarURL: nil, range: .init(), rawSuggestionText: ""))
|
||||
MentionSuggestionItemView(mediaProvider: mockMediaProvider, item: .init(suggestionType: .user(.init(id: "test", displayName: "Test", avatarURL: .mockMXCUserAvatar)), range: .init(), rawSuggestionText: ""))
|
||||
.previewDisplayName("User")
|
||||
MentionSuggestionItemView(mediaProvider: mockMediaProvider, item: .init(suggestionType: .user(.init(id: "test2", displayName: nil, avatarURL: nil)), range: .init(), rawSuggestionText: ""))
|
||||
.previewDisplayName("User no display name")
|
||||
MentionSuggestionItemView(mediaProvider: mockMediaProvider, item: .init(suggestionType: .allUsers(.room(id: "room", name: "Room", avatarURL: .mockMXCAvatar)), range: .init(), rawSuggestionText: ""))
|
||||
.previewDisplayName("All users")
|
||||
MentionSuggestionItemView(mediaProvider: mockMediaProvider,
|
||||
item: .init(suggestionType: .room(.init(id: "room",
|
||||
canonicalAlias: "#room:matrix.org",
|
||||
name: "Room",
|
||||
avatar: .room(id: "room",
|
||||
name: "Room", avatarURL: .mockMXCAvatar))),
|
||||
range: .init(),
|
||||
rawSuggestionText: ""))
|
||||
.previewDisplayName("Room")
|
||||
}
|
||||
}
|
||||
|
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreen.iPad-en-GB-0.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreen.iPad-en-GB-0.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreen.iPad-pseudo-0.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreen.iPad-pseudo-0.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreen.iPhone-16-en-GB-0.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreen.iPhone-16-en-GB-0.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreen.iPhone-16-pseudo-0.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreen.iPhone-16-pseudo-0.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.All-users-iPad-en-GB.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.All-users-iPad-en-GB.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.All-users-iPad-pseudo.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.All-users-iPad-pseudo.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.All-users-iPhone-16-en-GB.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.All-users-iPhone-16-en-GB.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.All-users-iPhone-16-pseudo.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.All-users-iPhone-16-pseudo.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.Room-iPad-en-GB.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.Room-iPad-en-GB.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.Room-iPad-pseudo.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.Room-iPad-pseudo.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.Room-iPhone-16-en-GB.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.Room-iPhone-16-en-GB.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.Room-iPhone-16-pseudo.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.Room-iPhone-16-pseudo.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.iPad-en-GB-0.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.iPad-en-GB-0.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.iPad-pseudo-0.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.iPad-pseudo-0.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.iPhone-16-en-GB-0.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.iPhone-16-en-GB-0.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.iPhone-16-pseudo-0.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.iPhone-16-pseudo-0.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPad-en-GB-0.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPad-en-GB-0.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPad-pseudo-0.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPad-pseudo-0.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPhone-16-en-GB-0.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPhone-16-en-GB-0.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPhone-16-pseudo-0.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPhone-16-pseudo-0.png
(Stored with Git LFS)
Binary file not shown.
@ -21,8 +21,10 @@ final class CompletionSuggestionServiceTests: XCTestCase {
|
||||
func testUserSuggestions() async throws {
|
||||
let alice: RoomMemberProxyMock = .mockAlice
|
||||
let members: [RoomMemberProxyMock] = [alice, .mockBob, .mockCharlie, .mockMe]
|
||||
let roomProxyMock = JoinedRoomProxyMock(.init(name: "test", members: members))
|
||||
let service = CompletionSuggestionService(roomProxy: roomProxyMock)
|
||||
let roomProxyMock = JoinedRoomProxyMock(.init(id: "roomID", name: "test", members: members))
|
||||
let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
|
||||
let service = CompletionSuggestionService(roomProxy: roomProxyMock,
|
||||
roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher())
|
||||
|
||||
var deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
|
||||
suggestions == []
|
||||
@ -31,7 +33,7 @@ final class CompletionSuggestionServiceTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
|
||||
suggestions == [.user(item: .init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL, range: .init(), rawSuggestionText: "ali"))]
|
||||
suggestions == [.init(suggestionType: .user(.init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL)), range: .init(), rawSuggestionText: "ali")]
|
||||
}
|
||||
service.setSuggestionTrigger(.init(type: .user, text: "ali", range: .init()))
|
||||
try await deferred.fulfill()
|
||||
@ -58,8 +60,10 @@ final class CompletionSuggestionServiceTests: XCTestCase {
|
||||
func testUserSuggestionsIncludingAllUsers() async throws {
|
||||
let alice: RoomMemberProxyMock = .mockAlice
|
||||
let members: [RoomMemberProxyMock] = [alice, .mockBob, .mockCharlie, .mockMe]
|
||||
let roomProxyMock = JoinedRoomProxyMock(.init(name: "test", members: members, canUserTriggerRoomNotification: true))
|
||||
let service = CompletionSuggestionService(roomProxy: roomProxyMock)
|
||||
let roomProxyMock = JoinedRoomProxyMock(.init(id: "roomID", name: "test", members: members, canUserTriggerRoomNotification: true))
|
||||
let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
|
||||
let service = CompletionSuggestionService(roomProxy: roomProxyMock,
|
||||
roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher())
|
||||
|
||||
var deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
|
||||
suggestions == []
|
||||
@ -68,13 +72,13 @@ final class CompletionSuggestionServiceTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
|
||||
suggestions == [.allUsers(item: .allUsersMention(roomAvatar: nil, rawSuggestionText: "ro"))]
|
||||
suggestions == [.init(suggestionType: .allUsers(.room(id: "roomID", name: "test", avatarURL: nil)), range: .init(), rawSuggestionText: "ro")]
|
||||
}
|
||||
service.setSuggestionTrigger(.init(type: .user, text: "ro", range: .init()))
|
||||
try await deferred.fulfill()
|
||||
|
||||
deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
|
||||
suggestions == [.allUsers(item: .allUsersMention(roomAvatar: nil, rawSuggestionText: "every"))]
|
||||
suggestions == [.init(suggestionType: .allUsers(.room(id: "roomID", name: "test", avatarURL: nil)), range: .init(), rawSuggestionText: "every")]
|
||||
}
|
||||
service.setSuggestionTrigger(.init(type: .user, text: "every", range: .init()))
|
||||
try await deferred.fulfill()
|
||||
@ -84,8 +88,10 @@ final class CompletionSuggestionServiceTests: XCTestCase {
|
||||
let alice: RoomMemberProxyMock = .mockAlice
|
||||
let bob: RoomMemberProxyMock = .mockBob
|
||||
let members: [RoomMemberProxyMock] = [alice, bob, .mockMe]
|
||||
let roomProxyMock = JoinedRoomProxyMock(.init(name: "test", members: members, canUserTriggerRoomNotification: true))
|
||||
let service = CompletionSuggestionService(roomProxy: roomProxyMock)
|
||||
let roomProxyMock = JoinedRoomProxyMock(.init(id: "roomID", name: "test", members: members, canUserTriggerRoomNotification: true))
|
||||
let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
|
||||
let service = CompletionSuggestionService(roomProxy: roomProxyMock,
|
||||
roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher())
|
||||
|
||||
var deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
|
||||
suggestions == []
|
||||
@ -94,34 +100,45 @@ final class CompletionSuggestionServiceTests: XCTestCase {
|
||||
try await deferred.fulfill()
|
||||
|
||||
deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
|
||||
suggestions == [.allUsers(item: .allUsersMention(roomAvatar: nil, rawSuggestionText: "")),
|
||||
.user(item: .init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL, range: .init(), rawSuggestionText: "")),
|
||||
.user(item: .init(id: bob.userID, displayName: bob.displayName, avatarURL: bob.avatarURL, range: .init(), rawSuggestionText: ""))]
|
||||
suggestions == [.init(suggestionType: .allUsers(.room(id: "roomID", name: "test", avatarURL: nil)), range: .init(), rawSuggestionText: ""),
|
||||
.init(suggestionType: .user(.init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL)), range: .init(), rawSuggestionText: ""),
|
||||
.init(suggestionType: .user(.init(id: bob.userID, displayName: bob.displayName, avatarURL: bob.avatarURL)), range: .init(), rawSuggestionText: "")]
|
||||
}
|
||||
service.setSuggestionTrigger(.init(type: .user, text: "", range: .init()))
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Let's test the same with the processTextMessage method
|
||||
deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
|
||||
suggestions == [.init(suggestionType: .allUsers(.room(id: "roomID", name: "test", avatarURL: nil)), range: .init(location: 0, length: 1), rawSuggestionText: ""),
|
||||
.init(suggestionType: .user(.init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL)), range: .init(location: 0, length: 1), rawSuggestionText: ""),
|
||||
.init(suggestionType: .user(.init(id: bob.userID, displayName: bob.displayName, avatarURL: bob.avatarURL)), range: .init(location: 0, length: 1), rawSuggestionText: "")]
|
||||
}
|
||||
service.processTextMessage("@", selectedRange: .init(location: 0, length: 1))
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testUserSuggestionInDifferentMessagePositions() async throws {
|
||||
let alice: RoomMemberProxyMock = .mockAlice
|
||||
let members: [RoomMemberProxyMock] = [alice, .mockBob, .mockCharlie, .mockMe]
|
||||
let roomProxyMock = JoinedRoomProxyMock(.init(name: "test", members: members))
|
||||
let service = CompletionSuggestionService(roomProxy: roomProxyMock)
|
||||
let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
|
||||
let service = CompletionSuggestionService(roomProxy: roomProxyMock,
|
||||
roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher())
|
||||
|
||||
var deferred = deferFulfillment(service.suggestionsPublisher) { suggestion in
|
||||
suggestion == [.user(item: .init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL, range: .init(location: 0, length: 3), rawSuggestionText: "al"))]
|
||||
var deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
|
||||
suggestions == [.init(suggestionType: .user(.init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL)), range: .init(location: 0, length: 3), rawSuggestionText: "al")]
|
||||
}
|
||||
service.processTextMessage("@al hello", selectedRange: .init(location: 0, length: 1))
|
||||
try await deferred.fulfill()
|
||||
|
||||
deferred = deferFulfillment(service.suggestionsPublisher) { suggestion in
|
||||
suggestion == [.user(item: .init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL, range: .init(location: 5, length: 3), rawSuggestionText: "al"))]
|
||||
deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
|
||||
suggestions == [.init(suggestionType: .user(.init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL)), range: .init(location: 5, length: 3), rawSuggestionText: "al")]
|
||||
}
|
||||
service.processTextMessage("test @al", selectedRange: .init(location: 5, length: 1))
|
||||
try await deferred.fulfill()
|
||||
|
||||
deferred = deferFulfillment(service.suggestionsPublisher) { suggestion in
|
||||
suggestion == [.user(item: .init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL, range: .init(location: 5, length: 3), rawSuggestionText: "al"))]
|
||||
deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
|
||||
suggestions == [.init(suggestionType: .user(.init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL)), range: .init(location: 5, length: 3), rawSuggestionText: "al")]
|
||||
}
|
||||
service.processTextMessage("test @al test", selectedRange: .init(location: 5, length: 1))
|
||||
try await deferred.fulfill()
|
||||
@ -132,16 +149,18 @@ final class CompletionSuggestionServiceTests: XCTestCase {
|
||||
let bob: RoomMemberProxyMock = .mockBob
|
||||
let members: [RoomMemberProxyMock] = [alice, bob, .mockCharlie, .mockMe]
|
||||
let roomProxyMock = JoinedRoomProxyMock(.init(name: "test", members: members))
|
||||
let service = CompletionSuggestionService(roomProxy: roomProxyMock)
|
||||
let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
|
||||
let service = CompletionSuggestionService(roomProxy: roomProxyMock,
|
||||
roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher())
|
||||
|
||||
var deffered = deferFulfillment(service.suggestionsPublisher) { suggestion in
|
||||
suggestion == [.user(item: .init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL, range: .init(location: 0, length: 3), rawSuggestionText: "al"))]
|
||||
var deffered = deferFulfillment(service.suggestionsPublisher) { suggestions in
|
||||
suggestions == [.init(suggestionType: .user(.init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL)), range: .init(location: 0, length: 3), rawSuggestionText: "al")]
|
||||
}
|
||||
service.processTextMessage("@al test @bo", selectedRange: .init(location: 0, length: 1))
|
||||
try await deffered.fulfill()
|
||||
|
||||
deffered = deferFulfillment(service.suggestionsPublisher) { suggestion in
|
||||
suggestion == [.user(item: .init(id: bob.userID, displayName: bob.displayName, avatarURL: bob.avatarURL, range: .init(location: 9, length: 3), rawSuggestionText: "bo"))]
|
||||
deffered = deferFulfillment(service.suggestionsPublisher) { suggestions in
|
||||
suggestions == [.init(suggestionType: .user(.init(id: bob.userID, displayName: bob.displayName, avatarURL: bob.avatarURL)), range: .init(location: 9, length: 3), rawSuggestionText: "bo")]
|
||||
}
|
||||
service.processTextMessage("@al test @bo", selectedRange: .init(location: 9, length: 1))
|
||||
try await deffered.fulfill()
|
||||
@ -152,10 +171,205 @@ final class CompletionSuggestionServiceTests: XCTestCase {
|
||||
service.processTextMessage("@al test @bo", selectedRange: .init(location: 4, length: 1))
|
||||
try await deffered.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
private extension MentionSuggestionItem {
|
||||
static func allUsersMention(roomAvatar: URL?, rawSuggestionText: String) -> Self {
|
||||
MentionSuggestionItem(id: PillConstants.atRoom, displayName: PillConstants.everyone, avatarURL: roomAvatar, range: .init(), rawSuggestionText: rawSuggestionText)
|
||||
|
||||
func testRoomSuggestions() async throws {
|
||||
let alice: RoomMemberProxyMock = .mockAlice
|
||||
// We keep the users in the tests since they should not appear in the suggestions when using the room trigger
|
||||
let members: [RoomMemberProxyMock] = [alice, .mockBob, .mockCharlie, .mockMe]
|
||||
let roomProxyMock = JoinedRoomProxyMock(.init(id: "roomID", name: "test", members: members))
|
||||
let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
|
||||
let service = CompletionSuggestionService(roomProxy: roomProxyMock,
|
||||
roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher())
|
||||
|
||||
var deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
|
||||
suggestions == []
|
||||
}
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
// The empty # should trigger suggestions from any room with an alias
|
||||
deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
|
||||
suggestions == [.init(suggestionType: .room(.init(id: "2",
|
||||
canonicalAlias: "#foundation-and-empire:matrix.org",
|
||||
name: "Foundation and Empire",
|
||||
avatar: .room(id: "2",
|
||||
name: "Foundation and Empire",
|
||||
avatarURL: .mockMXCAvatar))),
|
||||
range: .init(),
|
||||
rawSuggestionText: ""),
|
||||
.init(suggestionType: .room(.init(id: "6",
|
||||
canonicalAlias: "#prelude-foundation:matrix.org",
|
||||
name: "Prelude to Foundation",
|
||||
avatar: .room(id: "6",
|
||||
name: "Prelude to Foundation",
|
||||
avatarURL: nil))),
|
||||
range: .init(),
|
||||
rawSuggestionText: "")]
|
||||
}
|
||||
service.setSuggestionTrigger(.init(type: .room, text: "", range: .init()))
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Same but with the processTextMessage method
|
||||
deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
|
||||
suggestions == [.init(suggestionType: .room(.init(id: "2",
|
||||
canonicalAlias: "#foundation-and-empire:matrix.org",
|
||||
name: "Foundation and Empire",
|
||||
avatar: .room(id: "2",
|
||||
name: "Foundation and Empire",
|
||||
avatarURL: .mockMXCAvatar))),
|
||||
range: .init(location: 0, length: 1),
|
||||
rawSuggestionText: ""),
|
||||
.init(suggestionType: .room(.init(id: "6",
|
||||
canonicalAlias: "#prelude-foundation:matrix.org",
|
||||
name: "Prelude to Foundation",
|
||||
avatar: .room(id: "6",
|
||||
name: "Prelude to Foundation",
|
||||
avatarURL: nil))),
|
||||
range: .init(location: 0, length: 1),
|
||||
rawSuggestionText: "")]
|
||||
}
|
||||
service.processTextMessage("#", selectedRange: .init(location: 0, length: 1))
|
||||
try await deferred.fulfill()
|
||||
|
||||
deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
|
||||
suggestions == [.init(suggestionType: .room(.init(id: "6",
|
||||
canonicalAlias: "#prelude-foundation:matrix.org",
|
||||
name: "Prelude to Foundation",
|
||||
avatar: .room(id: "6",
|
||||
name: "Prelude to Foundation",
|
||||
avatarURL: nil))),
|
||||
range: .init(),
|
||||
rawSuggestionText: "prelude")]
|
||||
}
|
||||
service.setSuggestionTrigger(.init(type: .room, text: "prelude", range: .init()))
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testRoomSuggestionInDifferentMessagePositions() async throws {
|
||||
let alice: RoomMemberProxyMock = .mockAlice
|
||||
// We keep the users in the tests since they should not appear in the suggestions when using the room trigger
|
||||
let members: [RoomMemberProxyMock] = [alice, .mockBob, .mockCharlie, .mockMe]
|
||||
let roomProxyMock = JoinedRoomProxyMock(.init(id: "roomID", name: "test", members: members))
|
||||
let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
|
||||
let service = CompletionSuggestionService(roomProxy: roomProxyMock,
|
||||
roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher())
|
||||
|
||||
var deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
|
||||
suggestions == [.init(suggestionType: .room(.init(id: "6",
|
||||
canonicalAlias: "#prelude-foundation:matrix.org",
|
||||
name: "Prelude to Foundation",
|
||||
avatar: .room(id: "6",
|
||||
name: "Prelude to Foundation",
|
||||
avatarURL: nil))),
|
||||
range: .init(location: 0, length: 3),
|
||||
rawSuggestionText: "pr")]
|
||||
}
|
||||
service.processTextMessage("#pr hello", selectedRange: .init(location: 0, length: 1))
|
||||
try await deferred.fulfill()
|
||||
|
||||
deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
|
||||
suggestions == [.init(suggestionType: .room(.init(id: "6",
|
||||
canonicalAlias: "#prelude-foundation:matrix.org",
|
||||
name: "Prelude to Foundation",
|
||||
avatar: .room(id: "6",
|
||||
name: "Prelude to Foundation",
|
||||
avatarURL: nil))),
|
||||
range: .init(location: 5, length: 3),
|
||||
rawSuggestionText: "pr")]
|
||||
}
|
||||
service.processTextMessage("test #pr", selectedRange: .init(location: 5, length: 1))
|
||||
try await deferred.fulfill()
|
||||
|
||||
deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
|
||||
suggestions == [.init(suggestionType: .room(.init(id: "6",
|
||||
canonicalAlias: "#prelude-foundation:matrix.org",
|
||||
name: "Prelude to Foundation",
|
||||
avatar: .room(id: "6",
|
||||
name: "Prelude to Foundation",
|
||||
avatarURL: nil))),
|
||||
range: .init(location: 5, length: 3),
|
||||
rawSuggestionText: "pr")]
|
||||
}
|
||||
service.processTextMessage("test #pr test", selectedRange: .init(location: 5, length: 1))
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
func testRoomSuggestionWithMultipleMentionSymbol() async throws {
|
||||
let alice: RoomMemberProxyMock = .mockAlice
|
||||
// We keep the users in the tests since they should not appear in the suggestions when using the room trigger
|
||||
let members: [RoomMemberProxyMock] = [alice, .mockBob, .mockCharlie, .mockMe]
|
||||
let roomProxyMock = JoinedRoomProxyMock(.init(id: "roomID", name: "test", members: members))
|
||||
let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
|
||||
let service = CompletionSuggestionService(roomProxy: roomProxyMock,
|
||||
roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher())
|
||||
|
||||
var deffered = deferFulfillment(service.suggestionsPublisher) { suggestions in
|
||||
suggestions == [.init(suggestionType: .room(.init(id: "6",
|
||||
canonicalAlias: "#prelude-foundation:matrix.org",
|
||||
name: "Prelude to Foundation",
|
||||
avatar: .room(id: "6",
|
||||
name: "Prelude to Foundation",
|
||||
avatarURL: nil))),
|
||||
range: .init(location: 0, length: 3),
|
||||
rawSuggestionText: "pr")]
|
||||
}
|
||||
service.processTextMessage("#pr test #fo", selectedRange: .init(location: 0, length: 1))
|
||||
try await deffered.fulfill()
|
||||
|
||||
deffered = deferFulfillment(service.suggestionsPublisher) { suggestions in
|
||||
suggestions == [.init(suggestionType: .room(.init(id: "2",
|
||||
canonicalAlias: "#foundation-and-empire:matrix.org",
|
||||
name: "Foundation and Empire",
|
||||
avatar: .room(id: "2",
|
||||
name: "Foundation and Empire",
|
||||
avatarURL: .mockMXCAvatar))),
|
||||
range: .init(location: 9, length: 3),
|
||||
rawSuggestionText: "fo"),
|
||||
.init(suggestionType: .room(.init(id: "6",
|
||||
canonicalAlias: "#prelude-foundation:matrix.org",
|
||||
name: "Prelude to Foundation",
|
||||
avatar: .room(id: "6",
|
||||
name: "Prelude to Foundation",
|
||||
avatarURL: nil))),
|
||||
range: .init(location: 9, length: 3),
|
||||
rawSuggestionText: "fo")]
|
||||
}
|
||||
service.processTextMessage("#pr test #fo", selectedRange: .init(location: 9, length: 1))
|
||||
try await deffered.fulfill()
|
||||
|
||||
deffered = deferFulfillment(service.suggestionsPublisher) { suggestion in
|
||||
suggestion == []
|
||||
}
|
||||
service.processTextMessage("#pr test #fo", selectedRange: .init(location: 4, length: 1))
|
||||
try await deffered.fulfill()
|
||||
}
|
||||
|
||||
func testSuggestionsWithMultipleDifferentTriggers() async throws {
|
||||
let alice: RoomMemberProxyMock = .mockAlice
|
||||
// We keep the users in the tests since they should not appear in the suggestions when using the room trigger
|
||||
let members: [RoomMemberProxyMock] = [alice, .mockBob, .mockCharlie, .mockMe]
|
||||
let roomProxyMock = JoinedRoomProxyMock(.init(id: "roomID", name: "test", members: members))
|
||||
let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
|
||||
let service = CompletionSuggestionService(roomProxy: roomProxyMock,
|
||||
roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher())
|
||||
|
||||
var deffered = deferFulfillment(service.suggestionsPublisher) { suggestions in
|
||||
suggestions == [.init(suggestionType: .room(.init(id: "6",
|
||||
canonicalAlias: "#prelude-foundation:matrix.org",
|
||||
name: "Prelude to Foundation",
|
||||
avatar: .room(id: "6",
|
||||
name: "Prelude to Foundation",
|
||||
avatarURL: nil))),
|
||||
range: .init(location: 0, length: 3),
|
||||
rawSuggestionText: "pr")]
|
||||
}
|
||||
service.processTextMessage("#pr test @al", selectedRange: .init(location: 0, length: 1))
|
||||
try await deffered.fulfill()
|
||||
|
||||
deffered = deferFulfillment(service.suggestionsPublisher) { suggestions in
|
||||
suggestions == [.init(suggestionType: .user(.init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL)), range: .init(location: 9, length: 3), rawSuggestionText: "al")]
|
||||
}
|
||||
service.processTextMessage("#pr test @al", selectedRange: .init(location: 9, length: 1))
|
||||
try await deffered.fulfill()
|
||||
}
|
||||
}
|
||||
|
@ -91,8 +91,8 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testSuggestions() {
|
||||
let suggestions: [SuggestionItem] = [.user(item: MentionSuggestionItem(id: "@user_mention_1:matrix.org", displayName: "User 1", avatarURL: nil, range: .init(), rawSuggestionText: "")),
|
||||
.user(item: MentionSuggestionItem(id: "@user_mention_2:matrix.org", displayName: "User 2", avatarURL: .mockMXCAvatar, range: .init(), rawSuggestionText: ""))]
|
||||
let suggestions: [SuggestionItem] = [.init(suggestionType: .user(.init(id: "@user_mention_1:matrix.org", displayName: "User 1", avatarURL: nil)), range: .init(), rawSuggestionText: ""),
|
||||
.init(suggestionType: .user(.init(id: "@user_mention_2:matrix.org", displayName: "User 2", avatarURL: nil)), range: .init(), rawSuggestionText: "")]
|
||||
let mockCompletionSuggestionService = CompletionSuggestionServiceMock(configuration: .init(suggestions: suggestions))
|
||||
|
||||
viewModel = ComposerToolbarViewModel(roomProxy: JoinedRoomProxyMock(.init()),
|
||||
@ -107,25 +107,43 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testSuggestionTrigger() async throws {
|
||||
let deferred = deferFulfillment(wysiwygViewModel.$attributedContent) { $0.plainText == "#not_implemented_yay" }
|
||||
wysiwygViewModel.setMarkdownContent("@test")
|
||||
wysiwygViewModel.setMarkdownContent("#not_implemented_yay")
|
||||
let deferred = deferFulfillment(wysiwygViewModel.$attributedContent) { $0.plainText == "#room-alias-test" }
|
||||
wysiwygViewModel.setMarkdownContent("@user-test")
|
||||
wysiwygViewModel.setMarkdownContent("#room-alias-test")
|
||||
try await deferred.fulfill()
|
||||
|
||||
// The first one is nil because when initialised the view model is empty
|
||||
XCTAssertEqual(completionSuggestionServiceMock.setSuggestionTriggerReceivedInvocations, [nil, .init(type: .user, text: "test", range: .init(location: 0, length: 5)), nil])
|
||||
XCTAssertEqual(completionSuggestionServiceMock.setSuggestionTriggerReceivedInvocations, [nil,
|
||||
.init(type: .user, text: "user-test", range: .init(location: 0, length: 10)),
|
||||
.init(type: .room, text: "room-alias-test",
|
||||
range: .init(location: 0, length: 16))])
|
||||
}
|
||||
|
||||
func testSelectedUserSuggestion() {
|
||||
let suggestion = SuggestionItem.user(item: .init(id: "@test:matrix.org", displayName: "Test", avatarURL: nil, range: .init(), rawSuggestionText: ""))
|
||||
let suggestion = SuggestionItem(suggestionType: .user(.init(id: "@test:matrix.org", displayName: "Test", avatarURL: nil)), range: .init(), rawSuggestionText: "")
|
||||
viewModel.context.send(viewAction: .selectedSuggestion(suggestion))
|
||||
|
||||
// The display name can be used for HTML injection in the rich text editor and it's useless anyway as the clients don't use it when resolving display names
|
||||
XCTAssertEqual(wysiwygViewModel.content.html, "<a href=\"https://matrix.to/#/@test:matrix.org\">@test:matrix.org</a> ")
|
||||
}
|
||||
|
||||
func testSelectedRoomSuggestion() {
|
||||
let suggestion = SuggestionItem(suggestionType: .room(.init(id: "!room:matrix.org",
|
||||
canonicalAlias: "#room-alias:matrix.org",
|
||||
name: "Room",
|
||||
avatar: .room(id: "!room:matrix.org",
|
||||
name: "Room",
|
||||
avatarURL: nil))),
|
||||
range: .init(), rawSuggestionText: "")
|
||||
viewModel.context.send(viewAction: .selectedSuggestion(suggestion))
|
||||
|
||||
// The display name can be used for HTML injection in the rich text editor and it's useless anyway as the clients don't use it when resolving display names
|
||||
|
||||
XCTAssertEqual(wysiwygViewModel.content.html, "<a href=\"https://matrix.to/#/%23room-alias:matrix.org\">#room-alias:matrix.org</a> ")
|
||||
}
|
||||
|
||||
func testAllUsersSuggestion() {
|
||||
let suggestion = SuggestionItem.allUsers(item: .allUsersMention(roomAvatar: nil))
|
||||
let suggestion = SuggestionItem(suggestionType: .allUsers(.room(id: "", name: nil, avatarURL: nil)), range: .init(), rawSuggestionText: "")
|
||||
viewModel.context.send(viewAction: .selectedSuggestion(suggestion))
|
||||
|
||||
var string = "@room"
|
||||
@ -138,17 +156,28 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
viewModel.context.send(viewAction: .composerAppeared)
|
||||
await Task.yield()
|
||||
let userID = "@test:matrix.org"
|
||||
let suggestion = SuggestionItem.user(item: .init(id: userID, displayName: "Test", avatarURL: nil, range: .init(), rawSuggestionText: ""))
|
||||
let suggestion = SuggestionItem(suggestionType: .user(.init(id: userID, displayName: "Test", avatarURL: nil)), range: .init(), rawSuggestionText: "")
|
||||
viewModel.context.send(viewAction: .selectedSuggestion(suggestion))
|
||||
|
||||
let attachment = wysiwygViewModel.textView.attributedText.attribute(.attachment, at: 0, effectiveRange: nil) as? PillTextAttachment
|
||||
XCTAssertEqual(attachment?.pillData?.type, .user(userID: userID))
|
||||
}
|
||||
|
||||
func testRoomMentionPillInRTE() async {
|
||||
viewModel.context.send(viewAction: .composerAppeared)
|
||||
await Task.yield()
|
||||
let roomAlias = "#test:matrix.org"
|
||||
let suggestion = SuggestionItem(suggestionType: .room(.init(id: "room-id", canonicalAlias: roomAlias, name: "Room", avatar: .room(id: "room-id", name: "Room", avatarURL: nil))), range: .init(), rawSuggestionText: "")
|
||||
viewModel.context.send(viewAction: .selectedSuggestion(suggestion))
|
||||
|
||||
let attachment = wysiwygViewModel.textView.attributedText.attribute(.attachment, at: 0, effectiveRange: nil) as? PillTextAttachment
|
||||
XCTAssertEqual(attachment?.pillData?.type, .roomAlias(roomAlias))
|
||||
}
|
||||
|
||||
func testAllUsersMentionPillInRTE() async {
|
||||
viewModel.context.send(viewAction: .composerAppeared)
|
||||
await Task.yield()
|
||||
let suggestion = SuggestionItem.allUsers(item: .allUsersMention(roomAvatar: nil))
|
||||
let suggestion = SuggestionItem(suggestionType: .allUsers(.room(id: "", name: nil, avatarURL: nil)), range: .init(), rawSuggestionText: "")
|
||||
viewModel.context.send(viewAction: .selectedSuggestion(suggestion))
|
||||
|
||||
let attachment = wysiwygViewModel.textView.attributedText.attribute(.attachment, at: 0, effectiveRange: nil) as? PillTextAttachment
|
||||
@ -770,9 +799,3 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
viewModel.context.composerFormattingEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
private extension MentionSuggestionItem {
|
||||
static func allUsersMention(roomAvatar: URL?) -> Self {
|
||||
MentionSuggestionItem(id: PillConstants.atRoom, displayName: PillConstants.everyone, avatarURL: roomAvatar, range: .init(), rawSuggestionText: "")
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user