mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
Hook up the emoji picker to the custom status row. (#5902)
* Refactor EmojiPickerScreen to take a continuation instead of a timeline/itemID. * Hook up the emoji picker to the custom status row.
This commit is contained in:
@@ -430,13 +430,12 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
presentMediaUploadPickerWithMode(mode, caption: caption, timelineController: timelineController, animated: animated)
|
presentMediaUploadPickerWithMode(mode, caption: caption, timelineController: timelineController, animated: animated)
|
||||||
|
|
||||||
case (_, .presentEmojiPicker, .emojiPicker(let itemID, let selectedEmoji, _)):
|
case (_, .presentEmojiPicker, .emojiPicker(let selectedEmojis, _)):
|
||||||
guard let timelineController = (context.userInfo as? EventUserInfo)?.timelineController else {
|
guard let continuation = (context.userInfo as? EventUserInfo)?.emojiPickerContinuation else {
|
||||||
fatalError("Missing required TimelineController")
|
fatalError("Missing required emoji continuation")
|
||||||
}
|
}
|
||||||
presentEmojiPicker(for: itemID,
|
presentEmojiPicker(selectedEmojis: selectedEmojis,
|
||||||
selectedEmoji: selectedEmoji,
|
emojiPickerContinuation: continuation,
|
||||||
timelineController: timelineController,
|
|
||||||
animated: animated)
|
animated: animated)
|
||||||
|
|
||||||
case (_, .presentMessageForwarding(let forwardingItem), .messageForwarding):
|
case (_, .presentMessageForwarding(let forwardingItem), .messageForwarding):
|
||||||
@@ -706,9 +705,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
case .presentMediaUploadPreviewScreen(let url, let caption):
|
case .presentMediaUploadPreviewScreen(let url, let caption):
|
||||||
stateMachine.tryEvent(.presentMediaUploadPreview(mediaURLs: url, caption: caption),
|
stateMachine.tryEvent(.presentMediaUploadPreview(mediaURLs: url, caption: caption),
|
||||||
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
|
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
|
||||||
case .presentEmojiPicker(let itemID, let selectedEmojis):
|
case .presentEmojiPicker(let selectedEmojis, let continuation):
|
||||||
stateMachine.tryEvent(.presentEmojiPicker(itemID: itemID, selectedEmojis: selectedEmojis),
|
stateMachine.tryEvent(.presentEmojiPicker(selectedEmojis: selectedEmojis),
|
||||||
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
|
userInfo: EventUserInfo(animated: animated, emojiPickerContinuation: continuation))
|
||||||
case .presentLocationPicker:
|
case .presentLocationPicker:
|
||||||
stateMachine.tryEvent(.presentMapNavigator(interactionMode: .picker(shouldShowLiveLocationOption: true)),
|
stateMachine.tryEvent(.presentMapNavigator(interactionMode: .picker(shouldShowLiveLocationOption: true)),
|
||||||
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
|
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
|
||||||
@@ -831,9 +830,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
stateMachine.tryEvent(.presentMapNavigator(interactionMode: .viewStatic(location)),
|
stateMachine.tryEvent(.presentMapNavigator(interactionMode: .viewStatic(location)),
|
||||||
userInfo: EventUserInfo(animated: animated,
|
userInfo: EventUserInfo(animated: animated,
|
||||||
timelineController: timelineController))
|
timelineController: timelineController))
|
||||||
case .presentEmojiPicker(let itemID, let selectedEmojis):
|
case .presentEmojiPicker(let selectedEmojis, let continuation):
|
||||||
stateMachine.tryEvent(.presentEmojiPicker(itemID: itemID, selectedEmojis: selectedEmojis),
|
stateMachine.tryEvent(.presentEmojiPicker(selectedEmojis: selectedEmojis),
|
||||||
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
|
userInfo: EventUserInfo(animated: animated, emojiPickerContinuation: continuation))
|
||||||
case .presentRoomMemberDetails(let userID):
|
case .presentRoomMemberDetails(let userID):
|
||||||
stateMachine.tryEvent(.startMembersFlow(entryPoint: .roomMember(userID: userID)))
|
stateMachine.tryEvent(.startMembersFlow(entryPoint: .roomMember(userID: userID)))
|
||||||
case .presentMessageForwarding(let forwardingItem):
|
case .presentMessageForwarding(let forwardingItem):
|
||||||
@@ -1146,14 +1145,12 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func presentEmojiPicker(for itemID: TimelineItemIdentifier,
|
private func presentEmojiPicker(selectedEmojis: Set<String>,
|
||||||
selectedEmoji: Set<String>,
|
emojiPickerContinuation: EmojiPickerScreenContinuation,
|
||||||
timelineController: TimelineControllerProtocol,
|
|
||||||
animated: Bool) {
|
animated: Bool) {
|
||||||
let params = EmojiPickerScreenCoordinatorParameters(itemID: itemID,
|
let params = EmojiPickerScreenCoordinatorParameters(selectedEmojis: selectedEmojis,
|
||||||
selectedEmojis: selectedEmoji,
|
|
||||||
emojiProvider: flowParameters.emojiProvider,
|
emojiProvider: flowParameters.emojiProvider,
|
||||||
timelineController: timelineController)
|
continuation: emojiPickerContinuation)
|
||||||
let coordinator = EmojiPickerScreenCoordinator(parameters: params)
|
let coordinator = EmojiPickerScreenCoordinator(parameters: params)
|
||||||
|
|
||||||
coordinator.actions.sink { [weak self] action in
|
coordinator.actions.sink { [weak self] action in
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ extension RoomFlowCoordinator {
|
|||||||
case inviteUsersScreen(flow: InviteUsersFlow, previousState: State)
|
case inviteUsersScreen(flow: InviteUsersFlow, previousState: State)
|
||||||
case mediaUploadPicker(mode: MediaPickerScreenMode, caption: NSAttributedString, previousState: State)
|
case mediaUploadPicker(mode: MediaPickerScreenMode, caption: NSAttributedString, previousState: State)
|
||||||
case mediaUploadPreview(mediaURLs: [URL], caption: NSAttributedString?, previousState: State)
|
case mediaUploadPreview(mediaURLs: [URL], caption: NSAttributedString?, previousState: State)
|
||||||
case emojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set<String>, previousState: State)
|
case emojiPicker(selectedEmojis: Set<String>, previousState: State)
|
||||||
case mapNavigator(previousState: State)
|
case mapNavigator(previousState: State)
|
||||||
case messageForwarding(forwardingItem: MessageForwardingItem, previousState: State)
|
case messageForwarding(forwardingItem: MessageForwardingItem, previousState: State)
|
||||||
case reportContent(itemID: TimelineItemIdentifier, senderID: String, previousState: State)
|
case reportContent(itemID: TimelineItemIdentifier, senderID: String, previousState: State)
|
||||||
@@ -90,6 +90,7 @@ extension RoomFlowCoordinator {
|
|||||||
var timelineController: TimelineControllerProtocol?
|
var timelineController: TimelineControllerProtocol?
|
||||||
var spaceRoomListProxy: SpaceRoomListProxyProtocol?
|
var spaceRoomListProxy: SpaceRoomListProxyProtocol?
|
||||||
var authorizedSpacesSelection: AuthorizedSpacesSelection?
|
var authorizedSpacesSelection: AuthorizedSpacesSelection?
|
||||||
|
var emojiPickerContinuation: EmojiPickerScreenContinuation?
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Event: EventType {
|
enum Event: EventType {
|
||||||
@@ -133,7 +134,7 @@ extension RoomFlowCoordinator {
|
|||||||
case presentMediaUploadPreview(mediaURLs: [URL], caption: NSAttributedString?)
|
case presentMediaUploadPreview(mediaURLs: [URL], caption: NSAttributedString?)
|
||||||
case dismissMediaUploadPreview
|
case dismissMediaUploadPreview
|
||||||
|
|
||||||
case presentEmojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set<String>)
|
case presentEmojiPicker(selectedEmojis: Set<String>)
|
||||||
case dismissEmojiPicker
|
case dismissEmojiPicker
|
||||||
|
|
||||||
case presentMapNavigator(interactionMode: LocationSharingInteractionMode)
|
case presentMapNavigator(interactionMode: LocationSharingInteractionMode)
|
||||||
@@ -204,8 +205,8 @@ extension RoomFlowCoordinator {
|
|||||||
case (.room, .presentMediaUploadPreview(let mediaURLs, let caption)):
|
case (.room, .presentMediaUploadPreview(let mediaURLs, let caption)):
|
||||||
return .mediaUploadPreview(mediaURLs: mediaURLs, caption: caption, previousState: fromState)
|
return .mediaUploadPreview(mediaURLs: mediaURLs, caption: caption, previousState: fromState)
|
||||||
|
|
||||||
case (.room, .presentEmojiPicker(let itemID, let selectedEmoji)):
|
case (.room, .presentEmojiPicker(let selectedEmojis)):
|
||||||
return .emojiPicker(itemID: itemID, selectedEmojis: selectedEmoji, previousState: fromState)
|
return .emojiPicker(selectedEmojis: selectedEmojis, previousState: fromState)
|
||||||
|
|
||||||
case (.room, .presentMessageForwarding(let forwardingItem)):
|
case (.room, .presentMessageForwarding(let forwardingItem)):
|
||||||
return .messageForwarding(forwardingItem: forwardingItem, previousState: fromState)
|
return .messageForwarding(forwardingItem: forwardingItem, previousState: fromState)
|
||||||
@@ -253,8 +254,8 @@ extension RoomFlowCoordinator {
|
|||||||
case (.thread, .presentMediaUploadPreview(let mediaURLs, let caption)):
|
case (.thread, .presentMediaUploadPreview(let mediaURLs, let caption)):
|
||||||
return .mediaUploadPreview(mediaURLs: mediaURLs, caption: caption, previousState: fromState)
|
return .mediaUploadPreview(mediaURLs: mediaURLs, caption: caption, previousState: fromState)
|
||||||
|
|
||||||
case (.thread, .presentEmojiPicker(let itemID, let selectedEmoji)):
|
case (.thread, .presentEmojiPicker(let selectedEmojis)):
|
||||||
return .emojiPicker(itemID: itemID, selectedEmojis: selectedEmoji, previousState: fromState)
|
return .emojiPicker(selectedEmojis: selectedEmojis, previousState: fromState)
|
||||||
|
|
||||||
case (.thread, .presentMessageForwarding(let forwardingItem)):
|
case (.thread, .presentMessageForwarding(let forwardingItem)):
|
||||||
return .messageForwarding(forwardingItem: forwardingItem, previousState: fromState)
|
return .messageForwarding(forwardingItem: forwardingItem, previousState: fromState)
|
||||||
@@ -273,7 +274,7 @@ extension RoomFlowCoordinator {
|
|||||||
case (.mediaUploadPicker(_, _, let previousState), .dismissMediaUploadPicker):
|
case (.mediaUploadPicker(_, _, let previousState), .dismissMediaUploadPicker):
|
||||||
return previousState
|
return previousState
|
||||||
|
|
||||||
case (.emojiPicker(_, _, let previouState), .dismissEmojiPicker):
|
case (.emojiPicker(_, let previouState), .dismissEmojiPicker):
|
||||||
return previouState
|
return previouState
|
||||||
|
|
||||||
case (.reportContent(_, _, let previousState), .dismissReportContent):
|
case (.reportContent(_, _, let previousState), .dismissReportContent):
|
||||||
|
|||||||
@@ -91,6 +91,8 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
startEncryptionSettingsFlow()
|
startEncryptionSettingsFlow()
|
||||||
case .userDetails:
|
case .userDetails:
|
||||||
presentUserDetailsEditScreen()
|
presentUserDetailsEditScreen()
|
||||||
|
case let .userStatusEmojiPicker(continuation):
|
||||||
|
presentEmojiPicker(emojiPickerContinuation: continuation)
|
||||||
case .linkNewDevice:
|
case .linkNewDevice:
|
||||||
startLinkNewDeviceFlow()
|
startLinkNewDeviceFlow()
|
||||||
case let .manageAccount(url):
|
case let .manageAccount(url):
|
||||||
@@ -159,6 +161,22 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
coordinator.start()
|
coordinator.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func presentEmojiPicker(emojiPickerContinuation: EmojiPickerScreenContinuation) {
|
||||||
|
let coordinator = EmojiPickerScreenCoordinator(parameters: .init(selectedEmojis: [],
|
||||||
|
emojiProvider: flowParameters.emojiProvider,
|
||||||
|
continuation: emojiPickerContinuation))
|
||||||
|
coordinator.actions
|
||||||
|
.sink { [weak self] action in
|
||||||
|
switch action {
|
||||||
|
case .dismiss:
|
||||||
|
self?.navigationStackCoordinator.setSheetCoordinator(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
navigationStackCoordinator.setSheetCoordinator(coordinator)
|
||||||
|
}
|
||||||
|
|
||||||
private func presentUserDetailsEditScreen() {
|
private func presentUserDetailsEditScreen() {
|
||||||
let coordinator = UserDetailsEditScreenCoordinator(parameters: .init(orientationManager: flowParameters.windowManager,
|
let coordinator = UserDetailsEditScreenCoordinator(parameters: .init(orientationManager: flowParameters.windowManager,
|
||||||
userSession: flowParameters.userSession,
|
userSession: flowParameters.userSession,
|
||||||
|
|||||||
@@ -10,10 +10,11 @@ import Combine
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct EmojiPickerScreenCoordinatorParameters {
|
struct EmojiPickerScreenCoordinatorParameters {
|
||||||
let itemID: TimelineItemIdentifier
|
/// Any emojis that should be displayed as already selected.
|
||||||
let selectedEmojis: Set<String>
|
let selectedEmojis: Set<String>
|
||||||
let emojiProvider: EmojiProviderProtocol
|
let emojiProvider: EmojiProviderProtocol
|
||||||
let timelineController: TimelineControllerProtocol
|
/// A continuation that yields the selected emoji.
|
||||||
|
let continuation: EmojiPickerScreenContinuation
|
||||||
}
|
}
|
||||||
|
|
||||||
enum EmojiPickerScreenCoordinatorAction {
|
enum EmojiPickerScreenCoordinatorAction {
|
||||||
@@ -31,10 +32,9 @@ final class EmojiPickerScreenCoordinator: CoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init(parameters: EmojiPickerScreenCoordinatorParameters) {
|
init(parameters: EmojiPickerScreenCoordinatorParameters) {
|
||||||
viewModel = EmojiPickerScreenViewModel(itemID: parameters.itemID,
|
viewModel = EmojiPickerScreenViewModel(selectedEmojis: parameters.selectedEmojis,
|
||||||
selectedEmojis: parameters.selectedEmojis,
|
|
||||||
emojiProvider: parameters.emojiProvider,
|
emojiProvider: parameters.emojiProvider,
|
||||||
timelineController: parameters.timelineController)
|
continuation: parameters.continuation)
|
||||||
}
|
}
|
||||||
|
|
||||||
func start() {
|
func start() {
|
||||||
@@ -50,6 +50,10 @@ final class EmojiPickerScreenCoordinator: CoordinatorProtocol {
|
|||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func stop() {
|
||||||
|
viewModel.stop()
|
||||||
|
}
|
||||||
|
|
||||||
func toPresentable() -> AnyView {
|
func toPresentable() -> AnyView {
|
||||||
AnyView(EmojiPickerScreen(context: viewModel.context))
|
AnyView(EmojiPickerScreen(context: viewModel.context))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
typealias EmojiPickerScreenContinuation = AsyncStream<String>.Continuation
|
||||||
|
|
||||||
enum EmojiPickerScreenViewModelAction {
|
enum EmojiPickerScreenViewModelAction {
|
||||||
case dismiss
|
case dismiss
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,26 +12,28 @@ import SwiftUI
|
|||||||
typealias EmojiPickerScreenViewModelType = StateStoreViewModelV2<EmojiPickerScreenViewState, EmojiPickerScreenViewAction>
|
typealias EmojiPickerScreenViewModelType = StateStoreViewModelV2<EmojiPickerScreenViewState, EmojiPickerScreenViewAction>
|
||||||
|
|
||||||
class EmojiPickerScreenViewModel: EmojiPickerScreenViewModelType, EmojiPickerScreenViewModelProtocol {
|
class EmojiPickerScreenViewModel: EmojiPickerScreenViewModelType, EmojiPickerScreenViewModelProtocol {
|
||||||
private let itemID: TimelineItemIdentifier
|
|
||||||
private let emojiProvider: EmojiProviderProtocol
|
private let emojiProvider: EmojiProviderProtocol
|
||||||
private let timelineController: TimelineControllerProtocol
|
private let continuation: EmojiPickerScreenContinuation
|
||||||
|
|
||||||
private var actionsSubject: PassthroughSubject<EmojiPickerScreenViewModelAction, Never> = .init()
|
private var actionsSubject: PassthroughSubject<EmojiPickerScreenViewModelAction, Never> = .init()
|
||||||
var actions: AnyPublisher<EmojiPickerScreenViewModelAction, Never> {
|
var actions: AnyPublisher<EmojiPickerScreenViewModelAction, Never> {
|
||||||
actionsSubject.eraseToAnyPublisher()
|
actionsSubject.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
init(itemID: TimelineItemIdentifier, selectedEmojis: Set<String>, emojiProvider: EmojiProviderProtocol, timelineController: TimelineControllerProtocol) {
|
init(selectedEmojis: Set<String>, emojiProvider: EmojiProviderProtocol, continuation: EmojiPickerScreenContinuation) {
|
||||||
let initialViewState = EmojiPickerScreenViewState(categories: [], selectedEmojis: selectedEmojis)
|
let initialViewState = EmojiPickerScreenViewState(categories: [], selectedEmojis: selectedEmojis)
|
||||||
self.itemID = itemID
|
|
||||||
self.emojiProvider = emojiProvider
|
self.emojiProvider = emojiProvider
|
||||||
self.timelineController = timelineController
|
self.continuation = continuation
|
||||||
super.init(initialViewState: initialViewState)
|
super.init(initialViewState: initialViewState)
|
||||||
loadEmojis()
|
loadEmojis()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Public
|
// MARK: - Public
|
||||||
|
|
||||||
|
func stop() {
|
||||||
|
continuation.finish() // Ensure the continuation always finishes even without a selection.
|
||||||
|
}
|
||||||
|
|
||||||
override func process(viewAction: EmojiPickerScreenViewAction) {
|
override func process(viewAction: EmojiPickerScreenViewAction) {
|
||||||
switch viewAction {
|
switch viewAction {
|
||||||
case let .search(searchString: searchString):
|
case let .search(searchString: searchString):
|
||||||
@@ -40,7 +42,7 @@ class EmojiPickerScreenViewModel: EmojiPickerScreenViewModelType, EmojiPickerScr
|
|||||||
state.categories = convert(emojiCategories: categories)
|
state.categories = convert(emojiCategories: categories)
|
||||||
}
|
}
|
||||||
case let .emojiTapped(emoji: emoji):
|
case let .emojiTapped(emoji: emoji):
|
||||||
Task { await selectEmoji(emoji) }
|
selectEmoji(emoji)
|
||||||
case .dismiss:
|
case .dismiss:
|
||||||
actionsSubject.send(.dismiss)
|
actionsSubject.send(.dismiss)
|
||||||
}
|
}
|
||||||
@@ -66,16 +68,12 @@ class EmojiPickerScreenViewModel: EmojiPickerScreenViewModelType, EmojiPickerScr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func selectEmoji(_ emoji: EmojiPickerEmojiViewData) async {
|
private func selectEmoji(_ emoji: EmojiPickerEmojiViewData) {
|
||||||
MXLog.debug("Selected \(emoji) for \(itemID)")
|
|
||||||
emojiProvider.markEmojiAsFrequentlyUsed(emoji.value)
|
emojiProvider.markEmojiAsFrequentlyUsed(emoji.value)
|
||||||
|
|
||||||
guard case let .event(_, eventOrTransactionID) = itemID else { fatalError("Attempted to react to a virtual item.") }
|
continuation.yield(emoji.value)
|
||||||
|
continuation.finish()
|
||||||
|
|
||||||
// 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)
|
actionsSubject.send(.dismiss)
|
||||||
|
|
||||||
await timelineController.toggleReaction(emoji.value, to: eventOrTransactionID)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,4 +11,6 @@ import Combine
|
|||||||
protocol EmojiPickerScreenViewModelProtocol {
|
protocol EmojiPickerScreenViewModelProtocol {
|
||||||
var actions: AnyPublisher<EmojiPickerScreenViewModelAction, Never> { get }
|
var actions: AnyPublisher<EmojiPickerScreenViewModelAction, Never> { get }
|
||||||
var context: EmojiPickerScreenViewModelType.Context { get }
|
var context: EmojiPickerScreenViewModelType.Context { get }
|
||||||
|
|
||||||
|
func stop()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,10 +102,9 @@ struct EmojiPickerScreen: View {
|
|||||||
|
|
||||||
@available(iOS 26.0, *)
|
@available(iOS 26.0, *)
|
||||||
struct EmojiPickerScreen_Previews: PreviewProvider, TestablePreview {
|
struct EmojiPickerScreen_Previews: PreviewProvider, TestablePreview {
|
||||||
static let viewModel = EmojiPickerScreenViewModel(itemID: .randomEvent,
|
static let viewModel = EmojiPickerScreenViewModel(selectedEmojis: ["😀", "😄"],
|
||||||
selectedEmojis: ["😀", "😄"],
|
|
||||||
emojiProvider: EmojiProvider(appSettings: .volatile()),
|
emojiProvider: EmojiProvider(appSettings: .volatile()),
|
||||||
timelineController: TimelineControllerMock(.init()))
|
continuation: AsyncStream<String>.makeStream().continuation)
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
EmojiPickerScreen(context: viewModel.context)
|
EmojiPickerScreen(context: viewModel.context)
|
||||||
@@ -115,10 +114,9 @@ struct EmojiPickerScreen_Previews: PreviewProvider, TestablePreview {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct EmojiPickerScreenSheet_Previews: PreviewProvider {
|
struct EmojiPickerScreenSheet_Previews: PreviewProvider {
|
||||||
static let viewModel = EmojiPickerScreenViewModel(itemID: .randomEvent,
|
static let viewModel = EmojiPickerScreenViewModel(selectedEmojis: ["😀", "😄"],
|
||||||
selectedEmojis: ["😀", "😄"],
|
|
||||||
emojiProvider: EmojiProvider(appSettings: .volatile()),
|
emojiProvider: EmojiProvider(appSettings: .volatile()),
|
||||||
timelineController: TimelineControllerMock(.init()))
|
continuation: AsyncStream<String>.makeStream().continuation)
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
Text("Timeline view")
|
Text("Timeline view")
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ enum RoomScreenCoordinatorAction {
|
|||||||
case presentPollForm(mode: PollFormMode)
|
case presentPollForm(mode: PollFormMode)
|
||||||
case presentLocationViewer(StaticLocationData)
|
case presentLocationViewer(StaticLocationData)
|
||||||
case presentLiveLocationViewer(sender: TimelineItemSender?, initialLiveLocationShare: LiveLocationShare?)
|
case presentLiveLocationViewer(sender: TimelineItemSender?, initialLiveLocationShare: LiveLocationShare?)
|
||||||
case presentEmojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set<String>)
|
case presentEmojiPicker(selectedEmojis: Set<String>, continuation: EmojiPickerScreenContinuation)
|
||||||
case presentRoomMemberDetails(userID: String)
|
case presentRoomMemberDetails(userID: String)
|
||||||
case presentMessageForwarding(forwardingItem: MessageForwardingItem)
|
case presentMessageForwarding(forwardingItem: MessageForwardingItem)
|
||||||
case presentCallScreen(isVoiceCall: Bool)
|
case presentCallScreen(isVoiceCall: Bool)
|
||||||
@@ -117,8 +117,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
|||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
|
||||||
switch action {
|
switch action {
|
||||||
case .displayEmojiPicker(let itemID, let selectedEmojis):
|
case .displayEmojiPicker(let selectedEmojis, let continuation):
|
||||||
actionsSubject.send(.presentEmojiPicker(itemID: itemID, selectedEmojis: selectedEmojis))
|
actionsSubject.send(.presentEmojiPicker(selectedEmojis: selectedEmojis, continuation: continuation))
|
||||||
case .displayReportContent(let itemID, let senderID):
|
case .displayReportContent(let itemID, let senderID):
|
||||||
actionsSubject.send(.presentReportContent(itemID: itemID, senderID: senderID))
|
actionsSubject.send(.presentReportContent(itemID: itemID, senderID: senderID))
|
||||||
case .displayCameraPicker:
|
case .displayCameraPicker:
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ enum SettingsScreenCoordinatorAction {
|
|||||||
case logout
|
case logout
|
||||||
case secureBackup
|
case secureBackup
|
||||||
case userDetails
|
case userDetails
|
||||||
|
case userStatusEmojiPicker(EmojiPickerScreenContinuation)
|
||||||
case analytics
|
case analytics
|
||||||
case appLock
|
case appLock
|
||||||
case bugReport
|
case bugReport
|
||||||
@@ -64,6 +65,8 @@ final class SettingsScreenCoordinator: CoordinatorProtocol {
|
|||||||
actionsSubject.send(.dismiss)
|
actionsSubject.send(.dismiss)
|
||||||
case .userDetails:
|
case .userDetails:
|
||||||
actionsSubject.send(.userDetails)
|
actionsSubject.send(.userDetails)
|
||||||
|
case let .userStatusEmojiPicker(continuation):
|
||||||
|
actionsSubject.send(.userStatusEmojiPicker(continuation))
|
||||||
case .linkNewDevice:
|
case .linkNewDevice:
|
||||||
actionsSubject.send(.linkNewDevice)
|
actionsSubject.send(.linkNewDevice)
|
||||||
case let .manageAccount(url):
|
case let .manageAccount(url):
|
||||||
|
|||||||
@@ -8,9 +8,10 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
enum SettingsScreenViewModelAction: Equatable {
|
enum SettingsScreenViewModelAction {
|
||||||
case close
|
case close
|
||||||
case userDetails
|
case userDetails
|
||||||
|
case userStatusEmojiPicker(EmojiPickerScreenContinuation)
|
||||||
case linkNewDevice
|
case linkNewDevice
|
||||||
case manageAccount(url: URL)
|
case manageAccount(url: URL)
|
||||||
case analytics
|
case analytics
|
||||||
@@ -55,7 +56,7 @@ struct SettingsScreenViewState: BindableState {
|
|||||||
|
|
||||||
var userStatusRowMode: SettingsScreenUserStatusRow.Mode {
|
var userStatusRowMode: SettingsScreenUserStatusRow.Mode {
|
||||||
if bindings.isShowingCustomStatusField {
|
if bindings.isShowingCustomStatusField {
|
||||||
.custom
|
.custom(emoji: bindings.customStatusEmoji)
|
||||||
} else if let rawStatus = userProfile.status.raw {
|
} else if let rawStatus = userProfile.status.raw {
|
||||||
.show(rawStatus)
|
.show(rawStatus)
|
||||||
} else {
|
} else {
|
||||||
@@ -66,7 +67,15 @@ struct SettingsScreenViewState: BindableState {
|
|||||||
|
|
||||||
struct SettingsScreenViewStateBindings {
|
struct SettingsScreenViewStateBindings {
|
||||||
var isPresentingStatusPicker = false
|
var isPresentingStatusPicker = false
|
||||||
var isShowingCustomStatusField = false
|
var customStatusEmoji: Character = "😄"
|
||||||
|
var isShowingCustomStatusField = false {
|
||||||
|
didSet {
|
||||||
|
if !isShowingCustomStatusField {
|
||||||
|
customStatusEmoji = "😄" // Reset the emoji.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var isPresentingAccountDeactivationConfirmation = false
|
var isPresentingAccountDeactivationConfirmation = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo
|
|||||||
state.bindings.isPresentingStatusPicker = false
|
state.bindings.isPresentingStatusPicker = false
|
||||||
state.bindings.isShowingCustomStatusField = true
|
state.bindings.isShowingCustomStatusField = true
|
||||||
case .userStatus(.pickCustomEmoji):
|
case .userStatus(.pickCustomEmoji):
|
||||||
break // Hook-up the emoji picker here.
|
pickCustomEmoji()
|
||||||
case .userStatus(.set(let status)):
|
case .userStatus(.set(let status)):
|
||||||
Task { await setUserStatus(status) }
|
Task { await setUserStatus(status) }
|
||||||
case .userStatus(.cancel):
|
case .userStatus(.cancel):
|
||||||
@@ -144,6 +144,19 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo
|
|||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
|
private var pickCustomEmojiCancellable: AnyCancellable?
|
||||||
|
private func pickCustomEmoji() {
|
||||||
|
let (stream, continuation) = AsyncStream<String>.makeStream()
|
||||||
|
actionsSubject.send(.userStatusEmojiPicker(continuation))
|
||||||
|
|
||||||
|
pickCustomEmojiCancellable = Task { [weak self] in
|
||||||
|
for await emoji in stream {
|
||||||
|
self?.state.bindings.customStatusEmoji = Character(emoji)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.asCancellable()
|
||||||
|
}
|
||||||
|
|
||||||
func setUserStatus(_ status: UserStatus.Raw?) async {
|
func setUserStatus(_ status: UserStatus.Raw?) async {
|
||||||
// Loading state tbc
|
// Loading state tbc
|
||||||
state.bindings.isPresentingStatusPicker = false
|
state.bindings.isPresentingStatusPicker = false
|
||||||
|
|||||||
+6
-7
@@ -9,11 +9,10 @@ import Compound
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct SettingsScreenUserStatusRow: View {
|
struct SettingsScreenUserStatusRow: View {
|
||||||
enum Mode: Equatable { case pick, custom, show(UserStatus.Raw) }
|
enum Mode: Equatable { case pick, custom(emoji: Character), show(UserStatus.Raw) }
|
||||||
let mode: Mode
|
let mode: Mode
|
||||||
let action: (SettingsScreenViewAction.UserStatusAction) -> Void
|
let action: (SettingsScreenViewAction.UserStatusAction) -> Void
|
||||||
|
|
||||||
@State private var customEmoji = "😄"
|
|
||||||
@State private var customText = ""
|
@State private var customText = ""
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -21,15 +20,15 @@ struct SettingsScreenUserStatusRow: View {
|
|||||||
case .pick:
|
case .pick:
|
||||||
ListRow(label: .default(title: L10n.screenSettingsUserStatusPlaceholder, icon: \.reaction),
|
ListRow(label: .default(title: L10n.screenSettingsUserStatusPlaceholder, icon: \.reaction),
|
||||||
kind: .button { action(.pickStatus) })
|
kind: .button { action(.pickStatus) })
|
||||||
case .custom:
|
case .custom(let emoji):
|
||||||
ListRow(kind: .custom {
|
ListRow(kind: .custom {
|
||||||
HStack(spacing: ListRowPadding.labelIconSpacing) {
|
HStack(spacing: ListRowPadding.labelIconSpacing) {
|
||||||
Button { action(.pickCustomEmoji) } label: {
|
Button { action(.pickCustomEmoji) } label: {
|
||||||
Text(customEmoji)
|
Text(String(emoji))
|
||||||
.font(.compound.headingSM)
|
.font(.compound.headingSM)
|
||||||
.foregroundStyle(.compound.textPrimary)
|
.foregroundStyle(.compound.textPrimary)
|
||||||
.accessibilityLabel(L10n.a11yCustomEmoji)
|
.accessibilityLabel(L10n.a11yCustomEmoji)
|
||||||
.accessibilityValue(customEmoji)
|
.accessibilityValue(String(emoji))
|
||||||
}
|
}
|
||||||
.buttonStyle(EditEmojiButtonStyle())
|
.buttonStyle(EditEmojiButtonStyle())
|
||||||
|
|
||||||
@@ -81,7 +80,7 @@ struct SettingsScreenUserStatusRow: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func saveCustomStatus() {
|
private func saveCustomStatus() {
|
||||||
guard let emoji = customEmoji.first else { return }
|
guard case let .custom(emoji) = mode else { return }
|
||||||
action(.set(.init(text: customText, emoji: emoji)))
|
action(.set(.init(text: customText, emoji: emoji)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +108,7 @@ struct SettingsScreenUserStatusRow_Previews: PreviewProvider, TestablePreview {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
SettingsScreenUserStatusRow(mode: .custom) { _ in }
|
SettingsScreenUserStatusRow(mode: .custom(emoji: "😄")) { _ in }
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ enum ThreadTimelineScreenCoordinatorAction {
|
|||||||
case presentLocationViewer(StaticLocationData)
|
case presentLocationViewer(StaticLocationData)
|
||||||
case presentLiveLocationViewer(sender: TimelineItemSender, initialLiveLocationShare: LiveLocationShare)
|
case presentLiveLocationViewer(sender: TimelineItemSender, initialLiveLocationShare: LiveLocationShare)
|
||||||
case presentPollForm(mode: PollFormMode)
|
case presentPollForm(mode: PollFormMode)
|
||||||
case presentEmojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set<String>)
|
case presentEmojiPicker(selectedEmojis: Set<String>, continuation: EmojiPickerScreenContinuation)
|
||||||
case presentRoomMemberDetails(userID: String)
|
case presentRoomMemberDetails(userID: String)
|
||||||
case presentMessageForwarding(forwardingItem: MessageForwardingItem)
|
case presentMessageForwarding(forwardingItem: MessageForwardingItem)
|
||||||
case presentResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, sendHandle: SendHandleProxy)
|
case presentResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, sendHandle: SendHandleProxy)
|
||||||
@@ -102,8 +102,8 @@ final class ThreadTimelineScreenCoordinator: CoordinatorProtocol {
|
|||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
|
||||||
switch action {
|
switch action {
|
||||||
case .displayEmojiPicker(let itemID, let selectedEmojis):
|
case .displayEmojiPicker(let selectedEmojis, let continuation):
|
||||||
actionsSubject.send(.presentEmojiPicker(itemID: itemID, selectedEmojis: selectedEmojis))
|
actionsSubject.send(.presentEmojiPicker(selectedEmojis: selectedEmojis, continuation: continuation))
|
||||||
case .displayReportContent(let itemID, let senderID):
|
case .displayReportContent(let itemID, let senderID):
|
||||||
actionsSubject.send(.presentReportContent(itemID: itemID, senderID: senderID))
|
actionsSubject.send(.presentReportContent(itemID: itemID, senderID: senderID))
|
||||||
case .displayCameraPicker:
|
case .displayCameraPicker:
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import UIKit
|
|||||||
enum TimelineInteractionHandlerAction {
|
enum TimelineInteractionHandlerAction {
|
||||||
case composer(action: TimelineComposerAction)
|
case composer(action: TimelineComposerAction)
|
||||||
|
|
||||||
case displayEmojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set<String>)
|
case displayEmojiPicker(selectedEmojis: Set<String>, continuation: EmojiPickerScreenContinuation)
|
||||||
case displayReportContent(itemID: TimelineItemIdentifier, senderID: String)
|
case displayReportContent(itemID: TimelineItemIdentifier, senderID: String)
|
||||||
case displayMessageForwarding(itemID: TimelineItemIdentifier)
|
case displayMessageForwarding(itemID: TimelineItemIdentifier)
|
||||||
case displayMediaUploadPreviewScreen(mediaURLs: [URL])
|
case displayMediaUploadPreviewScreen(mediaURLs: [URL])
|
||||||
@@ -59,6 +59,8 @@ class TimelineInteractionHandler {
|
|||||||
|
|
||||||
private var resumeVoiceMessagePlaybackAfterScrubbing = false
|
private var resumeVoiceMessagePlaybackAfterScrubbing = false
|
||||||
|
|
||||||
|
private var emojiPickerCancellable: AnyCancellable?
|
||||||
|
|
||||||
init(roomProxy: JoinedRoomProxyProtocol,
|
init(roomProxy: JoinedRoomProxyProtocol,
|
||||||
timelineController: TimelineControllerProtocol,
|
timelineController: TimelineControllerProtocol,
|
||||||
userSession: UserSessionProtocol,
|
userSession: UserSessionProtocol,
|
||||||
@@ -518,11 +520,21 @@ class TimelineInteractionHandler {
|
|||||||
func displayEmojiPicker(for itemID: TimelineItemIdentifier) {
|
func displayEmojiPicker(for itemID: TimelineItemIdentifier) {
|
||||||
guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID),
|
guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID),
|
||||||
timelineItem.isReactable,
|
timelineItem.isReactable,
|
||||||
let eventTimelineItem = timelineItem as? EventBasedTimelineItemProtocol else {
|
let eventTimelineItem = timelineItem as? EventBasedTimelineItemProtocol,
|
||||||
|
case let .event(_, eventOrTransactionID) = itemID else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let selectedEmojis = Set(eventTimelineItem.properties.reactions.compactMap { $0.isHighlighted ? $0.key : nil })
|
let selectedEmojis = Set(eventTimelineItem.properties.reactions.compactMap { $0.isHighlighted ? $0.key : nil })
|
||||||
actionsSubject.send(.displayEmojiPicker(itemID: itemID, selectedEmojis: selectedEmojis))
|
|
||||||
|
let (stream, continuation) = AsyncStream<String>.makeStream()
|
||||||
|
actionsSubject.send(.displayEmojiPicker(selectedEmojis: selectedEmojis, continuation: continuation))
|
||||||
|
|
||||||
|
emojiPickerCancellable = Task { [weak self] in
|
||||||
|
for await emoji in stream {
|
||||||
|
await self?.timelineController.toggleReaction(emoji, to: eventOrTransactionID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.asCancellable()
|
||||||
}
|
}
|
||||||
|
|
||||||
func processItemTap(_ itemID: TimelineItemIdentifier) async -> TimelineControllerAction {
|
func processItemTap(_ itemID: TimelineItemIdentifier) async -> TimelineControllerAction {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import OrderedCollections
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
enum TimelineViewModelAction {
|
enum TimelineViewModelAction {
|
||||||
case displayEmojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set<String>)
|
case displayEmojiPicker(selectedEmojis: Set<String>, continuation: EmojiPickerScreenContinuation)
|
||||||
case displayReportContent(itemID: TimelineItemIdentifier, senderID: String)
|
case displayReportContent(itemID: TimelineItemIdentifier, senderID: String)
|
||||||
case displayCameraPicker
|
case displayCameraPicker
|
||||||
case displayMediaPicker
|
case displayMediaPicker
|
||||||
|
|||||||
@@ -511,8 +511,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
|||||||
displayAlert(.audioRecodingPermissionError)
|
displayAlert(.audioRecodingPermissionError)
|
||||||
case .displayErrorToast(let title):
|
case .displayErrorToast(let title):
|
||||||
displayErrorToast(title)
|
displayErrorToast(title)
|
||||||
case .displayEmojiPicker(let itemID, let selectedEmojis):
|
case .displayEmojiPicker(let selectedEmojis, let continuation):
|
||||||
actionsSubject.send(.displayEmojiPicker(itemID: itemID, selectedEmojis: selectedEmojis))
|
actionsSubject.send(.displayEmojiPicker(selectedEmojis: selectedEmojis, continuation: continuation))
|
||||||
case .displayMessageForwarding(let itemID):
|
case .displayMessageForwarding(let itemID):
|
||||||
Task { await self.forwardMessage(itemID: itemID) }
|
Task { await self.forwardMessage(itemID: itemID) }
|
||||||
case .displayEditPollForm(let eventID, let poll):
|
case .displayEditPollForm(let eventID, let poll):
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import Testing
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
struct EmojiPickerScreenViewModelTests {
|
struct EmojiPickerScreenViewModelTests {
|
||||||
var timelineProxy: TimelineProxyMock!
|
var emojiPickerStream: AsyncStream<String>!
|
||||||
|
|
||||||
var viewModel: EmojiPickerScreenViewModel!
|
var viewModel: EmojiPickerScreenViewModel!
|
||||||
var context: EmojiPickerScreenViewModel.Context {
|
var context: EmojiPickerScreenViewModel.Context {
|
||||||
@@ -19,33 +19,43 @@ struct EmojiPickerScreenViewModelTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
mutating func toggleReaction() async throws {
|
mutating func selectEmoji() async throws {
|
||||||
|
// Given a freshly presented emoji picker.
|
||||||
setupViewModel()
|
setupViewModel()
|
||||||
let reaction = "👋"
|
let reaction = "👋"
|
||||||
|
|
||||||
|
// When the user taps an emoji.
|
||||||
let deferred = deferFulfillment(viewModel.actions) { $0 == .dismiss }
|
let deferred = deferFulfillment(viewModel.actions) { $0 == .dismiss }
|
||||||
|
context.send(viewAction: .emojiTapped(emoji: .init(id: "wave", value: reaction)))
|
||||||
|
|
||||||
await waitForConfirmation(timeout: .seconds(5)) { confirmation in
|
// Then the screen should dismiss and yield the emoji before finishing the stream.
|
||||||
timelineProxy.toggleReactionToClosure = { toggledReaction, _ in
|
|
||||||
defer { confirmation() }
|
|
||||||
#expect(toggledReaction == reaction)
|
|
||||||
return .success(())
|
|
||||||
}
|
|
||||||
|
|
||||||
context.send(viewAction: .emojiTapped(emoji: .init(id: "wave", value: reaction)))
|
|
||||||
}
|
|
||||||
|
|
||||||
try await deferred.fulfill()
|
try await deferred.fulfill()
|
||||||
|
var iterator = emojiPickerStream.makeAsyncIterator()
|
||||||
|
#expect(await iterator.next() == reaction)
|
||||||
|
#expect(await iterator.next() == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
mutating func stopFinishesTheStream() async {
|
||||||
|
// Given a freshly presented emoji picker.
|
||||||
|
setupViewModel()
|
||||||
|
|
||||||
|
// When it is stopped without a selection (e.g. the picker is dismissed).
|
||||||
|
viewModel.stop()
|
||||||
|
|
||||||
|
// Then the stream should finish without yielding an emoji.
|
||||||
|
var iterator = emojiPickerStream.makeAsyncIterator()
|
||||||
|
#expect(await iterator.next() == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Helpers
|
// MARK: - Helpers
|
||||||
|
|
||||||
private mutating func setupViewModel(selectedEmojis: Set<String> = []) {
|
private mutating func setupViewModel(selectedEmojis: Set<String> = []) {
|
||||||
timelineProxy = TimelineProxyMock(.init())
|
let (stream, continuation) = AsyncStream<String>.makeStream()
|
||||||
|
emojiPickerStream = stream
|
||||||
|
|
||||||
viewModel = EmojiPickerScreenViewModel(itemID: .randomEvent,
|
viewModel = EmojiPickerScreenViewModel(selectedEmojis: selectedEmojis,
|
||||||
selectedEmojis: selectedEmojis,
|
|
||||||
emojiProvider: EmojiProvider(appSettings: .volatile()),
|
emojiProvider: EmojiProvider(appSettings: .volatile()),
|
||||||
timelineController: TimelineControllerMock(.init(timelineProxy: timelineProxy)))
|
continuation: continuation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ struct SettingsScreenViewModelTests {
|
|||||||
// Then the picker should be dismissed and the custom status field shown.
|
// Then the picker should be dismissed and the custom status field shown.
|
||||||
#expect(!context.viewState.bindings.isPresentingStatusPicker)
|
#expect(!context.viewState.bindings.isPresentingStatusPicker)
|
||||||
#expect(context.viewState.bindings.isShowingCustomStatusField)
|
#expect(context.viewState.bindings.isShowingCustomStatusField)
|
||||||
#expect(context.viewState.userStatusRowMode == .custom)
|
#expect(context.viewState.userStatusRowMode == .custom(emoji: "😄"))
|
||||||
|
|
||||||
// When cancelling.
|
// When cancelling.
|
||||||
context.send(viewAction: .userStatus(.cancel))
|
context.send(viewAction: .userStatus(.cancel))
|
||||||
@@ -101,11 +101,32 @@ struct SettingsScreenViewModelTests {
|
|||||||
#expect(context.viewState.userStatusRowMode == .show(rawStatus))
|
#expect(context.viewState.userStatusRowMode == .show(rawStatus))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
mutating func selectingCustomStatusEmoji() async throws {
|
||||||
|
// Given a custom status field showing the default emoji.
|
||||||
|
setupViewModel()
|
||||||
|
context.send(viewAction: .userStatus(.customStatus))
|
||||||
|
#expect(context.viewState.userStatusRowMode == .custom(emoji: "😄"))
|
||||||
|
|
||||||
|
// When selecting an emoji from the picker.
|
||||||
|
try await selectCustomStatusEmoji("🎉")
|
||||||
|
|
||||||
|
// Then the custom status field should show the selected emoji.
|
||||||
|
#expect(context.viewState.userStatusRowMode == .custom(emoji: "🎉"))
|
||||||
|
|
||||||
|
// When cancelling and returning to the custom status field.
|
||||||
|
context.send(viewAction: .userStatus(.cancel))
|
||||||
|
context.send(viewAction: .userStatus(.customStatus))
|
||||||
|
|
||||||
|
// Then the emoji should have been reset to the default.
|
||||||
|
#expect(context.viewState.userStatusRowMode == .custom(emoji: "😄"))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
mutating func reportBug() async throws {
|
mutating func reportBug() async throws {
|
||||||
setupViewModel()
|
setupViewModel()
|
||||||
|
|
||||||
let deferred = deferFulfillment(viewModel.actions) { $0 == .reportBug }
|
let deferred = deferFulfillment(viewModel.actions) { $0.isReportBug }
|
||||||
context.send(viewAction: .reportBug)
|
context.send(viewAction: .reportBug)
|
||||||
try await deferred.fulfill()
|
try await deferred.fulfill()
|
||||||
}
|
}
|
||||||
@@ -114,7 +135,7 @@ struct SettingsScreenViewModelTests {
|
|||||||
mutating func analytics() async throws {
|
mutating func analytics() async throws {
|
||||||
setupViewModel()
|
setupViewModel()
|
||||||
|
|
||||||
let deferred = deferFulfillment(viewModel.actions) { $0 == .analytics }
|
let deferred = deferFulfillment(viewModel.actions) { $0.isAnalytics }
|
||||||
context.send(viewAction: .analytics)
|
context.send(viewAction: .analytics)
|
||||||
try await deferred.fulfill()
|
try await deferred.fulfill()
|
||||||
}
|
}
|
||||||
@@ -123,7 +144,7 @@ struct SettingsScreenViewModelTests {
|
|||||||
mutating func logout() async throws {
|
mutating func logout() async throws {
|
||||||
setupViewModel()
|
setupViewModel()
|
||||||
|
|
||||||
let deferred = deferFulfillment(viewModel.actions) { $0 == .logout }
|
let deferred = deferFulfillment(viewModel.actions) { $0.isLogout }
|
||||||
context.send(viewAction: .logout)
|
context.send(viewAction: .logout)
|
||||||
try await deferred.fulfill()
|
try await deferred.fulfill()
|
||||||
}
|
}
|
||||||
@@ -138,4 +159,48 @@ struct SettingsScreenViewModelTests {
|
|||||||
isInSecondaryWindow: false,
|
isInSecondaryWindow: false,
|
||||||
userIndicatorController: UserIndicatorControllerMock())
|
userIndicatorController: UserIndicatorControllerMock())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shows the emoji picker and selects the provided emoji through its continuation.
|
||||||
|
private func selectCustomStatusEmoji(_ emoji: String) async throws {
|
||||||
|
let pickerPresented = deferFulfillment(viewModel.actions) { $0.isUserStatusEmojiPicker }
|
||||||
|
context.send(viewAction: .userStatus(.pickCustomEmoji))
|
||||||
|
guard case let .userStatusEmojiPicker(continuation) = try await pickerPresented.fulfill() else {
|
||||||
|
Issue.record("Expected the emoji picker to be presented.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let emojiUpdated = deferFulfillment(context.observe(\.viewState.bindings.customStatusEmoji)) { $0 == Character(emoji) }
|
||||||
|
continuation.yield(emoji)
|
||||||
|
try await emojiUpdated.fulfill()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension SettingsScreenViewModelAction {
|
||||||
|
var isUserStatusEmojiPicker: Bool {
|
||||||
|
switch self {
|
||||||
|
case .userStatusEmojiPicker: true
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isReportBug: Bool {
|
||||||
|
switch self {
|
||||||
|
case .reportBug: true
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isAnalytics: Bool {
|
||||||
|
switch self {
|
||||||
|
case .analytics: true
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isLogout: Bool {
|
||||||
|
switch self {
|
||||||
|
case .logout: true
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user