mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-30 13:16:58 +02:00
Split out models, add some tests and fix some formatting.
This commit is contained in:
+43
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
enum MockComposerCreateActionListScreenState: MockScreenState, CaseIterable {
|
||||
case partialList
|
||||
case fullList
|
||||
|
||||
var screenType: Any.Type {
|
||||
ComposerCreateActionList.self
|
||||
}
|
||||
|
||||
var screenView: ([Any], AnyView) {
|
||||
let actions: [ComposerCreateAction]
|
||||
switch self {
|
||||
case .partialList:
|
||||
actions = [.photoLibrary, .polls]
|
||||
case .fullList:
|
||||
actions = ComposerCreateAction.allCases
|
||||
}
|
||||
let viewModel = ComposerCreateActionListViewModel(initialViewState: ComposerCreateActionListViewState(actions: actions))
|
||||
|
||||
return (
|
||||
[viewModel],
|
||||
AnyView(ComposerCreateActionList(viewModel: viewModel.context))
|
||||
)
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
@objc enum ComposerCreateAction: Int {
|
||||
case photoLibrary
|
||||
case stickers
|
||||
case attachments
|
||||
case polls
|
||||
case location
|
||||
case camera
|
||||
}
|
||||
|
||||
extension ComposerCreateAction: Equatable, CaseIterable, Identifiable {
|
||||
var id: Self { self }
|
||||
}
|
||||
|
||||
extension ComposerCreateAction {
|
||||
var title: String {
|
||||
switch self {
|
||||
case .photoLibrary:
|
||||
return VectorL10n.wysiwygComposerStartActionMediaPicker
|
||||
case .stickers:
|
||||
return VectorL10n.wysiwygComposerStartActionStickers
|
||||
case .attachments:
|
||||
return VectorL10n.wysiwygComposerStartActionAttachments
|
||||
case .polls:
|
||||
return VectorL10n.wysiwygComposerStartActionPolls
|
||||
case .location:
|
||||
return VectorL10n.wysiwygComposerStartActionLocation
|
||||
case .camera:
|
||||
return VectorL10n.wysiwygComposerStartActionCamera
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ComposerCreateActionListViewState: BindableState {
|
||||
let actions: [ComposerCreateAction]
|
||||
}
|
||||
|
||||
enum ComposerCreateActionListViewModelResult: Equatable {
|
||||
case done(ComposerCreateAction)
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// 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 RiotSwiftUI
|
||||
import XCTest
|
||||
|
||||
class ComposerCreateActionListUITests: MockScreenTestCase {
|
||||
func testFullList() throws {
|
||||
app.goToScreenWithIdentifier(MockComposerCreateActionListScreenState.fullList.title)
|
||||
|
||||
XCTAssert(app.staticTexts["Photo Library"].exists)
|
||||
XCTAssert(app.staticTexts["Location"].exists)
|
||||
}
|
||||
|
||||
func testPartialList() throws {
|
||||
app.goToScreenWithIdentifier(MockComposerCreateActionListScreenState.partialList.title)
|
||||
|
||||
XCTAssert(app.staticTexts["Photo Library"].exists)
|
||||
XCTAssertFalse(app.staticTexts["Location"].exists)
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
@testable import RiotSwiftUI
|
||||
import SwiftUI
|
||||
import XCTest
|
||||
|
||||
class ComposerCreateActionListTests: XCTestCase {
|
||||
var viewModel: ComposerCreateActionListViewModel!
|
||||
var context: ComposerCreateActionListViewModel.Context!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
viewModel = ComposerCreateActionListViewModel(initialViewState: ComposerCreateActionListViewState(actions: ComposerCreateAction.allCases))
|
||||
context = viewModel.context
|
||||
}
|
||||
|
||||
func testSelection() throws {
|
||||
let actionToSelect: ComposerCreateAction = .attachments
|
||||
var result: ComposerCreateActionListViewModelResult?
|
||||
viewModel.callback = { callbackResult in
|
||||
result = callbackResult
|
||||
}
|
||||
|
||||
viewModel.context.send(viewAction: .selectAction(actionToSelect))
|
||||
|
||||
XCTAssertEqual(result, .done(actionToSelect))
|
||||
}
|
||||
}
|
||||
+5
-6
@@ -17,15 +17,12 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ComposerCreateActionList: View {
|
||||
|
||||
@Environment(\.theme) private var theme: ThemeSwiftUI
|
||||
|
||||
|
||||
@ObservedObject var viewModel: ComposerCreateActionListViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
|
||||
VStack{
|
||||
VStack {
|
||||
VStack(alignment: .leading) {
|
||||
ForEach(viewModel.viewState.actions) { action in
|
||||
HStack(spacing: 16) {
|
||||
@@ -51,9 +48,11 @@ struct ComposerCreateActionList: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct ComposerCreateActionList_Previews: PreviewProvider {
|
||||
static let viewModel = ComposerCreateActionListViewModel(initialViewState: ComposerCreateActionListViewState(actions: ComposerCreateAction.allCases))
|
||||
static let stateRenderer = MockComposerCreateActionListScreenState.stateRenderer
|
||||
static var previews: some View {
|
||||
ComposerCreateActionList(viewModel: viewModel.context)
|
||||
stateRenderer.screenGroup()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,10 +15,9 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import WysiwygComposer
|
||||
import SwiftUI
|
||||
import WysiwygComposer
|
||||
|
||||
@available(iOS 15.0, *)
|
||||
enum MockComposerScreenState: MockScreenState, CaseIterable {
|
||||
case composer
|
||||
|
||||
@@ -26,18 +25,12 @@ enum MockComposerScreenState: MockScreenState, CaseIterable {
|
||||
Composer.self
|
||||
}
|
||||
|
||||
// var screenContainer: some View {
|
||||
// VStack{
|
||||
// Spacer()
|
||||
// Composer(viewModel: viewModel)
|
||||
// }
|
||||
// }
|
||||
var screenView: ([Any], AnyView) {
|
||||
var screenView: ([Any], AnyView) {
|
||||
let viewModel = WysiwygComposerViewModel(minHeight: 20, maxHeight: 360)
|
||||
|
||||
return (
|
||||
[viewModel],
|
||||
AnyView(VStack{
|
||||
AnyView(VStack {
|
||||
Spacer()
|
||||
Composer(viewModel: viewModel, sendMessageAction: { _ in }, showSendMediaActions: { })
|
||||
}.frame(
|
||||
|
||||
-58
@@ -31,15 +31,6 @@ enum FormatType {
|
||||
case underline
|
||||
}
|
||||
|
||||
@objc enum ComposerCreateAction: Int {
|
||||
case photoLibrary
|
||||
case stickers
|
||||
case attachments
|
||||
case polls
|
||||
case location
|
||||
case camera
|
||||
}
|
||||
|
||||
extension FormatType: CaseIterable, Identifiable {
|
||||
var id: Self { self }
|
||||
}
|
||||
@@ -104,52 +95,3 @@ extension FormatType {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposerCreateAction: CaseIterable, Identifiable {
|
||||
var id: Self { self }
|
||||
}
|
||||
|
||||
extension ComposerCreateAction {
|
||||
var title: String {
|
||||
switch self {
|
||||
case .photoLibrary:
|
||||
return VectorL10n.wysiwygComposerStartActionMediaPicker
|
||||
case .stickers:
|
||||
return VectorL10n.wysiwygComposerStartActionStickers
|
||||
case .attachments:
|
||||
return VectorL10n.wysiwygComposerStartActionAttachments
|
||||
case .polls:
|
||||
return VectorL10n.wysiwygComposerStartActionPolls
|
||||
case .location:
|
||||
return VectorL10n.wysiwygComposerStartActionLocation
|
||||
case .camera:
|
||||
return VectorL10n.wysiwygComposerStartActionCamera
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ComposerCreateActionListViewState: BindableState {
|
||||
let actions: [ComposerCreateAction]
|
||||
}
|
||||
|
||||
enum ComposerCreateActionListViewModelResult {
|
||||
case done(ComposerCreateAction)
|
||||
}
|
||||
|
||||
@@ -14,32 +14,29 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import DSBottomSheet
|
||||
import SwiftUI
|
||||
import WysiwygComposer
|
||||
import DSBottomSheet
|
||||
|
||||
@available(iOS 15.0, *)
|
||||
struct Composer: View {
|
||||
|
||||
@Environment(\.theme) private var theme: ThemeSwiftUI
|
||||
|
||||
@ObservedObject var viewModel: WysiwygComposerViewModel
|
||||
let sendMessageAction: (WysiwygComposerContent) -> Void
|
||||
let showSendMediaActions: () -> Void
|
||||
var textColor = Color(.label)
|
||||
|
||||
|
||||
@State private var showSendButton = false
|
||||
|
||||
private let borderHeight: CGFloat = 44
|
||||
private let minTextViewHeight: CGFloat = 20
|
||||
|
||||
private var verticalPadding: CGFloat {
|
||||
(borderHeight - minTextViewHeight) / 2
|
||||
}
|
||||
|
||||
private var formatItems: [FormatItem] {
|
||||
FormatType.allCases.map { type in
|
||||
return FormatItem(
|
||||
FormatItem(
|
||||
type: type,
|
||||
active: viewModel.reversedActions.contains(type.composerAction),
|
||||
disabled: viewModel.disabledActions.contains(type.composerAction)
|
||||
@@ -48,8 +45,9 @@ struct Composer: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
let rect = RoundedRectangle(cornerRadius: borderHeight / 2)
|
||||
VStack {
|
||||
let rect = RoundedRectangle(cornerRadius: borderHeight / 2)
|
||||
ZStack(alignment: .topTrailing) {
|
||||
WysiwygComposerView(
|
||||
content: viewModel.content,
|
||||
replaceText: viewModel.replaceText,
|
||||
@@ -62,71 +60,68 @@ struct Composer: View {
|
||||
.onAppear {
|
||||
viewModel.setup()
|
||||
}
|
||||
.overlay(alignment: .topTrailing) {
|
||||
Button {
|
||||
withAnimation(.easeInOut(duration: 0.25)) {
|
||||
viewModel.maximised.toggle()
|
||||
}
|
||||
} label: {
|
||||
Image(viewModel.maximised ? Asset.Images.minimiseComposer.name : Asset.Images.maximiseComposer.name)
|
||||
.foregroundColor(theme.colors.tertiaryContent)
|
||||
Button {
|
||||
withAnimation(.easeInOut(duration: 0.25)) {
|
||||
viewModel.maximised.toggle()
|
||||
}
|
||||
.padding(.top, 4)
|
||||
.padding(.trailing, 12)
|
||||
} label: {
|
||||
Image(viewModel.maximised ? Asset.Images.minimiseComposer.name : Asset.Images.maximiseComposer.name)
|
||||
.foregroundColor(theme.colors.tertiaryContent)
|
||||
}
|
||||
.padding(.vertical, verticalPadding)
|
||||
.clipShape(rect)
|
||||
.overlay(rect.stroke(theme.colors.quinaryContent, lineWidth: 2))
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.top, 8)
|
||||
.padding(.bottom, 4)
|
||||
HStack{
|
||||
Button {
|
||||
showSendMediaActions()
|
||||
} label: {
|
||||
Image(Asset.Images.startComposeModule.name)
|
||||
.foregroundColor(theme.colors.tertiaryContent)
|
||||
.padding(11)
|
||||
.background(Circle().fill(theme.colors.system))
|
||||
}
|
||||
FormattingToolbar(formatItems: formatItems) { type in
|
||||
viewModel.apply(type.action)
|
||||
}
|
||||
Spacer()
|
||||
ZStack{
|
||||
Button {
|
||||
|
||||
} label: {
|
||||
Image(Asset.Images.voiceMessageRecordButtonDefault.name)
|
||||
.foregroundColor(theme.colors.tertiaryContent)
|
||||
}
|
||||
// TODO Add support for voice messages
|
||||
// .isHidden(showSendButton)
|
||||
.isHidden(true)
|
||||
Button {
|
||||
sendMessageAction(viewModel.content)
|
||||
viewModel.clearContent()
|
||||
} 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)
|
||||
.padding(.bottom, 4)
|
||||
.animation(.none)
|
||||
.padding(.top, 4)
|
||||
.padding(.trailing, 12)
|
||||
}
|
||||
.padding(.vertical, verticalPadding)
|
||||
.clipShape(rect)
|
||||
.overlay(rect.stroke(theme.colors.quinaryContent, lineWidth: 2))
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.top, 8)
|
||||
.padding(.bottom, 4)
|
||||
HStack {
|
||||
Button {
|
||||
showSendMediaActions()
|
||||
} label: {
|
||||
Image(Asset.Images.startComposeModule.name)
|
||||
.foregroundColor(theme.colors.tertiaryContent)
|
||||
.padding(11)
|
||||
.background(Circle().fill(theme.colors.system))
|
||||
}
|
||||
FormattingToolbar(formatItems: formatItems) { type in
|
||||
viewModel.apply(type.action)
|
||||
}
|
||||
Spacer()
|
||||
ZStack {
|
||||
// TODO: Add support for voice messages
|
||||
// Button {
|
||||
//
|
||||
// } label: {
|
||||
// Image(Asset.Images.voiceMessageRecordButtonDefault.name)
|
||||
// .foregroundColor(theme.colors.tertiaryContent)
|
||||
// }
|
||||
// .isHidden(showSendButton)
|
||||
// .isHidden(true)
|
||||
Button {
|
||||
sendMessageAction(viewModel.content)
|
||||
viewModel.clearContent()
|
||||
} 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)
|
||||
.padding(.bottom, 4)
|
||||
.animation(.none)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@available(iOS 15.0, *)
|
||||
struct Composer_Previews: PreviewProvider {
|
||||
static let stateRenderer = MockComposerScreenState.stateRenderer
|
||||
static var previews: some View {
|
||||
|
||||
Reference in New Issue
Block a user