diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index 87b5f0367b..db8c04b3b7 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -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, diff --git a/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift b/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift index b65612b570..d4bc0a4e02 100644 --- a/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift +++ b/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift @@ -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, diff --git a/ElementX/Sources/Other/Avatars.swift b/ElementX/Sources/Other/Avatars.swift index 2a2f91b436..244695b66b 100644 --- a/ElementX/Sources/Other/Avatars.swift +++ b/ElementX/Sources/Other/Avatars.swift @@ -133,6 +133,7 @@ enum RoomAvatarSizeOnScreen { case notificationSettings case roomDirectorySearch case joinRoom + case suggestions var value: CGFloat { switch self { @@ -142,6 +143,8 @@ enum RoomAvatarSizeOnScreen { return 32 case .roomDirectorySearch: return 32 + case .suggestions: + return 32 case .messageForwarding: return 36 case .globalSearch: diff --git a/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift b/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift index 2eb9ef85fe..4ccea1cd1a 100644 --- a/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift +++ b/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift @@ -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) diff --git a/ElementX/Sources/Other/HTMLParsing/ElementXAttributeScope.swift b/ElementX/Sources/Other/HTMLParsing/ElementXAttributeScope.swift index 97f0e0700b..490062c90a 100644 --- a/ElementX/Sources/Other/HTMLParsing/ElementXAttributeScope.swift +++ b/ElementX/Sources/Other/HTMLParsing/ElementXAttributeScope.swift @@ -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 diff --git a/ElementX/Sources/Other/Pills/MentionBuilder.swift b/ElementX/Sources/Other/Pills/MentionBuilder.swift index d140c026fc..396524aac4 100644 --- a/ElementX/Sources/Other/Pills/MentionBuilder.swift +++ b/ElementX/Sources/Other/Pills/MentionBuilder.swift @@ -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 + } + } } diff --git a/ElementX/Sources/Other/Pills/PlainMentionBuilder.swift b/ElementX/Sources/Other/Pills/PlainMentionBuilder.swift index 2ad6f1db17..111c8b49cc 100644 --- a/ElementX/Sources/Other/Pills/PlainMentionBuilder.swift +++ b/ElementX/Sources/Other/Pills/PlainMentionBuilder.swift @@ -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) { } diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/CompletionSuggestionService.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/CompletionSuggestionService.swift index 3871561092..0026819d56 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/CompletionSuggestionService.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/CompletionSuggestionService.swift @@ -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(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()) + } } diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/CompletionSuggestionServiceProtocol.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/CompletionSuggestionServiceProtocol.swift index 3d59bdb8f5..6102d33a3d 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/CompletionSuggestionServiceProtocol.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/CompletionSuggestionServiceProtocol.swift @@ -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) +struct SuggestionItem: Identifiable, Equatable { + enum SuggestionType: Equatable { + case user(User) + case allUsers(RoomAvatar) + case room(Room) + } + + struct User: 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 self { + switch suggestionType { case .user(let user): - return user.id + user.id case .allUsers: - return PillConstants.atRoom + PillConstants.atRoom + case .room(let room): + room.id } } - var range: NSRange { - switch self { - case .user(let item), .allUsers(let item): - return item.range + 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 } } -} - -struct MentionSuggestionItem: Identifiable, Equatable { - let id: String - let displayName: String? - let avatarURL: URL? - let range: NSRange - let rawSuggestionText: String } // 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 } diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModel.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModel.swift index 8408a096f9..385a166614 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModel.swift @@ -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 } } diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/CompletionSuggestionView.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/CompletionSuggestionView.swift index 917e49bf5d..28dce80032 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/CompletionSuggestionView.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/CompletionSuggestionView.swift @@ -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: 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 } diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/ComposerToolbar.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/ComposerToolbar.swift index e4323a9674..32c98e3342 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/ComposerToolbar.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/ComposerToolbar.swift @@ -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) diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MentionSuggestionItemView.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MentionSuggestionItemView.swift index 55fe8bc205..5e1e2532c4 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MentionSuggestionItemView.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MentionSuggestionItemView.swift @@ -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: .suggestions), mediaProvider: mediaProvider) + case .allUsers(let avatar): + RoomAvatarImage(avatar: avatar, avatarSize: .room(on: .suggestions), mediaProvider: mediaProvider) + case .room(let room): + RoomAvatarImage(avatar: room.avatar, avatarSize: .room(on: .suggestions), 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") } } diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreen.iPad-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreen.iPad-en-GB-0.png index 40f302627c..eae437f6e7 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreen.iPad-en-GB-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreen.iPad-en-GB-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:61ebfb0d15c41532a779c91b2ff490a7eb2daaeed9cbc8d9b804535e6d721758 -size 158570 +oid sha256:35951ab2ca021aacb1d40c8ae270efe23f47b38d0715b0475b93e0878300253d +size 163854 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreen.iPad-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreen.iPad-pseudo-0.png index 89a6b35f0a..bd78981f5c 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreen.iPad-pseudo-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreen.iPad-pseudo-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5bf11205a434cc0ff90b275e4475e86134ed15e861f21fb37f96a9fc3b25e262 -size 158954 +oid sha256:31ddb9be88bf2a6469fc8d3c8178170ca246b7c5429f9db98bb6f737cb282703 +size 164238 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreen.iPhone-16-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreen.iPhone-16-en-GB-0.png index 9d042a8815..c96a20ce4e 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreen.iPhone-16-en-GB-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreen.iPhone-16-en-GB-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b0088c448b0a3e1106a869a747c7dc85c882c3a3553e529eb3e502ccbbb22e25 -size 94620 +oid sha256:8687afad034254066ea656378022ae61aed58cc2b2214edaf323ab13aded7c73 +size 99720 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreen.iPhone-16-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreen.iPhone-16-pseudo-0.png index fea211012f..380e0548b8 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreen.iPhone-16-pseudo-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreen.iPhone-16-pseudo-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ba7e2d7a550a145a8f2872986b8eb6d437306390feaa2019329192c16398f642 -size 94955 +oid sha256:03d9f85d77b1e75bbc8ab40ce84756067a30d35d09a7ce6e821302ba35dbdece +size 100052 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.All-users-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.All-users-iPad-en-GB.png new file mode 100644 index 0000000000..d6b0460119 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.All-users-iPad-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e19ea504637c4aefa97fb4588c1731bb077fb825ba67fa4cfe1263722a9d13e2 +size 80213 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.All-users-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.All-users-iPad-pseudo.png new file mode 100644 index 0000000000..7938ff341e --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.All-users-iPad-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dab315a14ac8d0214c38ed83b4dd618311b90340b7f3a5b0e53c25fb1a83f2de +size 80559 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.All-users-iPhone-16-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.All-users-iPhone-16-en-GB.png new file mode 100644 index 0000000000..83f2b40c51 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.All-users-iPhone-16-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c4762b524a5d07f3dc8523b33ca2f724a48bf8c9c6153b317f74069e22870d8d +size 39035 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.All-users-iPhone-16-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.All-users-iPhone-16-pseudo.png new file mode 100644 index 0000000000..a80a445b32 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.All-users-iPhone-16-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c9c3c27bc47a307caee04d14dc733be9ed81407e9850012d351de55f75a84e40 +size 40647 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.Room-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.Room-iPad-en-GB.png new file mode 100644 index 0000000000..65e39458c5 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.Room-iPad-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d4d9e70940d588b07861ad1439dae36303e298b020602e0f912c3cdd6a245fac +size 82208 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.Room-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.Room-iPad-pseudo.png new file mode 100644 index 0000000000..65e39458c5 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.Room-iPad-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d4d9e70940d588b07861ad1439dae36303e298b020602e0f912c3cdd6a245fac +size 82208 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.Room-iPhone-16-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.Room-iPhone-16-en-GB.png new file mode 100644 index 0000000000..35909286b3 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.Room-iPhone-16-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:098f483b0d38058c35128d0afc97984fd4ce8279bcc3be9f45accb709a0f5641 +size 41394 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.Room-iPhone-16-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.Room-iPhone-16-pseudo.png new file mode 100644 index 0000000000..35909286b3 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.Room-iPhone-16-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:098f483b0d38058c35128d0afc97984fd4ce8279bcc3be9f45accb709a0f5641 +size 41394 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.iPad-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.User-iPad-en-GB.png similarity index 100% rename from PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.iPad-en-GB-0.png rename to PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.User-iPad-en-GB.png diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.iPad-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.User-iPad-pseudo.png similarity index 100% rename from PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.iPad-pseudo-0.png rename to PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.User-iPad-pseudo.png diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.iPhone-16-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.User-iPhone-16-en-GB.png similarity index 100% rename from PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.iPhone-16-en-GB-0.png rename to PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.User-iPhone-16-en-GB.png diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.iPhone-16-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.User-iPhone-16-pseudo.png similarity index 100% rename from PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.iPhone-16-pseudo-0.png rename to PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.User-iPhone-16-pseudo.png diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.iPad-en-GB-1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.User-no-display-name-iPad-en-GB.png similarity index 100% rename from PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.iPad-en-GB-1.png rename to PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.User-no-display-name-iPad-en-GB.png diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.iPad-pseudo-1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.User-no-display-name-iPad-pseudo.png similarity index 100% rename from PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.iPad-pseudo-1.png rename to PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.User-no-display-name-iPad-pseudo.png diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.iPhone-16-en-GB-1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.User-no-display-name-iPhone-16-en-GB.png similarity index 100% rename from PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.iPhone-16-en-GB-1.png rename to PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.User-no-display-name-iPhone-16-en-GB.png diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.iPhone-16-pseudo-1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.User-no-display-name-iPhone-16-pseudo.png similarity index 100% rename from PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.iPhone-16-pseudo-1.png rename to PreviewTests/Sources/__Snapshots__/PreviewTests/mentionSuggestionItemView.User-no-display-name-iPhone-16-pseudo.png diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.iPad-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.iPad-en-GB-0.png index 5c4683c8f0..8e96b2dca8 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.iPad-en-GB-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.iPad-en-GB-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9c92c6e353e7759adf0f75cf9a21297656c5c6daad3cfdbbf39301440b5bf8a0 -size 159902 +oid sha256:18a5a2705faf3823e0db694088206dc582621b4c52f31b7e5bdf5682889d5b94 +size 165467 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.iPad-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.iPad-pseudo-0.png index 9e7764fc1b..7b6a4d8420 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.iPad-pseudo-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.iPad-pseudo-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dae65098eaf100bb2b762b5a25b4caa3a366e2780690e9a41a976d0bce707a74 -size 161798 +oid sha256:cae43b115dc17061874c1338042c62d71b0e41eebfee30e8c6e8c094490be64a +size 167363 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.iPhone-16-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.iPhone-16-en-GB-0.png index b55e66f7af..27a303fbc3 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.iPhone-16-en-GB-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.iPhone-16-en-GB-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bac9d7d51106483c23a118e85a567aafa466ba0a118eae797bd27e8f69d97417 -size 108070 +oid sha256:70ad8dc0e88cd46109fea561c0b7e220ca6bad6c7eb124d5b9f14b233f537820 +size 112987 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.iPhone-16-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.iPhone-16-pseudo-0.png index a15cc2952d..1ca291f7f8 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.iPhone-16-pseudo-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.iPhone-16-pseudo-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:94508db7f079602a32529ba110fd027d9660c75c1d81f38a2e28a9448f12d5cc -size 107929 +oid sha256:b2c4306135e1efa0552deddcaa09e22cae4f23687b0dcf840c9e39bad61635c0 +size 112846 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPad-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPad-en-GB-0.png index e48d445e50..8684c48479 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPad-en-GB-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPad-en-GB-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:29ecf5613971aad7c7890ed563d8b69d3c13ad1b2d9189c6cb3a80af53c6568b -size 156966 +oid sha256:69544d0b3042e3997689f0131e87af191929ef1b62647b22b13bfe362307db1d +size 162531 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPad-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPad-pseudo-0.png index 7f4f3b8d6f..153a14eb73 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPad-pseudo-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPad-pseudo-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:270c52cdad79a7495bded4560a8500f4b9266e0af8d3ec0742a42a2fd7c79888 -size 158449 +oid sha256:d224a0f3ea5243508744166a1ce4b635eeb4f08363e6c2558bf1ec2574ce8a1d +size 164014 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPhone-16-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPhone-16-en-GB-0.png index c6b0b25203..fcb69de22a 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPhone-16-en-GB-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPhone-16-en-GB-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:429ace579d36361e16639266d2b14b6a515da57b14a25205c5e2460a1a8fe85e -size 105969 +oid sha256:a995e503150a60c80666fba9135b70b3d21a59134ec400f8caf9bac3df16c54c +size 110886 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPhone-16-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPhone-16-pseudo-0.png index 0f7ac0010a..6b91338d4a 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPhone-16-pseudo-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPhone-16-pseudo-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f29a6ad8b2d81b1e3ffe1d9d8b526488d7c021bf17b04e74d8d3db38f2955944 -size 107269 +oid sha256:f5921b93c625428f436a43fec74d940a277d11fabbcf0e1412efbe574222101d +size 112186 diff --git a/UnitTests/Sources/CompletionSuggestionServiceTests.swift b/UnitTests/Sources/CompletionSuggestionServiceTests.swift index f93dec8cd2..b565201019 100644 --- a/UnitTests/Sources/CompletionSuggestionServiceTests.swift +++ b/UnitTests/Sources/CompletionSuggestionServiceTests.swift @@ -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() } } diff --git a/UnitTests/Sources/ComposerToolbarViewModelTests.swift b/UnitTests/Sources/ComposerToolbarViewModelTests.swift index 3a70b90c04..b786e2eb1d 100644 --- a/UnitTests/Sources/ComposerToolbarViewModelTests.swift +++ b/UnitTests/Sources/ComposerToolbarViewModelTests.swift @@ -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, "@test:matrix.org ") } + 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, "#room-alias:matrix.org ") + } + 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: "") - } -}