mirror of
https://github.com/vector-im/element-ios.git
synced 2024-09-30 16:22:39 +00:00
206 lines
8 KiB
Swift
206 lines
8 KiB
Swift
//
|
|
// 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 DSBottomSheet
|
|
import SwiftUI
|
|
import WysiwygComposer
|
|
|
|
struct Composer: View {
|
|
// MARK: - Properties
|
|
|
|
// MARK: Private
|
|
|
|
@Environment(\.theme) private var theme: ThemeSwiftUI
|
|
|
|
@State private var focused = false
|
|
@State private var isActionButtonShowing = false
|
|
|
|
private let horizontalPadding: CGFloat = 12
|
|
private let borderHeight: CGFloat = 40
|
|
private let minTextViewHeight: CGFloat = 20
|
|
private var verticalPadding: CGFloat {
|
|
(borderHeight - minTextViewHeight) / 2
|
|
}
|
|
|
|
private var topPadding: CGFloat {
|
|
viewModel.viewState.shouldDisplayContext ? 0 : verticalPadding
|
|
}
|
|
|
|
private var cornerRadius: CGFloat {
|
|
if viewModel.viewState.shouldDisplayContext || wysiwygViewModel.idealHeight > minTextViewHeight {
|
|
return 14
|
|
} else {
|
|
return borderHeight / 2
|
|
}
|
|
}
|
|
|
|
private var actionButtonAccessibilityIdentifier: String {
|
|
viewModel.viewState.sendMode == .edit ? "editButton" : "sendButton"
|
|
}
|
|
|
|
private var toggleButtonAcccessibilityIdentifier: String {
|
|
wysiwygViewModel.maximised ? "minimiseToggle" : "maximisedToggle"
|
|
}
|
|
|
|
private var toggleButtonImageName: String {
|
|
wysiwygViewModel.maximised ? Asset.Images.minimiseComposer.name : Asset.Images.maximiseComposer.name
|
|
}
|
|
|
|
private var borderColor: Color {
|
|
focused ? theme.colors.quarterlyContent : theme.colors.quinaryContent
|
|
}
|
|
|
|
private var formatItems: [FormatItem] {
|
|
FormatType.allCases.map { type in
|
|
FormatItem(
|
|
type: type,
|
|
active: wysiwygViewModel.reversedActions.contains(type.composerAction),
|
|
disabled: wysiwygViewModel.disabledActions.contains(type.composerAction)
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: Public
|
|
|
|
@ObservedObject var viewModel: ComposerViewModelType.Context
|
|
@ObservedObject var wysiwygViewModel: WysiwygComposerViewModel
|
|
|
|
let sendMessageAction: (WysiwygComposerContent) -> Void
|
|
let showSendMediaActions: () -> Void
|
|
|
|
var body: some View {
|
|
VStack(spacing: 8) {
|
|
let rect = RoundedRectangle(cornerRadius: cornerRadius)
|
|
// TODO: Fix maximise animation bugs before re-enabling
|
|
// ZStack(alignment: .topTrailing) {
|
|
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(.horizontal, horizontalPadding)
|
|
.padding(.top, topPadding)
|
|
.padding(.bottom, verticalPadding)
|
|
}
|
|
.clipShape(rect)
|
|
.overlay(rect.stroke(borderColor, lineWidth: 1))
|
|
.padding(.horizontal, horizontalPadding)
|
|
.padding(.top, 8)
|
|
.onTapGesture {
|
|
if !focused {
|
|
focused = true
|
|
}
|
|
}
|
|
.animation(.easeInOut(duration: 0.1), value: wysiwygViewModel.idealHeight)
|
|
HStack(spacing: 0) {
|
|
Button {
|
|
showSendMediaActions()
|
|
} label: {
|
|
Image(Asset.Images.startComposeModule.name)
|
|
.resizable()
|
|
.foregroundColor(theme.colors.tertiaryContent)
|
|
.frame(width: 14, height: 14)
|
|
}
|
|
.frame(width: 36, height: 36)
|
|
.background(Circle().fill(theme.colors.system))
|
|
.padding(.trailing, 8)
|
|
.accessibilityLabel(VectorL10n.create)
|
|
FormattingToolbar(formatItems: formatItems) { type in
|
|
wysiwygViewModel.apply(type.action)
|
|
}
|
|
.frame(height: 44)
|
|
Spacer()
|
|
Button {
|
|
sendMessageAction(wysiwygViewModel.content)
|
|
wysiwygViewModel.clearContent()
|
|
} label: {
|
|
if viewModel.viewState.sendMode == .edit {
|
|
Image(Asset.Images.saveIcon.name)
|
|
} else {
|
|
Image(Asset.Images.sendIcon.name)
|
|
}
|
|
}
|
|
.frame(width: 36, height: 36)
|
|
.padding(.leading, 8)
|
|
.isHidden(!isActionButtonShowing)
|
|
.accessibilityIdentifier(actionButtonAccessibilityIdentifier)
|
|
.accessibilityLabel(VectorL10n.send)
|
|
.onChange(of: wysiwygViewModel.isContentEmpty) { isEmpty in
|
|
viewModel.send(viewAction: .contentDidChange(isEmpty: isEmpty))
|
|
withAnimation(.easeInOut(duration: 0.15)) {
|
|
isActionButtonShowing = !isEmpty
|
|
}
|
|
}
|
|
}
|
|
.padding(.horizontal, 12)
|
|
.padding(.bottom, 4)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: Previews
|
|
|
|
struct Composer_Previews: PreviewProvider {
|
|
static let stateRenderer = MockComposerScreenState.stateRenderer
|
|
static var previews: some View {
|
|
stateRenderer.screenGroup()
|
|
}
|
|
}
|