From 0272dcadbd14b150d5ba0b5257b85495ba17ed38 Mon Sep 17 00:00:00 2001 From: Scott Clampet <110618242+scottkicks@users.noreply.github.com> Date: Thu, 2 Nov 2023 08:33:26 -0500 Subject: [PATCH] [MBL-1021] Block User Action Sheet (#1874) * create BlockUserActionSheetView * present action sheet when tapping creator cell on project screen * gated behind featureBlockUsersEnabled() * goes directly to creator profile if flag is false. this is the current app behavior * formatting * show action sheet when tapping top-level comment cell header * give action sheet view a tag so that we can avoid adding to the view stack multiple times * present actions sheet on reply comment cell header tap * present action sheet when tapping MessageCell header from within a message * created a new outlet from Message.storyboard so that we can present the action sheet when the message sender's avatar, name, or project name is tapped * includes MessageCellViewModel test * update todo comments with ticket links * remove SwiftUI view and build action sheet using UIAlertController * added a custom helper in our UIAlertController extension * add accessibility identifier and value to alert controller * Increase shallow fetch depth to 100, to make Danger work * reference UIUserInterfaceSizeClass.horizontalSizeClass instead of userInterfaceIdiom * cleanup * update RootCommentCell * also includes some further delegate naming cleanup * format --------- Co-authored-by: Amy --- .../CommentRepliesViewController.swift | 42 +++++++++++++++++++ .../Controllers/CommentsViewController.swift | 17 ++++++++ .../Comments/Views/Cells/CommentCell.swift | 16 +++++++ .../Views/Cells/RootCommentCell.swift | 24 +++++++++++ .../Controller/MessagesViewController.swift | 28 +++++++++++-- .../Messages/Storyboard/Messages.storyboard | 41 +++++++++--------- .../Messages/Views/Cells/MessageCell.swift | 23 ++++++++++ .../ProjectPageViewController.swift | 41 +++++++++++++----- Library/UIAlertController.swift | 29 +++++++++++++ Library/ViewModels/CommentCellViewModel.swift | 15 +++++++ .../CommentCellViewModelTests.swift | 15 +++++++ Library/ViewModels/MessageCellViewModel.swift | 16 +++++++ .../MessageCellViewModelTests.swift | 15 +++++++ .../ViewModels/RootCommentCellViewModel.swift | 16 +++++++ .../RootCommentCellViewModelTests.swift | 15 +++++++ 15 files changed, 320 insertions(+), 33 deletions(-) diff --git a/Kickstarter-iOS/Features/Comments/Controllers/CommentRepliesViewController.swift b/Kickstarter-iOS/Features/Comments/Controllers/CommentRepliesViewController.swift index c60bca8034..cfa38eccce 100644 --- a/Kickstarter-iOS/Features/Comments/Controllers/CommentRepliesViewController.swift +++ b/Kickstarter-iOS/Features/Comments/Controllers/CommentRepliesViewController.swift @@ -160,6 +160,23 @@ final class CommentRepliesViewController: UITableViewController { } } } + + private func blockUser() { + // Scott TODO: present popup UI [mbl-1036](https://kickstarter.atlassian.net/browse/MBL-1036) + } + + private func handleCommentCellHeaderTapped(in cell: UITableViewCell, _: Comment.Author) { + guard AppEnvironment.current.currentUser != nil, featureBlockUsersEnabled() else { return } + + let actionSheet = UIAlertController + .blockUserActionSheet( + blockUserHandler: { _ in self.blockUser() }, + sourceView: cell, + isIPad: self.traitCollection.horizontalSizeClass == .regular + ) + + self.present(actionSheet, animated: true) + } } // MARK: - UITableViewDelegate @@ -179,6 +196,11 @@ extension CommentRepliesViewController { self.viewModel.inputs.didSelectComment(comment) } } + + override func tableView(_: UITableView, willDisplay cell: UITableViewCell, forRowAt _: IndexPath) { + (cell as? RootCommentCell)?.delegate = self + (cell as? CommentCell)?.delegate = self + } } // MARK: - CommentComposerViewDelegate @@ -189,6 +211,26 @@ extension CommentRepliesViewController: CommentComposerViewDelegate { } } +// MARK: - CommentCellDelegate + +extension CommentRepliesViewController: CommentCellDelegate { + func commentCellDidTapHeader(_ cell: CommentCell, _ author: Comment.Author) { + self.handleCommentCellHeaderTapped(in: cell, author) + } + + func commentCellDidTapReply(_: CommentCell, comment _: Comment) {} + + func commentCellDidTapViewReplies(_: CommentCell, comment _: Comment) {} +} + +// MARK: - RootCommentCellDelegate + +extension CommentRepliesViewController: RootCommentCellDelegate { + func commentCellDidTapHeader(_ cell: RootCommentCell, _ author: Comment.Author) { + self.handleCommentCellHeaderTapped(in: cell, author) + } +} + // MARK: - Styles private let tableViewStyle: TableViewStyle = { tableView in diff --git a/Kickstarter-iOS/Features/Comments/Controllers/CommentsViewController.swift b/Kickstarter-iOS/Features/Comments/Controllers/CommentsViewController.swift index 9a9e7d938a..e4415b5488 100644 --- a/Kickstarter-iOS/Features/Comments/Controllers/CommentsViewController.swift +++ b/Kickstarter-iOS/Features/Comments/Controllers/CommentsViewController.swift @@ -179,6 +179,10 @@ internal final class CommentsViewController: UITableViewController { } } + private func blockUser() { + // Scott TODO: present popup ui + } + // MARK: - Actions @objc private func refresh() { @@ -237,6 +241,19 @@ extension CommentsViewController { // MARK: - CommentCellDelegate extension CommentsViewController: CommentCellDelegate { + func commentCellDidTapHeader(_ cell: CommentCell, _: Comment.Author) { + guard AppEnvironment.current.currentUser != nil, featureBlockUsersEnabled() else { return } + + let actionSheet = UIAlertController + .blockUserActionSheet( + blockUserHandler: { _ in self.blockUser() }, + sourceView: cell, + isIPad: self.traitCollection.horizontalSizeClass == .regular + ) + + self.present(actionSheet, animated: true) + } + func commentCellDidTapReply(_: CommentCell, comment: Comment) { self.viewModel.inputs.commentCellDidTapReply(comment: comment) } diff --git a/Kickstarter-iOS/Features/Comments/Views/Cells/CommentCell.swift b/Kickstarter-iOS/Features/Comments/Views/Cells/CommentCell.swift index b84b54c8e4..89245c2d88 100644 --- a/Kickstarter-iOS/Features/Comments/Views/Cells/CommentCell.swift +++ b/Kickstarter-iOS/Features/Comments/Views/Cells/CommentCell.swift @@ -4,6 +4,7 @@ import Prelude import UIKit protocol CommentCellDelegate: AnyObject { + func commentCellDidTapHeader(_ cell: CommentCell, _ author: Comment.Author) func commentCellDidTapReply(_ cell: CommentCell, comment: Comment) func commentCellDidTapViewReplies(_ cell: CommentCell, comment: Comment) } @@ -44,6 +45,10 @@ final class CommentCell: UITableViewCell, ValueCell { self.bindViewModel() self.replyButton.addTarget(self, action: #selector(self.replyButtonTapped), for: .touchUpInside) + self.commentCellHeaderStackView.addGestureRecognizer(UITapGestureRecognizer( + target: self, + action: #selector(self.commentCellHeaderTapped) + )) } required init?(coder aDecoder: NSCoder) { @@ -60,6 +65,10 @@ final class CommentCell: UITableViewCell, ValueCell { self.viewModel.inputs.viewRepliesButtonTapped() } + @objc private func commentCellHeaderTapped() { + self.viewModel.inputs.cellHeaderTapped() + } + // MARK: - Styles override func bindStyles() { @@ -128,6 +137,13 @@ final class CommentCell: UITableViewCell, ValueCell { self.postedButton.rac.hidden = self.viewModel.outputs.postedButtonIsHidden + self.viewModel.outputs.cellAuthor + .observeForUI() + .observeValues { [weak self] author in + guard let self = self else { return } + self.delegate?.commentCellDidTapHeader(self, author) + } + self.viewModel.outputs.replyCommentTapped .observeForUI() .observeValues { [weak self] comment in diff --git a/Kickstarter-iOS/Features/Comments/Views/Cells/RootCommentCell.swift b/Kickstarter-iOS/Features/Comments/Views/Cells/RootCommentCell.swift index c710e3dc49..e21731d1ef 100644 --- a/Kickstarter-iOS/Features/Comments/Views/Cells/RootCommentCell.swift +++ b/Kickstarter-iOS/Features/Comments/Views/Cells/RootCommentCell.swift @@ -9,9 +9,15 @@ private enum Layout { } } +protocol RootCommentCellDelegate: AnyObject { + func commentCellDidTapHeader(_ cell: RootCommentCell, _ author: Comment.Author) +} + final class RootCommentCell: UITableViewCell, ValueCell { // MARK: - Properties + weak var delegate: RootCommentCellDelegate? + private lazy var bodyTextView: UITextView = { UITextView(frame: .zero) }() private lazy var bottomBorder: UIView = { UIView(frame: .zero) @@ -38,6 +44,11 @@ final class RootCommentCell: UITableViewCell, ValueCell { self.bindStyles() self.configureViews() self.bindViewModel() + + self.commentCellHeaderStackView.addGestureRecognizer(UITapGestureRecognizer( + target: self, + action: #selector(self.commentCellHeaderTapped) + )) } required init?(coder aDecoder: NSCoder) { @@ -95,5 +106,18 @@ final class RootCommentCell: UITableViewCell, ValueCell { internal override func bindViewModel() { self.bodyTextView.rac.text = self.viewModel.outputs.body + + self.viewModel.outputs.commentAuthor + .observeForUI() + .observeValues { [weak self] author in + guard let self = self else { return } + self.delegate?.commentCellDidTapHeader(self, author) + } + } + + // MARK: - Actions + + @objc private func commentCellHeaderTapped() { + self.viewModel.inputs.commentCellHeaderTapped() } } diff --git a/Kickstarter-iOS/Features/Messages/Controller/MessagesViewController.swift b/Kickstarter-iOS/Features/Messages/Controller/MessagesViewController.swift index b6dd1b080f..0717834b50 100644 --- a/Kickstarter-iOS/Features/Messages/Controller/MessagesViewController.swift +++ b/Kickstarter-iOS/Features/Messages/Controller/MessagesViewController.swift @@ -103,9 +103,8 @@ internal final class MessagesViewController: UITableViewController { } internal override func tableView(_: UITableView, willDisplay cell: UITableViewCell, forRowAt _: IndexPath) { - if let cell = cell as? BackingCell, cell.delegate == nil { - cell.delegate = self - } + (cell as? BackingCell)?.delegate = self + (cell as? MessageCell)?.delegate = self } @IBAction fileprivate func replyButtonPressed() { @@ -144,6 +143,10 @@ internal final class MessagesViewController: UITableViewController { let vc = ManagePledgeViewController.controller(with: params) self.present(vc, animated: true) } + + private func blockUser() { + // Scott TODO: present popup UI [mbl-1036](https://kickstarter.atlassian.net/browse/MBL-1036) + } } extension MessagesViewController: MessageDialogViewControllerDelegate { @@ -156,8 +159,27 @@ extension MessagesViewController: MessageDialogViewControllerDelegate { } } +// MARK: - BackingCellDelegate + extension MessagesViewController: BackingCellDelegate { func backingCellGoToBackingInfo() { self.viewModel.inputs.backingInfoPressed() } } + +// MARK: - MessageCellDelegate + +extension MessagesViewController: MessageCellDelegate { + func messageCellDidTapHeader(_ cell: MessageCell, _: User) { + guard AppEnvironment.current.currentUser != nil, featureBlockUsersEnabled() else { return } + + let actionSheet = UIAlertController + .blockUserActionSheet( + blockUserHandler: { _ in self.blockUser() }, + sourceView: cell, + isIPad: self.traitCollection.horizontalSizeClass == .regular + ) + + self.present(actionSheet, animated: true) + } +} diff --git a/Kickstarter-iOS/Features/Messages/Storyboard/Messages.storyboard b/Kickstarter-iOS/Features/Messages/Storyboard/Messages.storyboard index 66142ee2a6..158537aa38 100644 --- a/Kickstarter-iOS/Features/Messages/Storyboard/Messages.storyboard +++ b/Kickstarter-iOS/Features/Messages/Storyboard/Messages.storyboard @@ -1,9 +1,9 @@ - + - + @@ -16,7 +16,7 @@ - + @@ -32,7 +32,7 @@ - + @@ -154,7 +154,7 @@ - + @@ -193,7 +193,7 @@ - + @@ -265,7 +265,7 @@ - + @@ -321,7 +321,7 @@ - + @@ -383,27 +383,27 @@ - + - + - + - + - + - + @@ -458,11 +458,12 @@ + - + @@ -530,7 +531,7 @@ - + @@ -615,7 +616,7 @@ - + @@ -646,15 +647,15 @@ - - + + - + diff --git a/Kickstarter-iOS/Features/Messages/Views/Cells/MessageCell.swift b/Kickstarter-iOS/Features/Messages/Views/Cells/MessageCell.swift index bf7529daca..a700bed8e8 100644 --- a/Kickstarter-iOS/Features/Messages/Views/Cells/MessageCell.swift +++ b/Kickstarter-iOS/Features/Messages/Views/Cells/MessageCell.swift @@ -4,6 +4,10 @@ import Prelude import ReactiveExtensions import UIKit +protocol MessageCellDelegate: AnyObject { + func messageCellDidTapHeader(_ cell: MessageCell, _ sender: User) +} + internal final class MessageCell: UITableViewCell, ValueCell { fileprivate let viewModel: MessageCellViewModelType = MessageCellViewModel() @@ -12,6 +16,9 @@ internal final class MessageCell: UITableViewCell, ValueCell { @IBOutlet private var nameLabel: UILabel! @IBOutlet private var timestampLabel: UILabel! @IBOutlet private var bodyTextView: UITextView! + @IBOutlet var participantStackView: UIStackView! + + weak var delegate: MessageCellDelegate? override func awakeFromNib() { super.awakeFromNib() @@ -19,6 +26,11 @@ internal final class MessageCell: UITableViewCell, ValueCell { // NB: removes the default padding around UITextView. self.bodyTextView.textContainerInset = UIEdgeInsets.zero self.bodyTextView.textContainer.lineFragmentPadding = 0 + + self.participantStackView.addGestureRecognizer(UITapGestureRecognizer( + target: self, + action: #selector(self.messageCellHeaderTapped) + )) } internal func configureWith(value message: Message) { @@ -71,5 +83,16 @@ internal final class MessageCell: UITableViewCell, ValueCell { .observeValues { [weak self] in self?.avatarImageView.af.setImage(withURL: $0) } + + self.viewModel.outputs.messageSender + .observeForUI() + .observeValues { [weak self] sender in + guard let self = self else { return } + self.delegate?.messageCellDidTapHeader(self, sender) + } + } + + @objc private func messageCellHeaderTapped() { + self.viewModel.inputs.cellHeaderTapped() } } diff --git a/Kickstarter-iOS/Features/ProjectPage/Controller/ProjectPageViewController.swift b/Kickstarter-iOS/Features/ProjectPage/Controller/ProjectPageViewController.swift index c365033788..637ab8f87a 100644 --- a/Kickstarter-iOS/Features/ProjectPage/Controller/ProjectPageViewController.swift +++ b/Kickstarter-iOS/Features/ProjectPage/Controller/ProjectPageViewController.swift @@ -724,6 +724,23 @@ public final class ProjectPageViewController: UIViewController, MessageBannerVie } } + private func blockUser() { + // Scott TODO: present popup UI [mbl-1036](https://kickstarter.atlassian.net/browse/MBL-1036) + } + + private func goToCreatorProfile(forProject project: Project) { + let vc = ProjectCreatorViewController.configuredWith(project: project) + + if self.traitCollection.userInterfaceIdiom == .pad { + let nav = UINavigationController(rootViewController: vc) + nav.modalPresentationStyle = UIModalPresentationStyle.formSheet + self.present(nav, animated: true, completion: nil) + } else { + self.viewModel.inputs.showNavigationBar(false) + self.navigationController?.pushViewController(vc, animated: true) + } + } + // MARK: - Selectors @objc private func didBackProject() { @@ -954,19 +971,23 @@ extension ProjectPageViewController: ProjectPamphletMainCellDelegate { } internal func projectPamphletMainCell( - _: ProjectPamphletMainCell, + _ cell: ProjectPamphletMainCell, goToCreatorForProject project: Project ) { - let vc = ProjectCreatorViewController.configuredWith(project: project) - - if self.traitCollection.userInterfaceIdiom == .pad { - let nav = UINavigationController(rootViewController: vc) - nav.modalPresentationStyle = UIModalPresentationStyle.formSheet - self.present(nav, animated: true, completion: nil) - } else { - self.viewModel.inputs.showNavigationBar(false) - self.navigationController?.pushViewController(vc, animated: true) + guard AppEnvironment.current.currentUser != nil, featureBlockUsersEnabled() else { + self.goToCreatorProfile(forProject: project) + return } + + let actionSheet = UIAlertController + .blockUserActionSheet( + blockUserHandler: { _ in self.blockUser() }, + viewProfileHandler: { _ in self.goToCreatorProfile(forProject: project) }, + sourceView: cell, + isIPad: self.traitCollection.horizontalSizeClass == .regular + ) + + self.present(actionSheet, animated: true) } } diff --git a/Library/UIAlertController.swift b/Library/UIAlertController.swift index 2a9a4e596b..1d3b26b9ee 100644 --- a/Library/UIAlertController.swift +++ b/Library/UIAlertController.swift @@ -356,4 +356,33 @@ public extension UIAlertController { return alertController } + + static func blockUserActionSheet( + blockUserHandler: @escaping (UIAlertAction) -> Void, + viewProfileHandler: ((UIAlertAction) -> Void)? = nil, + sourceView: UIView? = nil, + isIPad: Bool + ) -> UIAlertController { + let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + + if let profileHandler = viewProfileHandler { + // Scott TODO: Use localized strings once translations can be done [mbl-1037](https://kickstarter.atlassian.net/browse/MBL-1037) + alertController + .addAction(UIAlertAction(title: "View Profile", style: .default, handler: profileHandler)) + } + + // Scott TODO: Use localized strings once translations can be done [mbl-1037](https://kickstarter.atlassian.net/browse/MBL-1037) + alertController + .addAction(UIAlertAction(title: "Block this user", style: .destructive, handler: blockUserHandler)) + + alertController + .addAction(UIAlertAction(title: Strings.Cancel(), style: .cancel, handler: nil)) + + if isIPad { + alertController.modalPresentationStyle = .popover + alertController.popoverPresentationController?.sourceView = sourceView + } + + return alertController + } } diff --git a/Library/ViewModels/CommentCellViewModel.swift b/Library/ViewModels/CommentCellViewModel.swift index a8027bae8a..96b6472051 100644 --- a/Library/ViewModels/CommentCellViewModel.swift +++ b/Library/ViewModels/CommentCellViewModel.swift @@ -13,6 +13,9 @@ public protocol CommentCellViewModelInputs { /// Call when the textView delegate method for shouldInteractWith url is called func linkTapped(url: URL) + /// Call when the comment header is tapped + func cellHeaderTapped() + /// Call when the reply button is tapped func replyButtonTapped() @@ -36,6 +39,9 @@ public protocol CommentCellViewModelOutputs { /// Emits a Bool determining if the bottomRowStackView is hidden. var bottomRowStackViewIsHidden: Signal { get } + /// Emits the `Author` for the tapped cell. + var cellAuthor: Signal { get } + /// Emits the current status of a comment var commentStatus: Signal { get } @@ -148,6 +154,9 @@ public final class CommentCellViewModel: self.viewRepliesViewHidden = comment.map(\.replyCount) .map(viewRepliesStackViewHidden) + self.cellAuthor = comment + .takeWhen(self.cellHeaderTappedProperty.signal) + .map(\.author) self.replyCommentTapped = comment.takeWhen(self.replyButtonTappedProperty.signal) self.viewCommentReplies = comment.takeWhen(self.viewRepliesButtonTappedProperty.signal) @@ -170,6 +179,11 @@ public final class CommentCellViewModel: self.linkTappedProperty.value = url } + private var cellHeaderTappedProperty = MutableProperty(()) + public func cellHeaderTapped() { + self.cellHeaderTappedProperty.value = () + } + private var replyButtonTappedProperty = MutableProperty(()) public func replyButtonTapped() { self.replyButtonTappedProperty.value = () @@ -185,6 +199,7 @@ public final class CommentCellViewModel: public let authorName: Signal public let body: Signal public let bottomRowStackViewIsHidden: Signal + public let cellAuthor: Signal public let commentStatus: Signal public let flagButtonIsHidden: Signal public let notifyDelegateLinkTappedWithURL: Signal diff --git a/Library/ViewModels/CommentCellViewModelTests.swift b/Library/ViewModels/CommentCellViewModelTests.swift index ef4b7c7d5e..ac5f886a8e 100644 --- a/Library/ViewModels/CommentCellViewModelTests.swift +++ b/Library/ViewModels/CommentCellViewModelTests.swift @@ -13,6 +13,7 @@ internal final class CommentCellViewModelTests: TestCase { private let authorName = TestObserver() private let body = TestObserver() private let bottomRowStackViewIsHidden = TestObserver() + private let cellAuthor = TestObserver() private let commentStatus = TestObserver() private let flagButtonIsHidden = TestObserver() private let notifyDelegateLinkTappedWithURL = TestObserver() @@ -31,6 +32,7 @@ internal final class CommentCellViewModelTests: TestCase { self.vm.outputs.authorName.observe(self.authorName.observer) self.vm.outputs.body.observe(self.body.observer) self.vm.outputs.bottomRowStackViewIsHidden.observe(self.bottomRowStackViewIsHidden.observer) + self.vm.outputs.cellAuthor.observe(self.cellAuthor.observer) self.vm.outputs.commentStatus.observe(self.commentStatus.observer) self.vm.outputs.flagButtonIsHidden.observe(self.flagButtonIsHidden.observer) self.vm.outputs.notifyDelegateLinkTappedWithURL.observe(self.notifyDelegateLinkTappedWithURL.observer) @@ -76,6 +78,19 @@ internal final class CommentCellViewModelTests: TestCase { } } + func testOutput_CellHeaderTapped_EmitsCellAuthor() { + let comment = Comment.template + withEnvironment(currentUser: .template) { + self.vm.inputs.configureWith(comment: comment, project: .template) + self.cellAuthor.assertDidNotEmitValue() + + self.vm.inputs.cellHeaderTapped() + + self.cellAuthor + .assertValue(comment.author) + } + } + func testOutputs_bottomRowStackViewIsHidden_LoggedIn() { let user = User.template |> \.id .~ 12_345 diff --git a/Library/ViewModels/MessageCellViewModel.swift b/Library/ViewModels/MessageCellViewModel.swift index f9568c9f7d..e0b4a65fc4 100644 --- a/Library/ViewModels/MessageCellViewModel.swift +++ b/Library/ViewModels/MessageCellViewModel.swift @@ -4,6 +4,9 @@ import ReactiveSwift public protocol MessageCellViewModelInputs { func configureWith(message: Message) + + /// Call when the cell header is tapped + func cellHeaderTapped() } public protocol MessageCellViewModelOutputs { @@ -12,6 +15,9 @@ public protocol MessageCellViewModelOutputs { var timestamp: Signal { get } var timestampAccessibilityLabel: Signal { get } var body: Signal { get } + + /// Emits the message's `Sender` of the tapped message. + var messageSender: Signal { get } } public protocol MessageCellViewModelType { @@ -37,6 +43,10 @@ public final class MessageCellViewModel: MessageCellViewModelType, MessageCellVi } self.body = message.map { $0.body } + + self.messageSender = message + .takeWhen(self.cellHeaderTappedProperty.signal) + .map(\.sender) } fileprivate let messageProperty = MutableProperty(nil) @@ -44,11 +54,17 @@ public final class MessageCellViewModel: MessageCellViewModelType, MessageCellVi self.messageProperty.value = message } + fileprivate let cellHeaderTappedProperty = MutableProperty(()) + public func cellHeaderTapped() { + self.cellHeaderTappedProperty.value = () + } + public let avatarURL: Signal public let name: Signal public let timestamp: Signal public var timestampAccessibilityLabel: Signal public let body: Signal + public let messageSender: Signal public var inputs: MessageCellViewModelInputs { return self } public var outputs: MessageCellViewModelOutputs { return self } diff --git a/Library/ViewModels/MessageCellViewModelTests.swift b/Library/ViewModels/MessageCellViewModelTests.swift index 500ef63b4d..7fba68b9a0 100644 --- a/Library/ViewModels/MessageCellViewModelTests.swift +++ b/Library/ViewModels/MessageCellViewModelTests.swift @@ -12,6 +12,7 @@ internal final class MessageCellViewModelTests: TestCase { fileprivate let _name = TestObserver() fileprivate let timestamp = TestObserver() fileprivate let timestampAccessibilityLabel = TestObserver() + fileprivate let messageSender = TestObserver() override func setUp() { super.setUp() @@ -21,6 +22,7 @@ internal final class MessageCellViewModelTests: TestCase { self.vm.outputs.timestamp.observe(self.timestamp.observer) self.vm.outputs.timestampAccessibilityLabel.observe(self.timestampAccessibilityLabel.observer) self.vm.outputs.body.observe(self.body.observer) + self.vm.outputs.messageSender.observe(self.messageSender.observer) } func testOutputs() { @@ -33,4 +35,17 @@ internal final class MessageCellViewModelTests: TestCase { self.timestampAccessibilityLabel.assertValueCount(1) self.body.assertValues([message.body]) } + + func testOutput_CellHeaderTapped_EmitsCellAuthor() { + let message = Message.template + withEnvironment(currentUser: .template) { + self.vm.inputs.configureWith(message: message) + self.messageSender.assertDidNotEmitValue() + + self.vm.inputs.cellHeaderTapped() + + self.messageSender + .assertValue(message.sender) + } + } } diff --git a/Library/ViewModels/RootCommentCellViewModel.swift b/Library/ViewModels/RootCommentCellViewModel.swift index 59212202ef..6698eea5fa 100644 --- a/Library/ViewModels/RootCommentCellViewModel.swift +++ b/Library/ViewModels/RootCommentCellViewModel.swift @@ -7,6 +7,9 @@ public protocol RootCommentCellViewModelInputs { /// Call when bindStyles is called. func bindStyles() + /// Call when the comment header is tapped + func commentCellHeaderTapped() + /// Call to configure with a Comment func configureWith(comment: Comment) } @@ -24,6 +27,9 @@ public protocol RootCommentCellViewModelOutputs { /// Emits text containing comment body. var body: Signal { get } + /// Emits the `Author` for the tapped cell. + var commentAuthor: Signal { get } + /// Emits text relative time the comment was posted. var postTime: Signal { get } } @@ -61,6 +67,10 @@ public final class RootCommentCellViewModel: badge, badge.takeWhen(self.bindStylesProperty.signal) ) + + self.commentAuthor = comment + .takeWhen(self.cellHeaderTappedProperty.signal) + .map(\.author) } private var bindStylesProperty = MutableProperty(()) @@ -68,6 +78,11 @@ public final class RootCommentCellViewModel: self.bindStylesProperty.value = () } + private var cellHeaderTappedProperty = MutableProperty(()) + public func commentCellHeaderTapped() { + self.cellHeaderTappedProperty.value = () + } + fileprivate let comment = MutableProperty<(Comment)?>(nil) public func configureWith(comment: Comment) { self.comment.value = comment @@ -77,6 +92,7 @@ public final class RootCommentCellViewModel: public var authorImageURL: Signal public let authorName: Signal public let body: Signal + public let commentAuthor: Signal public let postTime: Signal public var inputs: RootCommentCellViewModelInputs { self } diff --git a/Library/ViewModels/RootCommentCellViewModelTests.swift b/Library/ViewModels/RootCommentCellViewModelTests.swift index b52b25a44a..ed9432ff0f 100644 --- a/Library/ViewModels/RootCommentCellViewModelTests.swift +++ b/Library/ViewModels/RootCommentCellViewModelTests.swift @@ -12,6 +12,7 @@ internal final class RootCommentCellViewModelTests: TestCase { private let authorImageURL = TestObserver() private let authorName = TestObserver() private let body = TestObserver() + private let commentAuthor = TestObserver() private let postTime = TestObserver() override func setUp() { @@ -20,6 +21,7 @@ internal final class RootCommentCellViewModelTests: TestCase { self.vm.outputs.authorImageURL.observe(self.authorImageURL.observer) self.vm.outputs.authorName.observe(self.authorName.observer) self.vm.outputs.body.observe(self.body.observer) + self.vm.outputs.commentAuthor.observe(self.commentAuthor.observer) self.vm.outputs.postTime.observe(self.postTime.observer) } @@ -87,4 +89,17 @@ internal final class RootCommentCellViewModelTests: TestCase { self.authorBadge.assertValues([.creator, .creator], "The author's badge is emitted.") } + + func testOutput_CellHeaderTapped_EmitsCellAuthor() { + let comment = Comment.backerTemplate + withEnvironment(currentUser: .template) { + self.vm.inputs.configureWith(comment: comment) + self.commentAuthor.assertDidNotEmitValue() + + self.vm.inputs.commentCellHeaderTapped() + + self.commentAuthor + .assertValue(comment.author) + } + } }