mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-21 00:52:43 +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
+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)->()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user