Skip to content

Commit

Permalink
[MBL-1027] Blocked User Messages UI (#1887)
Browse files Browse the repository at this point in the history
* "user has been blocked" banner

* setup participantIsBlocked output that triggers on viewWillAppear

* MessagesViewModel tests

* safety check

* make tag a constant and renames

* use existing MessageBanner instead of a surprisingly annoying swiftui view

* use localized string

* fix test

* typo

* get isBlocked from project and update tests
  • Loading branch information
scottkicks committed Nov 29, 2023
1 parent 1156229 commit a164c48
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 12 deletions.
Expand Up @@ -19,6 +19,7 @@ public final class MessageBannerViewController: UIViewController, NibLoading {
@IBOutlet fileprivate var messageLabel: UILabel!

private var bannerType: MessageBannerType?
private var dismissible: Bool = true

internal var bottomConstraint: NSLayoutConstraint?
private let viewModel: MessageBannerViewModelType = MessageBannerViewModel()
Expand Down Expand Up @@ -66,7 +67,13 @@ public final class MessageBannerViewController: UIViewController, NibLoading {
self.viewModel.outputs.messageBannerViewIsHidden
.observeForUI()
.observeValues { [weak self] isHidden in
self?.showViewAndAnimate(isHidden)
guard let self else { return }

if isHidden, !self.dismissible {
return
}

self.showViewAndAnimate(isHidden)
}

self.viewModel.outputs.iconTintColor
Expand Down Expand Up @@ -95,20 +102,26 @@ public final class MessageBannerViewController: UIViewController, NibLoading {
}
}

public func showBanner(with type: MessageBannerType, message: String) {
public func showBanner(with type: MessageBannerType, message: String, dismissible: Bool = true) {
self.bannerType = type
self.dismissible = dismissible
self.viewModel.inputs.update(with: (type, message))
self.viewModel.inputs.bannerViewWillShow(true)
}

private func showViewAndAnimate(_ isHidden: Bool) {
if !isHidden, !self.view.isHidden, !self.dismissible { return }

let duration = isHidden ? AnimationConstants.hideDuration : AnimationConstants.showDuration

let hiddenConstant = self.view.frame.height + (self.view.superview?.safeAreaInsets.bottom ?? 0)

if !isHidden {
self.view.superview?.bringSubviewToFront(self.view)
self.view.superview?.isUserInteractionEnabled = false

if self.dismissible {
self.view.superview?.isUserInteractionEnabled = false
}

self.view.isHidden = isHidden

Expand Down Expand Up @@ -162,7 +175,7 @@ public final class MessageBannerViewController: UIViewController, NibLoading {
}

@IBAction private func bannerViewPanned(_ sender: UIPanGestureRecognizer) {
guard let view = sender.view else {
guard self.dismissible, let view = sender.view else {
return
}

Expand Down Expand Up @@ -191,6 +204,8 @@ public final class MessageBannerViewController: UIViewController, NibLoading {
}

@IBAction private func bannerViewTapped(_: Any) {
guard self.dismissible else { return }

self.viewModel.inputs.bannerViewWillShow(false)
}
}
Expand Down
@@ -1,6 +1,7 @@
import KsApi
import Library
import Prelude
import SwiftUI
import UIKit

internal final class MessagesViewController: UITableViewController, MessageBannerViewControllerPresenting {
Expand Down Expand Up @@ -38,6 +39,18 @@ internal final class MessagesViewController: UITableViewController, MessageBanne
self.viewModel.inputs.viewDidLoad()
}

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

self.viewModel.inputs.viewWillAppear()
}

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)

self.updateTableViewBottomContentInset()
}

internal override func bindStyles() {
super.bindStyles()

Expand Down Expand Up @@ -78,6 +91,15 @@ internal final class MessagesViewController: UITableViewController, MessageBanne
self?.tableView.reloadData()
}

self.viewModel.outputs.participantPreviouslyBlocked
.observeForControllerAction()
.observeValues { [weak self] isBlocked in
guard let self, isBlocked == true else { return }

self.messageBannerViewController?
.showBanner(with: .error, message: Strings.This_user_has_been_blocked(), dismissible: false)
}

self.viewModel.outputs.presentMessageDialog
.observeForControllerAction()
.observeValues { [weak self] messageThread, context in
Expand All @@ -92,7 +114,7 @@ internal final class MessagesViewController: UITableViewController, MessageBanne
.observeForControllerAction()
.observeValues { [weak self] params in self?.goToBacking(with: params) }

self.viewModel.outputs.userBlocked
self.viewModel.outputs.didBlockUser
.observeForUI()
.observeValues { [weak self] success in

Expand Down Expand Up @@ -161,6 +183,12 @@ internal final class MessagesViewController: UITableViewController, MessageBanne
self.present(vc, animated: true)
}

private func updateTableViewBottomContentInset() {
if let messageBannerView = messageBannerViewController?.view {
self.tableView.contentInset.bottom = messageBannerView.bounds.height
}
}

private func presentBlockUserAlert(username: String) {
let alert = UIAlertController
.blockUserAlert(username: username, blockUserHandler: { _ in self.viewModel.inputs.blockUser() })
Expand Down
29 changes: 23 additions & 6 deletions Library/ViewModels/MessagesViewModel.swift
Expand Up @@ -24,6 +24,9 @@ public protocol MessagesViewModelInputs {

/// Call when the view loads.
func viewDidLoad()

/// Call when the view will appear.
func viewWillAppear()
}

public protocol MessagesViewModelOutputs {
Expand All @@ -45,6 +48,9 @@ public protocol MessagesViewModelOutputs {
/// Emits a list of messages to be displayed.
var messages: Signal<[Message], Never> { get }

/// Emits a bool whether the message participant has previously been blocked on viewWillAppear
var participantPreviouslyBlocked: Signal<Bool, Never> { get }

/// Emits when we should show the message dialog.
var presentMessageDialog: Signal<(MessageThread, KSRAnalytics.MessageDialogContext), Never> { get }

Expand All @@ -57,8 +63,8 @@ public protocol MessagesViewModelOutputs {
/// Emits when the thread has been marked as read.
var successfullyMarkedAsRead: Signal<(), Never> { get }

/// Emits when a request to block a user has been made
var userBlocked: Signal<Bool, Never> { get }
/// Emits whether a request to block a user was successful.
var didBlockUser: Signal<Bool, Never> { get }
}

public protocol MessagesViewModelType {
Expand Down Expand Up @@ -161,7 +167,8 @@ public final class MessagesViewModel: MessagesViewModelType, MessagesViewModelIn

self.replyButtonIsEnabled = Signal.merge(
self.viewDidLoadProperty.signal.mapConst(false),
self.messages.map { !$0.isEmpty }
self.messages.map { !$0.isEmpty },
participant.signal.map { $0.isBlocked == false }
)

self.presentMessageDialog = messageThreadEnvelope
Expand Down Expand Up @@ -191,12 +198,16 @@ public final class MessagesViewModel: MessagesViewModelType, MessagesViewModelIn
.ignoreValues()

// TODO: Call blocking GraphQL mutation
self.userBlocked = self.blockUserProperty.signal.map { true }
self.didBlockUser = self.blockUserProperty.signal.map { false }

self.userBlocked.observeValues { didBlock in
self.didBlockUser.observeValues { didBlock in
guard didBlock == true else { return }
NotificationCenter.default.post(.init(name: .ksr_blockedUser))
}

self.participantPreviouslyBlocked = self.project
.map { $0.creator.isBlocked ?? false }
.takeWhen(self.viewWillAppearProperty.signal)
}

private let backingInfoPressedProperty = MutableProperty(())
Expand Down Expand Up @@ -234,16 +245,22 @@ public final class MessagesViewModel: MessagesViewModelType, MessagesViewModelIn
self.viewDidLoadProperty.value = ()
}

private let viewWillAppearProperty = MutableProperty(())
public func viewWillAppear() {
self.viewWillAppearProperty.value = ()
}

public let backingAndProjectAndIsFromBacking: Signal<(Backing, Project, Bool), Never>
public let emptyStateIsVisibleAndMessageToUser: Signal<(Bool, String), Never>
public let goToBacking: Signal<ManagePledgeViewParamConfigData, Never>
public let goToProject: Signal<(Project, RefTag), Never>
public let messages: Signal<[Message], Never>
public let participantPreviouslyBlocked: Signal<Bool, Never>
public let presentMessageDialog: Signal<(MessageThread, KSRAnalytics.MessageDialogContext), Never>
public let project: Signal<Project, Never>
public let replyButtonIsEnabled: Signal<Bool, Never>
public let successfullyMarkedAsRead: Signal<(), Never>
public let userBlocked: Signal<Bool, Never>
public let didBlockUser: Signal<Bool, Never>

public var inputs: MessagesViewModelInputs { return self }
public var outputs: MessagesViewModelOutputs { return self }
Expand Down
52 changes: 51 additions & 1 deletion Library/ViewModels/MessagesViewModelTests.swift
Expand Up @@ -16,10 +16,12 @@ internal final class MessagesViewModelTests: TestCase {
fileprivate let goToProject = TestObserver<Project, Never>()
fileprivate let goToRefTag = TestObserver<RefTag, Never>()
fileprivate let messages = TestObserver<[Message], Never>()
fileprivate let participantPreviouslyBlocked = TestObserver<Bool, Never>()
fileprivate let presentMessageDialog = TestObserver<MessageThread, Never>()
fileprivate let project = TestObserver<Project, Never>()
fileprivate let replyButtonIsEnabled = TestObserver<Bool, Never>()
fileprivate let successfullyMarkedAsRead = TestObserver<(), Never>()
fileprivate let didBlockUser = TestObserver<Bool, Never>()

override func setUp() {
super.setUp()
Expand All @@ -33,10 +35,12 @@ internal final class MessagesViewModelTests: TestCase {
self.vm.outputs.goToProject.map { $0.0 }.observe(self.goToProject.observer)
self.vm.outputs.goToProject.map { $0.1 }.observe(self.goToRefTag.observer)
self.vm.outputs.messages.observe(self.messages.observer)
self.vm.outputs.participantPreviouslyBlocked.observe(self.participantPreviouslyBlocked.observer)
self.vm.outputs.presentMessageDialog.map { $0.0 }.observe(self.presentMessageDialog.observer)
self.vm.outputs.project.observe(self.project.observer)
self.vm.outputs.replyButtonIsEnabled.observe(self.replyButtonIsEnabled.observer)
self.vm.outputs.successfullyMarkedAsRead.observe(self.successfullyMarkedAsRead.observer)
self.vm.outputs.didBlockUser.observe(self.didBlockUser.observer)

AppEnvironment.login(AccessTokenEnvelope(accessToken: "deadbeef", user: User.template))
}
Expand Down Expand Up @@ -125,6 +129,7 @@ internal final class MessagesViewModelTests: TestCase {

withEnvironment(apiService: apiService, currentUser: .template) {
self.vm.inputs.configureWith(data: .right((project: project, backing: backing)))
self.vm.inputs.viewWillAppear()
self.vm.inputs.viewDidLoad()

self.scheduler.advance()
Expand Down Expand Up @@ -235,7 +240,7 @@ internal final class MessagesViewModelTests: TestCase {
self.scheduler.advance()

self.messages.assertValueCount(1)
self.replyButtonIsEnabled.assertValues([false, true])
self.replyButtonIsEnabled.assertValues([false, false, true])
self.emptyStateIsVisible.assertValues([false], "Empty state does not emit again.")

self.vm.inputs.replyButtonPressed()
Expand All @@ -250,6 +255,51 @@ internal final class MessagesViewModelTests: TestCase {
}
}

func testParticipantPreviouslyBlockedFlow_True() {
let creator = User.template
|> \.isBlocked .~ true

let project = Project.template
|> Project.lens.id .~ 42
|> Project.lens.creator .~ creator

withEnvironment(currentUser: .template) {
self.vm.inputs.configureWith(data: .right((project: project, backing: Backing.template)))

self.participantPreviouslyBlocked.assertValueCount(0)

self.vm.inputs.viewDidLoad()
self.vm.inputs.viewWillAppear()

self.scheduler.advance()

self.participantPreviouslyBlocked.assertValues([true])
}
}

func testParticipantPreviouslyBlockedFlow_False() {
let creator = User.template
|> \.id .~ 20
|> \.isBlocked .~ false

let project = Project.template
|> Project.lens.id .~ 42
|> Project.lens.creator .~ creator

withEnvironment(currentUser: .template) {
self.vm.inputs.configureWith(data: .right((project: project, backing: Backing.template)))

self.participantPreviouslyBlocked.assertValueCount(0)

self.vm.inputs.viewDidLoad()
self.vm.inputs.viewWillAppear()

self.scheduler.advance()

self.participantPreviouslyBlocked.assertValues([false])
}
}

func testMarkAsRead() {
self.vm.inputs.configureWith(data: .left(MessageThread.template))
self.vm.inputs.viewDidLoad()
Expand Down

0 comments on commit a164c48

Please sign in to comment.