Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MBL-1027] Blocked User Messages UI #1887

Merged
merged 11 commits into from
Nov 29, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
@IBOutlet fileprivate var messageLabel: UILabel!

private var bannerType: MessageBannerType?
private var dissmissable: Bool = true
scottkicks marked this conversation as resolved.
Show resolved Hide resolved

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

if isHidden, !self.dissmissable {
return

Check warning on line 73 in Kickstarter-iOS/Features/MessageBanner/Controller/MessageBannerViewController.swift

View check run for this annotation

Codecov / codecov/patch

Kickstarter-iOS/Features/MessageBanner/Controller/MessageBannerViewController.swift#L73

Added line #L73 was not covered by tests
}

self.showViewAndAnimate(isHidden)
scottkicks marked this conversation as resolved.
Show resolved Hide resolved
}

self.viewModel.outputs.iconTintColor
Expand Down Expand Up @@ -95,20 +102,26 @@
}
}

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

private func showViewAndAnimate(_ isHidden: Bool) {
if !isHidden, !self.view.isHidden, !self.dissmissable { 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.dissmissable {
self.view.superview?.isUserInteractionEnabled = false
scottkicks marked this conversation as resolved.
Show resolved Hide resolved
}

self.view.isHidden = isHidden

Expand Down Expand Up @@ -162,7 +175,7 @@
}

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

Expand Down Expand Up @@ -191,6 +204,8 @@
}

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

self.viewModel.inputs.bannerViewWillShow(false)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 @@
self.viewModel.inputs.viewDidLoad()
}

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

Check warning on line 43 in Kickstarter-iOS/Features/Messages/Controller/MessagesViewController.swift

View check run for this annotation

Codecov / codecov/patch

Kickstarter-iOS/Features/Messages/Controller/MessagesViewController.swift#L43

Added line #L43 was not covered by tests

self.viewModel.inputs.viewWillAppear()

Check warning on line 45 in Kickstarter-iOS/Features/Messages/Controller/MessagesViewController.swift

View check run for this annotation

Codecov / codecov/patch

Kickstarter-iOS/Features/Messages/Controller/MessagesViewController.swift#L45

Added line #L45 was not covered by tests
}

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

Check warning on line 49 in Kickstarter-iOS/Features/Messages/Controller/MessagesViewController.swift

View check run for this annotation

Codecov / codecov/patch

Kickstarter-iOS/Features/Messages/Controller/MessagesViewController.swift#L49

Added line #L49 was not covered by tests

self.updateTableViewBottomContentInset()

Check warning on line 51 in Kickstarter-iOS/Features/Messages/Controller/MessagesViewController.swift

View check run for this annotation

Codecov / codecov/patch

Kickstarter-iOS/Features/Messages/Controller/MessagesViewController.swift#L51

Added line #L51 was not covered by tests
}

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

Expand Down Expand Up @@ -78,6 +91,15 @@
self?.tableView.reloadData()
}

self.viewModel.outputs.participantPreviouslyBlocked
.observeForControllerAction()

Check warning on line 95 in Kickstarter-iOS/Features/Messages/Controller/MessagesViewController.swift

View check run for this annotation

Codecov / codecov/patch

Kickstarter-iOS/Features/Messages/Controller/MessagesViewController.swift#L94-L95

Added lines #L94 - L95 were not covered by tests
.observeValues { [weak self] isBlocked in
guard let self, isBlocked == true else { return }

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

Check warning on line 100 in Kickstarter-iOS/Features/Messages/Controller/MessagesViewController.swift

View check run for this annotation

Codecov / codecov/patch

Kickstarter-iOS/Features/Messages/Controller/MessagesViewController.swift#L99-L100

Added lines #L99 - L100 were not covered by tests
}

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

self.viewModel.outputs.userBlocked
self.viewModel.outputs.didBlockUser

Check warning on line 117 in Kickstarter-iOS/Features/Messages/Controller/MessagesViewController.swift

View check run for this annotation

Codecov / codecov/patch

Kickstarter-iOS/Features/Messages/Controller/MessagesViewController.swift#L117

Added line #L117 was not covered by tests
.observeForUI()
.observeValues { [weak self] success in

Expand Down Expand Up @@ -161,6 +183,12 @@
self.present(vc, animated: true)
}

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

Check warning on line 188 in Kickstarter-iOS/Features/Messages/Controller/MessagesViewController.swift

View check run for this annotation

Codecov / codecov/patch

Kickstarter-iOS/Features/Messages/Controller/MessagesViewController.swift#L188

Added line #L188 was not covered by tests
}
}

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
Original file line number Diff line number Diff line change
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))
}

/// TODO(MBL-1025): Get isBlocked status from the backend instead.
self.participantPreviouslyBlocked = self.viewWillAppearProperty.signal
.mapConst(false)
scottkicks marked this conversation as resolved.
Show resolved Hide resolved
}

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
27 changes: 26 additions & 1 deletion Library/ViewModels/MessagesViewModelTests.swift
Original file line number Diff line number Diff line change
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 @@ -235,7 +239,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 +254,27 @@ internal final class MessagesViewModelTests: TestCase {
}
}

func testparticipantPreviouslyBlockedFlow() {
let project = Project.template |> Project.lens.id .~ 42
let backing = Backing.template
let messageThread = .template
|> MessageThread.lens.project .~ project
|> MessageThread.lens.participant .~ .template

let apiService = MockService(fetchMessageThreadResult: Result.success(messageThread))

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

self.participantPreviouslyBlocked.assertValueCount(0)

self.vm.inputs.viewWillAppear()

/// TODO(MBL-1025): Once we are getting isBlocked status from backend we need to update this use that. This is hardcoded for now.
self.participantPreviouslyBlocked.assertValues([false])
}
}

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