From 6886cab4f5a8272b4da805e3c1ff160647fb6660 Mon Sep 17 00:00:00 2001 From: Scott Clampet <110618242+scottkicks@users.noreply.github.com> Date: Mon, 4 Dec 2023 13:19:07 -0600 Subject: [PATCH] [MBL-1024] Wire Up Block User Mutation (#1893) --- .../CommentRepliesViewController.swift | 33 ++++++---- .../Controllers/CommentsViewController.swift | 33 ++++++---- .../Controller/MessagesViewController.swift | 29 ++++---- .../ProjectPageViewController.swift | 33 ++++++---- KsApi/MockService.swift | 21 ++++-- KsApi/mutations/inputs/BlockUserInput.swift | 4 ++ .../ViewModels/CommentRepliesViewModel.swift | 37 +++++++---- .../CommentRepliesViewModelTests.swift | 66 ++++++++++++++++++- Library/ViewModels/CommentsViewModel.swift | 37 +++++++---- .../ViewModels/CommentsViewModelTests.swift | 49 ++++++++++++++ Library/ViewModels/MessagesViewModel.swift | 39 +++++++---- .../ViewModels/MessagesViewModelTests.swift | 49 +++++++++++++- Library/ViewModels/ProjectPageViewModel.swift | 37 +++++++---- .../ProjectPageViewModelTests.swift | 49 ++++++++++++++ 14 files changed, 405 insertions(+), 111 deletions(-) diff --git a/Kickstarter-iOS/Features/Comments/Controllers/CommentRepliesViewController.swift b/Kickstarter-iOS/Features/Comments/Controllers/CommentRepliesViewController.swift index b926c7d69d..19a5ff8d9e 100644 --- a/Kickstarter-iOS/Features/Comments/Controllers/CommentRepliesViewController.swift +++ b/Kickstarter-iOS/Features/Comments/Controllers/CommentRepliesViewController.swift @@ -169,26 +169,31 @@ final class CommentRepliesViewController: UITableViewController, MessageBannerVi } } - self.viewModel.outputs.userBlocked + self.viewModel.outputs.didBlockUser .observeForUI() - .observeValues { [weak self] success in - guard let self else { return } + .observeValues { [weak self] _ in + guard let self, let messageBanner = self.messageBannerViewController else { return } + self.commentComposer.isHidden = true + self.delegate?.commentRepliesViewControllerDidBlockUser(self) - if success { - self.messageBannerViewController? - .showBanner(with: .success, message: Strings.Block_user_success()) - self.delegate?.commentRepliesViewControllerDidBlockUser(self) - } else { - self.messageBannerViewController? - .showBanner(with: .error, message: Strings.Block_user_fail()) - } + messageBanner.showBanner(with: .success, message: Strings.Block_user_success()) + } + + self.viewModel.outputs.didBlockUserError + .observeForUI() + .observeValues { [weak self] _ in + guard let self, let messageBanner = self.messageBannerViewController else { return } + + messageBanner.showBanner(with: .error, message: Strings.Block_user_fail()) } } - private func presentBlockUserAlert(username: String) { + private func presentBlockUserAlert(username: String, userId: String) { let alert = UIAlertController - .blockUserAlert(username: username, blockUserHandler: { _ in self.viewModel.inputs.blockUser() }) + .blockUserAlert(username: username, blockUserHandler: { _ in + self.viewModel.inputs.blockUser(id: userId) + }) self.present(alert, animated: true) } @@ -198,7 +203,7 @@ final class CommentRepliesViewController: UITableViewController, MessageBannerVi let actionSheet = UIAlertController .blockUserActionSheet( - blockUserHandler: { _ in self.presentBlockUserAlert(username: author.name) }, + blockUserHandler: { _ in self.presentBlockUserAlert(username: author.name, userId: author.id) }, sourceView: cell, isIPad: self.traitCollection.horizontalSizeClass == .regular ) diff --git a/Kickstarter-iOS/Features/Comments/Controllers/CommentsViewController.swift b/Kickstarter-iOS/Features/Comments/Controllers/CommentsViewController.swift index 07a2776dd7..4fc9caec91 100644 --- a/Kickstarter-iOS/Features/Comments/Controllers/CommentsViewController.swift +++ b/Kickstarter-iOS/Features/Comments/Controllers/CommentsViewController.swift @@ -183,24 +183,29 @@ internal final class CommentsViewController: UITableViewController, MessageBanne self?.presentHelpWebViewController(with: helpType) } - self.viewModel.outputs.userBlocked + self.viewModel.outputs.didBlockUser .observeForUI() - .observeValues { [weak self] success in - self?.commentComposer.isHidden = true - - if success { - self?.messageBannerViewController? - .showBanner(with: .success, message: Strings.Block_user_success()) - } else { - self?.messageBannerViewController? - .showBanner(with: .error, message: Strings.Block_user_fail()) - } + .observeValues { [weak self] _ in + guard let self, let messageBanner = self.messageBannerViewController else { return } + + self.commentComposer.isHidden = true + messageBanner.showBanner(with: .success, message: Strings.Block_user_success()) + } + + self.viewModel.outputs.didBlockUserError + .observeForUI() + .observeValues { [weak self] _ in + guard let self, let messageBanner = self.messageBannerViewController else { return } + + messageBanner.showBanner(with: .error, message: Strings.Block_user_fail()) } } - private func presentBlockUserAlert(username: String) { + private func presentBlockUserAlert(username: String, userId: String) { let alert = UIAlertController - .blockUserAlert(username: username, blockUserHandler: { _ in self.viewModel.inputs.blockUser() }) + .blockUserAlert(username: username, blockUserHandler: { _ in + self.viewModel.inputs.blockUser(id: userId) + }) self.present(alert, animated: true) } @@ -268,7 +273,7 @@ extension CommentsViewController: CommentCellDelegate { let actionSheet = UIAlertController .blockUserActionSheet( - blockUserHandler: { _ in self.presentBlockUserAlert(username: author.name) }, + blockUserHandler: { _ in self.presentBlockUserAlert(username: author.name, userId: author.id) }, sourceView: cell, isIPad: self.traitCollection.horizontalSizeClass == .regular ) diff --git a/Kickstarter-iOS/Features/Messages/Controller/MessagesViewController.swift b/Kickstarter-iOS/Features/Messages/Controller/MessagesViewController.swift index 089ec184fa..9de37126e8 100644 --- a/Kickstarter-iOS/Features/Messages/Controller/MessagesViewController.swift +++ b/Kickstarter-iOS/Features/Messages/Controller/MessagesViewController.swift @@ -116,15 +116,18 @@ internal final class MessagesViewController: UITableViewController, MessageBanne self.viewModel.outputs.didBlockUser .observeForUI() - .observeValues { [weak self] success in - - if success { - self?.messageBannerViewController? - .showBanner(with: .success, message: Strings.Block_user_success()) - } else { - self?.messageBannerViewController? - .showBanner(with: .error, message: Strings.Block_user_fail()) - } + .observeValues { [weak self] _ in + guard let self, let messageBanner = self.messageBannerViewController else { return } + + messageBanner.showBanner(with: .success, message: Strings.Block_user_success()) + } + + self.viewModel.outputs.didBlockUserError + .observeForUI() + .observeValues { [weak self] _ in + guard let self, let messageBanner = self.messageBannerViewController else { return } + + messageBanner.showBanner(with: .error, message: Strings.Block_user_fail()) } } @@ -189,9 +192,11 @@ internal final class MessagesViewController: UITableViewController, MessageBanne } } - private func presentBlockUserAlert(username: String) { + private func presentBlockUserAlert(username: String, userId: Int) { let alert = UIAlertController - .blockUserAlert(username: username, blockUserHandler: { _ in self.viewModel.inputs.blockUser() }) + .blockUserAlert(username: username, blockUserHandler: { _ in + self.viewModel.inputs.blockUser(id: "\(userId)") + }) self.present(alert, animated: true) } } @@ -222,7 +227,7 @@ extension MessagesViewController: MessageCellDelegate { let actionSheet = UIAlertController .blockUserActionSheet( - blockUserHandler: { _ in self.presentBlockUserAlert(username: user.name) }, + blockUserHandler: { _ in self.presentBlockUserAlert(username: user.name, userId: user.id) }, sourceView: cell, isIPad: self.traitCollection.horizontalSizeClass == .regular ) diff --git a/Kickstarter-iOS/Features/ProjectPage/Controller/ProjectPageViewController.swift b/Kickstarter-iOS/Features/ProjectPage/Controller/ProjectPageViewController.swift index fe6c2102c6..df8d750bcc 100644 --- a/Kickstarter-iOS/Features/ProjectPage/Controller/ProjectPageViewController.swift +++ b/Kickstarter-iOS/Features/ProjectPage/Controller/ProjectPageViewController.swift @@ -537,16 +537,20 @@ public final class ProjectPageViewController: UIViewController, MessageBannerVie // TODO: Use this flag to hide or show the Report this project label [MBL-983](https://kickstarter.atlassian.net/browse/MBL-983) } - self.viewModel.outputs.userBlocked + self.viewModel.outputs.didBlockUser .observeForUI() - .observeValues { [weak self] success in - if success { - self?.messageBannerViewController? - .showBanner(with: .success, message: Strings.Block_user_success()) - } else { - self?.messageBannerViewController? - .showBanner(with: .error, message: Strings.Block_user_fail()) - } + .observeValues { [weak self] _ in + guard let self, let messageBanner = self.messageBannerViewController else { return } + + messageBanner.showBanner(with: .success, message: Strings.Block_user_success()) + } + + self.viewModel.outputs.didBlockUserError + .observeForUI() + .observeValues { [weak self] _ in + guard let self, let messageBanner = self.messageBannerViewController else { return } + + messageBanner.showBanner(with: .error, message: Strings.Block_user_fail()) } } @@ -737,9 +741,12 @@ public final class ProjectPageViewController: UIViewController, MessageBannerVie } } - private func presentBlockUserAlert(username: String) { + private func presentBlockUserAlert(username: String, userId: Int) { let alert = UIAlertController - .blockUserAlert(username: username, blockUserHandler: { _ in self.viewModel.inputs.blockUser() }) + .blockUserAlert(username: username, blockUserHandler: { _ in + self.viewModel.inputs.blockUser(id: userId) + }) + self.present(alert, animated: true) } @@ -996,7 +1003,9 @@ extension ProjectPageViewController: ProjectPamphletMainCellDelegate { let actionSheet = UIAlertController .blockUserActionSheet( - blockUserHandler: { _ in self.presentBlockUserAlert(username: project.creator.name) }, + blockUserHandler: { _ in + self.presentBlockUserAlert(username: project.creator.name, userId: project.creator.id) + }, viewProfileHandler: { _ in self.goToCreatorProfile(forProject: project) }, sourceView: cell, isIPad: self.traitCollection.horizontalSizeClass == .regular diff --git a/KsApi/MockService.swift b/KsApi/MockService.swift index cb828a7fff..dfba46a61c 100644 --- a/KsApi/MockService.swift +++ b/KsApi/MockService.swift @@ -19,6 +19,8 @@ fileprivate let addPaymentSheetPaymentSourceResult: Result? + fileprivate let blockUserResult: Result? + fileprivate let cancelBackingResult: Result? fileprivate let changeCurrencyResult: Result? @@ -219,6 +221,7 @@ addNewCreditCardResult: Result? = nil, addPaymentSheetPaymentSourceResult: Result? = nil, apolloClient: ApolloClientType? = nil, + blockUserResult: Result? = nil, cancelBackingResult: Result? = nil, changeEmailResult: Result? = nil, changePasswordResult: Result? = nil, @@ -332,6 +335,8 @@ self.apolloClient = apolloClient ?? MockGraphQLClient.shared.client + self.blockUserResult = blockUserResult + self.cancelBackingResult = cancelBackingResult self.changeEmailResult = changeEmailResult @@ -543,6 +548,18 @@ return client.performWithResult(mutation: mutation, result: self.addPaymentSheetPaymentSourceResult) } + public func blockUser(input: BlockUserInput) + -> SignalProducer { + guard let client = self.apolloClient else { + return .empty + } + + let mutation = GraphAPI + .BlockUserMutation(input: GraphAPI.BlockUserInput(blockUserId: input.blockUserId)) + + return client.performWithResult(mutation: mutation, result: self.blockUserResult) + } + public func cancelBacking(input: CancelBackingInput) -> SignalProducer { guard let client = self.apolloClient else { @@ -1681,10 +1698,6 @@ return SignalProducer(value: self.fetchUpdateResponse) } - func blockUser(input _: BlockUserInput) -> SignalProducer { - return SignalProducer(value: EmptyResponseEnvelope()) - } - internal func previewUrl(forDraft draft: UpdateDraft) -> URL? { return URL( string: "https://\(Secrets.Api.Endpoint.production)/projects/\(draft.update.projectId)/updates/" diff --git a/KsApi/mutations/inputs/BlockUserInput.swift b/KsApi/mutations/inputs/BlockUserInput.swift index 9accdb0cde..b64c70b68d 100644 --- a/KsApi/mutations/inputs/BlockUserInput.swift +++ b/KsApi/mutations/inputs/BlockUserInput.swift @@ -2,4 +2,8 @@ import Foundation public struct BlockUserInput: GraphMutationInput, Encodable { let blockUserId: String + + public init(blockUserId: String) { + self.blockUserId = blockUserId + } } diff --git a/Library/ViewModels/CommentRepliesViewModel.swift b/Library/ViewModels/CommentRepliesViewModel.swift index ffe4c96860..8382f7fae2 100644 --- a/Library/ViewModels/CommentRepliesViewModel.swift +++ b/Library/ViewModels/CommentRepliesViewModel.swift @@ -6,7 +6,7 @@ import ReactiveSwift public protocol CommentRepliesViewModelInputs { /// Call when block user is tapped - func blockUser() + func blockUser(id: String) /** Call with the comment and project that we are viewing replies for. `Comment` can be provided to minimize @@ -65,8 +65,11 @@ public protocol CommentRepliesViewModelOutputs { /// Emits when a pagination error has occurred. var showPaginationErrorState: Signal<(), Never> { get } - /// Emits when a request to block a user has been made - var userBlocked: Signal { get } + /// Emits when a block user request is successful. + var didBlockUser: Signal<(), Never> { get } + + /// Emits when a block user request fails. + var didBlockUserError: Signal<(), Never> { get } } public protocol CommentRepliesViewModelType { @@ -248,18 +251,25 @@ public final class CommentRepliesViewModel: CommentRepliesViewModelType, .skipNil() .takeWhen(self.dataSourceLoadedProperty.signal) - // TODO: Call blocking GraphQL mutation - self.userBlocked = self.blockUserProperty.signal.map { true } + let blockUserEvent = self.blockUserProperty.signal + .map(BlockUserInput.init(blockUserId:)) + .switchMap { input in + AppEnvironment.current.apiService + .blockUser(input: input) + .ksr_delay(AppEnvironment.current.apiDelayInterval, on: AppEnvironment.current.scheduler) + .materialize() + } - self.userBlocked.observeValues { didBlock in - guard didBlock == true else { return } - NotificationCenter.default.post(.init(name: .ksr_blockedUser)) - } + self.didBlockUser = blockUserEvent.values().ignoreValues() + .map { _ in NotificationCenter.default.post(.init(name: .ksr_blockedUser)) } + + // TODO: Display proper error messaging from the backend + self.didBlockUserError = blockUserEvent.errors().ignoreValues() } - private let blockUserProperty = MutableProperty(()) - public func blockUser() { - self.blockUserProperty.value = () + private let blockUserProperty = MutableProperty("") + public func blockUser(id: String) { + self.blockUserProperty.value = id } private let didSelectCommentProperty = MutableProperty(nil) @@ -319,7 +329,8 @@ public final class CommentRepliesViewModel: CommentRepliesViewModelType, public let resetCommentComposer: Signal<(), Never> public let scrollToReply: Signal public let showPaginationErrorState: Signal<(), Never> - public let userBlocked: Signal + public var didBlockUser: Signal<(), Never> + public var didBlockUserError: Signal<(), Never> public var inputs: CommentRepliesViewModelInputs { return self } public var outputs: CommentRepliesViewModelOutputs { return self } diff --git a/Library/ViewModels/CommentRepliesViewModelTests.swift b/Library/ViewModels/CommentRepliesViewModelTests.swift index 9d9e50151a..8cea1063da 100644 --- a/Library/ViewModels/CommentRepliesViewModelTests.swift +++ b/Library/ViewModels/CommentRepliesViewModelTests.swift @@ -12,6 +12,8 @@ internal final class CommentRepliesViewModelTests: TestCase { private let configureCommentComposerBecomeFirstResponder = TestObserver() private let configureCommentComposerViewURL = TestObserver() private let configureCommentComposerViewCanPostComment = TestObserver() + private let didBlockUser = TestObserver<(), Never>() + private let didBlockUserError = TestObserver<(), Never>() private let loadCommentIntoDataSourceComment = TestObserver() private let loadFailableReplyIntoDataSource = TestObserver() private let loadFailableCommentIDIntoDataSource = TestObserver() @@ -33,7 +35,8 @@ internal final class CommentRepliesViewModelTests: TestCase { self.vm.outputs.configureCommentComposerViewWithData.map(\.canPostComment) .observe(self.configureCommentComposerViewCanPostComment.observer) self.vm.outputs.loadCommentIntoDataSource.observe(self.loadCommentIntoDataSourceComment.observer) - + self.vm.outputs.didBlockUser.observe(self.didBlockUser.observer) + self.vm.outputs.didBlockUserError.observe(self.didBlockUserError.observer) self.vm.outputs.loadFailableReplyIntoDataSource.map(first) .observe(self.loadFailableReplyIntoDataSource.observer) @@ -108,6 +111,67 @@ internal final class CommentRepliesViewModelTests: TestCase { } } + func testDidBlockUser_EmitsOnSuccess() { + let envelope = EmptyResponseEnvelope() + + withEnvironment(apiService: MockService(blockUserResult: .success(envelope)), currentUser: .template) { + self.vm.inputs + .configureWith( + comment: .template, + project: .template, + update: nil, + inputAreaBecomeFirstResponder: false, + replyId: nil + ) + + self.vm.inputs.viewDidLoad() + self.vm.inputs.viewDidAppear() + + self.didBlockUser.assertValueCount(0) + self.didBlockUserError.assertValueCount(0) + + self.vm.inputs.blockUser(id: "\(User.template.id)") + + self.scheduler.advance() + + self.didBlockUser.assertValueCount(1) + self.didBlockUserError.assertValueCount(0) + } + } + + func testDidBlockUserError_EmitsOnFailure() { + let error = ErrorEnvelope( + errorMessages: ["block user request error"], + ksrCode: .GraphQLError, + httpCode: 401, + exception: nil + ) + + withEnvironment(apiService: MockService(blockUserResult: .failure(error)), currentUser: .template) { + self.vm.inputs + .configureWith( + comment: .template, + project: .template, + update: nil, + inputAreaBecomeFirstResponder: false, + replyId: nil + ) + + self.vm.inputs.viewDidLoad() + self.vm.inputs.viewDidAppear() + + self.didBlockUser.assertValueCount(0) + self.didBlockUserError.assertValueCount(0) + + self.vm.inputs.blockUser(id: "\(User.template.id)") + + self.scheduler.advance() + + self.didBlockUser.assertValueCount(0) + self.didBlockUserError.assertValueCount(1) + } + } + func testOutput_ConfigureCommentComposerViewWithData_IsLoggedIn_IsBacking_False_HasBlockedCommentComposer() { let user = User.template |> \.id .~ 12_345 diff --git a/Library/ViewModels/CommentsViewModel.swift b/Library/ViewModels/CommentsViewModel.swift index abc0e1dd7d..5961608a4f 100644 --- a/Library/ViewModels/CommentsViewModel.swift +++ b/Library/ViewModels/CommentsViewModel.swift @@ -6,7 +6,7 @@ import ReactiveSwift public protocol CommentsViewModelInputs { /// Call when block user is tapped - func blockUser() + func blockUser(id: String) /// Call when the delegate method for the CommentCellDelegate is called. func commentCellDidTapReply(comment: Comment) @@ -68,8 +68,11 @@ public protocol CommentsViewModelOutputs { /// Emits when a comment has been posted and we should scroll to top and reset the composer. var resetCommentComposerAndScrollToTop: Signal<(), Never> { get } - /// Emits when a request to block a user has been made - var userBlocked: Signal { get } + /// Emits when a block user request is successful. + var didBlockUser: Signal<(), Never> { get } + + /// Emits when a block user request fails. + var didBlockUserError: Signal<(), Never> { get } } public protocol CommentsViewModelType { @@ -357,13 +360,20 @@ public final class CommentsViewModel: CommentsViewModelType, .map(HelpType.helpType) .skipNil() - // TODO: Call blocking GraphQL mutation - self.userBlocked = self.blockUserProperty.signal.map { true } + let blockUserEvent = self.blockUserProperty.signal + .map(BlockUserInput.init(blockUserId:)) + .switchMap { input in + AppEnvironment.current.apiService + .blockUser(input: input) + .ksr_delay(AppEnvironment.current.apiDelayInterval, on: AppEnvironment.current.scheduler) + .materialize() + } + + self.didBlockUser = blockUserEvent.values().ignoreValues() + .map { _ in NotificationCenter.default.post(.init(name: .ksr_blockedUser)) } - self.userBlocked.observeValues { didBlock in - guard didBlock == true else { return } - NotificationCenter.default.post(.init(name: .ksr_blockedUser)) - } + // TODO: Display proper error messaging from the backend + self.didBlockUserError = blockUserEvent.errors().ignoreValues() } // Properties to assist with injecting these values into the existing data streams. @@ -371,9 +381,9 @@ public final class CommentsViewModel: CommentsViewModelType, private let retryingComment = MutableProperty<(Comment, String)?>(nil) private let failableOrComment = MutableProperty<(Comment, String)?>(nil) - private let blockUserProperty = MutableProperty(()) - public func blockUser() { - self.blockUserProperty.value = () + private let blockUserProperty = MutableProperty("") + public func blockUser(id: String) { + self.blockUserProperty.value = id } private let commentCellDidTapViewRepliesProperty = MutableProperty(nil) @@ -437,7 +447,8 @@ public final class CommentsViewModel: CommentsViewModelType, public let loadCommentsAndProjectIntoDataSource: Signal<([Comment], Project, Bool), Never> public let showHelpWebViewController: Signal public let resetCommentComposerAndScrollToTop: Signal<(), Never> - public let userBlocked: Signal + public let didBlockUser: Signal<(), Never> + public var didBlockUserError: Signal<(), Never> public var inputs: CommentsViewModelInputs { return self } public var outputs: CommentsViewModelOutputs { return self } diff --git a/Library/ViewModels/CommentsViewModelTests.swift b/Library/ViewModels/CommentsViewModelTests.swift index f38810cd90..cf79b984a5 100644 --- a/Library/ViewModels/CommentsViewModelTests.swift +++ b/Library/ViewModels/CommentsViewModelTests.swift @@ -15,6 +15,8 @@ internal final class CommentsViewModelTests: TestCase { private let configureCommentComposerViewURL = TestObserver() private let configureCommentComposerViewCanPostComment = TestObserver() private let configureFooterViewWithState = TestObserver() + private let didBlockUser = TestObserver<(), Never>() + private let didBlockUserError = TestObserver<(), Never>() private let goToCommentRepliesComment = TestObserver() private let goToCommentRepliesProject = TestObserver() private let goToCommentRepliesUpdate = TestObserver() @@ -35,6 +37,8 @@ internal final class CommentsViewModelTests: TestCase { .observe(self.configureCommentComposerViewURL.observer) self.vm.outputs.configureCommentComposerViewWithData.map(\.canPostComment) .observe(self.configureCommentComposerViewCanPostComment.observer) + self.vm.outputs.didBlockUser.observe(self.didBlockUser.observer) + self.vm.outputs.didBlockUserError.observe(self.didBlockUserError.observer) self.vm.outputs.configureFooterViewWithState.observe(self.configureFooterViewWithState.observer) self.vm.outputs.goToRepliesWithCommentProjectUpdateAndBecomeFirstResponder.map { $0.0 } .observe(self.goToCommentRepliesComment.observer) @@ -142,6 +146,51 @@ internal final class CommentsViewModelTests: TestCase { } } + func testDidBlockUser_EmitsOnSuccess() { + let envelope = EmptyResponseEnvelope() + + withEnvironment(apiService: MockService(blockUserResult: .success(envelope)), currentUser: .template) { + self.vm.inputs.configureWith(project: .template, update: nil) + + self.vm.inputs.viewDidLoad() + + self.didBlockUser.assertValueCount(0) + self.didBlockUserError.assertValueCount(0) + + self.vm.inputs.blockUser(id: "\(User.template.id)") + + self.scheduler.advance() + + self.didBlockUser.assertValueCount(1) + self.didBlockUserError.assertValueCount(0) + } + } + + func testDidBlockUserError_EmitsOnFailure() { + let error = ErrorEnvelope( + errorMessages: ["block user request error"], + ksrCode: .GraphQLError, + httpCode: 401, + exception: nil + ) + + withEnvironment(apiService: MockService(blockUserResult: .failure(error)), currentUser: .template) { + self.vm.inputs.configureWith(project: .template, update: nil) + + self.vm.inputs.viewDidLoad() + + self.didBlockUser.assertValueCount(0) + self.didBlockUserError.assertValueCount(0) + + self.vm.inputs.blockUser(id: "\(User.template.id)") + + self.scheduler.advance() + + self.didBlockUser.assertValueCount(0) + self.didBlockUserError.assertValueCount(1) + } + } + func testCommentComposerHidden_WhenUserIsNotLoggedIn() { withEnvironment(currentUser: nil) { self.vm.inputs.configureWith(project: .template, update: nil) diff --git a/Library/ViewModels/MessagesViewModel.swift b/Library/ViewModels/MessagesViewModel.swift index 408aeeeb03..d102c4b069 100644 --- a/Library/ViewModels/MessagesViewModel.swift +++ b/Library/ViewModels/MessagesViewModel.swift @@ -8,7 +8,7 @@ public protocol MessagesViewModelInputs { func backingInfoPressed() /// Call when block user is tapped - func blockUser() + func blockUser(id: String) /// Configures the view model with either a message thread or a project and a backing. func configureWith(data: Either) @@ -63,8 +63,11 @@ public protocol MessagesViewModelOutputs { /// Emits when the thread has been marked as read. var successfullyMarkedAsRead: Signal<(), Never> { get } - /// Emits whether a request to block a user was successful. - var didBlockUser: Signal { get } + /// Emits when a block user request is successful. + var didBlockUser: Signal<(), Never> { get } + + /// Emits when a block user request fails. + var didBlockUserError: Signal<(), Never> { get } } public protocol MessagesViewModelType { @@ -197,16 +200,23 @@ public final class MessagesViewModel: MessagesViewModelType, MessagesViewModelIn } .ignoreValues() - // TODO: Call blocking GraphQL mutation - self.didBlockUser = self.blockUserProperty.signal.map { false } + let blockUserEvent = self.blockUserProperty.signal + .map(BlockUserInput.init(blockUserId:)) + .switchMap { input in + AppEnvironment.current.apiService + .blockUser(input: input) + .ksr_delay(AppEnvironment.current.apiDelayInterval, on: AppEnvironment.current.scheduler) + .materialize() + } + + self.didBlockUser = blockUserEvent.values().ignoreValues() + .map { _ in NotificationCenter.default.post(.init(name: .ksr_blockedUser)) } - self.didBlockUser.observeValues { didBlock in - guard didBlock == true else { return } - NotificationCenter.default.post(.init(name: .ksr_blockedUser)) - } + // TODO: Display proper error messaging from the backend + self.didBlockUserError = blockUserEvent.errors().ignoreValues() self.participantPreviouslyBlocked = self.project - .map { $0.creator.isBlocked ?? false } + .map { $0.creator.isBlocked } .takeWhen(self.viewWillAppearProperty.signal) } @@ -215,9 +225,9 @@ public final class MessagesViewModel: MessagesViewModelType, MessagesViewModelIn self.backingInfoPressedProperty.value = () } - private let blockUserProperty = MutableProperty(()) - public func blockUser() { - self.blockUserProperty.value = () + private let blockUserProperty = MutableProperty("") + public func blockUser(id: String) { + self.blockUserProperty.value = id } private let configData = MutableProperty?>(nil) @@ -260,7 +270,8 @@ public final class MessagesViewModel: MessagesViewModelType, MessagesViewModelIn public let project: Signal public let replyButtonIsEnabled: Signal public let successfullyMarkedAsRead: Signal<(), Never> - public let didBlockUser: Signal + public let didBlockUser: Signal<(), Never> + public let didBlockUserError: Signal<(), Never> public var inputs: MessagesViewModelInputs { return self } public var outputs: MessagesViewModelOutputs { return self } diff --git a/Library/ViewModels/MessagesViewModelTests.swift b/Library/ViewModels/MessagesViewModelTests.swift index 3650e6c533..394d70d598 100644 --- a/Library/ViewModels/MessagesViewModelTests.swift +++ b/Library/ViewModels/MessagesViewModelTests.swift @@ -21,7 +21,8 @@ internal final class MessagesViewModelTests: TestCase { fileprivate let project = TestObserver() fileprivate let replyButtonIsEnabled = TestObserver() fileprivate let successfullyMarkedAsRead = TestObserver<(), Never>() - fileprivate let didBlockUser = TestObserver() + fileprivate let didBlockUser = TestObserver<(), Never>() + fileprivate let didBlockUserError = TestObserver<(), Never>() override func setUp() { super.setUp() @@ -41,6 +42,7 @@ internal final class MessagesViewModelTests: TestCase { self.vm.outputs.replyButtonIsEnabled.observe(self.replyButtonIsEnabled.observer) self.vm.outputs.successfullyMarkedAsRead.observe(self.successfullyMarkedAsRead.observer) self.vm.outputs.didBlockUser.observe(self.didBlockUser.observer) + self.vm.outputs.didBlockUserError.observe(self.didBlockUserError.observer) AppEnvironment.login(AccessTokenEnvelope(accessToken: "deadbeef", user: User.template)) } @@ -277,6 +279,51 @@ internal final class MessagesViewModelTests: TestCase { } } + func testDidBlockUser_EmitsOnSuccess() { + let envelope = EmptyResponseEnvelope() + + withEnvironment(apiService: MockService(blockUserResult: .success(envelope)), currentUser: .template) { + self.vm.inputs.configureWith(data: .right((project: .template, backing: .template))) + + self.vm.inputs.viewDidLoad() + + self.didBlockUser.assertValueCount(0) + self.didBlockUserError.assertValueCount(0) + + self.vm.inputs.blockUser(id: "\(User.template.id)") + + self.scheduler.advance() + + self.didBlockUser.assertValueCount(1) + self.didBlockUserError.assertValueCount(0) + } + } + + func testDidBlockUserError_EmitsOnFailure() { + let error = ErrorEnvelope( + errorMessages: ["block user request error"], + ksrCode: .GraphQLError, + httpCode: 401, + exception: nil + ) + + withEnvironment(apiService: MockService(blockUserResult: .failure(error)), currentUser: .template) { + self.vm.inputs.configureWith(data: .right((project: .template, backing: .template))) + + self.vm.inputs.viewDidLoad() + + self.didBlockUser.assertValueCount(0) + self.didBlockUserError.assertValueCount(0) + + self.vm.inputs.blockUser(id: "\(User.template.id)") + + self.scheduler.advance() + + self.didBlockUser.assertValueCount(0) + self.didBlockUserError.assertValueCount(1) + } + } + func testParticipantPreviouslyBlockedFlow_False() { let creator = User.template |> \.id .~ 20 diff --git a/Library/ViewModels/ProjectPageViewModel.swift b/Library/ViewModels/ProjectPageViewModel.swift index 779f20a1ca..1de1c890d8 100644 --- a/Library/ViewModels/ProjectPageViewModel.swift +++ b/Library/ViewModels/ProjectPageViewModel.swift @@ -11,7 +11,7 @@ public protocol ProjectPageViewModelInputs { func applicationDidEnterBackground() /// Call when block user is tapped - func blockUser() + func blockUser(id: Int) /// Call with the project given to the view controller. func configureWith(projectOrParam: Either, refTag: RefTag?) @@ -150,8 +150,11 @@ public protocol ProjectPageViewModelOutputs { /// Emits a prelaunch save state that updates the navigation bar's watch project state. var updateWatchProjectWithPrelaunchProjectState: Signal { get } - /// Emits when a request to block a user has been made - var userBlocked: Signal { get } + /// Emits when a block user request is successful. + var didBlockUser: Signal<(), Never> { get } + + /// Emits when a block user request fails. + var didBlockUserError: Signal<(), Never> { get } } public protocol ProjectPageViewModelType { @@ -502,13 +505,20 @@ public final class ProjectPageViewModel: ProjectPageViewModelType, ProjectPageVi self.goToURL = self.didSelectCampaignImageLinkProperty.signal.skipNil() - // TODO: Call blocking GraphQL mutation - self.userBlocked = self.blockUserProperty.signal.map { true } + let blockUserEvent = self.blockUserProperty.signal + .map { BlockUserInput.init(blockUserId: "\($0)") } + .switchMap { input in + AppEnvironment.current.apiService + .blockUser(input: input) + .ksr_delay(AppEnvironment.current.apiDelayInterval, on: AppEnvironment.current.scheduler) + .materialize() + } - self.userBlocked.observeValues { didBlock in - guard didBlock == true else { return } - NotificationCenter.default.post(.init(name: .ksr_blockedUser)) - } + self.didBlockUser = blockUserEvent.values().ignoreValues() + .map { _ in NotificationCenter.default.post(.init(name: .ksr_blockedUser)) } + + // TODO: Display proper error messaging from the backend + self.didBlockUserError = blockUserEvent.errors().ignoreValues() } fileprivate let askAQuestionCellTappedProperty = MutableProperty(()) @@ -521,9 +531,9 @@ public final class ProjectPageViewModel: ProjectPageViewModelType, ProjectPageVi self.applicationDidEnterBackgroundProperty.value = () } - fileprivate let blockUserProperty = MutableProperty(()) - public func blockUser() { - self.blockUserProperty.value = () + fileprivate let blockUserProperty = MutableProperty(0) + public func blockUser(id: Int) { + self.blockUserProperty.value = id } private let configDataProperty = MutableProperty<(Either, RefTag?)?>(nil) @@ -651,7 +661,8 @@ public final class ProjectPageViewModel: ProjectPageViewModelType, ProjectPageVi public let updateDataSource: Signal<(NavigationSection, Project, RefTag?, [Bool], [URL]), Never> public let updateFAQsInDataSource: Signal<(Project, RefTag?, [Bool]), Never> public let updateWatchProjectWithPrelaunchProjectState: Signal - public let userBlocked: Signal + public let didBlockUser: Signal<(), Never> + public let didBlockUserError: Signal<(), Never> public var inputs: ProjectPageViewModelInputs { return self } public var outputs: ProjectPageViewModelOutputs { return self } diff --git a/Library/ViewModels/ProjectPageViewModelTests.swift b/Library/ViewModels/ProjectPageViewModelTests.swift index ec2d787a55..32433ed21f 100644 --- a/Library/ViewModels/ProjectPageViewModelTests.swift +++ b/Library/ViewModels/ProjectPageViewModelTests.swift @@ -33,6 +33,8 @@ final class ProjectPageViewModelTests: TestCase { private let configurePledgeCTAViewIsLoading = TestObserver() private let configurePledgeCTAViewRefTag = TestObserver() private let configureProjectNavigationSelectorView = TestObserver<(Project, RefTag?), Never>() + private let didBlockUser = TestObserver<(), Never>() + private let didBlockUserError = TestObserver<(), Never>() private let dismissManagePledgeAndShowMessageBannerWithMessage = TestObserver() private let goToComments = TestObserver() private let goToManagePledgeProjectParam = TestObserver() @@ -99,6 +101,8 @@ final class ProjectPageViewModelTests: TestCase { self.vm.outputs.configurePledgeCTAView.map(second).observe(self.configurePledgeCTAViewIsLoading.observer) self.vm.outputs.configurePledgeCTAView.map(third).observe(self.configurePledgeCTAViewContext.observer) + self.vm.outputs.didBlockUser.observe(self.didBlockUser.observer) + self.vm.outputs.didBlockUserError.observe(self.didBlockUserError.observer) self.vm.outputs.dismissManagePledgeAndShowMessageBannerWithMessage .observe(self.dismissManagePledgeAndShowMessageBannerWithMessage.observer) self.vm.outputs.goToComments.observe(self.goToComments.observer) @@ -315,6 +319,51 @@ final class ProjectPageViewModelTests: TestCase { } } + func testDidBlockUser_EmitsOnSuccess() { + let envelope = EmptyResponseEnvelope() + + withEnvironment(apiService: MockService(blockUserResult: .success(envelope)), currentUser: .template) { + self.vm.inputs.configureWith(projectOrParam: .left(self.projectWithEmptyProperties), refTag: .category) + + self.vm.inputs.viewDidLoad() + + self.didBlockUser.assertValueCount(0) + self.didBlockUserError.assertValueCount(0) + + self.vm.inputs.blockUser(id: User.template.id) + + self.scheduler.advance() + + self.didBlockUser.assertValueCount(1) + self.didBlockUserError.assertValueCount(0) + } + } + + func testDidBlockUserError_EmitsOnFailure() { + let error = ErrorEnvelope( + errorMessages: ["block user request error"], + ksrCode: .GraphQLError, + httpCode: 401, + exception: nil + ) + + withEnvironment(apiService: MockService(blockUserResult: .failure(error)), currentUser: .template) { + self.vm.inputs.configureWith(projectOrParam: .left(self.projectWithEmptyProperties), refTag: .category) + + self.vm.inputs.viewDidLoad() + + self.didBlockUser.assertValueCount(0) + self.didBlockUserError.assertValueCount(0) + + self.vm.inputs.blockUser(id: User.template.id) + + self.scheduler.advance() + + self.didBlockUser.assertValueCount(0) + self.didBlockUserError.assertValueCount(1) + } + } + func testConfigureProjectPageViewControllerDataSourceProject_NonUS_ProjectCurrency_US_ProjectCountry() { let USCurrencyProject = self.projectWithEmptyProperties |> Project.lens.country .~ .us