mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-25 02:52:45 +02:00
Enable WYSIWYG plain text support
This commit is contained in:
+11
-2
@@ -18,6 +18,7 @@ import Foundation
|
||||
|
||||
@objc protocol ComposerCreateActionListBridgePresenterDelegate {
|
||||
func composerCreateActionListBridgePresenterDelegateDidComplete(_ coordinatorBridgePresenter: ComposerCreateActionListBridgePresenter, action: ComposerCreateAction)
|
||||
func composerCreateActionListBridgePresenterDelegateDidToggleTextFormatting(_ coordinatorBridgePresenter: ComposerCreateActionListBridgePresenter, enabled: Bool)
|
||||
func composerCreateActionListBridgePresenterDidDismissInteractively(_ coordinatorBridgePresenter: ComposerCreateActionListBridgePresenter)
|
||||
}
|
||||
|
||||
@@ -34,6 +35,8 @@ final class ComposerCreateActionListBridgePresenter: NSObject {
|
||||
// MARK: Private
|
||||
|
||||
private let actions: [ComposerCreateAction]
|
||||
private let wysiwygEnabled: Bool
|
||||
private let textFormattingEnabled: Bool
|
||||
private var coordinator: ComposerCreateActionListCoordinator?
|
||||
|
||||
// MARK: Public
|
||||
@@ -42,10 +45,12 @@ final class ComposerCreateActionListBridgePresenter: NSObject {
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(actions: [Int]) {
|
||||
init(actions: [Int], wysiwygEnabled: Bool, textFormattingEnabled: Bool) {
|
||||
self.actions = actions.compactMap {
|
||||
ComposerCreateAction(rawValue: $0)
|
||||
}
|
||||
self.wysiwygEnabled = wysiwygEnabled
|
||||
self.textFormattingEnabled = textFormattingEnabled
|
||||
super.init()
|
||||
}
|
||||
|
||||
@@ -57,12 +62,16 @@ final class ComposerCreateActionListBridgePresenter: NSObject {
|
||||
// }
|
||||
|
||||
func present(from viewController: UIViewController, animated: Bool) {
|
||||
let composerCreateActionListCoordinator = ComposerCreateActionListCoordinator(actions: actions)
|
||||
let composerCreateActionListCoordinator = ComposerCreateActionListCoordinator(actions: actions,
|
||||
wysiwygEnabled: wysiwygEnabled,
|
||||
textFormattingEnabled: textFormattingEnabled)
|
||||
composerCreateActionListCoordinator.callback = { [weak self] action in
|
||||
guard let self = self else { return }
|
||||
switch action {
|
||||
case .done(let composeAction):
|
||||
self.delegate?.composerCreateActionListBridgePresenterDelegateDidComplete(self, action: composeAction)
|
||||
case .toggleTextFormatting(let enabled):
|
||||
self.delegate?.composerCreateActionListBridgePresenterDelegateDidToggleTextFormatting(self, enabled: enabled)
|
||||
case .cancel:
|
||||
self.delegate?.composerCreateActionListBridgePresenterDidDismissInteractively(self)
|
||||
}
|
||||
|
||||
+8
-2
@@ -19,6 +19,7 @@ import SwiftUI
|
||||
/// Actions returned by the coordinator callback
|
||||
enum ComposerCreateActionListCoordinatorAction {
|
||||
case done(ComposerCreateAction)
|
||||
case toggleTextFormatting(Bool)
|
||||
case cancel
|
||||
}
|
||||
|
||||
@@ -39,8 +40,11 @@ final class ComposerCreateActionListCoordinator: NSObject, Coordinator, Presenta
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(actions: [ComposerCreateAction]) {
|
||||
viewModel = ComposerCreateActionListViewModel(initialViewState: ComposerCreateActionListViewState(actions: actions))
|
||||
init(actions: [ComposerCreateAction], wysiwygEnabled: Bool, textFormattingEnabled: Bool) {
|
||||
viewModel = ComposerCreateActionListViewModel(initialViewState: ComposerCreateActionListViewState(
|
||||
actions: actions,
|
||||
wysiwygEnabled: wysiwygEnabled,
|
||||
bindings: ComposerCreateActionListBindings(textFormattingEnabled: textFormattingEnabled)))
|
||||
view = ComposerCreateActionList(viewModel: viewModel.context)
|
||||
let hostingVC = VectorHostingController(rootView: view)
|
||||
hostingVC.bottomSheetPreferences = VectorHostingBottomSheetPreferences(
|
||||
@@ -61,6 +65,8 @@ final class ComposerCreateActionListCoordinator: NSObject, Coordinator, Presenta
|
||||
switch result {
|
||||
case .done(let action):
|
||||
self.callback?(.done(action))
|
||||
case .toggleTextFormatting(let enabled):
|
||||
self.callback?(.toggleTextFormatting(enabled))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-1
@@ -33,7 +33,10 @@ enum MockComposerCreateActionListScreenState: MockScreenState, CaseIterable {
|
||||
case .fullList:
|
||||
actions = ComposerCreateAction.allCases
|
||||
}
|
||||
let viewModel = ComposerCreateActionListViewModel(initialViewState: ComposerCreateActionListViewState(actions: actions))
|
||||
let viewModel = ComposerCreateActionListViewModel(initialViewState: ComposerCreateActionListViewState(
|
||||
actions: actions,
|
||||
wysiwygEnabled: true,
|
||||
bindings: ComposerCreateActionListBindings(textFormattingEnabled: true)))
|
||||
|
||||
return (
|
||||
[viewModel],
|
||||
|
||||
+11
@@ -21,11 +21,15 @@ import Foundation
|
||||
enum ComposerCreateActionListViewAction {
|
||||
// The user selected an action
|
||||
case selectAction(ComposerCreateAction)
|
||||
// The user toggled the text formatting action
|
||||
case toggleTextFormatting(Bool)
|
||||
}
|
||||
|
||||
enum ComposerCreateActionListViewModelResult: Equatable {
|
||||
// The user selected an action and is done with the screen
|
||||
case done(ComposerCreateAction)
|
||||
// The user toggled the text formatting setting but might not be done with the screen
|
||||
case toggleTextFormatting(Bool)
|
||||
}
|
||||
|
||||
// MARK: View
|
||||
@@ -33,6 +37,13 @@ enum ComposerCreateActionListViewModelResult: Equatable {
|
||||
struct ComposerCreateActionListViewState: BindableState {
|
||||
/// The list of composer create actions to display to the user
|
||||
let actions: [ComposerCreateAction]
|
||||
let wysiwygEnabled: Bool
|
||||
|
||||
var bindings: ComposerCreateActionListBindings
|
||||
}
|
||||
|
||||
struct ComposerCreateActionListBindings {
|
||||
var textFormattingEnabled: Bool
|
||||
}
|
||||
|
||||
@objc enum ComposerCreateAction: Int {
|
||||
|
||||
+62
-1
@@ -22,11 +22,17 @@ struct ComposerCreateActionList: View {
|
||||
// MARK: Private
|
||||
|
||||
@Environment(\.theme) private var theme: ThemeSwiftUI
|
||||
|
||||
private var textFormattingIcon: String {
|
||||
viewModel.textFormattingEnabled
|
||||
? Asset.Images.actionFormattingEnabled.name
|
||||
: Asset.Images.actionFormattingDisabled.name
|
||||
}
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@ObservedObject var viewModel: ComposerCreateActionListViewModel.Context
|
||||
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
VStack(alignment: .leading) {
|
||||
@@ -48,6 +54,29 @@ struct ComposerCreateActionList: View {
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
if viewModel.viewState.wysiwygEnabled {
|
||||
SeparatorLine()
|
||||
HStack(spacing: 16) {
|
||||
Image(textFormattingIcon)
|
||||
.renderingMode(.template)
|
||||
.foregroundColor(theme.colors.accent)
|
||||
Text(VectorL10n.wysiwygComposerStartActionTextFormatting)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
.font(theme.fonts.body)
|
||||
.accessibilityIdentifier("textFormatting")
|
||||
Spacer()
|
||||
Toggle("", isOn: $viewModel.textFormattingEnabled)
|
||||
.toggleStyle(ComposerToggleActionStyle())
|
||||
.labelsHidden()
|
||||
.onChange(of: viewModel.textFormattingEnabled) { isOn in
|
||||
viewModel.send(viewAction: .toggleTextFormatting(isOn))
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
|
||||
}
|
||||
}
|
||||
.padding(.top, 8)
|
||||
Spacer()
|
||||
@@ -63,3 +92,35 @@ struct ComposerCreateActionList_Previews: PreviewProvider {
|
||||
stateRenderer.screenGroup()
|
||||
}
|
||||
}
|
||||
|
||||
struct ComposerToggleActionStyle: ToggleStyle {
|
||||
@Environment(\.theme) private var theme
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
HStack {
|
||||
Rectangle()
|
||||
.foregroundColor(.clear)
|
||||
.frame(width: 50, height: 30, alignment: .center)
|
||||
.overlay(
|
||||
Rectangle()
|
||||
.foregroundColor(configuration.isOn
|
||||
? theme.colors.accent.opacity(0.5)
|
||||
: theme.colors.primaryContent.opacity(0.25))
|
||||
.cornerRadius(7)
|
||||
.padding(.all, 8)
|
||||
)
|
||||
.overlay(
|
||||
Circle()
|
||||
.foregroundColor(configuration.isOn
|
||||
? theme.colors.accent
|
||||
: theme.colors.background)
|
||||
.padding(.all, 3)
|
||||
.offset(x: configuration.isOn ? 11 : -11, y: 0)
|
||||
.shadow(radius: configuration.isOn ? 0.0 : 2.0)
|
||||
.animation(Animation.linear(duration: 0.1))
|
||||
|
||||
).cornerRadius(20)
|
||||
.onTapGesture { configuration.isOn.toggle() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -35,6 +35,8 @@ class ComposerCreateActionListViewModel: ComposerCreateActionListViewModelType,
|
||||
switch viewAction {
|
||||
case .selectAction(let action):
|
||||
callback?(.done(action))
|
||||
case .toggleTextFormatting(let enabled):
|
||||
callback?(.toggleTextFormatting(enabled))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import Foundation
|
||||
struct ComposerViewState: BindableState {
|
||||
var eventSenderDisplayName: String?
|
||||
var sendMode: ComposerSendMode = .send
|
||||
var textFormattingEnabled: Bool = RiotSettings.shared.enableWysiwygTextFormatting
|
||||
var placeholder: String?
|
||||
}
|
||||
|
||||
|
||||
@@ -83,73 +83,10 @@ struct Composer: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 8) {
|
||||
let rect = RoundedRectangle(cornerRadius: cornerRadius)
|
||||
VStack(spacing: 12) {
|
||||
if viewModel.viewState.shouldDisplayContext {
|
||||
HStack {
|
||||
if let imageName = viewModel.viewState.contextImageName {
|
||||
Image(imageName)
|
||||
.foregroundColor(theme.colors.tertiaryContent)
|
||||
}
|
||||
if let contextDescription = viewModel.viewState.contextDescription {
|
||||
Text(contextDescription)
|
||||
.accessibilityIdentifier("contextDescription")
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.foregroundColor(theme.colors.secondaryContent)
|
||||
}
|
||||
Spacer()
|
||||
Button {
|
||||
viewModel.send(viewAction: .cancel)
|
||||
} label: {
|
||||
Image(Asset.Images.inputCloseIcon.name)
|
||||
.foregroundColor(theme.colors.tertiaryContent)
|
||||
}
|
||||
.accessibilityIdentifier("cancelButton")
|
||||
}
|
||||
.padding(.top, 8)
|
||||
.padding(.horizontal, horizontalPadding)
|
||||
}
|
||||
HStack(alignment: .top, spacing: 0) {
|
||||
WysiwygComposerView(
|
||||
focused: $focused,
|
||||
content: wysiwygViewModel.content,
|
||||
replaceText: wysiwygViewModel.replaceText,
|
||||
select: wysiwygViewModel.select,
|
||||
didUpdateText: wysiwygViewModel.didUpdateText
|
||||
)
|
||||
.tintColor(theme.colors.accent)
|
||||
.placeholder(viewModel.viewState.placeholder, color: theme.colors.tertiaryContent)
|
||||
.frame(height: wysiwygViewModel.idealHeight)
|
||||
.onAppear {
|
||||
wysiwygViewModel.setup()
|
||||
}
|
||||
Button {
|
||||
wysiwygViewModel.maximised.toggle()
|
||||
} label: {
|
||||
Image(toggleButtonImageName)
|
||||
.resizable()
|
||||
.foregroundColor(theme.colors.tertiaryContent)
|
||||
.frame(width: 16, height: 16)
|
||||
}
|
||||
.accessibilityIdentifier(toggleButtonAcccessibilityIdentifier)
|
||||
.padding(.leading, 12)
|
||||
.padding(.trailing, 4)
|
||||
}
|
||||
.padding(.horizontal, horizontalPadding)
|
||||
.padding(.top, topPadding)
|
||||
.padding(.bottom, verticalPadding)
|
||||
if viewModel.viewState.textFormattingEnabled {
|
||||
composerContainer
|
||||
}
|
||||
.clipShape(rect)
|
||||
.overlay(rect.stroke(borderColor, lineWidth: 1))
|
||||
.animation(.easeInOut(duration: 0.1), value: wysiwygViewModel.idealHeight)
|
||||
.padding(.horizontal, horizontalPadding)
|
||||
.padding(.top, 8)
|
||||
.onTapGesture {
|
||||
if !focused {
|
||||
focused = true
|
||||
}
|
||||
}
|
||||
HStack(spacing: 0) {
|
||||
HStack(alignment: .bottom, spacing: 0) {
|
||||
Button {
|
||||
showSendMediaActions()
|
||||
} label: {
|
||||
@@ -162,13 +99,21 @@ struct Composer: View {
|
||||
.background(Circle().fill(theme.colors.system))
|
||||
.padding(.trailing, 8)
|
||||
.accessibilityLabel(VectorL10n.create)
|
||||
FormattingToolbar(formatItems: formatItems) { type in
|
||||
wysiwygViewModel.apply(type.action)
|
||||
if viewModel.viewState.textFormattingEnabled {
|
||||
FormattingToolbar(formatItems: formatItems) { type in
|
||||
wysiwygViewModel.apply(type.action)
|
||||
}
|
||||
.frame(height: 44)
|
||||
Spacer()
|
||||
} else {
|
||||
composerContainer
|
||||
}
|
||||
.frame(height: 44)
|
||||
Spacer()
|
||||
Button {
|
||||
sendMessageAction(wysiwygViewModel.content)
|
||||
if wysiwygViewModel.plainTextMode {
|
||||
sendMessageAction(wysiwygViewModel.plainTextModeContent)
|
||||
} else {
|
||||
sendMessageAction(wysiwygViewModel.content)
|
||||
}
|
||||
wysiwygViewModel.clearContent()
|
||||
} label: {
|
||||
if viewModel.viewState.sendMode == .edit {
|
||||
@@ -193,6 +138,79 @@ struct Composer: View {
|
||||
.padding(.bottom, 4)
|
||||
}
|
||||
}
|
||||
|
||||
private var composerContainer: some View {
|
||||
let rect = RoundedRectangle(cornerRadius: cornerRadius)
|
||||
return VStack(spacing: 12) {
|
||||
if viewModel.viewState.shouldDisplayContext {
|
||||
HStack {
|
||||
if let imageName = viewModel.viewState.contextImageName {
|
||||
Image(imageName)
|
||||
.foregroundColor(theme.colors.tertiaryContent)
|
||||
}
|
||||
if let contextDescription = viewModel.viewState.contextDescription {
|
||||
Text(contextDescription)
|
||||
.accessibilityIdentifier("contextDescription")
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.foregroundColor(theme.colors.secondaryContent)
|
||||
}
|
||||
Spacer()
|
||||
Button {
|
||||
viewModel.send(viewAction: .cancel)
|
||||
} label: {
|
||||
Image(Asset.Images.inputCloseIcon.name)
|
||||
.foregroundColor(theme.colors.tertiaryContent)
|
||||
}
|
||||
.accessibilityIdentifier("cancelButton")
|
||||
}
|
||||
.padding(.top, 8)
|
||||
.padding(.horizontal, horizontalPadding)
|
||||
}
|
||||
HStack(alignment: .top, spacing: 0) {
|
||||
WysiwygComposerView(
|
||||
focused: $focused,
|
||||
content: wysiwygViewModel.content,
|
||||
replaceText: wysiwygViewModel.replaceText,
|
||||
select: wysiwygViewModel.select,
|
||||
didUpdateText: wysiwygViewModel.didUpdateText
|
||||
)
|
||||
.tintColor(theme.colors.accent)
|
||||
.placeholder(viewModel.viewState.placeholder, color: theme.colors.tertiaryContent)
|
||||
.frame(height: wysiwygViewModel.idealHeight)
|
||||
.onAppear {
|
||||
if wysiwygViewModel.isContentEmpty {
|
||||
wysiwygViewModel.setup()
|
||||
}
|
||||
}
|
||||
if viewModel.viewState.textFormattingEnabled {
|
||||
Button {
|
||||
wysiwygViewModel.maximised.toggle()
|
||||
} label: {
|
||||
Image(toggleButtonImageName)
|
||||
.resizable()
|
||||
.foregroundColor(theme.colors.tertiaryContent)
|
||||
.frame(width: 16, height: 16)
|
||||
}
|
||||
.accessibilityIdentifier(toggleButtonAcccessibilityIdentifier)
|
||||
.padding(.leading, 12)
|
||||
.padding(.trailing, 4)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, horizontalPadding)
|
||||
.padding(.top, topPadding)
|
||||
.padding(.bottom, verticalPadding)
|
||||
}
|
||||
.clipShape(rect)
|
||||
.overlay(rect.stroke(borderColor, lineWidth: 1))
|
||||
.animation(.easeInOut(duration: 0.1), value: wysiwygViewModel.idealHeight)
|
||||
.padding(.horizontal, horizontalPadding)
|
||||
.padding(.top, 8)
|
||||
.onTapGesture {
|
||||
if !focused {
|
||||
focused = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Previews
|
||||
|
||||
@@ -35,6 +35,15 @@ final class ComposerViewModel: ComposerViewModelType, ComposerViewModelProtocol
|
||||
state.sendMode = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var textFormattingEnabled: Bool {
|
||||
get {
|
||||
state.textFormattingEnabled
|
||||
}
|
||||
set {
|
||||
state.textFormattingEnabled = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var eventSenderDisplayName: String? {
|
||||
get {
|
||||
|
||||
@@ -20,6 +20,7 @@ protocol ComposerViewModelProtocol {
|
||||
var context: ComposerViewModelType.Context { get }
|
||||
var callback: ((ComposerViewModelResult) -> Void)? { get set }
|
||||
var sendMode: ComposerSendMode { get set }
|
||||
var textFormattingEnabled: Bool { get set }
|
||||
var eventSenderDisplayName: String? { get set }
|
||||
var placeholder: String? { get set }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user