mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +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 {
|
struct ComposerToolbarViewState: BindableState {
|
||||||
|
let wysiwygViewModel: WysiwygComposerViewModel
|
||||||
|
|
||||||
var composerMode: ComposerMode = .default
|
var composerMode: ComposerMode = .default
|
||||||
var composerEmpty = true
|
var composerEmpty = true
|
||||||
/// Could be false if sending is disabled in the room
|
/// Could be false if sending is disabled in the room
|
||||||
@@ -76,6 +78,8 @@ struct ComposerToolbarViewState: BindableState {
|
|||||||
var isRoomEncrypted: Bool
|
var isRoomEncrypted: Bool
|
||||||
var isLocationSharingEnabled: Bool
|
var isLocationSharingEnabled: Bool
|
||||||
|
|
||||||
|
var keyCommands: [WysiwygKeyCommand] = []
|
||||||
|
|
||||||
var bindings: ComposerToolbarViewStateBindings
|
var bindings: ComposerToolbarViewStateBindings
|
||||||
|
|
||||||
var isUploading: Bool {
|
var isUploading: Bool {
|
||||||
|
|||||||
@@ -63,13 +63,20 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
|
|||||||
mentionBuilder = MentionBuilder()
|
mentionBuilder = MentionBuilder()
|
||||||
attributedStringBuilder = AttributedStringBuilder(cacheKey: "Composer", 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(),
|
audioRecorderState: .init(),
|
||||||
isRoomEncrypted: roomProxy.infoPublisher.value.isEncrypted,
|
isRoomEncrypted: roomProxy.infoPublisher.value.isEncrypted,
|
||||||
isLocationSharingEnabled: appSettings.mapTilerConfiguration.isEnabled,
|
isLocationSharingEnabled: appSettings.mapTilerConfiguration.isEnabled,
|
||||||
bindings: .init()),
|
bindings: .init()),
|
||||||
mediaProvider: mediaProvider)
|
mediaProvider: mediaProvider)
|
||||||
|
|
||||||
|
state.keyCommands = [
|
||||||
|
.enter { [weak self] in
|
||||||
|
self?.process(viewAction: .sendMessage)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
roomProxy.infoPublisher
|
roomProxy.infoPublisher
|
||||||
.map(\.isEncrypted)
|
.map(\.isEncrypted)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
@@ -100,6 +107,13 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
|
|||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.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
|
context.$viewState
|
||||||
.map(\.bindings.plainComposerText)
|
.map(\.bindings.plainComposerText)
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
@@ -269,14 +283,6 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
|
|||||||
handleSaveDraft(isVolatile: false)
|
handleSaveDraft(isVolatile: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
var keyCommands: [WysiwygKeyCommand] {
|
|
||||||
[
|
|
||||||
.enter { [weak self] in
|
|
||||||
self?.process(viewAction: .sendMessage)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
private func handleLoadDraft(_ draft: ComposerDraftProxy) {
|
private func handleLoadDraft(_ draft: ComposerDraftProxy) {
|
||||||
|
|||||||
-1
@@ -13,7 +13,6 @@ import WysiwygComposer
|
|||||||
protocol ComposerToolbarViewModelProtocol {
|
protocol ComposerToolbarViewModelProtocol {
|
||||||
var actions: AnyPublisher<ComposerToolbarViewModelAction, Never> { get }
|
var actions: AnyPublisher<ComposerToolbarViewModelAction, Never> { get }
|
||||||
var context: ComposerToolbarViewModelType.Context { get }
|
var context: ComposerToolbarViewModelType.Context { get }
|
||||||
var keyCommands: [WysiwygKeyCommand] { get }
|
|
||||||
|
|
||||||
func process(timelineAction: TimelineComposerAction)
|
func process(timelineAction: TimelineComposerAction)
|
||||||
func loadDraft() async
|
func loadDraft() async
|
||||||
|
|||||||
@@ -13,12 +13,6 @@ import WysiwygComposer
|
|||||||
|
|
||||||
struct ComposerToolbar: View {
|
struct ComposerToolbar: View {
|
||||||
@ObservedObject var context: ComposerToolbarViewModel.Context
|
@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
|
@FocusState private var composerFocused: Bool
|
||||||
@State private var frame: CGRect = .zero
|
@State private var frame: CGRect = .zero
|
||||||
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
||||||
@@ -249,9 +243,9 @@ struct ComposerToolbar: View {
|
|||||||
private var composerView: WysiwygComposerView {
|
private var composerView: WysiwygComposerView {
|
||||||
WysiwygComposerView(placeholder: placeholder,
|
WysiwygComposerView(placeholder: placeholder,
|
||||||
placeholderColor: .compound.textSecondary,
|
placeholderColor: .compound.textSecondary,
|
||||||
viewModel: wysiwygViewModel,
|
viewModel: context.viewState.wysiwygViewModel,
|
||||||
itemProviderHelper: ItemProviderHelper(),
|
itemProviderHelper: ItemProviderHelper(),
|
||||||
keyCommands: keyCommands) { provider in
|
keyCommands: context.viewState.keyCommands) { provider in
|
||||||
context.send(viewAction: .handlePasteOrDrop(provider: provider))
|
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
|
// Putting them is VStack allows the completion suggestion preview to work properly in tests
|
||||||
VStack(spacing: 8) {
|
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
|
// 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,
|
ComposerToolbar(context: composerViewModel.context)
|
||||||
wysiwygViewModel: wysiwygViewModel,
|
|
||||||
keyCommands: [])
|
|
||||||
}
|
}
|
||||||
.previewDisplayName("With Suggestions")
|
.previewDisplayName("With Suggestions")
|
||||||
|
|
||||||
@@ -386,9 +378,7 @@ extension ComposerToolbar {
|
|||||||
model.state.composerEmpty = focused
|
model.state.composerEmpty = focused
|
||||||
return model
|
return model
|
||||||
}
|
}
|
||||||
return ComposerToolbar(context: composerViewModel.context,
|
return ComposerToolbar(context: composerViewModel.context)
|
||||||
wysiwygViewModel: wysiwygViewModel,
|
|
||||||
keyCommands: [])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static func textWithVoiceMessage(focused: Bool = true) -> ComposerToolbar {
|
static func textWithVoiceMessage(focused: Bool = true) -> ComposerToolbar {
|
||||||
@@ -405,9 +395,7 @@ extension ComposerToolbar {
|
|||||||
model.state.composerEmpty = focused
|
model.state.composerEmpty = focused
|
||||||
return model
|
return model
|
||||||
}
|
}
|
||||||
return ComposerToolbar(context: composerViewModel.context,
|
return ComposerToolbar(context: composerViewModel.context)
|
||||||
wysiwygViewModel: wysiwygViewModel,
|
|
||||||
keyCommands: [])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static func voiceMessageRecordingMock() -> ComposerToolbar {
|
static func voiceMessageRecordingMock() -> ComposerToolbar {
|
||||||
@@ -424,9 +412,7 @@ extension ComposerToolbar {
|
|||||||
model.state.composerMode = .recordVoiceMessage(state: AudioRecorderState())
|
model.state.composerMode = .recordVoiceMessage(state: AudioRecorderState())
|
||||||
return model
|
return model
|
||||||
}
|
}
|
||||||
return ComposerToolbar(context: composerViewModel.context,
|
return ComposerToolbar(context: composerViewModel.context)
|
||||||
wysiwygViewModel: wysiwygViewModel,
|
|
||||||
keyCommands: [])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static func voiceMessagePreviewMock(uploading: Bool) -> ComposerToolbar {
|
static func voiceMessagePreviewMock(uploading: Bool) -> ComposerToolbar {
|
||||||
@@ -448,9 +434,7 @@ extension ComposerToolbar {
|
|||||||
isUploading: uploading)
|
isUploading: uploading)
|
||||||
return model
|
return model
|
||||||
}
|
}
|
||||||
return ComposerToolbar(context: composerViewModel.context,
|
return ComposerToolbar(context: composerViewModel.context)
|
||||||
wysiwygViewModel: wysiwygViewModel,
|
|
||||||
keyCommands: [])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static func replyLoadingPreviewMock(isLoading: Bool) -> ComposerToolbar {
|
static func replyLoadingPreviewMock(isLoading: Bool) -> ComposerToolbar {
|
||||||
@@ -473,9 +457,7 @@ extension ComposerToolbar {
|
|||||||
eventID: "", eventContent: .message(.text(.init(body: "Hello World!")))), isThread: false)
|
eventID: "", eventContent: .message(.text(.init(body: "Hello World!")))), isThread: false)
|
||||||
return model
|
return model
|
||||||
}
|
}
|
||||||
return ComposerToolbar(context: composerViewModel.context,
|
return ComposerToolbar(context: composerViewModel.context)
|
||||||
wysiwygViewModel: wysiwygViewModel,
|
|
||||||
keyCommands: [])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static func disabledPreviewMock() -> ComposerToolbar {
|
static func disabledPreviewMock() -> ComposerToolbar {
|
||||||
@@ -492,8 +474,6 @@ extension ComposerToolbar {
|
|||||||
model.state.canSend = false
|
model.state.canSend = false
|
||||||
return model
|
return model
|
||||||
}
|
}
|
||||||
return ComposerToolbar(context: composerViewModel.context,
|
return ComposerToolbar(context: composerViewModel.context)
|
||||||
wysiwygViewModel: wysiwygViewModel,
|
|
||||||
keyCommands: [])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
|||||||
private var roomViewModel: RoomScreenViewModelProtocol
|
private var roomViewModel: RoomScreenViewModelProtocol
|
||||||
private var timelineViewModel: TimelineViewModelProtocol
|
private var timelineViewModel: TimelineViewModelProtocol
|
||||||
private var composerViewModel: ComposerToolbarViewModelProtocol
|
private var composerViewModel: ComposerToolbarViewModelProtocol
|
||||||
private var wysiwygViewModel: WysiwygComposerViewModel
|
|
||||||
|
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
@@ -90,11 +89,11 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
|||||||
emojiProvider: parameters.emojiProvider,
|
emojiProvider: parameters.emojiProvider,
|
||||||
timelineControllerFactory: parameters.timelineControllerFactory,
|
timelineControllerFactory: parameters.timelineControllerFactory,
|
||||||
clientProxy: parameters.clientProxy)
|
clientProxy: parameters.clientProxy)
|
||||||
|
|
||||||
wysiwygViewModel = WysiwygComposerViewModel(minHeight: ComposerConstant.minHeight,
|
let wysiwygViewModel = WysiwygComposerViewModel(minHeight: ComposerConstant.minHeight,
|
||||||
maxCompressedHeight: ComposerConstant.maxHeight,
|
maxCompressedHeight: ComposerConstant.maxHeight,
|
||||||
maxExpandedHeight: ComposerConstant.maxHeight,
|
maxExpandedHeight: ComposerConstant.maxHeight,
|
||||||
parserStyle: .elementX)
|
parserStyle: .elementX)
|
||||||
let composerViewModel = ComposerToolbarViewModel(initialText: parameters.sharedText,
|
let composerViewModel = ComposerToolbarViewModel(initialText: parameters.sharedText,
|
||||||
roomProxy: parameters.roomProxy,
|
roomProxy: parameters.roomProxy,
|
||||||
wysiwygViewModel: wysiwygViewModel,
|
wysiwygViewModel: wysiwygViewModel,
|
||||||
@@ -215,9 +214,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func toPresentable() -> AnyView {
|
func toPresentable() -> AnyView {
|
||||||
let composerToolbar = ComposerToolbar(context: composerViewModel.context,
|
let composerToolbar = ComposerToolbar(context: composerViewModel.context)
|
||||||
wysiwygViewModel: wysiwygViewModel,
|
|
||||||
keyCommands: composerViewModel.keyCommands)
|
|
||||||
|
|
||||||
return AnyView(RoomScreen(context: roomViewModel.context,
|
return AnyView(RoomScreen(context: roomViewModel.context,
|
||||||
timelineContext: timelineViewModel.context,
|
timelineContext: timelineViewModel.context,
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ final class ThreadTimelineScreenCoordinator: CoordinatorProtocol {
|
|||||||
private let viewModel: ThreadTimelineScreenViewModelProtocol
|
private let viewModel: ThreadTimelineScreenViewModelProtocol
|
||||||
private let timelineViewModel: TimelineViewModelProtocol
|
private let timelineViewModel: TimelineViewModelProtocol
|
||||||
private var composerViewModel: ComposerToolbarViewModelProtocol
|
private var composerViewModel: ComposerToolbarViewModelProtocol
|
||||||
private var wysiwygViewModel: WysiwygComposerViewModel
|
|
||||||
|
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
@@ -70,10 +69,10 @@ final class ThreadTimelineScreenCoordinator: CoordinatorProtocol {
|
|||||||
timelineControllerFactory: parameters.timelineControllerFactory,
|
timelineControllerFactory: parameters.timelineControllerFactory,
|
||||||
clientProxy: parameters.clientProxy)
|
clientProxy: parameters.clientProxy)
|
||||||
|
|
||||||
wysiwygViewModel = WysiwygComposerViewModel(minHeight: ComposerConstant.minHeight,
|
let wysiwygViewModel = WysiwygComposerViewModel(minHeight: ComposerConstant.minHeight,
|
||||||
maxCompressedHeight: ComposerConstant.maxHeight,
|
maxCompressedHeight: ComposerConstant.maxHeight,
|
||||||
maxExpandedHeight: ComposerConstant.maxHeight,
|
maxExpandedHeight: ComposerConstant.maxHeight,
|
||||||
parserStyle: .elementX)
|
parserStyle: .elementX)
|
||||||
|
|
||||||
#warning("Drafts are not handled and they can't be without rust side changes")
|
#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 {
|
func toPresentable() -> AnyView {
|
||||||
let composerToolbar = ComposerToolbar(context: composerViewModel.context,
|
let composerToolbar = ComposerToolbar(context: composerViewModel.context)
|
||||||
wysiwygViewModel: wysiwygViewModel,
|
|
||||||
keyCommands: composerViewModel.keyCommands)
|
|
||||||
|
|
||||||
return AnyView(ThreadTimelineScreen(context: viewModel.context,
|
return AnyView(ThreadTimelineScreen(context: viewModel.context,
|
||||||
timelineContext: timelineViewModel.context,
|
timelineContext: timelineViewModel.context,
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class ComposerToolbarViewModelTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func testHandleKeyCommand() {
|
func testHandleKeyCommand() {
|
||||||
XCTAssertTrue(viewModel.keyCommands.count == 1)
|
XCTAssertTrue(viewModel.context.viewState.keyCommands.count == 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testComposerFocusAfterEnablingRTE() {
|
func testComposerFocusAfterEnablingRTE() {
|
||||||
|
|||||||
Reference in New Issue
Block a user