mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-30 05:06:58 +02:00
Made StateStoreViewModel state mutable and removed the reducer for all the features using it.
This commit is contained in:
committed by
Stefan Ceriu
parent
fc9e95aee8
commit
313b05485a
@@ -16,9 +16,6 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
// The state is never modified so this is unnecessary.
|
||||
enum AnalyticsPromptStateAction { }
|
||||
|
||||
enum AnalyticsPromptViewAction {
|
||||
/// Enable analytics.
|
||||
case enable
|
||||
|
||||
@@ -19,7 +19,7 @@ import Combine
|
||||
|
||||
@available(iOS 14, *)
|
||||
typealias AnalyticsPromptViewModelType = StateStoreViewModel<AnalyticsPromptViewState,
|
||||
AnalyticsPromptStateAction,
|
||||
Never,
|
||||
AnalyticsPromptViewAction>
|
||||
@available(iOS 14, *)
|
||||
class AnalyticsPromptViewModel: AnalyticsPromptViewModelType {
|
||||
@@ -54,10 +54,6 @@ class AnalyticsPromptViewModel: AnalyticsPromptViewModelType {
|
||||
openTermsURL()
|
||||
}
|
||||
}
|
||||
|
||||
override class func reducer(state: inout AnalyticsPromptViewState, action: AnalyticsPromptStateAction) {
|
||||
// There is no mutable state to reduce :)
|
||||
}
|
||||
|
||||
/// Enable analytics. The call to the Analytics class is made in the completion.
|
||||
private func enable() {
|
||||
|
||||
@@ -92,9 +92,9 @@ class StateStoreViewModel<State: BindableState, StateAction, ViewAction> {
|
||||
/// Constrained interface for passing to Views.
|
||||
var context: Context
|
||||
|
||||
/// State can be read within the 'ViewModel' but not modified outside of the reducer.
|
||||
var state: State {
|
||||
context.viewState
|
||||
get { context.viewState }
|
||||
set { context.viewState = newValue }
|
||||
}
|
||||
|
||||
// MARK: Setup
|
||||
@@ -110,12 +110,14 @@ class StateStoreViewModel<State: BindableState, StateAction, ViewAction> {
|
||||
|
||||
/// Send state actions to modify the state within the reducer.
|
||||
/// - Parameter action: The state action to send to the reducer.
|
||||
@available(*, deprecated, message: "Mutate state directly instead")
|
||||
func dispatch(action: StateAction) {
|
||||
Self.reducer(state: &context.viewState, action: action)
|
||||
}
|
||||
|
||||
/// Send state actions from a publisher to modify the state within the reducer.
|
||||
/// - Parameter actionPublisher: The publisher that produces actions to be sent to the reducer
|
||||
@available(*, deprecated, message: "Mutate state directly instead")
|
||||
func dispatch(actionPublisher: AnyPublisher<StateAction, Never>) {
|
||||
actionPublisher.sink { [weak self] action in
|
||||
guard let self = self else { return }
|
||||
|
||||
@@ -30,10 +30,6 @@ struct OnboardingSplashScreenPageContent {
|
||||
|
||||
// MARK: View model
|
||||
|
||||
enum OnboardingSplashScreenStateAction {
|
||||
case viewAction(OnboardingSplashScreenViewAction)
|
||||
}
|
||||
|
||||
enum OnboardingSplashScreenViewModelResult {
|
||||
case register
|
||||
case login
|
||||
|
||||
@@ -19,7 +19,7 @@ import Combine
|
||||
|
||||
@available(iOS 14, *)
|
||||
typealias OnboardingSplashScreenViewModelType = StateStoreViewModel<OnboardingSplashScreenViewState,
|
||||
OnboardingSplashScreenStateAction,
|
||||
Never,
|
||||
OnboardingSplashScreenViewAction>
|
||||
|
||||
protocol OnboardingSplashScreenViewModelProtocol {
|
||||
@@ -54,31 +54,18 @@ class OnboardingSplashScreenViewModel: OnboardingSplashScreenViewModelType, Onbo
|
||||
register()
|
||||
case .login:
|
||||
login()
|
||||
case .nextPage, .previousPage, .hiddenPage:
|
||||
dispatch(action: .viewAction(viewAction))
|
||||
case .nextPage:
|
||||
// Wrap back round to the first page index when reaching the end.
|
||||
state.bindings.pageIndex = (state.bindings.pageIndex + 1) % state.content.count
|
||||
case .previousPage:
|
||||
// Prevent the hidden page at index -1 from being shown.
|
||||
state.bindings.pageIndex = max(0, (state.bindings.pageIndex - 1))
|
||||
case .hiddenPage:
|
||||
// Hidden page for a nicer animation when looping back to the start.
|
||||
state.bindings.pageIndex = -1
|
||||
}
|
||||
}
|
||||
|
||||
override class func reducer(state: inout OnboardingSplashScreenViewState, action: OnboardingSplashScreenStateAction) {
|
||||
switch action {
|
||||
case .viewAction(let viewAction):
|
||||
switch viewAction {
|
||||
case .nextPage:
|
||||
// Wrap back round to the first page index when reaching the end.
|
||||
state.bindings.pageIndex = (state.bindings.pageIndex + 1) % state.content.count
|
||||
case .previousPage:
|
||||
// Prevent the hidden page at index -1 from being shown.
|
||||
state.bindings.pageIndex = max(0, (state.bindings.pageIndex - 1))
|
||||
case .hiddenPage:
|
||||
// Hidden page for a nicer animation when looping back to the start.
|
||||
state.bindings.pageIndex = -1
|
||||
case .login, .register:
|
||||
break
|
||||
}
|
||||
}
|
||||
UILog.debug("[OnboardingSplashScreenViewModel] reducer with action \(action) produced state: \(state)")
|
||||
}
|
||||
|
||||
private func register() {
|
||||
completion?(.register)
|
||||
}
|
||||
|
||||
+1
-1
@@ -21,5 +21,5 @@ import Combine
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
class OnboardingSplashScreenViewModelTests: XCTestCase {
|
||||
// TODO: Check for any useful tests when finished
|
||||
|
||||
}
|
||||
|
||||
+6
-13
@@ -33,12 +33,7 @@ final class LocationSharingCoordinator: Coordinator, Presentable {
|
||||
|
||||
private let parameters: LocationSharingCoordinatorParameters
|
||||
private let locationSharingHostingController: UIViewController
|
||||
private var _locationSharingViewModel: Any? = nil
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
fileprivate var locationSharingViewModel: LocationSharingViewModel {
|
||||
return _locationSharingViewModel as! LocationSharingViewModel
|
||||
}
|
||||
private var locationSharingViewModel: LocationSharingViewModelProtocol
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@@ -58,7 +53,7 @@ final class LocationSharingCoordinator: Coordinator, Presentable {
|
||||
let view = LocationSharingView(context: viewModel.context)
|
||||
.addDependency(AvatarService.instantiate(mediaManager: parameters.mediaManager))
|
||||
|
||||
_locationSharingViewModel = viewModel
|
||||
locationSharingViewModel = viewModel
|
||||
locationSharingHostingController = VectorHostingController(rootView: view)
|
||||
}
|
||||
|
||||
@@ -81,20 +76,18 @@ final class LocationSharingCoordinator: Coordinator, Presentable {
|
||||
return
|
||||
}
|
||||
|
||||
self.locationSharingViewModel.dispatch(action: .startLoading)
|
||||
self.locationSharingViewModel.startLoading()
|
||||
|
||||
self.parameters.roomDataSource.sendLocation(withLatitude: latitude,
|
||||
longitude: longitude,
|
||||
description: nil) { [weak self] _ in
|
||||
self.parameters.roomDataSource.sendLocation(withLatitude: latitude, longitude: longitude, description: nil) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.locationSharingViewModel.dispatch(action: .stopLoading(nil))
|
||||
self.locationSharingViewModel.stopLoading()
|
||||
self.completion?()
|
||||
} failure: { [weak self] error in
|
||||
guard let self = self else { return }
|
||||
|
||||
MXLog.error("[LocationSharingCoordinator] Failed sharing location with error: \(String(describing: error))")
|
||||
self.locationSharingViewModel.dispatch(action: .stopLoading(error))
|
||||
self.locationSharingViewModel.stopLoading(error: .locationSharingError)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,31 +19,23 @@ import SwiftUI
|
||||
import Combine
|
||||
import CoreLocation
|
||||
|
||||
enum LocationSharingViewError {
|
||||
case failedLoadingMap
|
||||
case failedLocatingUser
|
||||
case invalidLocationAuthorization
|
||||
case failedSharingLocation
|
||||
}
|
||||
|
||||
enum LocationSharingStateAction {
|
||||
case error(LocationSharingViewError, LocationSharingViewModelCallback?)
|
||||
case startLoading
|
||||
case stopLoading(Error?)
|
||||
}
|
||||
|
||||
enum LocationSharingViewAction {
|
||||
case cancel
|
||||
case share
|
||||
}
|
||||
|
||||
typealias LocationSharingViewModelCallback = ((LocationSharingViewModelResult) -> Void)
|
||||
|
||||
enum LocationSharingViewModelResult {
|
||||
case cancel
|
||||
case share(latitude: Double, longitude: Double)
|
||||
}
|
||||
|
||||
enum LocationSharingViewError {
|
||||
case failedLoadingMap
|
||||
case failedLocatingUser
|
||||
case invalidLocationAuthorization
|
||||
case failedSharingLocation
|
||||
}
|
||||
|
||||
@available(iOS 14, *)
|
||||
struct LocationSharingViewState: BindableState {
|
||||
let tileServerMapURL: URL
|
||||
@@ -80,6 +72,7 @@ struct LocationSharingErrorAlertInfo: Identifiable {
|
||||
|
||||
let id: AlertType
|
||||
let title: String
|
||||
var subtitle: String? = nil
|
||||
let primaryButton: (title: String, action: (() -> Void)?)
|
||||
let secondaryButton: (title: String, action: (() -> Void)?)?
|
||||
var secondaryButton: (title: String, action: (() -> Void)?)? = nil
|
||||
}
|
||||
|
||||
@@ -19,11 +19,11 @@ import Combine
|
||||
import CoreLocation
|
||||
|
||||
@available(iOS 14, *)
|
||||
typealias LocationSharingViewModelType = StateStoreViewModel< LocationSharingViewState,
|
||||
LocationSharingStateAction,
|
||||
LocationSharingViewAction >
|
||||
typealias LocationSharingViewModelType = StateStoreViewModel<LocationSharingViewState,
|
||||
Never,
|
||||
LocationSharingViewAction>
|
||||
@available(iOS 14, *)
|
||||
class LocationSharingViewModel: LocationSharingViewModelType {
|
||||
class LocationSharingViewModel: LocationSharingViewModelType, LocationSharingViewModelProtocol {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
@@ -41,7 +41,7 @@ class LocationSharingViewModel: LocationSharingViewModelType {
|
||||
|
||||
state.errorSubject.sink { [weak self] error in
|
||||
guard let self = self else { return }
|
||||
self.dispatch(action: .error(error, self.completion))
|
||||
self.processError(error)
|
||||
}.store(in: &cancellables)
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ class LocationSharingViewModel: LocationSharingViewModelType {
|
||||
}
|
||||
|
||||
guard let location = state.bindings.userLocation else {
|
||||
dispatch(action: .error(.failedLocatingUser, completion))
|
||||
processError(.failedLocatingUser)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -66,45 +66,54 @@ class LocationSharingViewModel: LocationSharingViewModelType {
|
||||
}
|
||||
}
|
||||
|
||||
override class func reducer(state: inout LocationSharingViewState, action: LocationSharingStateAction) {
|
||||
switch action {
|
||||
case .error(let error, let completion):
|
||||
|
||||
switch error {
|
||||
case .failedLoadingMap:
|
||||
state.bindings.alertInfo = LocationSharingErrorAlertInfo(id: .mapLoadingError,
|
||||
title: VectorL10n.locationSharingLoadingMapErrorTitle(AppInfo.current.displayName) ,
|
||||
primaryButton: (VectorL10n.ok, { completion?(.cancel) }),
|
||||
secondaryButton: nil)
|
||||
case .failedLocatingUser:
|
||||
state.bindings.alertInfo = LocationSharingErrorAlertInfo(id: .userLocatingError,
|
||||
title: VectorL10n.locationSharingLocatingUserErrorTitle(AppInfo.current.displayName),
|
||||
primaryButton: (VectorL10n.ok, { completion?(.cancel) }),
|
||||
secondaryButton: nil)
|
||||
case .invalidLocationAuthorization:
|
||||
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
|
||||
}
|
||||
|
||||
case .startLoading:
|
||||
state.showLoadingIndicator = true
|
||||
case .stopLoading(let error):
|
||||
state.showLoadingIndicator = false
|
||||
|
||||
if error != nil {
|
||||
state.bindings.alertInfo = LocationSharingErrorAlertInfo(id: .locationSharingError,
|
||||
title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle(AppInfo.current.displayName),
|
||||
primaryButton: (VectorL10n.ok, nil),
|
||||
secondaryButton: nil)
|
||||
}
|
||||
// MARK: - LocationSharingViewModelProtocol
|
||||
|
||||
public func startLoading() {
|
||||
state.showLoadingIndicator = true
|
||||
}
|
||||
|
||||
func stopLoading(error: LocationSharingErrorAlertInfo.AlertType?) {
|
||||
state.showLoadingIndicator = false
|
||||
|
||||
if let error = error {
|
||||
state.bindings.alertInfo = LocationSharingErrorAlertInfo(id: error,
|
||||
title: VectorL10n.locationSharingPostFailureTitle,
|
||||
subtitle: VectorL10n.locationSharingPostFailureSubtitle(AppInfo.current.displayName),
|
||||
primaryButton: (VectorL10n.ok, nil))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func processError(_ error: LocationSharingViewError) {
|
||||
guard state.bindings.alertInfo == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
let primaryButtonCompletion = { [weak self] () -> Void in
|
||||
self?.completion?(.cancel)
|
||||
}
|
||||
|
||||
switch error {
|
||||
case .failedLoadingMap:
|
||||
state.bindings.alertInfo = LocationSharingErrorAlertInfo(id: .mapLoadingError,
|
||||
title: VectorL10n.locationSharingLoadingMapErrorTitle(AppInfo.current.displayName),
|
||||
primaryButton: (VectorL10n.ok, primaryButtonCompletion))
|
||||
case .failedLocatingUser:
|
||||
state.bindings.alertInfo = LocationSharingErrorAlertInfo(id: .userLocatingError,
|
||||
title: VectorL10n.locationSharingLocatingUserErrorTitle(AppInfo.current.displayName),
|
||||
primaryButton: (VectorL10n.ok, primaryButtonCompletion))
|
||||
case .invalidLocationAuthorization:
|
||||
state.bindings.alertInfo = LocationSharingErrorAlertInfo(id: .authorizationError,
|
||||
title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle(AppInfo.current.displayName),
|
||||
primaryButton: (VectorL10n.locationSharingInvalidAuthorizationNotNow, primaryButtonCompletion),
|
||||
secondaryButton: (VectorL10n.locationSharingInvalidAuthorizationSettings, {
|
||||
if let applicationSettingsURL = URL(string:UIApplication.openSettingsURLString) {
|
||||
UIApplication.shared.open(applicationSettingsURL)
|
||||
}
|
||||
}))
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-3
@@ -16,7 +16,15 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
enum UserSuggestionStateAction {
|
||||
case updateWithItems([UserSuggestionItemProtocol])
|
||||
protocol LocationSharingViewModelProtocol {
|
||||
var completion: ((LocationSharingViewModelResult) -> Void)? { get set }
|
||||
|
||||
func startLoading()
|
||||
func stopLoading(error: LocationSharingErrorAlertInfo.AlertType?)
|
||||
}
|
||||
|
||||
extension LocationSharingViewModelProtocol {
|
||||
func stopLoading() {
|
||||
stopLoading(error: nil)
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ class LocationSharingUITests: XCTestCase {
|
||||
goToScreenWithIdentifier(MockLocationSharingScreenState.displayExistingLocation.title)
|
||||
|
||||
XCTAssertTrue(app.buttons["Cancel"].exists)
|
||||
XCTAssertTrue(app.buttons["location share icon"].exists)
|
||||
XCTAssertTrue(app.buttons["LocationSharingView.shareButton"].exists)
|
||||
XCTAssertTrue(app.otherElements["Map"].exists)
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -100,12 +100,12 @@ class LocationSharingViewModelTests: XCTestCase {
|
||||
func testLoading() {
|
||||
let viewModel = buildViewModel(withLocation: false)
|
||||
|
||||
viewModel.dispatch(action: .startLoading)
|
||||
viewModel.startLoading()
|
||||
|
||||
XCTAssertFalse(viewModel.context.viewState.shareButtonEnabled)
|
||||
XCTAssertTrue(viewModel.context.viewState.showLoadingIndicator)
|
||||
|
||||
viewModel.dispatch(action: .stopLoading(nil))
|
||||
viewModel.stopLoading()
|
||||
|
||||
XCTAssertTrue(viewModel.context.viewState.shareButtonEnabled)
|
||||
XCTAssertFalse(viewModel.context.viewState.showLoadingIndicator)
|
||||
|
||||
@@ -68,7 +68,7 @@ class LocationSharingMapViewCoordinator: NSObject, MGLMapViewDelegate {
|
||||
|
||||
private let avatarData: AvatarInputProtocol
|
||||
private let errorSubject: PassthroughSubject<LocationSharingViewError, Never>
|
||||
@Binding var userLocation: CLLocationCoordinate2D?
|
||||
@Binding private var userLocation: CLLocationCoordinate2D?
|
||||
|
||||
init(avatarData: AvatarInputProtocol,
|
||||
errorSubject: PassthroughSubject<LocationSharingViewError, Never>,
|
||||
@@ -89,6 +89,10 @@ class LocationSharingMapViewCoordinator: NSObject, MGLMapViewDelegate {
|
||||
}
|
||||
|
||||
func mapView(_ mapView: MGLMapView, didFailToLocateUserWithError error: Error) {
|
||||
guard mapView.showsUserLocation else {
|
||||
return
|
||||
}
|
||||
|
||||
errorSubject.send(.failedLocatingUser)
|
||||
}
|
||||
|
||||
@@ -97,11 +101,15 @@ class LocationSharingMapViewCoordinator: NSObject, MGLMapViewDelegate {
|
||||
}
|
||||
|
||||
func mapView(_ mapView: MGLMapView, didChangeLocationManagerAuthorization manager: MGLLocationManager) {
|
||||
guard mapView.showsUserLocation else {
|
||||
return
|
||||
}
|
||||
|
||||
switch manager.authorizationStatus {
|
||||
case .restricted:
|
||||
fallthrough
|
||||
case .denied:
|
||||
errorSubject.send(.failedLocatingUser)
|
||||
errorSubject.send(.invalidLocationAuthorization)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ struct LocationSharingView: View {
|
||||
context.send(viewAction: .share)
|
||||
} label: {
|
||||
Image(uiImage: Asset.Images.locationShareIcon.image)
|
||||
.accessibilityIdentifier("LocationSharingView.shareButton")
|
||||
}
|
||||
.disabled(!context.viewState.shareButtonEnabled)
|
||||
} else {
|
||||
@@ -69,6 +70,7 @@ struct LocationSharingView: View {
|
||||
.alert(item: $context.alertInfo) { info in
|
||||
if let secondaryButton = info.secondaryButton {
|
||||
return Alert(title: Text(info.title),
|
||||
message: subtitleTextForAlertInfo(info),
|
||||
primaryButton: .default(Text(info.primaryButton.title)) {
|
||||
info.primaryButton.action?()
|
||||
},
|
||||
@@ -77,6 +79,7 @@ struct LocationSharingView: View {
|
||||
})
|
||||
} else {
|
||||
return Alert(title: Text(info.title),
|
||||
message: subtitleTextForAlertInfo(info),
|
||||
dismissButton: .default(Text(info.primaryButton.title)) {
|
||||
info.primaryButton.action?()
|
||||
})
|
||||
@@ -93,6 +96,14 @@ struct LocationSharingView: View {
|
||||
ActivityIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
private func subtitleTextForAlertInfo(_ alertInfo: LocationSharingErrorAlertInfo) -> Text? {
|
||||
guard let subtitle = alertInfo.subtitle else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return Text(subtitle)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
@@ -31,12 +31,7 @@ final class PollEditFormCoordinator: Coordinator, Presentable {
|
||||
|
||||
private let parameters: PollEditFormCoordinatorParameters
|
||||
private let pollEditFormHostingController: UIViewController
|
||||
private var _pollEditFormViewModel: Any? = nil
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
fileprivate var pollEditFormViewModel: PollEditFormViewModel {
|
||||
return _pollEditFormViewModel as! PollEditFormViewModel
|
||||
}
|
||||
private var pollEditFormViewModel: PollEditFormViewModelProtocol
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@@ -64,7 +59,7 @@ final class PollEditFormCoordinator: Coordinator, Presentable {
|
||||
|
||||
let view = PollEditForm(viewModel: viewModel.context)
|
||||
|
||||
_pollEditFormViewModel = viewModel
|
||||
pollEditFormViewModel = viewModel
|
||||
pollEditFormHostingController = VectorHostingController(rootView: view)
|
||||
}
|
||||
|
||||
@@ -84,18 +79,18 @@ final class PollEditFormCoordinator: Coordinator, Presentable {
|
||||
|
||||
let pollStartContent = self.buildPollContentWithDetails(details)
|
||||
|
||||
self.pollEditFormViewModel.dispatch(action: .startLoading)
|
||||
self.pollEditFormViewModel.startLoading()
|
||||
|
||||
self.parameters.room.sendPollStart(withContent: pollStartContent, threadId: nil, localEcho: nil) { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.pollEditFormViewModel.dispatch(action: .stopLoading(nil))
|
||||
self.pollEditFormViewModel.stopLoading()
|
||||
self.completion?()
|
||||
} failure: { [weak self] error in
|
||||
guard let self = self else { return }
|
||||
|
||||
MXLog.error("Failed creating poll with error: \(String(describing: error))")
|
||||
self.pollEditFormViewModel.dispatch(action: .stopLoading(.failedCreatingPoll))
|
||||
self.pollEditFormViewModel.stopLoading(errorAlertType: .failedCreatingPoll)
|
||||
}
|
||||
|
||||
case .update(let details):
|
||||
@@ -103,10 +98,10 @@ final class PollEditFormCoordinator: Coordinator, Presentable {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
self.pollEditFormViewModel.dispatch(action: .startLoading)
|
||||
self.pollEditFormViewModel.startLoading()
|
||||
|
||||
guard let oldPollContent = MXEventContentPollStart(fromJSON: pollStartEvent.content) else {
|
||||
self.pollEditFormViewModel.dispatch(action: .stopLoading(.failedUpdatingPoll))
|
||||
self.pollEditFormViewModel.stopLoading(errorAlertType: .failedUpdatingPoll)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -117,13 +112,13 @@ final class PollEditFormCoordinator: Coordinator, Presentable {
|
||||
newContent: newPollContent, localEcho: nil) { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.pollEditFormViewModel.dispatch(action: .stopLoading(nil))
|
||||
self.pollEditFormViewModel.stopLoading()
|
||||
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))
|
||||
self.pollEditFormViewModel.stopLoading(errorAlertType: .failedUpdatingPoll)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,12 +38,6 @@ enum PollEditFormMode {
|
||||
case editing
|
||||
}
|
||||
|
||||
enum PollEditFormStateAction {
|
||||
case viewAction(PollEditFormViewAction)
|
||||
case startLoading
|
||||
case stopLoading(PollEditFormErrorAlertInfo.AlertType?)
|
||||
}
|
||||
|
||||
enum PollEditFormViewAction {
|
||||
case addAnswerOption
|
||||
case deleteAnswerOption(PollEditFormAnswerOption)
|
||||
|
||||
@@ -23,11 +23,11 @@ struct PollEditFormViewModelParameters {
|
||||
}
|
||||
|
||||
@available(iOS 14, *)
|
||||
typealias PollEditFormViewModelType = StateStoreViewModel< PollEditFormViewState,
|
||||
PollEditFormStateAction,
|
||||
PollEditFormViewAction >
|
||||
typealias PollEditFormViewModelType = StateStoreViewModel <PollEditFormViewState,
|
||||
Never,
|
||||
PollEditFormViewAction>
|
||||
@available(iOS 14, *)
|
||||
class PollEditFormViewModel: PollEditFormViewModelType {
|
||||
class PollEditFormViewModel: PollEditFormViewModelType, PollEditFormViewModelProtocol {
|
||||
|
||||
private struct Constants {
|
||||
static let minAnswerOptionsCount = 2
|
||||
@@ -71,40 +71,32 @@ class PollEditFormViewModel: PollEditFormViewModelType {
|
||||
completion?(.create(buildPollDetails()))
|
||||
case .update:
|
||||
completion?(.update(buildPollDetails()))
|
||||
default:
|
||||
dispatch(action: .viewAction(viewAction))
|
||||
case .addAnswerOption:
|
||||
state.bindings.answerOptions.append(PollEditFormAnswerOption(text: "", maxLength: Constants.maxAnswerOptionLength))
|
||||
case .deleteAnswerOption(let answerOption):
|
||||
state.bindings.answerOptions.removeAll { $0 == answerOption }
|
||||
}
|
||||
}
|
||||
|
||||
override class func reducer(state: inout PollEditFormViewState, action: PollEditFormStateAction) {
|
||||
switch action {
|
||||
case .viewAction(let viewAction):
|
||||
switch viewAction {
|
||||
case .deleteAnswerOption(let answerOption):
|
||||
state.bindings.answerOptions.removeAll { $0 == answerOption }
|
||||
case .addAnswerOption:
|
||||
state.bindings.answerOptions.append(PollEditFormAnswerOption(text: "", maxLength: Constants.maxAnswerOptionLength))
|
||||
default:
|
||||
break
|
||||
}
|
||||
case .startLoading:
|
||||
state.showLoadingIndicator = true
|
||||
break
|
||||
case .stopLoading(let error):
|
||||
state.showLoadingIndicator = false
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// MARK: - PollEditFormViewModelProtocol
|
||||
|
||||
func startLoading() {
|
||||
state.showLoadingIndicator = true
|
||||
}
|
||||
|
||||
func stopLoading(errorAlertType: PollEditFormErrorAlertInfo.AlertType?) {
|
||||
state.showLoadingIndicator = false
|
||||
|
||||
switch errorAlertType {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -115,8 +107,8 @@ class PollEditFormViewModel: PollEditFormViewModelType {
|
||||
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
|
||||
}))
|
||||
let text = answerOption.text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return text.isEmpty ? nil : text
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
+11
-3
@@ -16,7 +16,15 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
@available(iOS 14, *)
|
||||
enum UserSuggestionViewAction {
|
||||
case selectedItem(UserSuggestionViewStateItem)
|
||||
protocol PollEditFormViewModelProtocol {
|
||||
var completion: ((PollEditFormViewModelResult) -> Void)? { get set }
|
||||
|
||||
func startLoading()
|
||||
func stopLoading(errorAlertType: PollEditFormErrorAlertInfo.AlertType?)
|
||||
}
|
||||
|
||||
extension PollEditFormViewModelProtocol {
|
||||
func stopLoading() {
|
||||
stopLoading(errorAlertType: nil)
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel
|
||||
private let selectedAnswerIdentifiersSubject = PassthroughSubject<[String], Never>()
|
||||
|
||||
private var pollAggregator: PollAggregator
|
||||
private var viewModel: TimelinePollViewModel!
|
||||
private var viewModel: TimelinePollViewModelProtocol!
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
// MARK: Public
|
||||
@@ -53,7 +53,7 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel
|
||||
pollAggregator.delegate = self
|
||||
|
||||
viewModel = TimelinePollViewModel(timelinePollDetails: buildTimelinePollFrom(pollAggregator.poll))
|
||||
viewModel.callback = { [weak self] result in
|
||||
viewModel.completion = { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
|
||||
switch result {
|
||||
@@ -76,7 +76,7 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel
|
||||
|
||||
MXLog.error("[TimelinePollCoordinator]] Failed submitting response with error \(String(describing: error))")
|
||||
|
||||
self.viewModel.dispatch(action: .showAnsweringFailure)
|
||||
self.viewModel.showAnsweringFailure()
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
@@ -102,14 +102,14 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel
|
||||
|
||||
func endPoll() {
|
||||
parameters.room.sendPollEnd(for: parameters.pollStartEvent, threadId: nil, localEcho: nil, success: nil) { [weak self] error in
|
||||
self?.viewModel.dispatch(action: .showClosingFailure)
|
||||
self?.viewModel.showClosingFailure()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - PollAggregatorDelegate
|
||||
|
||||
func pollAggregatorDidUpdateData(_ aggregator: PollAggregator) {
|
||||
viewModel.dispatch(action: .updateWithPoll(buildTimelinePollFrom(aggregator.poll)))
|
||||
viewModel.updateWithPollDetails(buildTimelinePollFrom(aggregator.poll))
|
||||
}
|
||||
|
||||
func pollAggregatorDidStartLoading(_ aggregator: PollAggregator) {
|
||||
|
||||
@@ -85,7 +85,7 @@ class TimelinePollViewModelTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testClosedSelection() {
|
||||
context.viewState.poll.closed = true
|
||||
viewModel.state.poll.closed = true
|
||||
|
||||
context.send(viewAction: .selectAnswerOptionWithIdentifier("1"))
|
||||
context.send(viewAction: .selectAnswerOptionWithIdentifier("3"))
|
||||
@@ -96,7 +96,7 @@ class TimelinePollViewModelTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testSingleSelectionOnMax2Allowed() {
|
||||
context.viewState.poll.maxAllowedSelections = 2
|
||||
viewModel.state.poll.maxAllowedSelections = 2
|
||||
|
||||
context.send(viewAction: .selectAnswerOptionWithIdentifier("1"))
|
||||
|
||||
@@ -106,7 +106,7 @@ class TimelinePollViewModelTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testSingleReselectionOnMax2Allowed() {
|
||||
context.viewState.poll.maxAllowedSelections = 2
|
||||
viewModel.state.poll.maxAllowedSelections = 2
|
||||
|
||||
context.send(viewAction: .selectAnswerOptionWithIdentifier("1"))
|
||||
context.send(viewAction: .selectAnswerOptionWithIdentifier("1"))
|
||||
@@ -117,7 +117,7 @@ class TimelinePollViewModelTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testMultipleSelectionOnMax2Allowed() {
|
||||
context.viewState.poll.maxAllowedSelections = 2
|
||||
viewModel.state.poll.maxAllowedSelections = 2
|
||||
|
||||
context.send(viewAction: .selectAnswerOptionWithIdentifier("1"))
|
||||
context.send(viewAction: .selectAnswerOptionWithIdentifier("3"))
|
||||
|
||||
@@ -19,13 +19,6 @@ import SwiftUI
|
||||
|
||||
typealias TimelinePollViewModelCallback = ((TimelinePollViewModelResult) -> Void)
|
||||
|
||||
enum TimelinePollStateAction {
|
||||
case viewAction(TimelinePollViewAction, TimelinePollViewModelCallback?)
|
||||
case updateWithPoll(TimelinePollDetails)
|
||||
case showAnsweringFailure
|
||||
case showClosingFailure
|
||||
}
|
||||
|
||||
enum TimelinePollViewAction {
|
||||
case selectAnswerOptionWithIdentifier(String)
|
||||
}
|
||||
@@ -39,7 +32,7 @@ enum TimelinePollType {
|
||||
case undisclosed
|
||||
}
|
||||
|
||||
class TimelinePollAnswerOption: Identifiable {
|
||||
struct TimelinePollAnswerOption: Identifiable {
|
||||
var id: String
|
||||
var text: String
|
||||
var count: UInt
|
||||
@@ -55,7 +48,15 @@ class TimelinePollAnswerOption: Identifiable {
|
||||
}
|
||||
}
|
||||
|
||||
class TimelinePollDetails {
|
||||
extension MutableCollection where Element == TimelinePollAnswerOption {
|
||||
mutating func updateEach(_ update: (inout Element) -> Void) {
|
||||
for index in indices {
|
||||
update(&self[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TimelinePollDetails {
|
||||
var question: String
|
||||
var answerOptions: [TimelinePollAnswerOption]
|
||||
var closed: Bool
|
||||
|
||||
@@ -19,10 +19,10 @@ import Combine
|
||||
|
||||
@available(iOS 14, *)
|
||||
typealias TimelinePollViewModelType = StateStoreViewModel<TimelinePollViewState,
|
||||
TimelinePollStateAction,
|
||||
Never,
|
||||
TimelinePollViewAction>
|
||||
@available(iOS 14, *)
|
||||
class TimelinePollViewModel: TimelinePollViewModelType {
|
||||
class TimelinePollViewModel: TimelinePollViewModelType, TimelinePollViewModelProtocol {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
@@ -30,7 +30,7 @@ class TimelinePollViewModel: TimelinePollViewModelType {
|
||||
|
||||
// MARK: Public
|
||||
|
||||
var callback: TimelinePollViewModelCallback?
|
||||
var completion: TimelinePollViewModelCallback?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
@@ -42,49 +42,47 @@ class TimelinePollViewModel: TimelinePollViewModelType {
|
||||
|
||||
override func process(viewAction: TimelinePollViewAction) {
|
||||
switch viewAction {
|
||||
case .selectAnswerOptionWithIdentifier(_):
|
||||
dispatch(action: .viewAction(viewAction, callback))
|
||||
}
|
||||
}
|
||||
|
||||
override class func reducer(state: inout TimelinePollViewState, action: TimelinePollStateAction) {
|
||||
switch action {
|
||||
case .viewAction(let viewAction, let callback):
|
||||
switch viewAction {
|
||||
|
||||
// Update local state. An update will be pushed from the coordinator once sent.
|
||||
case .selectAnswerOptionWithIdentifier(let identifier):
|
||||
guard !state.poll.closed else {
|
||||
return
|
||||
}
|
||||
|
||||
if (state.poll.maxAllowedSelections == 1) {
|
||||
updateSingleSelectPollLocalState(&state, selectedAnswerIdentifier: identifier, callback: callback)
|
||||
} else {
|
||||
updateMultiSelectPollLocalState(&state, selectedAnswerIdentifier: identifier, callback: callback)
|
||||
}
|
||||
|
||||
// Update local state. An update will be pushed from the coordinator once sent.
|
||||
case .selectAnswerOptionWithIdentifier(let identifier):
|
||||
guard !state.poll.closed else {
|
||||
return
|
||||
}
|
||||
|
||||
if (state.poll.maxAllowedSelections == 1) {
|
||||
updateSingleSelectPollLocalState(selectedAnswerIdentifier: identifier, callback: completion)
|
||||
} else {
|
||||
updateMultiSelectPollLocalState(&state, selectedAnswerIdentifier: identifier, callback: completion)
|
||||
}
|
||||
case .updateWithPoll(let poll):
|
||||
state.poll = poll
|
||||
case .showAnsweringFailure:
|
||||
state.bindings.alertInfo = TimelinePollErrorAlertInfo(id: .failedSubmittingAnswer,
|
||||
title: VectorL10n.pollTimelineVoteNotRegisteredTitle,
|
||||
subtitle: VectorL10n.pollTimelineVoteNotRegisteredSubtitle)
|
||||
case .showClosingFailure:
|
||||
state.bindings.alertInfo = TimelinePollErrorAlertInfo(id: .failedClosingPoll,
|
||||
title: VectorL10n.pollTimelineNotClosedTitle,
|
||||
subtitle: VectorL10n.pollTimelineNotClosedSubtitle)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - TimelinePollViewModelProtocol
|
||||
|
||||
func updateWithPollDetails(_ pollDetails: TimelinePollDetails) {
|
||||
state.poll = pollDetails
|
||||
}
|
||||
|
||||
func showAnsweringFailure() {
|
||||
state.bindings.alertInfo = TimelinePollErrorAlertInfo(id: .failedSubmittingAnswer,
|
||||
title: VectorL10n.pollTimelineVoteNotRegisteredTitle,
|
||||
subtitle: VectorL10n.pollTimelineVoteNotRegisteredSubtitle)
|
||||
}
|
||||
|
||||
func showClosingFailure() {
|
||||
state.bindings.alertInfo = TimelinePollErrorAlertInfo(id: .failedClosingPoll,
|
||||
title: VectorL10n.pollTimelineNotClosedTitle,
|
||||
subtitle: VectorL10n.pollTimelineNotClosedSubtitle)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
static func updateSingleSelectPollLocalState(_ state: inout TimelinePollViewState, selectedAnswerIdentifier: String, callback: TimelinePollViewModelCallback?) {
|
||||
for answerOption in state.poll.answerOptions {
|
||||
func updateSingleSelectPollLocalState(selectedAnswerIdentifier: String, callback: TimelinePollViewModelCallback?) {
|
||||
state.poll.answerOptions.updateEach { answerOption in
|
||||
if answerOption.selected {
|
||||
answerOption.selected = false
|
||||
|
||||
if(answerOption.count > 0) {
|
||||
if(state.poll.answerOptions.count > 0) {
|
||||
answerOption.count = answerOption.count - 1
|
||||
state.poll.totalAnswerCount -= 1
|
||||
}
|
||||
@@ -100,7 +98,7 @@ class TimelinePollViewModel: TimelinePollViewModelType {
|
||||
informCoordinatorOfSelectionUpdate(state: state, callback: callback)
|
||||
}
|
||||
|
||||
static func updateMultiSelectPollLocalState(_ state: inout TimelinePollViewState, selectedAnswerIdentifier: String, callback: TimelinePollViewModelCallback?) {
|
||||
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
|
||||
@@ -109,7 +107,11 @@ class TimelinePollViewModel: TimelinePollViewModelType {
|
||||
return
|
||||
}
|
||||
|
||||
for answerOption in state.poll.answerOptions where answerOption.id == selectedAnswerIdentifier {
|
||||
state.poll.answerOptions.updateEach { answerOption in
|
||||
if (answerOption.id != selectedAnswerIdentifier) {
|
||||
return
|
||||
}
|
||||
|
||||
if answerOption.selected {
|
||||
answerOption.selected = false
|
||||
answerOption.count -= 1
|
||||
@@ -124,7 +126,7 @@ class TimelinePollViewModel: TimelinePollViewModelType {
|
||||
informCoordinatorOfSelectionUpdate(state: state, callback: callback)
|
||||
}
|
||||
|
||||
static func informCoordinatorOfSelectionUpdate(state: TimelinePollViewState, callback: TimelinePollViewModelCallback?) {
|
||||
func informCoordinatorOfSelectionUpdate(state: TimelinePollViewState, callback: TimelinePollViewModelCallback?) {
|
||||
let selectedIdentifiers = state.poll.answerOptions.compactMap { answerOption in
|
||||
answerOption.selected ? answerOption.id : nil
|
||||
}
|
||||
|
||||
+8
-3
@@ -16,7 +16,12 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct UserSuggestionCoordinatorParameters {
|
||||
let mediaManager: MXMediaManager
|
||||
let room: MXRoom
|
||||
protocol TimelinePollViewModelProtocol {
|
||||
@available(iOS 14, *)
|
||||
var context: TimelinePollViewModelType.Context { get }
|
||||
var completion: ((TimelinePollViewModelResult) -> Void)? { get set }
|
||||
|
||||
func updateWithPollDetails(_ pollDetails: TimelinePollDetails)
|
||||
func showAnsweringFailure()
|
||||
func showClosingFailure()
|
||||
}
|
||||
@@ -23,6 +23,11 @@ protocol UserSuggestionCoordinatorDelegate: AnyObject {
|
||||
func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didRequestMentionForMember member: MXRoomMember, textTrigger: String?)
|
||||
}
|
||||
|
||||
struct UserSuggestionCoordinatorParameters {
|
||||
let mediaManager: MXMediaManager
|
||||
let room: MXRoom
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
final class UserSuggestionCoordinator: Coordinator, Presentable {
|
||||
|
||||
@@ -53,11 +58,12 @@ final class UserSuggestionCoordinator: Coordinator, Presentable {
|
||||
|
||||
roomMemberProvider = UserSuggestionCoordinatorRoomMemberProvider(room: parameters.room)
|
||||
userSuggestionService = UserSuggestionService(roomMemberProvider: roomMemberProvider)
|
||||
userSuggestionViewModel = UserSuggestionViewModel.makeUserSuggestionViewModel(userSuggestionService: userSuggestionService)
|
||||
|
||||
let view = UserSuggestionList(viewModel: userSuggestionViewModel.context)
|
||||
let viewModel = UserSuggestionViewModel(userSuggestionService: userSuggestionService)
|
||||
let view = UserSuggestionList(viewModel: viewModel.context)
|
||||
.addDependency(AvatarService.instantiate(mediaManager: parameters.mediaManager))
|
||||
|
||||
userSuggestionViewModel = viewModel
|
||||
userSuggestionHostingController = VectorHostingController(rootView: view)
|
||||
|
||||
userSuggestionViewModel.completion = { [weak self] result in
|
||||
@@ -90,7 +96,6 @@ final class UserSuggestionCoordinator: Coordinator, Presentable {
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
private class UserSuggestionCoordinatorRoomMemberProvider: RoomMembersProviderProtocol {
|
||||
|
||||
private let room: MXRoom
|
||||
|
||||
@@ -1,22 +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 Foundation
|
||||
|
||||
@available(iOS 14, *)
|
||||
enum UserSuggestionViewModelResult {
|
||||
case selectedItemWithIdentifier(String)
|
||||
}
|
||||
@@ -17,19 +17,16 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct RoomMembersProviderMember {
|
||||
var userId: String
|
||||
var displayName: String
|
||||
var avatarUrl: String
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
protocol RoomMembersProviderProtocol {
|
||||
func fetchMembers(_ members: @escaping ([RoomMembersProviderMember]) -> Void)
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct UserSuggestionServiceItem: UserSuggestionItemProtocol {
|
||||
let userId: String
|
||||
let displayName: String?
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
protocol UserSuggestionItemProtocol: Avatarable {
|
||||
var userId: String { get }
|
||||
var displayName: String? { get }
|
||||
@@ -36,7 +35,6 @@ protocol UserSuggestionServiceProtocol {
|
||||
|
||||
// MARK: Avatarable
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
extension UserSuggestionItemProtocol {
|
||||
var mxContentUri: String? {
|
||||
avatarUrl
|
||||
|
||||
+8
-3
@@ -15,16 +15,21 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
enum UserSuggestionViewAction {
|
||||
case selectedItem(UserSuggestionViewStateItem)
|
||||
}
|
||||
|
||||
enum UserSuggestionViewModelResult {
|
||||
case selectedItemWithIdentifier(String)
|
||||
}
|
||||
|
||||
struct UserSuggestionViewStateItem: Identifiable {
|
||||
let id: String
|
||||
let avatar: AvatarInputProtocol?
|
||||
let displayName: String?
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct UserSuggestionViewState: BindableState {
|
||||
var items: [UserSuggestionViewStateItem]
|
||||
}
|
||||
+1
-1
@@ -29,7 +29,7 @@ enum MockUserSuggestionScreenState: MockScreenState, CaseIterable {
|
||||
|
||||
var screenView: ([Any], AnyView) {
|
||||
let service = UserSuggestionService(roomMemberProvider: self)
|
||||
let listViewModel = UserSuggestionViewModel.makeUserSuggestionViewModel(userSuggestionService: service)
|
||||
let listViewModel = UserSuggestionViewModel(userSuggestionService: service)
|
||||
|
||||
let viewModel = UserSuggestionListWithInputViewModel(listViewModel: listViewModel) { textMessage in
|
||||
service.processTextMessage(textMessage)
|
||||
+14
-31
@@ -17,11 +17,12 @@
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
@available(iOS 14, *)
|
||||
@available(iOS 14.0, *)
|
||||
typealias UserSuggestionViewModelType = StateStoreViewModel <UserSuggestionViewState,
|
||||
UserSuggestionStateAction,
|
||||
Never,
|
||||
UserSuggestionViewAction>
|
||||
@available(iOS 14, *)
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
class UserSuggestionViewModel: UserSuggestionViewModelType, UserSuggestionViewModelProtocol {
|
||||
|
||||
// MARK: - Properties
|
||||
@@ -35,30 +36,21 @@ class UserSuggestionViewModel: UserSuggestionViewModelType, UserSuggestionViewMo
|
||||
var completion: ((UserSuggestionViewModelResult) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
static func makeUserSuggestionViewModel(userSuggestionService: UserSuggestionServiceProtocol) -> UserSuggestionViewModelProtocol {
|
||||
return UserSuggestionViewModel(userSuggestionService: userSuggestionService)
|
||||
}
|
||||
|
||||
private init(userSuggestionService: UserSuggestionServiceProtocol) {
|
||||
init(userSuggestionService: UserSuggestionServiceProtocol) {
|
||||
self.userSuggestionService = userSuggestionService
|
||||
super.init(initialViewState: Self.defaultState(userSuggestionService: userSuggestionService))
|
||||
setupItemsObserving()
|
||||
}
|
||||
|
||||
private func setupItemsObserving() {
|
||||
let updatePublisher = userSuggestionService.items
|
||||
.map(UserSuggestionStateAction.updateWithItems)
|
||||
.eraseToAnyPublisher()
|
||||
dispatch(actionPublisher: updatePublisher)
|
||||
}
|
||||
|
||||
private static func defaultState(userSuggestionService: UserSuggestionServiceProtocol) -> UserSuggestionViewState {
|
||||
let viewStateItems = userSuggestionService.items.value.map { suggestionItem in
|
||||
|
||||
let items = userSuggestionService.items.value.map { suggestionItem in
|
||||
return UserSuggestionViewStateItem(id: suggestionItem.userId, avatar: suggestionItem, displayName: suggestionItem.displayName)
|
||||
}
|
||||
|
||||
return UserSuggestionViewState(items: viewStateItems)
|
||||
super.init(initialViewState: UserSuggestionViewState(items: items))
|
||||
|
||||
userSuggestionService.items.sink { items in
|
||||
self.state.items = items.map({ item in
|
||||
UserSuggestionViewStateItem(id: item.userId, avatar: item, displayName: item.displayName)
|
||||
})
|
||||
}.store(in: &cancellables)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
@@ -69,13 +61,4 @@ class UserSuggestionViewModel: UserSuggestionViewModelType, UserSuggestionViewMo
|
||||
completion?(.selectedItemWithIdentifier(item.id))
|
||||
}
|
||||
}
|
||||
|
||||
override class func reducer(state: inout UserSuggestionViewState, action: UserSuggestionStateAction) {
|
||||
switch action {
|
||||
case .updateWithItems(let items):
|
||||
state.items = items.map({ item in
|
||||
UserSuggestionViewStateItem(id: item.userId, avatar: item, displayName: item.displayName)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
-6
@@ -16,12 +16,6 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
@available(iOS 14, *)
|
||||
protocol UserSuggestionViewModelProtocol {
|
||||
|
||||
static func makeUserSuggestionViewModel(userSuggestionService: UserSuggestionServiceProtocol) -> UserSuggestionViewModelProtocol
|
||||
|
||||
var context: UserSuggestionViewModelType.Context { get }
|
||||
|
||||
var completion: ((UserSuggestionViewModelResult) -> Void)? { get set }
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import SwiftUI
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct UserSuggestionListWithInputViewModel {
|
||||
let listViewModel: UserSuggestionViewModelProtocol
|
||||
let listViewModel: UserSuggestionViewModel
|
||||
let callback: (String)->()
|
||||
}
|
||||
|
||||
|
||||
@@ -47,10 +47,6 @@ extension TemplateSimpleScreenPromptType: Identifiable, CaseIterable {
|
||||
|
||||
// MARK: View model
|
||||
|
||||
enum TemplateSimpleScreenStateAction {
|
||||
case viewAction(TemplateSimpleScreenViewAction)
|
||||
}
|
||||
|
||||
enum TemplateSimpleScreenViewModelResult {
|
||||
case accept
|
||||
case cancel
|
||||
|
||||
+5
-18
@@ -18,7 +18,7 @@ import SwiftUI
|
||||
|
||||
@available(iOS 14, *)
|
||||
typealias TemplateSimpleScreenViewModelType = StateStoreViewModel<TemplateSimpleScreenViewState,
|
||||
TemplateSimpleScreenStateAction,
|
||||
Never,
|
||||
TemplateSimpleScreenViewAction>
|
||||
@available(iOS 14, *)
|
||||
class TemplateSimpleScreenViewModel: TemplateSimpleScreenViewModelType, TemplateSimpleScreenViewModelProtocol {
|
||||
@@ -45,23 +45,10 @@ class TemplateSimpleScreenViewModel: TemplateSimpleScreenViewModelType, Template
|
||||
completion?(.accept)
|
||||
case .cancel:
|
||||
completion?(.cancel)
|
||||
case .incrementCount, .decrementCount:
|
||||
dispatch(action: .viewAction(viewAction))
|
||||
case .incrementCount:
|
||||
state.count += 1
|
||||
case .decrementCount:
|
||||
state.count -= 1
|
||||
}
|
||||
}
|
||||
|
||||
override class func reducer(state: inout TemplateSimpleScreenViewState, action: TemplateSimpleScreenStateAction) {
|
||||
switch action {
|
||||
case .viewAction(let viewAction):
|
||||
switch viewAction {
|
||||
case .incrementCount:
|
||||
state.count += 1
|
||||
case .decrementCount:
|
||||
state.count -= 1
|
||||
case .accept, .cancel:
|
||||
break
|
||||
}
|
||||
}
|
||||
UILog.debug("[TemplateSimpleScreenViewModel] reducer with action \(action) produced state: \(state)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,11 +41,6 @@ extension TemplateUserProfilePresence: Identifiable, CaseIterable {
|
||||
|
||||
// MARK: View model
|
||||
|
||||
enum TemplateUserProfileStateAction {
|
||||
case viewAction(TemplateUserProfileViewAction)
|
||||
case updatePresence(TemplateUserProfilePresence)
|
||||
}
|
||||
|
||||
enum TemplateUserProfileViewModelResult {
|
||||
case cancel
|
||||
case done
|
||||
|
||||
+15
-36
@@ -19,7 +19,7 @@ import Combine
|
||||
|
||||
@available(iOS 14, *)
|
||||
typealias TemplateUserProfileViewModelType = StateStoreViewModel<TemplateUserProfileViewState,
|
||||
TemplateUserProfileStateAction,
|
||||
Never,
|
||||
TemplateUserProfileViewAction>
|
||||
@available(iOS 14, *)
|
||||
class TemplateUserProfileViewModel: TemplateUserProfileViewModelType, TemplateUserProfileViewModelProtocol {
|
||||
@@ -54,49 +54,28 @@ class TemplateUserProfileViewModel: TemplateUserProfileViewModelType, TemplateUs
|
||||
count: 0
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private func setupPresenceObserving() {
|
||||
let presenceUpdatePublisher = templateUserProfileService.presenceSubject
|
||||
.map(TemplateUserProfileStateAction.updatePresence)
|
||||
.eraseToAnyPublisher()
|
||||
dispatch(actionPublisher: presenceUpdatePublisher)
|
||||
templateUserProfileService
|
||||
.presenceSubject
|
||||
.sink(receiveValue: { [weak self] presence in
|
||||
self?.state.presence = presence
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: TemplateUserProfileViewAction) {
|
||||
switch viewAction {
|
||||
case .cancel:
|
||||
cancel()
|
||||
completion?(.cancel)
|
||||
case .done:
|
||||
done()
|
||||
case .incrementCount, .decrementCount:
|
||||
dispatch(action: .viewAction(viewAction))
|
||||
completion?(.done)
|
||||
case .incrementCount:
|
||||
state.count += 1
|
||||
case .decrementCount:
|
||||
state.count -= 1
|
||||
}
|
||||
}
|
||||
|
||||
override class func reducer(state: inout TemplateUserProfileViewState, action: TemplateUserProfileStateAction) {
|
||||
switch action {
|
||||
case .updatePresence(let presence):
|
||||
state.presence = presence
|
||||
case .viewAction(let viewAction):
|
||||
switch viewAction {
|
||||
case .incrementCount:
|
||||
state.count += 1
|
||||
case .decrementCount:
|
||||
state.count -= 1
|
||||
case .cancel, .done:
|
||||
break
|
||||
}
|
||||
}
|
||||
UILog.debug("[TemplateUserProfileViewModel] reducer with action \(action) produced state: \(state)")
|
||||
}
|
||||
|
||||
private func done() {
|
||||
completion?(.done)
|
||||
}
|
||||
|
||||
private func cancel() {
|
||||
completion?(.cancel)
|
||||
}
|
||||
}
|
||||
|
||||
-7
@@ -83,13 +83,6 @@ enum TemplateRoomChatRoomInitializationStatus {
|
||||
case failedToInitialize
|
||||
}
|
||||
|
||||
/// Actions to be performed on the `ViewModel` State
|
||||
enum TemplateRoomChatStateAction {
|
||||
case updateRoomInitializationStatus(TemplateRoomChatRoomInitializationStatus)
|
||||
case updateBubbles([TemplateRoomChatBubble])
|
||||
case clearMessageInput
|
||||
}
|
||||
|
||||
/// Actions sent by the `ViewModel` to the `Coordinator`
|
||||
enum TemplateRoomChatViewModelAction {
|
||||
case done
|
||||
|
||||
+13
-29
@@ -19,7 +19,7 @@ import Combine
|
||||
|
||||
@available(iOS 14, *)
|
||||
typealias TemplateRoomChatViewModelType = StateStoreViewModel<TemplateRoomChatViewState,
|
||||
TemplateRoomChatStateAction,
|
||||
Never,
|
||||
TemplateRoomChatViewAction>
|
||||
|
||||
@available(iOS 14, *)
|
||||
@@ -48,21 +48,22 @@ class TemplateRoomChatViewModel: TemplateRoomChatViewModelType, TemplateRoomChat
|
||||
}
|
||||
|
||||
private func setupRoomInitializationObserving() {
|
||||
let initializationPublisher = templateRoomChatService
|
||||
templateRoomChatService
|
||||
.roomInitializationStatus
|
||||
.map(TemplateRoomChatStateAction.updateRoomInitializationStatus)
|
||||
.eraseToAnyPublisher()
|
||||
|
||||
dispatch(actionPublisher: initializationPublisher)
|
||||
.sink { [weak self] status in
|
||||
self?.state.roomInitializationStatus = status
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
|
||||
private func setupMessageObserving() {
|
||||
let messageActionPublisher = templateRoomChatService
|
||||
templateRoomChatService
|
||||
.chatMessagesSubject
|
||||
.map(Self.makeBubbles(messages:))
|
||||
.map(TemplateRoomChatStateAction.updateBubbles)
|
||||
.eraseToAnyPublisher()
|
||||
dispatch(actionPublisher: messageActionPublisher)
|
||||
.sink { [weak self] bubbles in
|
||||
self?.state.bubbles = bubbles
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private static func defaultState(templateRoomChatService: TemplateRoomChatServiceProtocol) -> TemplateRoomChatViewState {
|
||||
@@ -117,27 +118,10 @@ class TemplateRoomChatViewModel: TemplateRoomChatViewModelType, TemplateRoomChat
|
||||
override func process(viewAction: TemplateRoomChatViewAction) {
|
||||
switch viewAction {
|
||||
case .done:
|
||||
done()
|
||||
callback?(.done)
|
||||
case .sendMessage:
|
||||
templateRoomChatService.send(textMessage: state.bindings.messageInput)
|
||||
dispatch(action: .clearMessageInput)
|
||||
}
|
||||
}
|
||||
|
||||
override class func reducer(state: inout TemplateRoomChatViewState, action: TemplateRoomChatStateAction) {
|
||||
switch action {
|
||||
case .updateRoomInitializationStatus(let status):
|
||||
state.roomInitializationStatus = status
|
||||
case .clearMessageInput:
|
||||
state.bindings.messageInput = ""
|
||||
case .updateBubbles(let bubbles):
|
||||
state.bubbles = bubbles
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func done() {
|
||||
callback?(.done)
|
||||
}
|
||||
}
|
||||
|
||||
-5
@@ -32,11 +32,6 @@ enum TemplateRoomListCoordinatorAction {
|
||||
|
||||
// MARK: - View model
|
||||
|
||||
/// Actions to be performed on the `ViewModel` State
|
||||
enum TemplateRoomListStateAction {
|
||||
case updateRooms([TemplateRoomListRoom])
|
||||
}
|
||||
|
||||
/// Actions sent by the`ViewModel` to the `Coordinator`.
|
||||
enum TemplateRoomListViewModelAction {
|
||||
case didSelectRoom(String)
|
||||
|
||||
+7
-12
@@ -19,7 +19,7 @@ import Combine
|
||||
|
||||
@available(iOS 14, *)
|
||||
typealias TemplateRoomListViewModelType = StateStoreViewModel<TemplateRoomListViewState,
|
||||
TemplateRoomListStateAction,
|
||||
Never,
|
||||
TemplateRoomListViewAction>
|
||||
@available(iOS 14.0, *)
|
||||
class TemplateRoomListViewModel: TemplateRoomListViewModelType, TemplateRoomListViewModelProtocol {
|
||||
@@ -47,10 +47,12 @@ class TemplateRoomListViewModel: TemplateRoomListViewModelType, TemplateRoomList
|
||||
}
|
||||
|
||||
private func startObservingRooms() {
|
||||
let roomsUpdatePublisher = templateRoomListService.roomsSubject
|
||||
.map(TemplateRoomListStateAction.updateRooms)
|
||||
.eraseToAnyPublisher()
|
||||
dispatch(actionPublisher: roomsUpdatePublisher)
|
||||
templateRoomListService
|
||||
.roomsSubject
|
||||
.sink { [weak self] rooms in
|
||||
self?.state.rooms = rooms
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
@@ -64,13 +66,6 @@ class TemplateRoomListViewModel: TemplateRoomListViewModelType, TemplateRoomList
|
||||
}
|
||||
}
|
||||
|
||||
override class func reducer(state: inout TemplateRoomListViewState, action: TemplateRoomListStateAction) {
|
||||
switch action {
|
||||
case .updateRooms(let rooms):
|
||||
state.rooms = rooms
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func done() {
|
||||
|
||||
Reference in New Issue
Block a user