Use the Emoji/Map/Poll view models. (#4458)

* Toggle emojis in the EmojiPickerScreenViewModel.

* Send locations in the StaticLocationScreen.

* Send polls in the PollFormScreen.
This commit is contained in:
Doug
2025-09-03 16:52:28 +01:00
committed by GitHub
parent 441f5ae975
commit eda37eb626
21 changed files with 398 additions and 255 deletions
@@ -12,12 +12,13 @@ struct PollFormScreenCoordinatorParameters {
let mode: PollFormMode
/// The max number of allowed options, if no value provided the default value of the view model will be used.
var maxNumberOfOptions: Int?
let timelineController: TimelineControllerProtocol
let analytics: AnalyticsService
let userIndicatorController: UserIndicatorControllerProtocol
}
enum PollFormScreenCoordinatorAction {
case cancel
case delete
case submit(question: String, options: [String], pollKind: Poll.Kind)
case close
}
final class PollFormScreenCoordinator: CoordinatorProtocol {
@@ -30,7 +31,11 @@ final class PollFormScreenCoordinator: CoordinatorProtocol {
}
init(parameters: PollFormScreenCoordinatorParameters) {
viewModel = PollFormScreenViewModel(mode: parameters.mode, maxNumberOfOptions: parameters.maxNumberOfOptions)
viewModel = PollFormScreenViewModel(mode: parameters.mode,
maxNumberOfOptions: parameters.maxNumberOfOptions,
timelineController: parameters.timelineController,
analytics: parameters.analytics,
userIndicatorController: parameters.userIndicatorController)
}
func start() {
@@ -39,12 +44,8 @@ final class PollFormScreenCoordinator: CoordinatorProtocol {
guard let self else { return }
switch action {
case .cancel:
self.actionsSubject.send(.cancel)
case .delete:
self.actionsSubject.send(.delete)
case let .submit(question, options, pollKind):
self.actionsSubject.send(.submit(question: question, options: options, pollKind: pollKind))
case .close:
self.actionsSubject.send(.close)
}
}
.store(in: &cancellables)
@@ -8,9 +8,7 @@
import Foundation
enum PollFormScreenViewModelAction: Equatable {
case cancel
case delete
case submit(question: String, options: [String], pollKind: Poll.Kind)
case close
}
struct PollFormScreenViewState: BindableState {
@@ -11,13 +11,24 @@ import SwiftUI
typealias PollFormScreenViewModelType = StateStoreViewModelV2<PollFormScreenViewState, PollFormScreenViewAction>
class PollFormScreenViewModel: PollFormScreenViewModelType, PollFormScreenViewModelProtocol {
private var actionsSubject: PassthroughSubject<PollFormScreenViewModelAction, Never> = .init()
private let timelineController: TimelineControllerProtocol
private let analytics: AnalyticsService
private let userIndicatorController: UserIndicatorControllerProtocol
private var actionsSubject: PassthroughSubject<PollFormScreenViewModelAction, Never> = .init()
var actions: AnyPublisher<PollFormScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(mode: PollFormMode, maxNumberOfOptions: Int? = nil) {
init(mode: PollFormMode,
maxNumberOfOptions: Int? = nil,
timelineController: TimelineControllerProtocol,
analytics: AnalyticsService,
userIndicatorController: UserIndicatorControllerProtocol) {
self.timelineController = timelineController
self.analytics = analytics
self.userIndicatorController = userIndicatorController
super.init(initialViewState: .init(mode: mode, maxNumberOfOptions: maxNumberOfOptions ?? 20))
}
@@ -26,24 +37,33 @@ class PollFormScreenViewModel: PollFormScreenViewModelType, PollFormScreenViewMo
override func process(viewAction: PollFormScreenViewAction) {
switch viewAction {
case .submit:
actionsSubject.send(.submit(question: state.bindings.question,
options: state.bindings.options.map(\.text),
pollKind: state.bindings.isUndisclosed ? .undisclosed : .disclosed))
let question = state.bindings.question
let options = state.bindings.options.map(\.text)
let pollKind = state.bindings.isUndisclosed ? Poll.Kind.undisclosed : .disclosed
Task {
switch state.mode {
case .new:
await createPoll(question: question, options: options, pollKind: pollKind)
case .edit(let eventID, _):
await editPoll(pollStartID: eventID, question: question, options: options, pollKind: pollKind)
}
}
case .delete:
state.bindings.alertInfo = .init(id: .init(),
title: L10n.screenEditPollDeleteConfirmationTitle,
message: L10n.screenEditPollDeleteConfirmation,
primaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil),
secondaryButton: .init(title: L10n.actionOk) { self.actionsSubject.send(.delete) })
secondaryButton: .init(title: L10n.actionOk) { Task { await self.deletePoll() } })
case .cancel:
if state.formContentHasChanged {
state.bindings.alertInfo = .init(id: .init(),
title: L10n.screenCreatePollCancelConfirmationTitleIos,
message: L10n.screenCreatePollCancelConfirmationContentIos,
primaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil),
secondaryButton: .init(title: L10n.actionOk) { self.actionsSubject.send(.cancel) })
secondaryButton: .init(title: L10n.actionOk) { self.actionsSubject.send(.close) })
} else {
actionsSubject.send(.cancel)
actionsSubject.send(.close)
}
case .deleteOption(let index):
// fixes a crash that caused an index out of range when an option with the keyboard focus was deleted
@@ -61,4 +81,45 @@ class PollFormScreenViewModel: PollFormScreenViewModelType, PollFormScreenViewMo
state.bindings.options.append(.init())
}
}
// MARK: - Private
private func createPoll(question: String, options: [String], pollKind: Poll.Kind) async {
guard case .success = await timelineController.createPoll(question: question, answers: options, pollKind: pollKind) else {
userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown))
return
}
actionsSubject.send(.close)
analytics.trackComposer(inThread: false,
isEditing: false,
isReply: false,
messageType: .Poll,
startsThread: nil)
analytics.trackPollCreated(isUndisclosed: pollKind == .undisclosed, numberOfAnswers: options.count)
}
private func editPoll(pollStartID: String, question: String, options: [String], pollKind: Poll.Kind) async {
switch await timelineController.editPoll(original: pollStartID, question: question, answers: options, pollKind: pollKind) {
case .success:
actionsSubject.send(.close)
case .failure:
userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown))
}
}
private func deletePoll() async {
// There aren't any local echoes for redactions, so dismiss the screen early
// until we have them: https://github.com/matrix-org/matrix-rust-sdk/issues/4162
actionsSubject.send(.close)
guard case .edit(let pollStartID, _) = state.mode else {
userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown))
return
}
await timelineController.redact(.eventID(pollStartID))
}
}
@@ -197,8 +197,8 @@ private struct PollFormOptionRow: View {
// MARK: - Previews
struct PollFormScreen_Previews: PreviewProvider, TestablePreview {
static let viewModel = PollFormScreenViewModel(mode: .new)
static let editViewModel = PollFormScreenViewModel(mode: .edit(eventID: "1234", poll: poll))
static let viewModel = makeViewModel(mode: .new)
static let editViewModel = makeViewModel(mode: .edit(eventID: "1234", poll: poll))
static let poll = Poll(question: "Cats or Dogs?",
kind: .disclosed,
maxSelections: 1,
@@ -222,6 +222,13 @@ struct PollFormScreen_Previews: PreviewProvider, TestablePreview {
}
.previewDisplayName("Edit")
}
static func makeViewModel(mode: PollFormMode) -> PollFormScreenViewModel {
PollFormScreenViewModel(mode: mode,
timelineController: MockTimelineController(),
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock())
}
}
private extension Binding where Value == String {