From f7bf4b6e203f56590b3a86ebc902d2a83852bd3a Mon Sep 17 00:00:00 2001 From: Mauro <34335419+Velin92@users.noreply.github.com> Date: Mon, 9 Oct 2023 14:26:53 +0200 Subject: [PATCH] Mention in RTE (but without the pill yet) (#1863) * suggestions in RTE, but they do not render as pills yet. * remove unused code * Update ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> * PR suggestions --------- Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> --- .../RoomFlowCoordinator.swift | 3 +- .../ComposerToolbarModels.swift | 1 + .../ComposerToolbarViewModel.swift | 18 ++++++++- .../View/CompletionSuggestionView.swift | 9 +++-- .../View/ComposerToolbar.swift | 10 +++-- .../View/RoomAttachmentPicker.swift | 3 +- .../RoomScreen/RoomScreenCoordinator.swift | 8 +++- .../UITests/UITestsAppCoordinator.swift | 39 ++++++++++++------- .../ComposerToolbarViewModelTests.swift | 15 +++++-- 9 files changed, 79 insertions(+), 27 deletions(-) diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index 7e7002ef9..4cf7bfb8c 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -346,7 +346,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { timelineController: timelineController, mediaProvider: userSession.mediaProvider, emojiProvider: emojiProvider, - completionSuggestionService: completionSuggestionService) + completionSuggestionService: completionSuggestionService, + appSettings: appSettings) let coordinator = RoomScreenCoordinator(parameters: parameters) coordinator.actions .sink { [weak self] action in diff --git a/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarModels.swift b/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarModels.swift index 2bb739048..cde05dbf2 100644 --- a/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarModels.swift +++ b/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarModels.swift @@ -45,6 +45,7 @@ enum ComposerToolbarViewAction { case handlePasteOrDrop(provider: NSItemProvider) case enableTextFormatting case composerAction(action: ComposerAction) + case selectedSuggestion(_ suggestion: SuggestionItem) } struct ComposerToolbarViewState: BindableState { diff --git a/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarViewModel.swift b/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarViewModel.swift index 2e39b2b99..85846d661 100644 --- a/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarViewModel.swift +++ b/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarViewModel.swift @@ -24,6 +24,8 @@ typealias ComposerToolbarViewModelType = StateStoreViewModel = .init() var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() @@ -38,9 +40,10 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool private var currentLinkData: WysiwygLinkData? - init(wysiwygViewModel: WysiwygComposerViewModel, completionSuggestionService: CompletionSuggestionServiceProtocol, mediaProvider: MediaProviderProtocol) { + init(wysiwygViewModel: WysiwygComposerViewModel, completionSuggestionService: CompletionSuggestionServiceProtocol, mediaProvider: MediaProviderProtocol, appSettings: AppSettings) { self.wysiwygViewModel = wysiwygViewModel self.completionSuggestionService = completionSuggestionService + self.appSettings = appSettings super.init(initialViewState: ComposerToolbarViewState(areSuggestionsEnabled: completionSuggestionService.areSuggestionsEnabled, bindings: .init()), imageProvider: mediaProvider) @@ -121,6 +124,8 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool } else { wysiwygViewModel.apply(action) } + case .selectedSuggestion(let suggestion): + handleSuggestion(suggestion) } } @@ -149,6 +154,17 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool } // MARK: - Private + + private func handleSuggestion(_ suggestion: SuggestionItem) { + switch suggestion { + case let .user(item): + guard let url = try? PermalinkBuilder.permalinkTo(userIdentifier: item.id, baseURL: appSettings.permalinkBaseURL) else { + MXLog.error("Could not build user permalink") + return + } + wysiwygViewModel.setMention(url: url.absoluteString, name: item.displayName ?? item.id, mentionType: .user) + } + } private func set(mode: RoomScreenComposerMode) { guard mode != state.composerMode else { return } diff --git a/ElementX/Sources/Screens/ComposerToolbar/View/CompletionSuggestionView.swift b/ElementX/Sources/Screens/ComposerToolbar/View/CompletionSuggestionView.swift index 02165121b..dd6460244 100644 --- a/ElementX/Sources/Screens/ComposerToolbar/View/CompletionSuggestionView.swift +++ b/ElementX/Sources/Screens/ComposerToolbar/View/CompletionSuggestionView.swift @@ -19,6 +19,7 @@ import SwiftUI struct CompletionSuggestionView: View { let imageProvider: ImageProviderProtocol? let items: [SuggestionItem] + let onTap: (SuggestionItem) -> Void private enum Constants { static let topPadding: CGFloat = 8.0 @@ -63,7 +64,9 @@ struct CompletionSuggestionView: View { private func list() -> some View { List(items) { item in - Button { } label: { + Button { + onTap(item) + } label: { switch item { case .user(let mention): MentionSuggestionItemView(imageProvider: imageProvider, item: mention) @@ -135,11 +138,11 @@ struct CompletionSuggestion_Previews: PreviewProvider, TestablePreview { VStack { CompletionSuggestionView(imageProvider: MockMediaProvider(), items: [.user(item: MentionSuggestionItem(id: "@user_mention_1:matrix.org", displayName: "User 1", avatarURL: nil)), - .user(item: MentionSuggestionItem(id: "@user_mention_2:matrix.org", displayName: "User 2", avatarURL: URL.documentsDirectory))]) + .user(item: MentionSuggestionItem(id: "@user_mention_2:matrix.org", displayName: "User 2", avatarURL: URL.documentsDirectory))]) { _ in } } VStack { CompletionSuggestionView(imageProvider: MockMediaProvider(), - items: multipleItems) + items: multipleItems) { _ in } } } } diff --git a/ElementX/Sources/Screens/ComposerToolbar/View/ComposerToolbar.swift b/ElementX/Sources/Screens/ComposerToolbar/View/ComposerToolbar.swift index 4eb86fe1a..4def925f8 100644 --- a/ElementX/Sources/Screens/ComposerToolbar/View/ComposerToolbar.swift +++ b/ElementX/Sources/Screens/ComposerToolbar/View/ComposerToolbar.swift @@ -49,7 +49,9 @@ struct ComposerToolbar: View { } private var suggestionView: some View { - CompletionSuggestionView(imageProvider: context.imageProvider, items: context.viewState.suggestions) + CompletionSuggestionView(imageProvider: context.imageProvider, items: context.viewState.suggestions) { suggestion in + context.send(viewAction: .selectedSuggestion(suggestion)) + } } private var topBar: some View { @@ -194,7 +196,8 @@ struct ComposerToolbar_Previews: PreviewProvider, TestablePreview { static let wysiwygViewModel = WysiwygComposerViewModel() static let composerViewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel, completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init(suggestions: suggestions)), - mediaProvider: MockMediaProvider()) + mediaProvider: MockMediaProvider(), + appSettings: ServiceLocator.shared.settings) static let suggestions: [SuggestionItem] = [.user(item: MentionSuggestionItem(id: "@user_mention_1:matrix.org", displayName: "User 1", avatarURL: nil)), .user(item: MentionSuggestionItem(id: "@user_mention_2:matrix.org", displayName: "User 2", avatarURL: URL.documentsDirectory))] @@ -219,7 +222,8 @@ extension ComposerToolbar { let wysiwygViewModel = WysiwygComposerViewModel() let composerViewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel, completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), - mediaProvider: MockMediaProvider()) + mediaProvider: MockMediaProvider(), + appSettings: ServiceLocator.shared.settings) return ComposerToolbar(context: composerViewModel.context, wysiwygViewModel: wysiwygViewModel, keyCommandHandler: { _ in false }) diff --git a/ElementX/Sources/Screens/ComposerToolbar/View/RoomAttachmentPicker.swift b/ElementX/Sources/Screens/ComposerToolbar/View/RoomAttachmentPicker.swift index bd0c33bc6..ee1ed6a61 100644 --- a/ElementX/Sources/Screens/ComposerToolbar/View/RoomAttachmentPicker.swift +++ b/ElementX/Sources/Screens/ComposerToolbar/View/RoomAttachmentPicker.swift @@ -117,7 +117,8 @@ struct RoomAttachmentPicker: View { struct RoomAttachmentPicker_Previews: PreviewProvider, TestablePreview { static let viewModel = ComposerToolbarViewModel(wysiwygViewModel: WysiwygComposerViewModel(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), - mediaProvider: MockMediaProvider()) + mediaProvider: MockMediaProvider(), + appSettings: ServiceLocator.shared.settings) static var previews: some View { RoomAttachmentPicker(context: viewModel.context) diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift index 712a07186..86f5a54e6 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift @@ -25,6 +25,7 @@ struct RoomScreenCoordinatorParameters { let mediaProvider: MediaProviderProtocol let emojiProvider: EmojiProviderProtocol let completionSuggestionService: CompletionSuggestionServiceProtocol + let appSettings: AppSettings } enum RoomScreenCoordinatorAction { @@ -59,7 +60,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol { viewModel = RoomScreenViewModel(timelineController: parameters.timelineController, mediaProvider: parameters.mediaProvider, roomProxy: parameters.roomProxy, - appSettings: ServiceLocator.shared.settings, + appSettings: parameters.appSettings, analytics: ServiceLocator.shared.analytics, userIndicatorController: ServiceLocator.shared.userIndicatorController) @@ -67,7 +68,10 @@ final class RoomScreenCoordinator: CoordinatorProtocol { maxCompressedHeight: ComposerConstant.maxHeight, maxExpandedHeight: ComposerConstant.maxHeight, parserStyle: .elementX) - composerViewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel, completionSuggestionService: parameters.completionSuggestionService, mediaProvider: parameters.mediaProvider) + composerViewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel, + completionSuggestionService: parameters.completionSuggestionService, + mediaProvider: parameters.mediaProvider, + appSettings: parameters.appSettings) } // MARK: - Public diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index 17a681e7e..a395b5fcf 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -236,7 +236,8 @@ class MockScreen: Identifiable { timelineController: MockRoomTimelineController(), mediaProvider: MockMediaProvider(), emojiProvider: EmojiProvider(), - completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init())) + completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), + appSettings: ServiceLocator.shared.settings) let coordinator = RoomScreenCoordinator(parameters: parameters) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator @@ -246,7 +247,8 @@ class MockScreen: Identifiable { timelineController: MockRoomTimelineController(), mediaProvider: MockMediaProvider(), emojiProvider: EmojiProvider(), - completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init())) + completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), + appSettings: ServiceLocator.shared.settings) let coordinator = RoomScreenCoordinator(parameters: parameters) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator @@ -258,7 +260,8 @@ class MockScreen: Identifiable { timelineController: timelineController, mediaProvider: MockMediaProvider(), emojiProvider: EmojiProvider(), - completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init())) + completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), + appSettings: ServiceLocator.shared.settings) let coordinator = RoomScreenCoordinator(parameters: parameters) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator @@ -270,7 +273,8 @@ class MockScreen: Identifiable { timelineController: timelineController, mediaProvider: MockMediaProvider(), emojiProvider: EmojiProvider(), - completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init())) + completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), + appSettings: ServiceLocator.shared.settings) let coordinator = RoomScreenCoordinator(parameters: parameters) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator @@ -283,7 +287,8 @@ class MockScreen: Identifiable { timelineController: timelineController, mediaProvider: MockMediaProvider(), emojiProvider: EmojiProvider(), - completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init())) + completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), + appSettings: ServiceLocator.shared.settings) let coordinator = RoomScreenCoordinator(parameters: parameters) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator @@ -298,7 +303,8 @@ class MockScreen: Identifiable { timelineController: timelineController, mediaProvider: MockMediaProvider(), emojiProvider: EmojiProvider(), - completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init())) + completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), + appSettings: ServiceLocator.shared.settings) let coordinator = RoomScreenCoordinator(parameters: parameters) navigationStackCoordinator.setRootCoordinator(coordinator) @@ -313,7 +319,8 @@ class MockScreen: Identifiable { timelineController: timelineController, mediaProvider: MockMediaProvider(), emojiProvider: EmojiProvider(), - completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init())) + completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), + appSettings: ServiceLocator.shared.settings) let coordinator = RoomScreenCoordinator(parameters: parameters) navigationStackCoordinator.setRootCoordinator(coordinator) @@ -328,7 +335,8 @@ class MockScreen: Identifiable { timelineController: timelineController, mediaProvider: MockMediaProvider(), emojiProvider: EmojiProvider(), - completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init())) + completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), + appSettings: ServiceLocator.shared.settings) let coordinator = RoomScreenCoordinator(parameters: parameters) navigationStackCoordinator.setRootCoordinator(coordinator) @@ -344,7 +352,8 @@ class MockScreen: Identifiable { timelineController: timelineController, mediaProvider: MockMediaProvider(), emojiProvider: EmojiProvider(), - completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init())) + completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), + appSettings: ServiceLocator.shared.settings) let coordinator = RoomScreenCoordinator(parameters: parameters) navigationStackCoordinator.setRootCoordinator(coordinator) @@ -359,7 +368,8 @@ class MockScreen: Identifiable { timelineController: timelineController, mediaProvider: MockMediaProvider(), emojiProvider: EmojiProvider(), - completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init())) + completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), + appSettings: ServiceLocator.shared.settings) let coordinator = RoomScreenCoordinator(parameters: parameters) navigationStackCoordinator.setRootCoordinator(coordinator) @@ -374,7 +384,8 @@ class MockScreen: Identifiable { timelineController: timelineController, mediaProvider: MockMediaProvider(), emojiProvider: EmojiProvider(), - completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init())) + completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), + appSettings: ServiceLocator.shared.settings) let coordinator = RoomScreenCoordinator(parameters: parameters) navigationStackCoordinator.setRootCoordinator(coordinator) @@ -389,7 +400,8 @@ class MockScreen: Identifiable { timelineController: timelineController, mediaProvider: MockMediaProvider(), emojiProvider: EmojiProvider(), - completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init())) + completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), + appSettings: ServiceLocator.shared.settings) let coordinator = RoomScreenCoordinator(parameters: parameters) navigationStackCoordinator.setRootCoordinator(coordinator) @@ -404,7 +416,8 @@ class MockScreen: Identifiable { timelineController: timelineController, mediaProvider: MockMediaProvider(), emojiProvider: EmojiProvider(), - completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init())) + completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), + appSettings: ServiceLocator.shared.settings) let coordinator = RoomScreenCoordinator(parameters: parameters) navigationStackCoordinator.setRootCoordinator(coordinator) diff --git a/UnitTests/Sources/ComposerToolbarViewModelTests.swift b/UnitTests/Sources/ComposerToolbarViewModelTests.swift index e6d143988..593113c90 100644 --- a/UnitTests/Sources/ComposerToolbarViewModelTests.swift +++ b/UnitTests/Sources/ComposerToolbarViewModelTests.swift @@ -36,7 +36,8 @@ class ComposerToolbarViewModelTests: XCTestCase { completionSuggestionServiceMock = CompletionSuggestionServiceMock(configuration: .init()) viewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel, completionSuggestionService: completionSuggestionServiceMock, - mediaProvider: MockMediaProvider()) + mediaProvider: MockMediaProvider(), + appSettings: ServiceLocator.shared.settings) } func testComposerFocus() { @@ -105,16 +106,24 @@ class ComposerToolbarViewModelTests: XCTestCase { let mockCompletionSuggestionService = CompletionSuggestionServiceMock(configuration: .init(suggestions: suggestions)) viewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel, completionSuggestionService: mockCompletionSuggestionService, - mediaProvider: MockMediaProvider()) + mediaProvider: MockMediaProvider(), + appSettings: ServiceLocator.shared.settings) XCTAssertEqual(viewModel.state.suggestions, suggestions) } func testSuggestionTrigger() { wysiwygViewModel.setMarkdownContent("@test") - wysiwygViewModel.setMarkdownContent("#not_implemented_yey") + wysiwygViewModel.setMarkdownContent("#not_implemented_yay") // The first one is nil because when initialised the view model is empty XCTAssertEqual(completionSuggestionServiceMock.setSuggestionTriggerReceivedInvocations, [nil, .init(type: .user, text: "test"), nil]) } + + func testSelectedSuggestion() { + let suggestion = SuggestionItem.user(item: .init(id: "@test:matrix.org", displayName: "Test", avatarURL: nil)) + viewModel.context.send(viewAction: .selectedSuggestion(suggestion)) + + XCTAssertEqual(wysiwygViewModel.content.html, "Test ") + } }