mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 12:07:55 +00:00
Move ComposerToolbar view required dependencies to the view model and read them, similarly to the other views, through the context's view state.
This commit is contained in:
committed by
Stefan Ceriu
parent
4072031f4e
commit
af97567edc
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
-1
@@ -13,7 +13,6 @@ import WysiwygComposer
|
||||
protocol ComposerToolbarViewModelProtocol {
|
||||
var actions: AnyPublisher<ComposerToolbarViewModelAction, Never> { get }
|
||||
var context: ComposerToolbarViewModelType.Context { get }
|
||||
var keyCommands: [WysiwygKeyCommand] { get }
|
||||
|
||||
func process(timelineAction: TimelineComposerAction)
|
||||
func loadDraft() async
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<AnyCancellable>()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<AnyCancellable>()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -67,7 +67,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testHandleKeyCommand() {
|
||||
XCTAssertTrue(viewModel.keyCommands.count == 1)
|
||||
XCTAssertTrue(viewModel.context.viewState.keyCommands.count == 1)
|
||||
}
|
||||
|
||||
func testComposerFocusAfterEnablingRTE() {
|
||||
|
||||
Reference in New Issue
Block a user