mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-26 11:30:50 +02:00
Merge branch 'develop' into ismail/5068_start_thread
This commit is contained in:
@@ -66,11 +66,11 @@ struct LocationSharingViewState: BindableState {
|
||||
}
|
||||
|
||||
struct LocationSharingViewStateBindings {
|
||||
var alertInfo: ErrorAlertInfo?
|
||||
var alertInfo: LocationSharingErrorAlertInfo?
|
||||
var userLocation: CLLocationCoordinate2D?
|
||||
}
|
||||
|
||||
struct ErrorAlertInfo: Identifiable {
|
||||
struct LocationSharingErrorAlertInfo: Identifiable {
|
||||
enum AlertType {
|
||||
case mapLoadingError
|
||||
case userLocatingError
|
||||
|
||||
@@ -25,7 +25,7 @@ enum MockLocationSharingScreenState: MockScreenState, CaseIterable {
|
||||
case displayExistingLocation
|
||||
|
||||
var screenType: Any.Type {
|
||||
MockLocationSharingScreenState.self
|
||||
LocationSharingView.self
|
||||
}
|
||||
|
||||
var screenView: ([Any], AnyView) {
|
||||
|
||||
@@ -72,24 +72,24 @@ class LocationSharingViewModel: LocationSharingViewModelType {
|
||||
|
||||
switch error {
|
||||
case .failedLoadingMap:
|
||||
state.bindings.alertInfo = ErrorAlertInfo(id: .mapLoadingError,
|
||||
title: VectorL10n.locationSharingLoadingMapErrorTitle(AppInfo.current.displayName) ,
|
||||
primaryButton: (VectorL10n.ok, { completion?(.cancel) }),
|
||||
secondaryButton: nil)
|
||||
state.bindings.alertInfo = LocationSharingErrorAlertInfo(id: .mapLoadingError,
|
||||
title: VectorL10n.locationSharingLoadingMapErrorTitle(AppInfo.current.displayName) ,
|
||||
primaryButton: (VectorL10n.ok, { completion?(.cancel) }),
|
||||
secondaryButton: nil)
|
||||
case .failedLocatingUser:
|
||||
state.bindings.alertInfo = ErrorAlertInfo(id: .userLocatingError,
|
||||
title: VectorL10n.locationSharingLocatingUserErrorTitle(AppInfo.current.displayName),
|
||||
primaryButton: (VectorL10n.ok, { completion?(.cancel) }),
|
||||
secondaryButton: nil)
|
||||
state.bindings.alertInfo = LocationSharingErrorAlertInfo(id: .userLocatingError,
|
||||
title: VectorL10n.locationSharingLocatingUserErrorTitle(AppInfo.current.displayName),
|
||||
primaryButton: (VectorL10n.ok, { completion?(.cancel) }),
|
||||
secondaryButton: nil)
|
||||
case .invalidLocationAuthorization:
|
||||
state.bindings.alertInfo = ErrorAlertInfo(id: .authorizationError,
|
||||
title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle(AppInfo.current.displayName),
|
||||
primaryButton: (VectorL10n.locationSharingInvalidAuthorizationNotNow, { completion?(.cancel) }),
|
||||
secondaryButton: (VectorL10n.locationSharingInvalidAuthorizationSettings, {
|
||||
if let applicationSettingsURL = URL(string:UIApplication.openSettingsURLString) {
|
||||
UIApplication.shared.open(applicationSettingsURL)
|
||||
}
|
||||
}))
|
||||
state.bindings.alertInfo = LocationSharingErrorAlertInfo(id: .authorizationError,
|
||||
title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle(AppInfo.current.displayName),
|
||||
primaryButton: (VectorL10n.locationSharingInvalidAuthorizationNotNow, { completion?(.cancel) }),
|
||||
secondaryButton: (VectorL10n.locationSharingInvalidAuthorizationSettings, {
|
||||
if let applicationSettingsURL = URL(string:UIApplication.openSettingsURLString) {
|
||||
UIApplication.shared.open(applicationSettingsURL)
|
||||
}
|
||||
}))
|
||||
default:
|
||||
break
|
||||
}
|
||||
@@ -100,10 +100,10 @@ class LocationSharingViewModel: LocationSharingViewModelType {
|
||||
state.showLoadingIndicator = false
|
||||
|
||||
if error != nil {
|
||||
state.bindings.alertInfo = ErrorAlertInfo(id: .locationSharingError,
|
||||
title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle(AppInfo.current.displayName),
|
||||
primaryButton: (VectorL10n.ok, nil),
|
||||
secondaryButton: nil)
|
||||
state.bindings.alertInfo = LocationSharingErrorAlertInfo(id: .locationSharingError,
|
||||
title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle(AppInfo.current.displayName),
|
||||
primaryButton: (VectorL10n.ok, nil),
|
||||
secondaryButton: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+15
-17
@@ -1,20 +1,18 @@
|
||||
// File created from ScreenTemplate
|
||||
// $ createScreen.sh Room/NotificationSettings RoomNotificationSettings
|
||||
/*
|
||||
Copyright 2021 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.
|
||||
*/
|
||||
//
|
||||
// Copyright 2021 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 UIKit
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/PollEditForm PollEditForm
|
||||
/*
|
||||
Copyright 2021 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.
|
||||
*/
|
||||
//
|
||||
// Copyright 2021 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 UIKit
|
||||
@@ -22,6 +20,7 @@ import SwiftUI
|
||||
|
||||
struct PollEditFormCoordinatorParameters {
|
||||
let room: MXRoom
|
||||
let pollStartEvent: MXEvent?
|
||||
}
|
||||
|
||||
final class PollEditFormCoordinator: Coordinator, Presentable {
|
||||
@@ -40,7 +39,7 @@ final class PollEditFormCoordinator: Coordinator, Presentable {
|
||||
}
|
||||
|
||||
// MARK: Public
|
||||
|
||||
|
||||
var childCoordinators: [Coordinator] = []
|
||||
|
||||
var completion: (() -> Void)?
|
||||
@@ -51,9 +50,20 @@ final class PollEditFormCoordinator: Coordinator, Presentable {
|
||||
init(parameters: PollEditFormCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
let viewModel = PollEditFormViewModel()
|
||||
let view = PollEditForm(viewModel: viewModel.context)
|
||||
var viewModel: PollEditFormViewModel
|
||||
if let startEvent = parameters.pollStartEvent,
|
||||
let pollContent = MXEventContentPollStart(fromJSON: startEvent.content) {
|
||||
viewModel = PollEditFormViewModel(parameters: PollEditFormViewModelParameters(mode: .editing,
|
||||
pollDetails: EditFormPollDetails(type: Self.pollKindKeyToDetailsType(pollContent.kind),
|
||||
question: pollContent.question,
|
||||
answerOptions: pollContent.answerOptions.map { $0.text })))
|
||||
|
||||
} else {
|
||||
viewModel = PollEditFormViewModel(parameters: PollEditFormViewModelParameters(mode: .creation, pollDetails: .default))
|
||||
}
|
||||
|
||||
let view = PollEditForm(viewModel: viewModel.context)
|
||||
|
||||
_pollEditFormViewModel = viewModel
|
||||
pollEditFormHostingController = VectorHostingController(rootView: view)
|
||||
}
|
||||
@@ -70,16 +80,9 @@ final class PollEditFormCoordinator: Coordinator, Presentable {
|
||||
switch result {
|
||||
case .cancel:
|
||||
self.completion?()
|
||||
case .create(let question, let answerOptions):
|
||||
var options = [MXEventContentPollStartAnswerOption]()
|
||||
for answerOption in answerOptions {
|
||||
options.append(MXEventContentPollStartAnswerOption(uuid: UUID().uuidString, text: answerOption))
|
||||
}
|
||||
case .create(let details):
|
||||
|
||||
let pollStartContent = MXEventContentPollStart(question: question,
|
||||
kind: kMXMessageContentKeyExtensiblePollKindDisclosed,
|
||||
maxSelections: 1,
|
||||
answerOptions: options)
|
||||
let pollStartContent = self.buildPollContentWithDetails(details)
|
||||
|
||||
self.pollEditFormViewModel.dispatch(action: .startLoading)
|
||||
|
||||
@@ -92,15 +95,72 @@ final class PollEditFormCoordinator: Coordinator, Presentable {
|
||||
guard let self = self else { return }
|
||||
|
||||
MXLog.error("Failed creating poll with error: \(String(describing: error))")
|
||||
self.pollEditFormViewModel.dispatch(action: .stopLoading(error))
|
||||
self.pollEditFormViewModel.dispatch(action: .stopLoading(.failedCreatingPoll))
|
||||
}
|
||||
|
||||
case .update(let details):
|
||||
guard let pollStartEvent = self.parameters.pollStartEvent else {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
self.pollEditFormViewModel.dispatch(action: .startLoading)
|
||||
|
||||
guard let oldPollContent = MXEventContentPollStart(fromJSON: pollStartEvent.content) else {
|
||||
self.pollEditFormViewModel.dispatch(action: .stopLoading(.failedUpdatingPoll))
|
||||
return
|
||||
}
|
||||
|
||||
let newPollContent = self.buildPollContentWithDetails(details)
|
||||
|
||||
self.parameters.room.sendPollUpdate(for: pollStartEvent,
|
||||
oldContent: oldPollContent,
|
||||
newContent: newPollContent, localEcho: nil) { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.pollEditFormViewModel.dispatch(action: .stopLoading(nil))
|
||||
self.completion?()
|
||||
} failure: { [weak self] error in
|
||||
guard let self = self else { return }
|
||||
|
||||
MXLog.error("Failed updating poll with error: \(String(describing: error))")
|
||||
self.pollEditFormViewModel.dispatch(action: .stopLoading(.failedUpdatingPoll))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
// MARK: - Presentable
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
return pollEditFormHostingController
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func buildPollContentWithDetails(_ details: EditFormPollDetails) -> MXEventContentPollStart {
|
||||
var options = [MXEventContentPollStartAnswerOption]()
|
||||
for answerOption in details.answerOptions {
|
||||
options.append(MXEventContentPollStartAnswerOption(uuid: UUID().uuidString, text: answerOption))
|
||||
}
|
||||
|
||||
return MXEventContentPollStart(question: details.question,
|
||||
kind: Self.pollDetailsTypeToKindKey(details.type),
|
||||
maxSelections: NSNumber(value: details.maxSelections),
|
||||
answerOptions: options)
|
||||
|
||||
}
|
||||
|
||||
private static func pollDetailsTypeToKindKey(_ type: EditFormPollType) -> String {
|
||||
let mapping = [EditFormPollType.disclosed : kMXMessageContentKeyExtensiblePollKindDisclosed,
|
||||
EditFormPollType.undisclosed : kMXMessageContentKeyExtensiblePollKindUndisclosed]
|
||||
|
||||
return mapping[type] ?? kMXMessageContentKeyExtensiblePollKindDisclosed
|
||||
}
|
||||
|
||||
private static func pollKindKeyToDetailsType(_ key: String) -> EditFormPollType {
|
||||
let mapping = [kMXMessageContentKeyExtensiblePollKindDisclosed : EditFormPollType.disclosed,
|
||||
kMXMessageContentKeyExtensiblePollKindUndisclosed : EditFormPollType.undisclosed]
|
||||
|
||||
return mapping[key] ?? EditFormPollType.disclosed
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/PollEditForm PollEditForm
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
@@ -19,10 +17,31 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
enum EditFormPollType {
|
||||
case disclosed
|
||||
case undisclosed
|
||||
}
|
||||
|
||||
struct EditFormPollDetails {
|
||||
let type: EditFormPollType
|
||||
let question: String
|
||||
let answerOptions: [String]
|
||||
let maxSelections: UInt = 1
|
||||
|
||||
static var `default`: EditFormPollDetails {
|
||||
EditFormPollDetails(type: .disclosed, question: "", answerOptions: ["", ""])
|
||||
}
|
||||
}
|
||||
|
||||
enum PollEditFormMode {
|
||||
case creation
|
||||
case editing
|
||||
}
|
||||
|
||||
enum PollEditFormStateAction {
|
||||
case viewAction(PollEditFormViewAction)
|
||||
case startLoading
|
||||
case stopLoading(Error?)
|
||||
case stopLoading(PollEditFormErrorAlertInfo.AlertType?)
|
||||
}
|
||||
|
||||
enum PollEditFormViewAction {
|
||||
@@ -30,11 +49,13 @@ enum PollEditFormViewAction {
|
||||
case deleteAnswerOption(PollEditFormAnswerOption)
|
||||
case cancel
|
||||
case create
|
||||
case update
|
||||
}
|
||||
|
||||
enum PollEditFormViewModelResult {
|
||||
case cancel
|
||||
case create(String, [String])
|
||||
case create(EditFormPollDetails)
|
||||
case update(EditFormPollDetails)
|
||||
}
|
||||
|
||||
struct PollEditFormQuestion {
|
||||
@@ -60,12 +81,14 @@ struct PollEditFormAnswerOption: Identifiable, Equatable {
|
||||
}
|
||||
|
||||
struct PollEditFormViewState: BindableState {
|
||||
var minAnswerOptionsCount: Int
|
||||
var maxAnswerOptionsCount: Int
|
||||
var mode: PollEditFormMode
|
||||
var bindings: PollEditFormViewStateBindings
|
||||
|
||||
var confirmationButtonEnabled: Bool {
|
||||
!bindings.question.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty &&
|
||||
bindings.answerOptions.filter({ !$0.text.isEmpty }).count >= 2
|
||||
bindings.answerOptions.filter({ !$0.text.isEmpty }).count >= minAnswerOptionsCount
|
||||
}
|
||||
|
||||
var addAnswerOptionButtonEnabled: Bool {
|
||||
@@ -78,6 +101,18 @@ struct PollEditFormViewState: BindableState {
|
||||
struct PollEditFormViewStateBindings {
|
||||
var question: PollEditFormQuestion
|
||||
var answerOptions: [PollEditFormAnswerOption]
|
||||
var type: EditFormPollType
|
||||
|
||||
var showsFailureAlert: Bool = false
|
||||
var alertInfo: PollEditFormErrorAlertInfo?
|
||||
}
|
||||
|
||||
struct PollEditFormErrorAlertInfo: Identifiable {
|
||||
enum AlertType {
|
||||
case failedCreatingPoll
|
||||
case failedUpdatingPoll
|
||||
}
|
||||
|
||||
let id: AlertType
|
||||
let title: String
|
||||
let subtitle: String
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
@@ -24,11 +22,11 @@ enum MockPollEditFormScreenState: MockScreenState, CaseIterable {
|
||||
case standard
|
||||
|
||||
var screenType: Any.Type {
|
||||
MockPollEditFormScreenState.self
|
||||
PollEditForm.self
|
||||
}
|
||||
|
||||
var screenView: ([Any], AnyView) {
|
||||
let viewModel = PollEditFormViewModel()
|
||||
let viewModel = PollEditFormViewModel(parameters: PollEditFormViewModelParameters(mode: .creation, pollDetails: .default))
|
||||
return ([viewModel], AnyView(PollEditForm(viewModel: viewModel.context)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/PollEditForm PollEditForm
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
@@ -19,6 +17,11 @@
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
struct PollEditFormViewModelParameters {
|
||||
let mode: PollEditFormMode
|
||||
let pollDetails: EditFormPollDetails
|
||||
}
|
||||
|
||||
@available(iOS 14, *)
|
||||
typealias PollEditFormViewModelType = StateStoreViewModel< PollEditFormViewState,
|
||||
PollEditFormStateAction,
|
||||
@@ -27,6 +30,7 @@ typealias PollEditFormViewModelType = StateStoreViewModel< PollEditFormViewState
|
||||
class PollEditFormViewModel: PollEditFormViewModelType {
|
||||
|
||||
private struct Constants {
|
||||
static let minAnswerOptionsCount = 2
|
||||
static let maxAnswerOptionsCount = 20
|
||||
static let maxQuestionLength = 340
|
||||
static let maxAnswerOptionLength = 340
|
||||
@@ -42,20 +46,19 @@ class PollEditFormViewModel: PollEditFormViewModelType {
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init() {
|
||||
super.init(initialViewState: Self.defaultState())
|
||||
}
|
||||
|
||||
private static func defaultState() -> PollEditFormViewState {
|
||||
return PollEditFormViewState(
|
||||
init(parameters: PollEditFormViewModelParameters) {
|
||||
let state = PollEditFormViewState(
|
||||
minAnswerOptionsCount: Constants.minAnswerOptionsCount,
|
||||
maxAnswerOptionsCount: Constants.maxAnswerOptionsCount,
|
||||
mode: parameters.mode,
|
||||
bindings: PollEditFormViewStateBindings(
|
||||
question: PollEditFormQuestion(text: "", maxLength: Constants.maxQuestionLength),
|
||||
answerOptions: [PollEditFormAnswerOption(text: "", maxLength: Constants.maxAnswerOptionLength),
|
||||
PollEditFormAnswerOption(text: "", maxLength: Constants.maxAnswerOptionLength)
|
||||
]
|
||||
question: PollEditFormQuestion(text: parameters.pollDetails.question, maxLength: Constants.maxQuestionLength),
|
||||
answerOptions: parameters.pollDetails.answerOptions.map { PollEditFormAnswerOption(text: $0, maxLength: Constants.maxAnswerOptionLength) },
|
||||
type: parameters.pollDetails.type
|
||||
)
|
||||
)
|
||||
|
||||
super.init(initialViewState: state)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
@@ -65,11 +68,9 @@ class PollEditFormViewModel: PollEditFormViewModelType {
|
||||
case .cancel:
|
||||
completion?(.cancel)
|
||||
case .create:
|
||||
completion?(.create(state.bindings.question.text.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
state.bindings.answerOptions.compactMap({ answerOption in
|
||||
let text = answerOption.text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return text.isEmpty ? nil : text
|
||||
})))
|
||||
completion?(.create(buildPollDetails()))
|
||||
case .update:
|
||||
completion?(.update(buildPollDetails()))
|
||||
default:
|
||||
dispatch(action: .viewAction(viewAction))
|
||||
}
|
||||
@@ -92,10 +93,30 @@ class PollEditFormViewModel: PollEditFormViewModelType {
|
||||
case .stopLoading(let error):
|
||||
state.showLoadingIndicator = false
|
||||
|
||||
if error != nil {
|
||||
state.bindings.showsFailureAlert = true
|
||||
switch error {
|
||||
case .failedCreatingPoll:
|
||||
state.bindings.alertInfo = PollEditFormErrorAlertInfo(id: .failedCreatingPoll,
|
||||
title: VectorL10n.pollEditFormPostFailureTitle,
|
||||
subtitle: VectorL10n.pollEditFormPostFailureSubtitle)
|
||||
case .failedUpdatingPoll:
|
||||
state.bindings.alertInfo = PollEditFormErrorAlertInfo(id: .failedUpdatingPoll,
|
||||
title: VectorL10n.pollEditFormUpdateFailureTitle,
|
||||
subtitle: VectorL10n.pollEditFormUpdateFailureSubtitle)
|
||||
case .none:
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func buildPollDetails() -> EditFormPollDetails {
|
||||
return EditFormPollDetails(type: state.bindings.type,
|
||||
question: state.bindings.question.text.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
answerOptions: state.bindings.answerOptions.compactMap({ answerOption in
|
||||
let text = answerOption.text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return text.isEmpty ? nil : text
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/PollEditForm PollEditForm
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/PollEditForm PollEditForm
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
@@ -28,10 +26,10 @@ class PollEditFormViewModelTests: XCTestCase {
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
|
||||
override func setUpWithError() throws {
|
||||
viewModel = PollEditFormViewModel()
|
||||
viewModel = PollEditFormViewModel(parameters: PollEditFormViewModelParameters(mode: .creation, pollDetails: .default))
|
||||
context = viewModel.context
|
||||
}
|
||||
|
||||
|
||||
func testInitialState() {
|
||||
XCTAssertTrue(context.question.text.isEmpty)
|
||||
XCTAssertFalse(context.viewState.confirmationButtonEnabled)
|
||||
@@ -100,14 +98,14 @@ class PollEditFormViewModelTests: XCTestCase {
|
||||
let thirdAnswer = " "
|
||||
|
||||
viewModel.completion = { result in
|
||||
if case PollEditFormViewModelResult.create(let resultQuestion, let resultAnswerOptions) = result {
|
||||
XCTAssertEqual(question.trimmingCharacters(in: .whitespacesAndNewlines), resultQuestion)
|
||||
if case PollEditFormViewModelResult.create(let result) = result {
|
||||
XCTAssertEqual(question.trimmingCharacters(in: .whitespacesAndNewlines), result.question)
|
||||
|
||||
// The last answer option should be automatically dropped as it's empty
|
||||
XCTAssertEqual(resultAnswerOptions.count, 2)
|
||||
XCTAssertEqual(result.answerOptions.count, 2)
|
||||
|
||||
XCTAssertEqual(resultAnswerOptions[0], firstAnswer.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||
XCTAssertEqual(resultAnswerOptions[1], secondAnswer.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||
XCTAssertEqual(result.answerOptions[0], firstAnswer.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||
XCTAssertEqual(result.answerOptions[1], secondAnswer.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/PollEditForm PollEditForm
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
@@ -37,6 +35,9 @@ struct PollEditForm: View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 32.0) {
|
||||
|
||||
// Intentionally disabled until platform parity.
|
||||
// PollEditFormTypePicker(selectedType: $viewModel.type)
|
||||
|
||||
VStack(alignment: .leading, spacing: 16.0) {
|
||||
Text(VectorL10n.pollEditFormPollQuestionOrTopic)
|
||||
.font(theme.fonts.title3SB)
|
||||
@@ -58,7 +59,7 @@ struct PollEditForm: View {
|
||||
|
||||
ForEach(0..<viewModel.answerOptions.count, id: \.self) { index in
|
||||
SafeBindingCollectionEnumerator($viewModel.answerOptions, index: index) { binding in
|
||||
AnswerOptionGroup(text: binding.text, index: index) {
|
||||
PollEditFormAnswerOptionView(text: binding.text, index: index) {
|
||||
withAnimation(.easeInOut(duration: 0.2)) {
|
||||
viewModel.send(viewAction: .deleteAnswerOption(viewModel.answerOptions[index]))
|
||||
}
|
||||
@@ -76,17 +77,20 @@ struct PollEditForm: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(VectorL10n.pollEditFormCreatePoll) {
|
||||
viewModel.send(viewAction: .create)
|
||||
if viewModel.viewState.mode == .creation {
|
||||
Button(VectorL10n.pollEditFormCreatePoll) {
|
||||
viewModel.send(viewAction: .create)
|
||||
}
|
||||
.buttonStyle(PrimaryActionButtonStyle())
|
||||
.disabled(!viewModel.viewState.confirmationButtonEnabled)
|
||||
}
|
||||
.buttonStyle(PrimaryActionButtonStyle())
|
||||
.disabled(!viewModel.viewState.confirmationButtonEnabled)
|
||||
}
|
||||
.padding()
|
||||
.padding(.vertical, 24.0)
|
||||
.padding(.horizontal, 16.0)
|
||||
.activityIndicator(show: viewModel.viewState.showLoadingIndicator)
|
||||
.alert(isPresented: $viewModel.showsFailureAlert) {
|
||||
Alert(title: Text(VectorL10n.pollEditFormPostFailureTitle),
|
||||
message: Text(VectorL10n.pollEditFormPostFailureSubtitle),
|
||||
.alert(item: $viewModel.alertInfo) { info in
|
||||
Alert(title: Text(info.title),
|
||||
message: Text(info.subtitle),
|
||||
dismissButton: .default(Text(VectorL10n.ok)))
|
||||
}
|
||||
.frame(minHeight: proxy.size.height) // Make the VStack fill the ScrollView's parent
|
||||
@@ -101,6 +105,15 @@ struct PollEditForm: View {
|
||||
.font(.headline)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
if viewModel.viewState.mode == .editing {
|
||||
Button(VectorL10n.save, action: {
|
||||
viewModel.send(viewAction: .update)
|
||||
})
|
||||
.disabled(!viewModel.viewState.confirmationButtonEnabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
@@ -111,40 +124,6 @@ struct PollEditForm: View {
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
private struct AnswerOptionGroup: View {
|
||||
|
||||
@Environment(\.theme) private var theme: ThemeSwiftUI
|
||||
|
||||
@State private var focused = false
|
||||
|
||||
@Binding var text: String
|
||||
|
||||
let index: Int
|
||||
let onDelete: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8.0) {
|
||||
Text(VectorL10n.pollEditFormOptionNumber(index + 1))
|
||||
.font(theme.fonts.subheadline)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
|
||||
HStack(spacing: 16.0) {
|
||||
TextField(VectorL10n.pollEditFormInputPlaceholder, text: $text, onEditingChanged: { edit in
|
||||
self.focused = edit
|
||||
})
|
||||
.textFieldStyle(BorderedInputFieldStyle(theme: _theme, isEditing: focused))
|
||||
Button {
|
||||
onDelete()
|
||||
} label: {
|
||||
Image(uiImage:Asset.Images.pollDeleteOptionIcon.image)
|
||||
}
|
||||
.accessibilityIdentifier("Delete answer option")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
//
|
||||
// Copyright 2021 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
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct PollEditFormAnswerOptionView: View {
|
||||
|
||||
@Environment(\.theme) private var theme: ThemeSwiftUI
|
||||
|
||||
@State private var focused = false
|
||||
|
||||
@Binding var text: String
|
||||
|
||||
let index: Int
|
||||
let onDelete: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8.0) {
|
||||
Text(VectorL10n.pollEditFormOptionNumber(index + 1))
|
||||
.font(theme.fonts.subheadline)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
|
||||
HStack(spacing: 16.0) {
|
||||
TextField(VectorL10n.pollEditFormInputPlaceholder, text: $text, onEditingChanged: { edit in
|
||||
self.focused = edit
|
||||
})
|
||||
.textFieldStyle(BorderedInputFieldStyle(theme: _theme, isEditing: focused))
|
||||
Button(action: onDelete) {
|
||||
Image(uiImage:Asset.Images.pollDeleteOptionIcon.image)
|
||||
}
|
||||
.accessibilityIdentifier("Delete answer option")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct PollEditFormAnswerOptionView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VStack(spacing: 32.0) {
|
||||
PollEditFormAnswerOptionView(text: Binding.constant(""), index: 0) {
|
||||
|
||||
}
|
||||
PollEditFormAnswerOptionView(text: Binding.constant("Test"), index: 5) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
//
|
||||
// Copyright 2021 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
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct PollEditFormTypePicker: View {
|
||||
@Environment(\.theme) private var theme: ThemeSwiftUI
|
||||
|
||||
@Binding var selectedType: EditFormPollType
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 16.0) {
|
||||
Text(VectorL10n.pollEditFormPollType)
|
||||
.font(theme.fonts.title3SB)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
PollEditFormTypeButton(type: .disclosed, selectedType: $selectedType)
|
||||
PollEditFormTypeButton(type: .undisclosed, selectedType: $selectedType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
private struct PollEditFormTypeButton: View {
|
||||
@Environment(\.theme) private var theme: ThemeSwiftUI
|
||||
|
||||
let type: EditFormPollType
|
||||
@Binding var selectedType: EditFormPollType
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
selectedType = type
|
||||
} label: {
|
||||
HStack(alignment: .top, spacing: 8.0) {
|
||||
|
||||
Image(uiImage: selectionImage)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(title)
|
||||
.font(theme.fonts.body)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
Text(description)
|
||||
.font(theme.fonts.footnote)
|
||||
.foregroundColor(theme.colors.secondaryContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var title: String {
|
||||
switch type {
|
||||
case .disclosed:
|
||||
return VectorL10n.pollEditFormPollTypeOpen
|
||||
case .undisclosed:
|
||||
return VectorL10n.pollEditFormPollTypeClosed
|
||||
}
|
||||
}
|
||||
|
||||
private var description: String {
|
||||
switch type {
|
||||
case .disclosed:
|
||||
return VectorL10n.pollEditFormPollTypeOpenDescription
|
||||
case .undisclosed:
|
||||
return VectorL10n.pollEditFormPollTypeClosedDescription
|
||||
}
|
||||
}
|
||||
|
||||
private var selectionImage: UIImage {
|
||||
if type == selectedType {
|
||||
return Asset.Images.pollTypeCheckboxSelected.image
|
||||
} else {
|
||||
return Asset.Images.pollTypeCheckboxDefault.image
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct PollEditFormTypePicker_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VStack {
|
||||
PollEditFormTypePicker(selectedType: Binding.constant(.disclosed))
|
||||
PollEditFormTypePicker(selectedType: Binding.constant(.undisclosed))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// Copyright 2021 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
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
enum MockPollTimelineScreenState: MockScreenState, CaseIterable {
|
||||
case open
|
||||
case closed
|
||||
|
||||
var screenType: Any.Type {
|
||||
MockPollTimelineScreenState.self
|
||||
}
|
||||
|
||||
var screenView: ([Any], AnyView) {
|
||||
let answerOptions = [TimelineAnswerOption(id: "1", text: "First", count: 10, winner: false, selected: false),
|
||||
TimelineAnswerOption(id: "2", text: "Second", count: 5, winner: false, selected: true),
|
||||
TimelineAnswerOption(id: "3", text: "Third", count: 15, winner: true, selected: false)]
|
||||
|
||||
let poll = TimelinePoll(question: "Question",
|
||||
answerOptions: answerOptions,
|
||||
closed: (self == .closed ? true : false),
|
||||
totalAnswerCount: 20,
|
||||
type: .disclosed,
|
||||
maxAllowedSelections: 1)
|
||||
|
||||
let viewModel = PollTimelineViewModel(timelinePoll: poll)
|
||||
|
||||
return ([viewModel], AnyView(PollTimelineView(viewModel: viewModel.context)))
|
||||
}
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
//
|
||||
// Copyright 2021 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
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct PollTimelineAnswerOptionButton: View {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@Environment(\.theme) private var theme: ThemeSwiftUI
|
||||
|
||||
let answerOption: TimelineAnswerOption
|
||||
let pollClosed: Bool
|
||||
let showResults: Bool
|
||||
let totalAnswerCount: UInt
|
||||
let action: () -> Void
|
||||
|
||||
// MARK: Public
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
let rect = RoundedRectangle(cornerRadius: 4.0)
|
||||
answerOptionLabel
|
||||
.padding(.horizontal, 8.0)
|
||||
.padding(.top, 12.0)
|
||||
.padding(.bottom, 4.0)
|
||||
.clipShape(rect)
|
||||
.overlay(rect.stroke(borderAccentColor, lineWidth: 1.0))
|
||||
.accentColor(progressViewAccentColor)
|
||||
}
|
||||
}
|
||||
|
||||
var answerOptionLabel: some View {
|
||||
VStack(alignment: .leading, spacing: 12.0) {
|
||||
HStack(alignment: .top, spacing: 8.0) {
|
||||
|
||||
if !pollClosed {
|
||||
Image(uiImage: answerOption.selected ? Asset.Images.pollCheckboxSelected.image : Asset.Images.pollCheckboxDefault.image)
|
||||
}
|
||||
|
||||
Text(answerOption.text)
|
||||
.font(theme.fonts.body)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
|
||||
if pollClosed && answerOption.winner {
|
||||
Spacer()
|
||||
Image(uiImage: Asset.Images.pollWinnerIcon.image)
|
||||
}
|
||||
}
|
||||
|
||||
HStack {
|
||||
ProgressView(value: Double(showResults ? answerOption.count : 0),
|
||||
total: Double(totalAnswerCount))
|
||||
.progressViewStyle(LinearProgressViewStyle())
|
||||
.scaleEffect(x: 1.0, y: 1.2, anchor: .center)
|
||||
.padding(.vertical, 8.0)
|
||||
|
||||
if (showResults) {
|
||||
Text(answerOption.count == 1 ? VectorL10n.pollTimelineOneVote : VectorL10n.pollTimelineVotesCount(Int(answerOption.count)))
|
||||
.font(theme.fonts.footnote)
|
||||
.foregroundColor(pollClosed && answerOption.winner ? theme.colors.accent : theme.colors.secondaryContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var borderAccentColor: Color {
|
||||
guard !pollClosed else {
|
||||
return (answerOption.winner ? theme.colors.accent : theme.colors.quinaryContent)
|
||||
}
|
||||
|
||||
return answerOption.selected ? theme.colors.accent : theme.colors.quinaryContent
|
||||
}
|
||||
|
||||
var progressViewAccentColor: Color {
|
||||
guard !pollClosed else {
|
||||
return (answerOption.winner ? theme.colors.accent : theme.colors.quarterlyContent)
|
||||
}
|
||||
|
||||
return answerOption.selected ? theme.colors.accent : theme.colors.quarterlyContent
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct PollTimelineAnswerOptionButton_Previews: PreviewProvider {
|
||||
static let stateRenderer = MockPollTimelineScreenState.stateRenderer
|
||||
static var previews: some View {
|
||||
|
||||
Group {
|
||||
VStack {
|
||||
PollTimelineAnswerOptionButton(answerOption: TimelineAnswerOption(id: "", text: "Test", count: 5, winner: false, selected: false),
|
||||
pollClosed: false, showResults: true, totalAnswerCount: 100, action: {})
|
||||
|
||||
PollTimelineAnswerOptionButton(answerOption: TimelineAnswerOption(id: "", text: "Test", count: 5, winner: false, selected: false),
|
||||
pollClosed: false, showResults: false, totalAnswerCount: 100, action: {})
|
||||
|
||||
PollTimelineAnswerOptionButton(answerOption: TimelineAnswerOption(id: "", text: "Test", count: 8, winner: false, selected: true),
|
||||
pollClosed: false, showResults: true, totalAnswerCount: 100, action: {})
|
||||
|
||||
PollTimelineAnswerOptionButton(answerOption: TimelineAnswerOption(id: "", text: "Test", count: 8, winner: false, selected: true),
|
||||
pollClosed: false, showResults: false, totalAnswerCount: 100, action: {})
|
||||
|
||||
PollTimelineAnswerOptionButton(answerOption: TimelineAnswerOption(id: "",
|
||||
text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
|
||||
count: 200, winner: false, selected: false),
|
||||
pollClosed: false, showResults: true, totalAnswerCount: 1000, action: {})
|
||||
|
||||
PollTimelineAnswerOptionButton(answerOption: TimelineAnswerOption(id: "",
|
||||
text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
|
||||
count: 200, winner: false, selected: false),
|
||||
pollClosed: false, showResults: false, totalAnswerCount: 1000, action: {})
|
||||
}
|
||||
|
||||
VStack {
|
||||
PollTimelineAnswerOptionButton(answerOption: TimelineAnswerOption(id: "", text: "Test", count: 5, winner: false, selected: false),
|
||||
pollClosed: true, showResults: true, totalAnswerCount: 100, action: {})
|
||||
|
||||
PollTimelineAnswerOptionButton(answerOption: TimelineAnswerOption(id: "", text: "Test", count: 5, winner: true, selected: false),
|
||||
pollClosed: true, showResults: true, totalAnswerCount: 100, action: {})
|
||||
|
||||
PollTimelineAnswerOptionButton(answerOption: TimelineAnswerOption(id: "", text: "Test", count: 8, winner: false, selected: true),
|
||||
pollClosed: true, showResults: true, totalAnswerCount: 100, action: {})
|
||||
|
||||
PollTimelineAnswerOptionButton(answerOption: TimelineAnswerOption(id: "", text: "Test", count: 8, winner: true, selected: true),
|
||||
pollClosed: true, showResults: true, totalAnswerCount: 100, action: {})
|
||||
|
||||
PollTimelineAnswerOptionButton(answerOption: TimelineAnswerOption(id: "",
|
||||
text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
|
||||
count: 200, winner: false, selected: false),
|
||||
pollClosed: true, showResults: true, totalAnswerCount: 1000, action: {})
|
||||
|
||||
PollTimelineAnswerOptionButton(answerOption: TimelineAnswerOption(id: "",
|
||||
text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
|
||||
count: 200, winner: true, selected: false),
|
||||
pollClosed: true, showResults: true, totalAnswerCount: 1000, action: {})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+46
-35
@@ -1,43 +1,41 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/PollTimeline PollTimeline
|
||||
/*
|
||||
Copyright 2021 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.
|
||||
*/
|
||||
//
|
||||
// Copyright 2021 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 MatrixSDK
|
||||
import Combine
|
||||
|
||||
struct PollTimelineCoordinatorParameters {
|
||||
struct TimelinePollCoordinatorParameters {
|
||||
let session: MXSession
|
||||
let room: MXRoom
|
||||
let pollStartEvent: MXEvent
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
final class PollTimelineCoordinator: Coordinator, Presentable, PollAggregatorDelegate {
|
||||
final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDelegate {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let parameters: PollTimelineCoordinatorParameters
|
||||
private let parameters: TimelinePollCoordinatorParameters
|
||||
private let selectedAnswerIdentifiersSubject = PassthroughSubject<[String], Never>()
|
||||
|
||||
private var pollAggregator: PollAggregator
|
||||
private var pollTimelineViewModel: PollTimelineViewModel!
|
||||
private var viewModel: TimelinePollViewModel!
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
// MARK: Public
|
||||
@@ -48,14 +46,14 @@ final class PollTimelineCoordinator: Coordinator, Presentable, PollAggregatorDel
|
||||
// MARK: - Setup
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
init(parameters: PollTimelineCoordinatorParameters) throws {
|
||||
init(parameters: TimelinePollCoordinatorParameters) throws {
|
||||
self.parameters = parameters
|
||||
|
||||
try pollAggregator = PollAggregator(session: parameters.session, room: parameters.room, pollStartEvent: parameters.pollStartEvent)
|
||||
try pollAggregator = PollAggregator(session: parameters.session, room: parameters.room, pollStartEventId: parameters.pollStartEvent.eventId)
|
||||
pollAggregator.delegate = self
|
||||
|
||||
pollTimelineViewModel = PollTimelineViewModel(timelinePoll: buildTimelinePollFrom(pollAggregator.poll))
|
||||
pollTimelineViewModel.callback = { [weak self] result in
|
||||
viewModel = TimelinePollViewModel(timelinePollDetails: buildTimelinePollFrom(pollAggregator.poll))
|
||||
viewModel.callback = { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
|
||||
switch result {
|
||||
@@ -76,9 +74,9 @@ final class PollTimelineCoordinator: Coordinator, Presentable, PollAggregatorDel
|
||||
localEcho: nil, success: nil) { [weak self] error in
|
||||
guard let self = self else { return }
|
||||
|
||||
MXLog.error("[PollTimelineCoordinator]] Failed submitting response with error \(String(describing: error))")
|
||||
MXLog.error("[TimelinePollCoordinator]] Failed submitting response with error \(String(describing: error))")
|
||||
|
||||
self.pollTimelineViewModel.dispatch(action: .showAnsweringFailure)
|
||||
self.viewModel.dispatch(action: .showAnsweringFailure)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
@@ -90,23 +88,28 @@ final class PollTimelineCoordinator: Coordinator, Presentable, PollAggregatorDel
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
return VectorHostingController(rootView: PollTimelineView(viewModel: pollTimelineViewModel.context))
|
||||
return VectorHostingController(rootView: TimelinePollView(viewModel: viewModel.context))
|
||||
}
|
||||
|
||||
func canEndPoll() -> Bool {
|
||||
return pollAggregator.poll.isClosed == false
|
||||
}
|
||||
|
||||
func canEditPoll() -> Bool {
|
||||
return false // Intentionally disabled until platform parity.
|
||||
// return pollAggregator.poll.isClosed == false && pollAggregator.poll.totalAnswerCount == 0
|
||||
}
|
||||
|
||||
func endPoll() {
|
||||
parameters.room.sendPollEnd(for: parameters.pollStartEvent, threadId: nil, localEcho: nil, success: nil) { [weak self] error in
|
||||
self?.pollTimelineViewModel.dispatch(action: .showClosingFailure)
|
||||
self?.viewModel.dispatch(action: .showClosingFailure)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - PollAggregatorDelegate
|
||||
|
||||
func pollAggregatorDidUpdateData(_ aggregator: PollAggregator) {
|
||||
pollTimelineViewModel.dispatch(action: .updateWithPoll(buildTimelinePollFrom(aggregator.poll)))
|
||||
viewModel.dispatch(action: .updateWithPoll(buildTimelinePollFrom(aggregator.poll)))
|
||||
}
|
||||
|
||||
func pollAggregatorDidStartLoading(_ aggregator: PollAggregator) {
|
||||
@@ -125,20 +128,28 @@ final class PollTimelineCoordinator: Coordinator, Presentable, PollAggregatorDel
|
||||
|
||||
// PollProtocol is intentionally not available in the SwiftUI target as we don't want
|
||||
// to add the SDK as a dependency to it. We need to translate from one to the other on this level.
|
||||
func buildTimelinePollFrom(_ poll: PollProtocol) -> TimelinePoll {
|
||||
func buildTimelinePollFrom(_ poll: PollProtocol) -> TimelinePollDetails {
|
||||
let answerOptions = poll.answerOptions.map { pollAnswerOption in
|
||||
TimelineAnswerOption(id: pollAnswerOption.id,
|
||||
TimelinePollAnswerOption(id: pollAnswerOption.id,
|
||||
text: pollAnswerOption.text,
|
||||
count: pollAnswerOption.count,
|
||||
winner: pollAnswerOption.isWinner,
|
||||
selected: pollAnswerOption.isCurrentUserSelection)
|
||||
}
|
||||
|
||||
return TimelinePoll(question: poll.text,
|
||||
return TimelinePollDetails(question: poll.text,
|
||||
answerOptions: answerOptions,
|
||||
closed: poll.isClosed,
|
||||
totalAnswerCount: poll.totalAnswerCount,
|
||||
type: (poll.kind == .disclosed ? .disclosed : .undisclosed),
|
||||
maxAllowedSelections: poll.maxAllowedSelections)
|
||||
type: pollKindToTimelinePollType(poll.kind),
|
||||
maxAllowedSelections: poll.maxAllowedSelections,
|
||||
hasBeenEdited: poll.hasBeenEdited)
|
||||
}
|
||||
|
||||
private func pollKindToTimelinePollType(_ kind: PollKind) -> TimelinePollType {
|
||||
let mapping = [PollKind.disclosed: TimelinePollType.disclosed,
|
||||
PollKind.undisclosed: TimelinePollType.undisclosed]
|
||||
|
||||
return mapping[kind] ?? .disclosed
|
||||
}
|
||||
}
|
||||
+7
-7
@@ -17,11 +17,11 @@
|
||||
import Foundation
|
||||
|
||||
@available(iOS 14, *)
|
||||
class PollTimelineProvider {
|
||||
static let shared = PollTimelineProvider()
|
||||
class TimelinePollProvider {
|
||||
static let shared = TimelinePollProvider()
|
||||
|
||||
var session: MXSession?
|
||||
var coordinatorsForEventIdentifiers = [String: PollTimelineCoordinator]()
|
||||
var coordinatorsForEventIdentifiers = [String: TimelinePollCoordinator]()
|
||||
|
||||
private init() {
|
||||
|
||||
@@ -29,7 +29,7 @@ class PollTimelineProvider {
|
||||
|
||||
/// Create or retrieve the poll timeline coordinator for this event and return
|
||||
/// a view to be displayed in the timeline
|
||||
func buildPollTimelineViewForEvent(_ event: MXEvent) -> UIView? {
|
||||
func buildTimelinePollViewForEvent(_ event: MXEvent) -> UIView? {
|
||||
guard let session = session, let room = session.room(withRoomId: event.roomId) else {
|
||||
return nil
|
||||
}
|
||||
@@ -38,8 +38,8 @@ class PollTimelineProvider {
|
||||
return coordinator.toPresentable().view
|
||||
}
|
||||
|
||||
let parameters = PollTimelineCoordinatorParameters(session: session, room: room, pollStartEvent: event)
|
||||
guard let coordinator = try? PollTimelineCoordinator(parameters: parameters) else {
|
||||
let parameters = TimelinePollCoordinatorParameters(session: session, room: room, pollStartEvent: event)
|
||||
guard let coordinator = try? TimelinePollCoordinator(parameters: parameters) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ class PollTimelineProvider {
|
||||
}
|
||||
|
||||
/// Retrieve the poll timeline coordinator for the given event or nil if it hasn't been created yet
|
||||
func pollTimelineCoordinatorForEventIdentifier(_ eventIdentifier: String) -> PollTimelineCoordinator? {
|
||||
func timelinePollCoordinatorForEventIdentifier(_ eventIdentifier: String) -> TimelinePollCoordinator? {
|
||||
return coordinatorsForEventIdentifiers[eventIdentifier]
|
||||
}
|
||||
}
|
||||
+44
-7
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/PollTimeline PollTimeline
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
@@ -20,7 +18,7 @@ import XCTest
|
||||
import RiotSwiftUI
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
class PollTimelineUITests: XCTestCase {
|
||||
class TimelinePollUITests: XCTestCase {
|
||||
|
||||
private var app: XCUIApplication!
|
||||
|
||||
@@ -31,8 +29,8 @@ class PollTimelineUITests: XCTestCase {
|
||||
app.launch()
|
||||
}
|
||||
|
||||
func testOpenPoll() {
|
||||
app.goToScreenWithIdentifier(MockPollTimelineScreenState.open.title)
|
||||
func testOpenDisclosedPoll() {
|
||||
app.goToScreenWithIdentifier(MockTimelinePollScreenState.openDisclosed.title)
|
||||
|
||||
XCTAssert(app.staticTexts["Question"].exists)
|
||||
XCTAssert(app.staticTexts["20 votes cast"].exists)
|
||||
@@ -69,9 +67,48 @@ class PollTimelineUITests: XCTestCase {
|
||||
XCTAssertEqual(app.buttons["Third, 16 votes"].value as! String, "80%")
|
||||
}
|
||||
|
||||
func testClosedPoll() {
|
||||
app.goToScreenWithIdentifier(MockPollTimelineScreenState.closed.title)
|
||||
func testOpenUndisclosedPoll() {
|
||||
app.goToScreenWithIdentifier(MockTimelinePollScreenState.openUndisclosed.title)
|
||||
|
||||
XCTAssert(app.staticTexts["Question"].exists)
|
||||
XCTAssert(app.staticTexts["20 votes cast"].exists)
|
||||
|
||||
XCTAssert(!app.buttons["First, 10 votes"].exists)
|
||||
XCTAssert(app.buttons["First"].exists)
|
||||
XCTAssertTrue((app.buttons["First"].value as! String).isEmpty)
|
||||
|
||||
XCTAssert(!app.buttons["Second, 5 votes"].exists)
|
||||
XCTAssert(app.buttons["Second"].exists)
|
||||
XCTAssertTrue((app.buttons["Second"].value as! String).isEmpty)
|
||||
|
||||
XCTAssert(!app.buttons["Third, 15 votes"].exists)
|
||||
XCTAssert(app.buttons["Third"].exists)
|
||||
XCTAssertTrue((app.buttons["Third"].value as! String).isEmpty)
|
||||
|
||||
app.buttons["First"].tap()
|
||||
|
||||
XCTAssert(app.buttons["First"].exists)
|
||||
XCTAssert(app.buttons["Second"].exists)
|
||||
XCTAssert(app.buttons["Third"].exists)
|
||||
|
||||
app.buttons["Third"].tap()
|
||||
|
||||
XCTAssert(app.buttons["First"].exists)
|
||||
XCTAssert(app.buttons["Second"].exists)
|
||||
XCTAssert(app.buttons["Third"].exists)
|
||||
}
|
||||
|
||||
func testClosedDisclosedPoll() {
|
||||
app.goToScreenWithIdentifier(MockTimelinePollScreenState.closedDisclosed.title)
|
||||
checkClosedPoll()
|
||||
}
|
||||
|
||||
func testClosedUndisclosedPoll() {
|
||||
app.goToScreenWithIdentifier(MockTimelinePollScreenState.closedUndisclosed.title)
|
||||
checkClosedPoll()
|
||||
}
|
||||
|
||||
private func checkClosedPoll() {
|
||||
XCTAssert(app.staticTexts["Question"].exists)
|
||||
XCTAssert(app.staticTexts["Final results based on 20 votes"].exists)
|
||||
|
||||
+14
-15
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/PollTimeline PollTimeline
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
@@ -22,24 +20,25 @@ import Combine
|
||||
@testable import RiotSwiftUI
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
class PollTimelineViewModelTests: XCTestCase {
|
||||
var viewModel: PollTimelineViewModel!
|
||||
var context: PollTimelineViewModelType.Context!
|
||||
class TimelinePollViewModelTests: XCTestCase {
|
||||
var viewModel: TimelinePollViewModel!
|
||||
var context: TimelinePollViewModelType.Context!
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
|
||||
override func setUpWithError() throws {
|
||||
let answerOptions = [TimelineAnswerOption(id: "1", text: "1", count: 1, winner: false, selected: false),
|
||||
TimelineAnswerOption(id: "2", text: "2", count: 1, winner: false, selected: false),
|
||||
TimelineAnswerOption(id: "3", text: "3", count: 1, winner: false, selected: false)]
|
||||
let answerOptions = [TimelinePollAnswerOption(id: "1", text: "1", count: 1, winner: false, selected: false),
|
||||
TimelinePollAnswerOption(id: "2", text: "2", count: 1, winner: false, selected: false),
|
||||
TimelinePollAnswerOption(id: "3", text: "3", count: 1, winner: false, selected: false)]
|
||||
|
||||
let timelinePoll = TimelinePoll(question: "Question",
|
||||
answerOptions: answerOptions,
|
||||
closed: false,
|
||||
totalAnswerCount: 3,
|
||||
type: .disclosed,
|
||||
maxAllowedSelections: 1)
|
||||
let timelinePoll = TimelinePollDetails(question: "Question",
|
||||
answerOptions: answerOptions,
|
||||
closed: false,
|
||||
totalAnswerCount: 3,
|
||||
type: .disclosed,
|
||||
maxAllowedSelections: 1,
|
||||
hasBeenEdited: false)
|
||||
|
||||
viewModel = PollTimelineViewModel(timelinePoll: timelinePoll)
|
||||
viewModel = TimelinePollViewModel(timelinePollDetails: timelinePoll)
|
||||
context = viewModel.context
|
||||
}
|
||||
|
||||
+40
-18
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/PollTimeline PollTimeline
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
@@ -19,20 +17,20 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
typealias PollTimelineViewModelCallback = ((PollTimelineViewModelResult) -> Void)
|
||||
typealias TimelinePollViewModelCallback = ((TimelinePollViewModelResult) -> Void)
|
||||
|
||||
enum PollTimelineStateAction {
|
||||
case viewAction(PollTimelineViewAction, PollTimelineViewModelCallback?)
|
||||
case updateWithPoll(TimelinePoll)
|
||||
enum TimelinePollStateAction {
|
||||
case viewAction(TimelinePollViewAction, TimelinePollViewModelCallback?)
|
||||
case updateWithPoll(TimelinePollDetails)
|
||||
case showAnsweringFailure
|
||||
case showClosingFailure
|
||||
}
|
||||
|
||||
enum PollTimelineViewAction {
|
||||
enum TimelinePollViewAction {
|
||||
case selectAnswerOptionWithIdentifier(String)
|
||||
}
|
||||
|
||||
enum PollTimelineViewModelResult {
|
||||
enum TimelinePollViewModelResult {
|
||||
case selectedAnswerOptionsWithIdentifiers([String])
|
||||
}
|
||||
|
||||
@@ -41,7 +39,7 @@ enum TimelinePollType {
|
||||
case undisclosed
|
||||
}
|
||||
|
||||
class TimelineAnswerOption: Identifiable {
|
||||
class TimelinePollAnswerOption: Identifiable {
|
||||
var id: String
|
||||
var text: String
|
||||
var count: UInt
|
||||
@@ -57,35 +55,59 @@ class TimelineAnswerOption: Identifiable {
|
||||
}
|
||||
}
|
||||
|
||||
class TimelinePoll {
|
||||
class TimelinePollDetails {
|
||||
var question: String
|
||||
var answerOptions: [TimelineAnswerOption]
|
||||
var answerOptions: [TimelinePollAnswerOption]
|
||||
var closed: Bool
|
||||
var totalAnswerCount: UInt
|
||||
var type: TimelinePollType
|
||||
var maxAllowedSelections: UInt
|
||||
var hasBeenEdited: Bool = true
|
||||
|
||||
init(question: String, answerOptions: [TimelineAnswerOption], closed: Bool, totalAnswerCount: UInt, type: TimelinePollType, maxAllowedSelections: UInt) {
|
||||
init(question: String, answerOptions: [TimelinePollAnswerOption],
|
||||
closed: Bool,
|
||||
totalAnswerCount: UInt,
|
||||
type: TimelinePollType,
|
||||
maxAllowedSelections: UInt,
|
||||
hasBeenEdited: Bool) {
|
||||
self.question = question
|
||||
self.answerOptions = answerOptions
|
||||
self.closed = closed
|
||||
self.totalAnswerCount = totalAnswerCount
|
||||
self.type = type
|
||||
self.maxAllowedSelections = maxAllowedSelections
|
||||
self.hasBeenEdited = hasBeenEdited
|
||||
}
|
||||
|
||||
var hasCurrentUserVoted: Bool {
|
||||
answerOptions.filter { $0.selected == true}.count > 0
|
||||
}
|
||||
|
||||
var shouldDiscloseResults: Bool {
|
||||
if closed {
|
||||
return totalAnswerCount > 0
|
||||
} else {
|
||||
return type == .disclosed && totalAnswerCount > 0 && hasCurrentUserVoted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PollTimelineViewState: BindableState {
|
||||
var poll: TimelinePoll
|
||||
var bindings: PollTimelineViewStateBindings
|
||||
struct TimelinePollViewState: BindableState {
|
||||
var poll: TimelinePollDetails
|
||||
var bindings: TimelinePollViewStateBindings
|
||||
}
|
||||
|
||||
struct PollTimelineViewStateBindings {
|
||||
var showsAnsweringFailureAlert: Bool = false
|
||||
var showsClosingFailureAlert: Bool = false
|
||||
struct TimelinePollViewStateBindings {
|
||||
var alertInfo: TimelinePollErrorAlertInfo?
|
||||
}
|
||||
|
||||
struct TimelinePollErrorAlertInfo: Identifiable {
|
||||
enum AlertType {
|
||||
case failedClosingPoll
|
||||
case failedSubmittingAnswer
|
||||
}
|
||||
|
||||
let id: AlertType
|
||||
let title: String
|
||||
let subtitle: String
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// Copyright 2021 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
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
enum MockTimelinePollScreenState: MockScreenState, CaseIterable {
|
||||
case openDisclosed
|
||||
case closedDisclosed
|
||||
case openUndisclosed
|
||||
case closedUndisclosed
|
||||
|
||||
var screenType: Any.Type {
|
||||
TimelinePollDetails.self
|
||||
}
|
||||
|
||||
var screenView: ([Any], AnyView) {
|
||||
let answerOptions = [TimelinePollAnswerOption(id: "1", text: "First", count: 10, winner: false, selected: false),
|
||||
TimelinePollAnswerOption(id: "2", text: "Second", count: 5, winner: false, selected: true),
|
||||
TimelinePollAnswerOption(id: "3", text: "Third", count: 15, winner: true, selected: false)]
|
||||
|
||||
let poll = TimelinePollDetails(question: "Question",
|
||||
answerOptions: answerOptions,
|
||||
closed: (self == .closedDisclosed || self == .closedUndisclosed ? true : false),
|
||||
totalAnswerCount: 20,
|
||||
type: (self == .closedDisclosed || self == .openDisclosed ? .disclosed : .undisclosed),
|
||||
maxAllowedSelections: 1,
|
||||
hasBeenEdited: false)
|
||||
|
||||
let viewModel = TimelinePollViewModel(timelinePollDetails: poll)
|
||||
|
||||
return ([viewModel], AnyView(TimelinePollView(viewModel: viewModel.context)))
|
||||
}
|
||||
}
|
||||
+18
-16
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/PollTimeline PollTimeline
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
@@ -20,11 +18,11 @@ import SwiftUI
|
||||
import Combine
|
||||
|
||||
@available(iOS 14, *)
|
||||
typealias PollTimelineViewModelType = StateStoreViewModel<PollTimelineViewState,
|
||||
PollTimelineStateAction,
|
||||
PollTimelineViewAction>
|
||||
typealias TimelinePollViewModelType = StateStoreViewModel<TimelinePollViewState,
|
||||
TimelinePollStateAction,
|
||||
TimelinePollViewAction>
|
||||
@available(iOS 14, *)
|
||||
class PollTimelineViewModel: PollTimelineViewModelType {
|
||||
class TimelinePollViewModel: TimelinePollViewModelType {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
@@ -32,24 +30,24 @@ class PollTimelineViewModel: PollTimelineViewModelType {
|
||||
|
||||
// MARK: Public
|
||||
|
||||
var callback: PollTimelineViewModelCallback?
|
||||
var callback: TimelinePollViewModelCallback?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(timelinePoll: TimelinePoll) {
|
||||
super.init(initialViewState: PollTimelineViewState(poll: timelinePoll, bindings: PollTimelineViewStateBindings()))
|
||||
init(timelinePollDetails: TimelinePollDetails) {
|
||||
super.init(initialViewState: TimelinePollViewState(poll: timelinePollDetails, bindings: TimelinePollViewStateBindings()))
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: PollTimelineViewAction) {
|
||||
override func process(viewAction: TimelinePollViewAction) {
|
||||
switch viewAction {
|
||||
case .selectAnswerOptionWithIdentifier(_):
|
||||
dispatch(action: .viewAction(viewAction, callback))
|
||||
}
|
||||
}
|
||||
|
||||
override class func reducer(state: inout PollTimelineViewState, action: PollTimelineStateAction) {
|
||||
override class func reducer(state: inout TimelinePollViewState, action: TimelinePollStateAction) {
|
||||
switch action {
|
||||
case .viewAction(let viewAction, let callback):
|
||||
switch viewAction {
|
||||
@@ -69,15 +67,19 @@ class PollTimelineViewModel: PollTimelineViewModelType {
|
||||
case .updateWithPoll(let poll):
|
||||
state.poll = poll
|
||||
case .showAnsweringFailure:
|
||||
state.bindings.showsAnsweringFailureAlert = true
|
||||
state.bindings.alertInfo = TimelinePollErrorAlertInfo(id: .failedSubmittingAnswer,
|
||||
title: VectorL10n.pollTimelineVoteNotRegisteredTitle,
|
||||
subtitle: VectorL10n.pollTimelineVoteNotRegisteredSubtitle)
|
||||
case .showClosingFailure:
|
||||
state.bindings.showsClosingFailureAlert = true
|
||||
state.bindings.alertInfo = TimelinePollErrorAlertInfo(id: .failedClosingPoll,
|
||||
title: VectorL10n.pollTimelineNotClosedTitle,
|
||||
subtitle: VectorL10n.pollTimelineNotClosedSubtitle)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
static func updateSingleSelectPollLocalState(_ state: inout PollTimelineViewState, selectedAnswerIdentifier: String, callback: PollTimelineViewModelCallback?) {
|
||||
static func updateSingleSelectPollLocalState(_ state: inout TimelinePollViewState, selectedAnswerIdentifier: String, callback: TimelinePollViewModelCallback?) {
|
||||
for answerOption in state.poll.answerOptions {
|
||||
if answerOption.selected {
|
||||
answerOption.selected = false
|
||||
@@ -98,7 +100,7 @@ class PollTimelineViewModel: PollTimelineViewModelType {
|
||||
informCoordinatorOfSelectionUpdate(state: state, callback: callback)
|
||||
}
|
||||
|
||||
static func updateMultiSelectPollLocalState(_ state: inout PollTimelineViewState, selectedAnswerIdentifier: String, callback: PollTimelineViewModelCallback?) {
|
||||
static func updateMultiSelectPollLocalState(_ state: inout TimelinePollViewState, selectedAnswerIdentifier: String, callback: TimelinePollViewModelCallback?) {
|
||||
let selectedAnswerOptions = state.poll.answerOptions.filter { $0.selected == true }
|
||||
|
||||
let isDeselecting = selectedAnswerOptions.filter { $0.id == selectedAnswerIdentifier }.count > 0
|
||||
@@ -122,7 +124,7 @@ class PollTimelineViewModel: PollTimelineViewModelType {
|
||||
informCoordinatorOfSelectionUpdate(state: state, callback: callback)
|
||||
}
|
||||
|
||||
static func informCoordinatorOfSelectionUpdate(state: PollTimelineViewState, callback: PollTimelineViewModelCallback?) {
|
||||
static func informCoordinatorOfSelectionUpdate(state: TimelinePollViewState, callback: TimelinePollViewModelCallback?) {
|
||||
let selectedIdentifiers = state.poll.answerOptions.compactMap { answerOption in
|
||||
answerOption.selected ? answerOption.id : nil
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
//
|
||||
// Copyright 2021 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
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct TimelinePollAnswerOptionButton: View {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@Environment(\.theme) private var theme: ThemeSwiftUI
|
||||
|
||||
let poll: TimelinePollDetails
|
||||
let answerOption: TimelinePollAnswerOption
|
||||
let action: () -> Void
|
||||
|
||||
// MARK: Public
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
let rect = RoundedRectangle(cornerRadius: 4.0)
|
||||
answerOptionLabel
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.horizontal, 8.0)
|
||||
.padding(.top, 12.0)
|
||||
.padding(.bottom, 12.0)
|
||||
.clipShape(rect)
|
||||
.overlay(rect.stroke(borderAccentColor, lineWidth: 1.0))
|
||||
.accentColor(progressViewAccentColor)
|
||||
}
|
||||
}
|
||||
|
||||
var answerOptionLabel: some View {
|
||||
VStack(alignment: .leading, spacing: 12.0) {
|
||||
HStack(alignment: .top, spacing: 8.0) {
|
||||
|
||||
if !poll.closed {
|
||||
Image(uiImage: answerOption.selected ? Asset.Images.pollCheckboxSelected.image : Asset.Images.pollCheckboxDefault.image)
|
||||
}
|
||||
|
||||
Text(answerOption.text)
|
||||
.font(theme.fonts.body)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
|
||||
if poll.closed && answerOption.winner {
|
||||
Spacer()
|
||||
Image(uiImage: Asset.Images.pollWinnerIcon.image)
|
||||
}
|
||||
}
|
||||
|
||||
if poll.type == .disclosed || poll.closed {
|
||||
HStack {
|
||||
ProgressView(value: Double(poll.shouldDiscloseResults ? answerOption.count : 0),
|
||||
total: Double(poll.totalAnswerCount))
|
||||
.progressViewStyle(LinearProgressViewStyle())
|
||||
.scaleEffect(x: 1.0, y: 1.2, anchor: .center)
|
||||
|
||||
if (poll.shouldDiscloseResults) {
|
||||
Text(answerOption.count == 1 ? VectorL10n.pollTimelineOneVote : VectorL10n.pollTimelineVotesCount(Int(answerOption.count)))
|
||||
.font(theme.fonts.footnote)
|
||||
.foregroundColor(poll.closed && answerOption.winner ? theme.colors.accent : theme.colors.secondaryContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var borderAccentColor: Color {
|
||||
guard !poll.closed else {
|
||||
return (answerOption.winner ? theme.colors.accent : theme.colors.quinaryContent)
|
||||
}
|
||||
|
||||
return answerOption.selected ? theme.colors.accent : theme.colors.quinaryContent
|
||||
}
|
||||
|
||||
var progressViewAccentColor: Color {
|
||||
guard !poll.closed else {
|
||||
return (answerOption.winner ? theme.colors.accent : theme.colors.quarterlyContent)
|
||||
}
|
||||
|
||||
return answerOption.selected ? theme.colors.accent : theme.colors.quarterlyContent
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct TimelinePollAnswerOptionButton_Previews: PreviewProvider {
|
||||
static let stateRenderer = MockTimelinePollScreenState.stateRenderer
|
||||
|
||||
static var previews: some View {
|
||||
Group {
|
||||
let pollTypes: [TimelinePollType] = [.disclosed, .undisclosed]
|
||||
|
||||
ForEach(pollTypes, id: \.self) { type in
|
||||
VStack {
|
||||
TimelinePollAnswerOptionButton(poll: buildPoll(closed: false, type: type),
|
||||
answerOption: buildAnswerOption(selected: false),
|
||||
action: {})
|
||||
|
||||
TimelinePollAnswerOptionButton(poll: buildPoll(closed: false, type: type),
|
||||
answerOption: buildAnswerOption(selected: true),
|
||||
action: {})
|
||||
|
||||
TimelinePollAnswerOptionButton(poll: buildPoll(closed: true, type: type),
|
||||
answerOption: buildAnswerOption(selected: false, winner: false),
|
||||
action: {})
|
||||
|
||||
TimelinePollAnswerOptionButton(poll: buildPoll(closed: true, type: type),
|
||||
answerOption: buildAnswerOption(selected: false, winner: true),
|
||||
action: {})
|
||||
|
||||
TimelinePollAnswerOptionButton(poll: buildPoll(closed: true, type: type),
|
||||
answerOption: buildAnswerOption(selected: true, winner: false),
|
||||
action: {})
|
||||
|
||||
TimelinePollAnswerOptionButton(poll: buildPoll(closed: true, type: type),
|
||||
answerOption: buildAnswerOption(selected: true, winner: true),
|
||||
action: {})
|
||||
|
||||
let longText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
|
||||
|
||||
TimelinePollAnswerOptionButton(poll: buildPoll(closed: true, type: type),
|
||||
answerOption: buildAnswerOption(text: longText, selected: true, winner: true),
|
||||
action: {})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func buildPoll(closed: Bool, type: TimelinePollType) -> TimelinePollDetails {
|
||||
TimelinePollDetails(question: "",
|
||||
answerOptions: [],
|
||||
closed: closed,
|
||||
totalAnswerCount: 100,
|
||||
type: type,
|
||||
maxAllowedSelections: 1,
|
||||
hasBeenEdited: false)
|
||||
}
|
||||
|
||||
static func buildAnswerOption(text: String = "Test", selected: Bool, winner: Bool = false) -> TimelinePollAnswerOption {
|
||||
TimelinePollAnswerOption(id: "1", text: text, count: 5, winner: winner, selected: selected)
|
||||
}
|
||||
}
|
||||
+19
-30
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/PollEditForm PollEditForm
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
@@ -19,7 +17,7 @@
|
||||
import SwiftUI
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct PollTimelineView: View {
|
||||
struct TimelinePollView: View {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
@@ -29,29 +27,26 @@ struct PollTimelineView: View {
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@ObservedObject var viewModel: PollTimelineViewModel.Context
|
||||
@ObservedObject var viewModel: TimelinePollViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
let poll = viewModel.viewState.poll
|
||||
|
||||
VStack(alignment: .leading, spacing: 16.0) {
|
||||
|
||||
Text(poll.question)
|
||||
.font(theme.fonts.bodySB)
|
||||
.foregroundColor(theme.colors.primaryContent) +
|
||||
Text(editedText)
|
||||
.font(theme.fonts.footnote)
|
||||
.foregroundColor(theme.colors.secondaryContent)
|
||||
|
||||
VStack(spacing: 24.0) {
|
||||
ForEach(poll.answerOptions) { answerOption in
|
||||
PollTimelineAnswerOptionButton(answerOption: answerOption,
|
||||
pollClosed: poll.closed,
|
||||
showResults: shouldDiscloseResults,
|
||||
totalAnswerCount: poll.totalAnswerCount) {
|
||||
TimelinePollAnswerOptionButton(poll: poll, answerOption: answerOption) {
|
||||
viewModel.send(viewAction: .selectAnswerOptionWithIdentifier(answerOption.id))
|
||||
}
|
||||
}
|
||||
.alert(isPresented: $viewModel.showsClosingFailureAlert) {
|
||||
Alert(title: Text(VectorL10n.pollTimelineNotClosedTitle),
|
||||
message: Text(VectorL10n.pollTimelineNotClosedSubtitle),
|
||||
dismissButton: .default(Text(VectorL10n.ok)))
|
||||
}
|
||||
}
|
||||
.disabled(poll.closed)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
@@ -59,14 +54,14 @@ struct PollTimelineView: View {
|
||||
Text(totalVotesString)
|
||||
.font(theme.fonts.footnote)
|
||||
.foregroundColor(theme.colors.tertiaryContent)
|
||||
.alert(isPresented: $viewModel.showsAnsweringFailureAlert) {
|
||||
Alert(title: Text(VectorL10n.pollTimelineVoteNotRegisteredTitle),
|
||||
message: Text(VectorL10n.pollTimelineVoteNotRegisteredSubtitle),
|
||||
dismissButton: .default(Text(VectorL10n.ok)))
|
||||
}
|
||||
}
|
||||
.padding([.horizontal, .top], 2.0)
|
||||
.padding([.bottom])
|
||||
.alert(item: $viewModel.alertInfo) { info in
|
||||
Alert(title: Text(info.title),
|
||||
message: Text(info.subtitle),
|
||||
dismissButton: .default(Text(VectorL10n.ok)))
|
||||
}
|
||||
}
|
||||
|
||||
private var totalVotesString: String {
|
||||
@@ -84,32 +79,26 @@ struct PollTimelineView: View {
|
||||
case 0:
|
||||
return VectorL10n.pollTimelineTotalNoVotes
|
||||
case 1:
|
||||
return (poll.hasCurrentUserVoted ?
|
||||
return (poll.hasCurrentUserVoted || poll.type == .undisclosed ?
|
||||
VectorL10n.pollTimelineTotalOneVote :
|
||||
VectorL10n.pollTimelineTotalOneVoteNotVoted)
|
||||
default:
|
||||
return (poll.hasCurrentUserVoted ?
|
||||
return (poll.hasCurrentUserVoted || poll.type == .undisclosed ?
|
||||
VectorL10n.pollTimelineTotalVotes(Int(poll.totalAnswerCount)) :
|
||||
VectorL10n.pollTimelineTotalVotesNotVoted(Int(poll.totalAnswerCount)))
|
||||
}
|
||||
}
|
||||
|
||||
private var shouldDiscloseResults: Bool {
|
||||
let poll = viewModel.viewState.poll
|
||||
|
||||
if poll.closed {
|
||||
return poll.totalAnswerCount > 0
|
||||
} else {
|
||||
return poll.type == .disclosed && poll.totalAnswerCount > 0 && poll.hasCurrentUserVoted
|
||||
}
|
||||
private var editedText: String {
|
||||
viewModel.viewState.poll.hasBeenEdited ? " \(VectorL10n.eventFormatterMessageEditedMention)" : ""
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct PollTimelineView_Previews: PreviewProvider {
|
||||
static let stateRenderer = MockPollTimelineScreenState.stateRenderer
|
||||
struct TimelinePollView_Previews: PreviewProvider {
|
||||
static let stateRenderer = MockTimelinePollScreenState.stateRenderer
|
||||
static var previews: some View {
|
||||
stateRenderer.screenGroup()
|
||||
}
|
||||
+15
-17
@@ -1,20 +1,18 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
/*
|
||||
Copyright 2021 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.
|
||||
*/
|
||||
//
|
||||
// Copyright 2021 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 UIKit
|
||||
|
||||
-2
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
|
||||
+1
-3
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
@@ -26,7 +24,7 @@ enum MockUserSuggestionScreenState: MockScreenState, CaseIterable {
|
||||
static private var members: [RoomMembersProviderMember]!
|
||||
|
||||
var screenType: Any.Type {
|
||||
MockUserSuggestionScreenState.self
|
||||
UserSuggestionList.self
|
||||
}
|
||||
|
||||
var screenView: ([Any], AnyView) {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
|
||||
-2
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user