mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-05-01 13:46:57 +02:00
add simple formatting and maximise support for replacement toolbar
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import WysiwygComposer
|
||||
|
||||
struct FormatItem {
|
||||
let type: FormatType
|
||||
let active: Bool
|
||||
let disabled: Bool
|
||||
}
|
||||
|
||||
enum FormatType {
|
||||
case bold
|
||||
case italic
|
||||
case strikethrough
|
||||
case underline
|
||||
}
|
||||
|
||||
enum ComposerModule {
|
||||
case photoLibrary
|
||||
case stickers
|
||||
case attachments
|
||||
case polls
|
||||
case location
|
||||
case camera
|
||||
}
|
||||
|
||||
extension FormatType: CaseIterable, Identifiable {
|
||||
var id: Self { self }
|
||||
}
|
||||
|
||||
|
||||
extension FormatItem: Identifiable {
|
||||
var id: FormatType { type }
|
||||
}
|
||||
|
||||
extension FormatItem {
|
||||
var icon: String {
|
||||
switch type {
|
||||
case .bold:
|
||||
return Asset.Images.bold.name
|
||||
case .italic:
|
||||
return Asset.Images.italic.name
|
||||
case .strikethrough:
|
||||
return Asset.Images.strikethrough.name
|
||||
case .underline:
|
||||
return Asset.Images.underlined.name
|
||||
}
|
||||
}
|
||||
|
||||
var accessibilityIdentifier: String {
|
||||
switch type {
|
||||
case .bold:
|
||||
return "boldButton"
|
||||
case .italic:
|
||||
return "italicButton"
|
||||
case .strikethrough:
|
||||
return "strikethroughButton"
|
||||
case .underline:
|
||||
return "underlineButton"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension FormatType {
|
||||
var action: WysiwygAction {
|
||||
switch self {
|
||||
case .bold:
|
||||
return .bold
|
||||
case .italic:
|
||||
return .italic
|
||||
case .strikethrough:
|
||||
return .strikeThrough
|
||||
case .underline:
|
||||
return .underline
|
||||
}
|
||||
}
|
||||
|
||||
var composerAction: ComposerAction {
|
||||
switch self {
|
||||
case .bold:
|
||||
return .bold
|
||||
case .italic:
|
||||
return .italic
|
||||
case .strikethrough:
|
||||
return .strikeThrough
|
||||
case .underline:
|
||||
return .underline
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposerModule: CaseIterable, Identifiable {
|
||||
var id: Self { self }
|
||||
}
|
||||
|
||||
extension ComposerModule {
|
||||
var title: String {
|
||||
switch self {
|
||||
case .photoLibrary:
|
||||
return "Photo Library"
|
||||
case .stickers:
|
||||
return "Stickers"
|
||||
case .attachments:
|
||||
return "Attachments"
|
||||
case .polls:
|
||||
return "Polls"
|
||||
case .location:
|
||||
return "Location"
|
||||
case .camera:
|
||||
return "Camera"
|
||||
}
|
||||
}
|
||||
|
||||
var icon: String {
|
||||
switch self {
|
||||
case .photoLibrary:
|
||||
return Asset.Images.actionMediaLibrary.name
|
||||
case .stickers:
|
||||
return Asset.Images.actionSticker.name
|
||||
case .attachments:
|
||||
return Asset.Images.actionFile.name
|
||||
case .polls:
|
||||
return Asset.Images.actionPoll.name
|
||||
case .location:
|
||||
return Asset.Images.actionLocation.name
|
||||
case .camera:
|
||||
return Asset.Images.actionCamera.name
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import WysiwygComposer
|
||||
import SwiftUI
|
||||
|
||||
@available(iOS 15.0, *)
|
||||
enum MockComposerScreenState: MockScreenState, CaseIterable {
|
||||
case composer
|
||||
|
||||
var screenType: Any.Type {
|
||||
Composer.self
|
||||
}
|
||||
|
||||
// var screenContainer: some View {
|
||||
// VStack{
|
||||
// Spacer()
|
||||
// Composer(viewModel: viewModel)
|
||||
// }
|
||||
// }
|
||||
var screenView: ([Any], AnyView) {
|
||||
let viewModel = WysiwygComposerViewModel()
|
||||
|
||||
return (
|
||||
[viewModel],
|
||||
AnyView(VStack{
|
||||
Spacer()
|
||||
Composer(viewModel: viewModel)
|
||||
}.frame(
|
||||
minWidth: 0,
|
||||
maxWidth: .infinity,
|
||||
minHeight: 0,
|
||||
maxHeight: .infinity,
|
||||
alignment: .topLeading
|
||||
))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import WysiwygComposer
|
||||
import DSBottomSheet
|
||||
|
||||
|
||||
class ComposerViewModel: ObservableObject {
|
||||
|
||||
@Published var totalHeight: CGFloat = .zero
|
||||
|
||||
init() {
|
||||
|
||||
}
|
||||
}
|
||||
@available(iOS 15.0, *)
|
||||
struct Composer: View {
|
||||
|
||||
@Environment(\.theme) private var theme: ThemeSwiftUI
|
||||
|
||||
@ObservedObject var viewModel: WysiwygComposerViewModel
|
||||
// @ObservedObject var composerViewModel: ComposerViewModel
|
||||
@State private var isBottomSheetExpanded = false
|
||||
@State private var showSendButton = false
|
||||
@State private var maximised = false
|
||||
|
||||
private let minTextViewHeight: CGFloat = 20
|
||||
private let maxTextViewHeight: CGFloat = 360
|
||||
private let borderHeight: CGFloat = 44
|
||||
|
||||
private var verticalPadding: CGFloat {
|
||||
(borderHeight - minTextViewHeight) / 2
|
||||
}
|
||||
|
||||
private var idealHeight: CGFloat {
|
||||
if maximised {
|
||||
return maxTextViewHeight
|
||||
} else {
|
||||
return min(maxTextViewHeight, max(minTextViewHeight, viewModel.idealHeight))
|
||||
}
|
||||
}
|
||||
|
||||
private var formatItems: [FormatItem] {
|
||||
FormatType.allCases.map { type in
|
||||
return FormatItem(
|
||||
type: type,
|
||||
active: viewModel.reversedActions.contains(type.composerAction),
|
||||
disabled: viewModel.disabledActions.contains(type.composerAction)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
// GeometryReader { geometry in
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
let rect = RoundedRectangle(cornerRadius: borderHeight / 2)
|
||||
WysiwygComposerView(
|
||||
content: viewModel.content,
|
||||
replaceText: viewModel.replaceText,
|
||||
select: viewModel.select,
|
||||
didUpdateText: viewModel.didUpdateText
|
||||
)
|
||||
// .fixedSize(horizontal: false, vertical: true)
|
||||
.frame(height: idealHeight)
|
||||
.padding(.horizontal, 12)
|
||||
.onAppear {
|
||||
viewModel.setup()
|
||||
}
|
||||
.overlay(alignment: .topTrailing) {
|
||||
Button {
|
||||
withAnimation(.easeInOut(duration: 0.25)) {
|
||||
maximised.toggle()
|
||||
viewModel.idealHeight = maxTextViewHeight
|
||||
}
|
||||
} label: {
|
||||
Image(maximised ? Asset.Images.minimiseComposer.name : Asset.Images.maximiseComposer.name)
|
||||
.foregroundColor(theme.colors.tertiaryContent)
|
||||
}
|
||||
.padding(.top, 4)
|
||||
.padding(.trailing, 12)
|
||||
}
|
||||
.padding(.vertical, verticalPadding)
|
||||
.clipShape(rect)
|
||||
.overlay(rect.stroke(theme.colors.quinaryContent, lineWidth: 2))
|
||||
.padding(.horizontal, 12)
|
||||
HStack{
|
||||
Button {
|
||||
isBottomSheetExpanded = true
|
||||
} label: {
|
||||
Image(Asset.Images.startComposeModule.name)
|
||||
.foregroundColor(theme.colors.tertiaryContent)
|
||||
}
|
||||
FormattingToolbar(formatItems: formatItems) { type in
|
||||
viewModel.apply(type.action)
|
||||
}
|
||||
Spacer()
|
||||
ZStack{
|
||||
Button {
|
||||
|
||||
} label: {
|
||||
Image(Asset.Images.voiceMessageRecordButtonDefault.name)
|
||||
.foregroundColor(theme.colors.tertiaryContent)
|
||||
}
|
||||
.isHidden(showSendButton)
|
||||
Button {
|
||||
|
||||
} label: {
|
||||
Image(Asset.Images.sendIcon.name)
|
||||
.foregroundColor(theme.colors.tertiaryContent)
|
||||
}
|
||||
.isHidden(!showSendButton)
|
||||
}.onChange(of: viewModel.isContentEmpty) { (empty) in
|
||||
withAnimation(.easeInOut(duration: 0.25)) {
|
||||
showSendButton = !empty
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
// .onAppear {
|
||||
// composerViewModel.totalHeight = geometry.size.height
|
||||
// }.onChange(of: geometry.size) { newSize in
|
||||
// composerViewModel.totalHeight = geometry.size.height
|
||||
// }
|
||||
.sheetWithDetents(
|
||||
isPresented: $isBottomSheetExpanded,
|
||||
detents: [.medium()]
|
||||
) {
|
||||
print("The sheet has been dismissed")
|
||||
} content: {
|
||||
moduleSelectionList
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
var moduleSelectionList: some View {
|
||||
VStack {
|
||||
VStack(alignment: .leading) {
|
||||
ForEach(ComposerModule.allCases) { module in
|
||||
HStack(spacing: 16) {
|
||||
Image(module.icon)
|
||||
.renderingMode(.template)
|
||||
.foregroundColor(theme.colors.accent)
|
||||
Text(module.title)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
.font(theme.fonts.body)
|
||||
Spacer()
|
||||
}
|
||||
.onTapGesture {
|
||||
// << action here !!
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
}
|
||||
.padding(.top, 16)
|
||||
.background(theme.colors.background.ignoresSafeArea())
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@available(iOS 15.0, *)
|
||||
struct Composer_Previews: PreviewProvider {
|
||||
static let stateRenderer = MockComposerScreenState.stateRenderer
|
||||
static var previews: some View {
|
||||
stateRenderer.screenGroup()
|
||||
}
|
||||
}
|
||||
|
||||
//struct Composer_Previews: PreviewProvider {
|
||||
// static let stateRenderer = MockComposerScreenState.stateRenderer
|
||||
// static var previews: some View {
|
||||
// stateRenderer.screenGroup()
|
||||
// }
|
||||
//}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import WysiwygComposer
|
||||
|
||||
struct FormattingToolbar: View {
|
||||
|
||||
@Environment(\.theme) private var theme: ThemeSwiftUI
|
||||
|
||||
var formatItems: [FormatItem]
|
||||
var formatAction: (FormatType) -> ()
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
ForEach(formatItems) { item in
|
||||
Button {
|
||||
print("action")
|
||||
formatAction(item.type)
|
||||
} label: {
|
||||
Image(item.icon)
|
||||
.renderingMode(.template)
|
||||
.foregroundColor(item.active ? theme.colors.accent : theme.colors.tertiaryContent)
|
||||
}
|
||||
.disabled(item.disabled)
|
||||
.background(item.active ? theme.colors.accent.opacity(0.1) : Color.white) // TODO get correct color for light accent
|
||||
.cornerRadius(8)
|
||||
.accessibilityIdentifier(item.accessibilityIdentifier)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FormattingToolbar_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
FormattingToolbar(formatItems: [
|
||||
FormatItem(type: .bold, active: true, disabled: false),
|
||||
FormatItem(type: .italic, active: false, disabled: false),
|
||||
FormatItem(type: .strikethrough, active: true, disabled: false),
|
||||
FormatItem(type: .underline, active: false, disabled: true)
|
||||
], formatAction: { _ in })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user