diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarModels.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarModels.swift index 6cc74a88e..5f6059534 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarModels.swift @@ -65,6 +65,8 @@ enum ComposerAttachmentType { } struct ComposerToolbarViewState: BindableState { + let wysiwygViewModel: WysiwygComposerViewModel + var composerMode: ComposerMode = .default var composerEmpty = true /// Could be false if sending is disabled in the room @@ -76,6 +78,8 @@ struct ComposerToolbarViewState: BindableState { var isRoomEncrypted: Bool var isLocationSharingEnabled: Bool + var keyCommands: [WysiwygKeyCommand] = [] + var bindings: ComposerToolbarViewStateBindings var isUploading: Bool { diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModel.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModel.swift index 99cbcd1c3..cd89e335f 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModel.swift @@ -63,13 +63,20 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool mentionBuilder = MentionBuilder() attributedStringBuilder = AttributedStringBuilder(cacheKey: "Composer", mentionBuilder: mentionBuilder) - super.init(initialViewState: ComposerToolbarViewState(audioPlayerState: .init(id: .recorderPreview, title: L10n.commonVoiceMessage, duration: 0), + super.init(initialViewState: ComposerToolbarViewState(wysiwygViewModel: wysiwygViewModel, + audioPlayerState: .init(id: .recorderPreview, title: L10n.commonVoiceMessage, duration: 0), audioRecorderState: .init(), isRoomEncrypted: roomProxy.infoPublisher.value.isEncrypted, isLocationSharingEnabled: appSettings.mapTilerConfiguration.isEnabled, bindings: .init()), mediaProvider: mediaProvider) + state.keyCommands = [ + .enter { [weak self] in + self?.process(viewAction: .sendMessage) + } + ] + roomProxy.infoPublisher .map(\.isEncrypted) .receive(on: DispatchQueue.main) @@ -100,6 +107,13 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool } .store(in: &cancellables) + // Needs to be observable or the placeholder and the dictation state will not be managed correctly. + wysiwygViewModel.objectWillChange + .sink { [weak self] _ in + self?.context.objectWillChange.send() + } + .store(in: &cancellables) + context.$viewState .map(\.bindings.plainComposerText) .removeDuplicates() @@ -269,14 +283,6 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool handleSaveDraft(isVolatile: false) } - var keyCommands: [WysiwygKeyCommand] { - [ - .enter { [weak self] in - self?.process(viewAction: .sendMessage) - } - ] - } - // MARK: - Private private func handleLoadDraft(_ draft: ComposerDraftProxy) { diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModelProtocol.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModelProtocol.swift index 170888837..7afcd5349 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModelProtocol.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModelProtocol.swift @@ -13,7 +13,6 @@ import WysiwygComposer protocol ComposerToolbarViewModelProtocol { var actions: AnyPublisher { get } var context: ComposerToolbarViewModelType.Context { get } - var keyCommands: [WysiwygKeyCommand] { get } func process(timelineAction: TimelineComposerAction) func loadDraft() async diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/ComposerToolbar.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/ComposerToolbar.swift index d84bf1c63..96c6e3027 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/ComposerToolbar.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/ComposerToolbar.swift @@ -13,12 +13,6 @@ import WysiwygComposer struct ComposerToolbar: View { @ObservedObject var context: ComposerToolbarViewModel.Context - - // Needs to be observable or the placeholder and the dictation state are not managed correctly. - @ObservedObject var wysiwygViewModel: WysiwygComposerViewModel - - let keyCommands: [WysiwygKeyCommand] - @FocusState private var composerFocused: Bool @State private var frame: CGRect = .zero @Environment(\.verticalSizeClass) private var verticalSizeClass @@ -249,9 +243,9 @@ struct ComposerToolbar: View { private var composerView: WysiwygComposerView { WysiwygComposerView(placeholder: placeholder, placeholderColor: .compound.textSecondary, - viewModel: wysiwygViewModel, + viewModel: context.viewState.wysiwygViewModel, itemProviderHelper: ItemProviderHelper(), - keyCommands: keyCommands) { provider in + keyCommands: context.viewState.keyCommands) { provider in context.send(viewAction: .handlePasteOrDrop(provider: provider)) } } @@ -343,9 +337,7 @@ struct ComposerToolbar_Previews: PreviewProvider, TestablePreview { // Putting them is VStack allows the completion suggestion preview to work properly in tests VStack(spacing: 8) { // The mock functon can't be used in this context because it does not hold a reference to the view model, losing the combine subscriptions - ComposerToolbar(context: composerViewModel.context, - wysiwygViewModel: wysiwygViewModel, - keyCommands: []) + ComposerToolbar(context: composerViewModel.context) } .previewDisplayName("With Suggestions") @@ -386,9 +378,7 @@ extension ComposerToolbar { model.state.composerEmpty = focused return model } - return ComposerToolbar(context: composerViewModel.context, - wysiwygViewModel: wysiwygViewModel, - keyCommands: []) + return ComposerToolbar(context: composerViewModel.context) } static func textWithVoiceMessage(focused: Bool = true) -> ComposerToolbar { @@ -405,9 +395,7 @@ extension ComposerToolbar { model.state.composerEmpty = focused return model } - return ComposerToolbar(context: composerViewModel.context, - wysiwygViewModel: wysiwygViewModel, - keyCommands: []) + return ComposerToolbar(context: composerViewModel.context) } static func voiceMessageRecordingMock() -> ComposerToolbar { @@ -424,9 +412,7 @@ extension ComposerToolbar { model.state.composerMode = .recordVoiceMessage(state: AudioRecorderState()) return model } - return ComposerToolbar(context: composerViewModel.context, - wysiwygViewModel: wysiwygViewModel, - keyCommands: []) + return ComposerToolbar(context: composerViewModel.context) } static func voiceMessagePreviewMock(uploading: Bool) -> ComposerToolbar { @@ -448,9 +434,7 @@ extension ComposerToolbar { isUploading: uploading) return model } - return ComposerToolbar(context: composerViewModel.context, - wysiwygViewModel: wysiwygViewModel, - keyCommands: []) + return ComposerToolbar(context: composerViewModel.context) } static func replyLoadingPreviewMock(isLoading: Bool) -> ComposerToolbar { @@ -473,9 +457,7 @@ extension ComposerToolbar { eventID: "", eventContent: .message(.text(.init(body: "Hello World!")))), isThread: false) return model } - return ComposerToolbar(context: composerViewModel.context, - wysiwygViewModel: wysiwygViewModel, - keyCommands: []) + return ComposerToolbar(context: composerViewModel.context) } static func disabledPreviewMock() -> ComposerToolbar { @@ -492,8 +474,6 @@ extension ComposerToolbar { model.state.canSend = false return model } - return ComposerToolbar(context: composerViewModel.context, - wysiwygViewModel: wysiwygViewModel, - keyCommands: []) + return ComposerToolbar(context: composerViewModel.context) } } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift index d11f7fade..a4e54ce58 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift @@ -52,7 +52,6 @@ final class RoomScreenCoordinator: CoordinatorProtocol { private var roomViewModel: RoomScreenViewModelProtocol private var timelineViewModel: TimelineViewModelProtocol private var composerViewModel: ComposerToolbarViewModelProtocol - private var wysiwygViewModel: WysiwygComposerViewModel private var cancellables = Set() @@ -90,11 +89,11 @@ final class RoomScreenCoordinator: CoordinatorProtocol { emojiProvider: parameters.emojiProvider, timelineControllerFactory: parameters.timelineControllerFactory, clientProxy: parameters.clientProxy) - - wysiwygViewModel = WysiwygComposerViewModel(minHeight: ComposerConstant.minHeight, - maxCompressedHeight: ComposerConstant.maxHeight, - maxExpandedHeight: ComposerConstant.maxHeight, - parserStyle: .elementX) + + let wysiwygViewModel = WysiwygComposerViewModel(minHeight: ComposerConstant.minHeight, + maxCompressedHeight: ComposerConstant.maxHeight, + maxExpandedHeight: ComposerConstant.maxHeight, + parserStyle: .elementX) let composerViewModel = ComposerToolbarViewModel(initialText: parameters.sharedText, roomProxy: parameters.roomProxy, wysiwygViewModel: wysiwygViewModel, @@ -215,9 +214,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol { } func toPresentable() -> AnyView { - let composerToolbar = ComposerToolbar(context: composerViewModel.context, - wysiwygViewModel: wysiwygViewModel, - keyCommands: composerViewModel.keyCommands) + let composerToolbar = ComposerToolbar(context: composerViewModel.context) return AnyView(RoomScreen(context: roomViewModel.context, timelineContext: timelineViewModel.context, diff --git a/ElementX/Sources/Screens/ThreadTimelineScreen/ThreadTimelineScreenCoordinator.swift b/ElementX/Sources/Screens/ThreadTimelineScreen/ThreadTimelineScreenCoordinator.swift index 8a3fa85d2..9203b2331 100644 --- a/ElementX/Sources/Screens/ThreadTimelineScreen/ThreadTimelineScreenCoordinator.swift +++ b/ElementX/Sources/Screens/ThreadTimelineScreen/ThreadTimelineScreenCoordinator.swift @@ -43,7 +43,6 @@ final class ThreadTimelineScreenCoordinator: CoordinatorProtocol { private let viewModel: ThreadTimelineScreenViewModelProtocol private let timelineViewModel: TimelineViewModelProtocol private var composerViewModel: ComposerToolbarViewModelProtocol - private var wysiwygViewModel: WysiwygComposerViewModel private var cancellables = Set() @@ -70,10 +69,10 @@ final class ThreadTimelineScreenCoordinator: CoordinatorProtocol { timelineControllerFactory: parameters.timelineControllerFactory, clientProxy: parameters.clientProxy) - wysiwygViewModel = WysiwygComposerViewModel(minHeight: ComposerConstant.minHeight, - maxCompressedHeight: ComposerConstant.maxHeight, - maxExpandedHeight: ComposerConstant.maxHeight, - parserStyle: .elementX) + let wysiwygViewModel = WysiwygComposerViewModel(minHeight: ComposerConstant.minHeight, + maxCompressedHeight: ComposerConstant.maxHeight, + maxExpandedHeight: ComposerConstant.maxHeight, + parserStyle: .elementX) #warning("Drafts are not handled and they can't be without rust side changes") @@ -144,9 +143,7 @@ final class ThreadTimelineScreenCoordinator: CoordinatorProtocol { } func toPresentable() -> AnyView { - let composerToolbar = ComposerToolbar(context: composerViewModel.context, - wysiwygViewModel: wysiwygViewModel, - keyCommands: composerViewModel.keyCommands) + let composerToolbar = ComposerToolbar(context: composerViewModel.context) return AnyView(ThreadTimelineScreen(context: viewModel.context, timelineContext: timelineViewModel.context, diff --git a/UnitTests/Sources/ComposerToolbarViewModelTests.swift b/UnitTests/Sources/ComposerToolbarViewModelTests.swift index e8fe76233..0a48fc8a1 100644 --- a/UnitTests/Sources/ComposerToolbarViewModelTests.swift +++ b/UnitTests/Sources/ComposerToolbarViewModelTests.swift @@ -67,7 +67,7 @@ class ComposerToolbarViewModelTests: XCTestCase { } func testHandleKeyCommand() { - XCTAssertTrue(viewModel.keyCommands.count == 1) + XCTAssertTrue(viewModel.context.viewState.keyCommands.count == 1) } func testComposerFocusAfterEnablingRTE() {