mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
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:
@@ -82,7 +82,7 @@ class PinnedEventsTimelineFlowCoordinator: FlowCoordinatorProtocol {
|
||||
case .displayUser(let userID):
|
||||
actionsSubject.send(.displayUser(userID: userID))
|
||||
case .presentLocationViewer(let geoURI, let description):
|
||||
presentMapNavigator(geoURI: geoURI, description: description)
|
||||
presentMapNavigator(geoURI: geoURI, description: description, timelineController: timelineController)
|
||||
case .displayMessageForwarding(let forwardingItem):
|
||||
presentMessageForwarding(with: forwardingItem)
|
||||
case .displayRoomScreenWithFocussedPin(let eventID):
|
||||
@@ -94,20 +94,20 @@ class PinnedEventsTimelineFlowCoordinator: FlowCoordinatorProtocol {
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
}
|
||||
|
||||
private func presentMapNavigator(geoURI: GeoURI, description: String?) {
|
||||
private func presentMapNavigator(geoURI: GeoURI, description: String?, timelineController: TimelineControllerProtocol) {
|
||||
let stackCoordinator = NavigationStackCoordinator()
|
||||
|
||||
let params = StaticLocationScreenCoordinatorParameters(interactionMode: .viewOnly(geoURI: geoURI, description: description),
|
||||
mapURLBuilder: flowParameters.appSettings.mapTilerConfiguration,
|
||||
appMediator: flowParameters.appMediator)
|
||||
timelineController: timelineController,
|
||||
appMediator: flowParameters.appMediator,
|
||||
analytics: flowParameters.analytics,
|
||||
userIndicatorController: flowParameters.userIndicatorController)
|
||||
let coordinator = StaticLocationScreenCoordinator(parameters: params)
|
||||
|
||||
coordinator.actions.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .selectedLocation:
|
||||
// We don't handle the sending/picker case in this flow
|
||||
break
|
||||
case .close:
|
||||
self.navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
}
|
||||
|
||||
@@ -1005,24 +1005,16 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
selectedEmoji: Set<String>,
|
||||
timelineController: TimelineControllerProtocol,
|
||||
animated: Bool) {
|
||||
let params = EmojiPickerScreenCoordinatorParameters(emojiProvider: flowParameters.emojiProvider,
|
||||
itemID: itemID, selectedEmojis: selectedEmoji)
|
||||
let params = EmojiPickerScreenCoordinatorParameters(itemID: itemID,
|
||||
selectedEmojis: selectedEmoji,
|
||||
emojiProvider: flowParameters.emojiProvider,
|
||||
timelineController: timelineController)
|
||||
let coordinator = EmojiPickerScreenCoordinator(parameters: params)
|
||||
|
||||
coordinator.actions.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case let .emojiSelected(emoji: emoji, itemID: itemID):
|
||||
MXLog.debug("Selected \(emoji) for \(itemID)")
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
Task {
|
||||
guard case let .event(_, eventOrTransactionID) = itemID else {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
await self.timelineController?.toggleReaction(emoji, to: eventOrTransactionID)
|
||||
}
|
||||
case .dismiss:
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
}
|
||||
@@ -1041,27 +1033,15 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
let params = StaticLocationScreenCoordinatorParameters(interactionMode: interactionMode,
|
||||
mapURLBuilder: flowParameters.appSettings.mapTilerConfiguration,
|
||||
appMediator: flowParameters.appMediator)
|
||||
timelineController: timelineController,
|
||||
appMediator: flowParameters.appMediator,
|
||||
analytics: flowParameters.analytics,
|
||||
userIndicatorController: flowParameters.userIndicatorController)
|
||||
let coordinator = StaticLocationScreenCoordinator(parameters: params)
|
||||
|
||||
coordinator.actions.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .selectedLocation(let geoURI, let isUserLocation):
|
||||
Task {
|
||||
_ = await timelineController.sendLocation(body: geoURI.bodyMessage,
|
||||
geoURI: geoURI,
|
||||
description: nil,
|
||||
zoomLevel: 15,
|
||||
assetType: isUserLocation ? .sender : .pin)
|
||||
self.navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
}
|
||||
|
||||
self.flowParameters.analytics.trackComposer(inThread: false,
|
||||
isEditing: false,
|
||||
isReply: false,
|
||||
messageType: isUserLocation ? .LocationUser : .LocationPin,
|
||||
startsThread: nil)
|
||||
case .close:
|
||||
self.navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
}
|
||||
@@ -1077,36 +1057,19 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
private func presentPollForm(mode: PollFormMode, timelineController: TimelineControllerProtocol) {
|
||||
let stackCoordinator = NavigationStackCoordinator()
|
||||
let coordinator = PollFormScreenCoordinator(parameters: .init(mode: mode))
|
||||
let coordinator = PollFormScreenCoordinator(parameters: .init(mode: mode,
|
||||
timelineController: timelineController,
|
||||
analytics: flowParameters.analytics,
|
||||
userIndicatorController: flowParameters.userIndicatorController))
|
||||
stackCoordinator.setRootCoordinator(coordinator)
|
||||
|
||||
coordinator.actions
|
||||
.sink { [weak self] action in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
self.navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .cancel:
|
||||
break
|
||||
case .delete:
|
||||
deletePoll(mode: mode)
|
||||
case let .submit(question, options, pollKind):
|
||||
switch mode {
|
||||
case .new:
|
||||
createPoll(question: question,
|
||||
options: options,
|
||||
pollKind: pollKind,
|
||||
timelineController: timelineController)
|
||||
case .edit(let eventID, _):
|
||||
editPoll(pollStartID: eventID,
|
||||
question: question,
|
||||
options: options,
|
||||
pollKind: pollKind,
|
||||
timelineController: timelineController)
|
||||
}
|
||||
case .close:
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
@@ -1116,58 +1079,6 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
private func createPoll(question: String, options: [String], pollKind: Poll.Kind, timelineController: TimelineControllerProtocol) {
|
||||
Task {
|
||||
let result = await timelineController.createPoll(question: question, answers: options, pollKind: pollKind)
|
||||
|
||||
self.flowParameters.analytics.trackComposer(inThread: false,
|
||||
isEditing: false,
|
||||
isReply: false,
|
||||
messageType: .Poll,
|
||||
startsThread: nil)
|
||||
|
||||
self.flowParameters.analytics.trackPollCreated(isUndisclosed: pollKind == .undisclosed, numberOfAnswers: options.count)
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure:
|
||||
self.flowParameters.userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func editPoll(pollStartID: String, question: String, options: [String], pollKind: Poll.Kind, timelineController: TimelineControllerProtocol) {
|
||||
Task {
|
||||
let result = await timelineController.editPoll(original: pollStartID, question: question, answers: options, pollKind: pollKind)
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure:
|
||||
self.flowParameters.userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func deletePoll(mode: PollFormMode) {
|
||||
Task {
|
||||
guard case .edit(let pollStartID, _) = mode else {
|
||||
self.flowParameters.userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown))
|
||||
return
|
||||
}
|
||||
|
||||
let result = await roomProxy.redact(pollStartID)
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure:
|
||||
self.flowParameters.userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func presentRoomPollsHistory(animated: Bool) async {
|
||||
let userID = userSession.clientProxy.userID
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ extension TimelineProxyMock {
|
||||
paginateBackwardsRequestSizeReturnValue = .success(())
|
||||
paginateForwardsRequestSizeReturnValue = .success(())
|
||||
sendReadReceiptForTypeReturnValue = .success(())
|
||||
createPollQuestionAnswersPollKindReturnValue = .success(())
|
||||
editPollOriginalQuestionAnswersPollKindReturnValue = .success(())
|
||||
|
||||
if configuration.isAutoUpdating {
|
||||
underlyingTimelineItemProvider = AutoUpdatingTimelineItemProviderMock()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -9,13 +9,13 @@ import Combine
|
||||
import SwiftUI
|
||||
|
||||
struct EmojiPickerScreenCoordinatorParameters {
|
||||
let emojiProvider: EmojiProviderProtocol
|
||||
let itemID: TimelineItemIdentifier
|
||||
let selectedEmojis: Set<String>
|
||||
let emojiProvider: EmojiProviderProtocol
|
||||
let timelineController: TimelineControllerProtocol
|
||||
}
|
||||
|
||||
enum EmojiPickerScreenCoordinatorAction {
|
||||
case emojiSelected(emoji: String, itemID: TimelineItemIdentifier)
|
||||
case dismiss
|
||||
}
|
||||
|
||||
@@ -33,7 +33,10 @@ final class EmojiPickerScreenCoordinator: CoordinatorProtocol {
|
||||
init(parameters: EmojiPickerScreenCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
viewModel = EmojiPickerScreenViewModel(emojiProvider: parameters.emojiProvider)
|
||||
viewModel = EmojiPickerScreenViewModel(itemID: parameters.itemID,
|
||||
selectedEmojis: parameters.selectedEmojis,
|
||||
emojiProvider: parameters.emojiProvider,
|
||||
timelineController: parameters.timelineController)
|
||||
}
|
||||
|
||||
func start() {
|
||||
@@ -42,8 +45,6 @@ final class EmojiPickerScreenCoordinator: CoordinatorProtocol {
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case let .emojiSelected(emoji: emoji):
|
||||
actionsSubject.send(.emojiSelected(emoji: emoji, itemID: self.parameters.itemID))
|
||||
case .dismiss:
|
||||
actionsSubject.send(.dismiss)
|
||||
}
|
||||
@@ -52,6 +53,6 @@ final class EmojiPickerScreenCoordinator: CoordinatorProtocol {
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(EmojiPickerScreen(context: viewModel.context, selectedEmojis: parameters.selectedEmojis))
|
||||
AnyView(EmojiPickerScreen(context: viewModel.context))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
import Foundation
|
||||
|
||||
enum EmojiPickerScreenViewModelAction {
|
||||
case emojiSelected(emoji: String)
|
||||
case dismiss
|
||||
}
|
||||
|
||||
struct EmojiPickerScreenViewState: BindableState {
|
||||
var categories: [EmojiPickerEmojiCategoryViewData]
|
||||
var selectedEmojis: Set<String>
|
||||
}
|
||||
|
||||
enum EmojiPickerScreenViewAction {
|
||||
|
||||
@@ -11,17 +11,20 @@ import SwiftUI
|
||||
typealias EmojiPickerScreenViewModelType = StateStoreViewModelV2<EmojiPickerScreenViewState, EmojiPickerScreenViewAction>
|
||||
|
||||
class EmojiPickerScreenViewModel: EmojiPickerScreenViewModelType, EmojiPickerScreenViewModelProtocol {
|
||||
private var actionsSubject: PassthroughSubject<EmojiPickerScreenViewModelAction, Never> = .init()
|
||||
private let itemID: TimelineItemIdentifier
|
||||
private let emojiProvider: EmojiProviderProtocol
|
||||
private let timelineController: TimelineControllerProtocol
|
||||
|
||||
private var actionsSubject: PassthroughSubject<EmojiPickerScreenViewModelAction, Never> = .init()
|
||||
var actions: AnyPublisher<EmojiPickerScreenViewModelAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
private let emojiProvider: EmojiProviderProtocol
|
||||
|
||||
init(emojiProvider: EmojiProviderProtocol) {
|
||||
let initialViewState = EmojiPickerScreenViewState(categories: [])
|
||||
init(itemID: TimelineItemIdentifier, selectedEmojis: Set<String>, emojiProvider: EmojiProviderProtocol, timelineController: TimelineControllerProtocol) {
|
||||
let initialViewState = EmojiPickerScreenViewState(categories: [], selectedEmojis: selectedEmojis)
|
||||
self.itemID = itemID
|
||||
self.emojiProvider = emojiProvider
|
||||
self.timelineController = timelineController
|
||||
super.init(initialViewState: initialViewState)
|
||||
loadEmojis()
|
||||
}
|
||||
@@ -36,8 +39,7 @@ class EmojiPickerScreenViewModel: EmojiPickerScreenViewModelType, EmojiPickerScr
|
||||
state.categories = convert(emojiCategories: categories)
|
||||
}
|
||||
case let .emojiTapped(emoji: emoji):
|
||||
emojiProvider.markEmojiAsFrequentlyUsed(emoji.value)
|
||||
actionsSubject.send(.emojiSelected(emoji: emoji.value))
|
||||
Task { await selectEmoji(emoji) }
|
||||
case .dismiss:
|
||||
actionsSubject.send(.dismiss)
|
||||
}
|
||||
@@ -62,4 +64,17 @@ class EmojiPickerScreenViewModel: EmojiPickerScreenViewModelType, EmojiPickerScr
|
||||
return EmojiPickerEmojiCategoryViewData(id: emojiCategory.id, emojis: emojisViewData)
|
||||
}
|
||||
}
|
||||
|
||||
private func selectEmoji(_ emoji: EmojiPickerEmojiViewData) async {
|
||||
MXLog.debug("Selected \(emoji) for \(itemID)")
|
||||
emojiProvider.markEmojiAsFrequentlyUsed(emoji.value)
|
||||
|
||||
guard case let .event(_, eventOrTransactionID) = itemID else { fatalError("Attempted to react to a virtual item.") }
|
||||
|
||||
// There aren't any local echoes when the toggle redacts, so dismiss the screen early
|
||||
// until we have them: https://github.com/matrix-org/matrix-rust-sdk/issues/4162
|
||||
actionsSubject.send(.dismiss)
|
||||
|
||||
await timelineController.toggleReaction(emoji.value, to: eventOrTransactionID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import SwiftUI
|
||||
struct EmojiPickerScreen: View {
|
||||
let context: EmojiPickerScreenViewModel.Context
|
||||
|
||||
var selectedEmojis = Set<String>()
|
||||
@State var searchString = ""
|
||||
@State private var isSearching = false
|
||||
|
||||
@@ -64,7 +63,7 @@ struct EmojiPickerScreen: View {
|
||||
}
|
||||
|
||||
private func accessibilityLabel(for emoji: String) -> String {
|
||||
if selectedEmojis.contains(emoji) {
|
||||
if context.viewState.selectedEmojis.contains(emoji) {
|
||||
return L10n.a11yRemoveReaction(emoji)
|
||||
} else {
|
||||
return L10n.a11yAddReaction(emoji)
|
||||
@@ -72,7 +71,7 @@ struct EmojiPickerScreen: View {
|
||||
}
|
||||
|
||||
private func emojiBackgroundColor(for emoji: String) -> Color {
|
||||
if selectedEmojis.contains(emoji) {
|
||||
if context.viewState.selectedEmojis.contains(emoji) {
|
||||
return .compound.bgActionPrimaryRest
|
||||
} else {
|
||||
return .clear
|
||||
@@ -101,22 +100,28 @@ struct EmojiPickerScreen: View {
|
||||
// MARK: - Previews
|
||||
|
||||
struct EmojiPickerScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = EmojiPickerScreenViewModel(emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings))
|
||||
static let viewModel = EmojiPickerScreenViewModel(itemID: .randomEvent,
|
||||
selectedEmojis: ["😀", "😄"],
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||
timelineController: MockTimelineController())
|
||||
|
||||
static var previews: some View {
|
||||
EmojiPickerScreen(context: viewModel.context, selectedEmojis: ["😀", "😄"])
|
||||
EmojiPickerScreen(context: viewModel.context)
|
||||
.previewDisplayName("Screen")
|
||||
.snapshotPreferences(expect: viewModel.context.observe(\.viewState.categories).map { !$0.isEmpty }.eraseToStream())
|
||||
}
|
||||
}
|
||||
|
||||
struct EmojiPickerScreenSheet_Previews: PreviewProvider {
|
||||
static let viewModel = EmojiPickerScreenViewModel(emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings))
|
||||
static let viewModel = EmojiPickerScreenViewModel(itemID: .randomEvent,
|
||||
selectedEmojis: ["😀", "😄"],
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||
timelineController: MockTimelineController())
|
||||
|
||||
static var previews: some View {
|
||||
Text("Timeline view")
|
||||
.sheet(isPresented: .constant(true)) {
|
||||
EmojiPickerScreen(context: viewModel.context, selectedEmojis: ["😀", "😄"])
|
||||
EmojiPickerScreen(context: viewModel.context)
|
||||
}
|
||||
.previewDisplayName("Sheet")
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ enum LocationSharingViewError: Error, Hashable {
|
||||
enum StaticLocationScreenViewModelAction {
|
||||
case close
|
||||
case openSystemSettings
|
||||
case sendLocation(GeoURI, isUserLocation: Bool)
|
||||
}
|
||||
|
||||
enum StaticLocationInteractionMode: Hashable {
|
||||
|
||||
@@ -11,12 +11,14 @@ import SwiftUI
|
||||
struct StaticLocationScreenCoordinatorParameters {
|
||||
let interactionMode: StaticLocationInteractionMode
|
||||
let mapURLBuilder: MapTilerURLBuilderProtocol
|
||||
let timelineController: TimelineControllerProtocol
|
||||
let appMediator: AppMediatorProtocol
|
||||
let analytics: AnalyticsService
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
}
|
||||
|
||||
enum StaticLocationScreenCoordinatorAction {
|
||||
case close
|
||||
case selectedLocation(GeoURI, isUserLocation: Bool)
|
||||
}
|
||||
|
||||
final class StaticLocationScreenCoordinator: CoordinatorProtocol {
|
||||
@@ -33,7 +35,11 @@ final class StaticLocationScreenCoordinator: CoordinatorProtocol {
|
||||
init(parameters: StaticLocationScreenCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
viewModel = StaticLocationScreenViewModel(interactionMode: parameters.interactionMode, mapURLBuilder: parameters.mapURLBuilder)
|
||||
viewModel = StaticLocationScreenViewModel(interactionMode: parameters.interactionMode,
|
||||
mapURLBuilder: parameters.mapURLBuilder,
|
||||
timelineController: parameters.timelineController,
|
||||
analytics: parameters.analytics,
|
||||
userIndicatorController: parameters.userIndicatorController)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
@@ -46,8 +52,6 @@ final class StaticLocationScreenCoordinator: CoordinatorProtocol {
|
||||
actionsSubject.send(.close)
|
||||
case .openSystemSettings:
|
||||
parameters.appMediator.openAppSettings()
|
||||
case .sendLocation(let geoURI, let isUserLocation):
|
||||
actionsSubject.send(.selectedLocation(geoURI, isUserLocation: isUserLocation))
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
@@ -11,13 +11,24 @@ import Foundation
|
||||
typealias StaticLocationScreenViewModelType = StateStoreViewModelV2<StaticLocationScreenViewState, StaticLocationScreenViewAction>
|
||||
|
||||
class StaticLocationScreenViewModel: StaticLocationScreenViewModelType, StaticLocationScreenViewModelProtocol {
|
||||
private let actionsSubject: PassthroughSubject<StaticLocationScreenViewModelAction, Never> = .init()
|
||||
private let timelineController: TimelineControllerProtocol
|
||||
private let analytics: AnalyticsService
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
|
||||
private let actionsSubject: PassthroughSubject<StaticLocationScreenViewModelAction, Never> = .init()
|
||||
var actions: AnyPublisher<StaticLocationScreenViewModelAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(interactionMode: StaticLocationInteractionMode, mapURLBuilder: MapTilerURLBuilderProtocol) {
|
||||
init(interactionMode: StaticLocationInteractionMode,
|
||||
mapURLBuilder: MapTilerURLBuilderProtocol,
|
||||
timelineController: TimelineControllerProtocol,
|
||||
analytics: AnalyticsService,
|
||||
userIndicatorController: UserIndicatorControllerProtocol) {
|
||||
self.timelineController = timelineController
|
||||
self.analytics = analytics
|
||||
self.userIndicatorController = userIndicatorController
|
||||
|
||||
super.init(initialViewState: .init(interactionMode: interactionMode, mapURLBuilder: mapURLBuilder))
|
||||
}
|
||||
|
||||
@@ -28,7 +39,7 @@ class StaticLocationScreenViewModel: StaticLocationScreenViewModelType, StaticLo
|
||||
case .selectLocation:
|
||||
guard let coordinate = state.bindings.mapCenterLocation else { return }
|
||||
let uncertainty = state.isSharingUserLocation ? context.geolocationUncertainty : nil
|
||||
actionsSubject.send(.sendLocation(.init(coordinate: coordinate, uncertainty: uncertainty), isUserLocation: state.isSharingUserLocation))
|
||||
Task { await sendLocation(.init(coordinate: coordinate, uncertainty: uncertainty), isUserLocation: state.isSharingUserLocation) }
|
||||
case .userDidPan:
|
||||
state.bindings.showsUserLocationMode = .show
|
||||
case .centerToUser:
|
||||
@@ -43,4 +54,34 @@ class StaticLocationScreenViewModel: StaticLocationScreenViewModelType, StaticLo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func sendLocation(_ geoURI: GeoURI, isUserLocation: Bool) async {
|
||||
guard case .success = await timelineController.sendLocation(body: geoURI.bodyMessage,
|
||||
geoURI: geoURI,
|
||||
description: nil,
|
||||
zoomLevel: 15,
|
||||
assetType: isUserLocation ? .sender : .pin) else {
|
||||
showErrorIndicator()
|
||||
return
|
||||
}
|
||||
|
||||
actionsSubject.send(.close)
|
||||
|
||||
analytics.trackComposer(inThread: false,
|
||||
isEditing: false,
|
||||
isReply: false,
|
||||
messageType: isUserLocation ? .LocationUser : .LocationPin,
|
||||
startsThread: nil)
|
||||
}
|
||||
|
||||
private func showErrorIndicator() {
|
||||
userIndicatorController.submitIndicator(UserIndicator(id: statusIndicatorID,
|
||||
type: .toast,
|
||||
title: L10n.errorUnknown,
|
||||
iconName: "xmark"))
|
||||
}
|
||||
|
||||
private var statusIndicatorID: String { "\(Self.self)-Status" }
|
||||
}
|
||||
|
||||
@@ -148,13 +148,22 @@ struct StaticLocationScreen: View {
|
||||
struct StaticLocationScreenViewer_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = StaticLocationScreenViewModel(interactionMode: .viewOnly(geoURI: .init(latitude: 41.9027835,
|
||||
longitude: 12.4963655)),
|
||||
mapURLBuilder: ServiceLocator.shared.settings.mapTilerConfiguration)
|
||||
mapURLBuilder: ServiceLocator.shared.settings.mapTilerConfiguration,
|
||||
timelineController: MockTimelineController(),
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
static let pickerViewModel = StaticLocationScreenViewModel(interactionMode: .picker,
|
||||
mapURLBuilder: ServiceLocator.shared.settings.mapTilerConfiguration)
|
||||
mapURLBuilder: ServiceLocator.shared.settings.mapTilerConfiguration,
|
||||
timelineController: MockTimelineController(),
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
static let descriptionViewModel = StaticLocationScreenViewModel(interactionMode: .viewOnly(geoURI: .init(latitude: 41.9027835,
|
||||
longitude: 12.4963655),
|
||||
description: "Cool position"),
|
||||
mapURLBuilder: ServiceLocator.shared.settings.mapTilerConfiguration)
|
||||
mapURLBuilder: ServiceLocator.shared.settings.mapTilerConfiguration,
|
||||
timelineController: MockTimelineController(),
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
|
||||
static var previews: some View {
|
||||
NavigationStack {
|
||||
|
||||
@@ -109,7 +109,11 @@ class MockTimelineController: TimelineControllerProtocol {
|
||||
|
||||
func processItemDisappearance(_ itemID: TimelineItemIdentifier) async { }
|
||||
|
||||
func toggleReaction(_ reaction: String, to eventID: TimelineItemIdentifier.EventOrTransactionID) async { }
|
||||
func toggleReaction(_ reaction: String, to eventID: TimelineItemIdentifier.EventOrTransactionID) async {
|
||||
if let timelineProxy {
|
||||
_ = await timelineProxy.toggleReaction(reaction, to: eventID)
|
||||
}
|
||||
}
|
||||
|
||||
func edit(_ eventOrTransactionID: TimelineItemIdentifier.EventOrTransactionID,
|
||||
message: String,
|
||||
@@ -125,6 +129,9 @@ class MockTimelineController: TimelineControllerProtocol {
|
||||
|
||||
private(set) var redactCalled = false
|
||||
func redact(_ eventOrTransactionID: TimelineItemIdentifier.EventOrTransactionID) async {
|
||||
if let timelineProxy {
|
||||
_ = await timelineProxy.redact(eventOrTransactionID, reason: nil)
|
||||
}
|
||||
redactCalled = true
|
||||
}
|
||||
|
||||
@@ -248,19 +255,31 @@ class MockTimelineController: TimelineControllerProtocol {
|
||||
// MARK: - Polls
|
||||
|
||||
func createPoll(question: String, answers: [String], pollKind: Poll.Kind) async -> Result<Void, TimelineControllerError> {
|
||||
.success(())
|
||||
if let timelineProxy {
|
||||
_ = await timelineProxy.createPoll(question: question, answers: answers, pollKind: pollKind)
|
||||
}
|
||||
return .success(())
|
||||
}
|
||||
|
||||
func editPoll(original eventID: String, question: String, answers: [String], pollKind: Poll.Kind) async -> Result<Void, TimelineControllerError> {
|
||||
.success(())
|
||||
if let timelineProxy {
|
||||
_ = await timelineProxy.editPoll(original: eventID, question: question, answers: answers, pollKind: pollKind)
|
||||
}
|
||||
return .success(())
|
||||
}
|
||||
|
||||
func sendPollResponse(pollStartID: String, answers: [String]) async -> Result<Void, TimelineControllerError> {
|
||||
.success(())
|
||||
if let timelineProxy {
|
||||
_ = await timelineProxy.sendPollResponse(pollStartID: pollStartID, answers: answers)
|
||||
}
|
||||
return .success(())
|
||||
}
|
||||
|
||||
func endPoll(pollStartID: String, text: String) async -> Result<Void, TimelineControllerError> {
|
||||
.success(())
|
||||
if let timelineProxy {
|
||||
_ = await timelineProxy.endPoll(pollStartID: pollStartID, text: text)
|
||||
}
|
||||
return .success(())
|
||||
}
|
||||
|
||||
// MARK: - UI Test signalling
|
||||
|
||||
@@ -685,7 +685,11 @@ class MockScreen: Identifiable {
|
||||
return navigationStackCoordinator
|
||||
case .createPoll:
|
||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||
let coordinator = PollFormScreenCoordinator(parameters: .init(mode: .new, maxNumberOfOptions: 10))
|
||||
let coordinator = PollFormScreenCoordinator(parameters: .init(mode: .new,
|
||||
maxNumberOfOptions: 10,
|
||||
timelineController: MockTimelineController(),
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
userIndicatorController: UserIndicatorControllerMock()))
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
return navigationStackCoordinator
|
||||
case .encryptionSettings, .encryptionSettingsOutOfSync:
|
||||
|
||||
Reference in New Issue
Block a user