Skip to content

Commit

Permalink
Merge pull request #695 from azatZul/failed-content-in-compound-messages
Browse files Browse the repository at this point in the history
Failed content in compound messages
  • Loading branch information
wiruzx authored Feb 18, 2021
2 parents 85c7087 + 5fbde69 commit ec04592
Show file tree
Hide file tree
Showing 10 changed files with 391 additions and 13 deletions.
12 changes: 12 additions & 0 deletions ChattoAdditions/ChattoAdditions.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@
CDC6100B1FD8268200C2588E /* FakePhotosInputDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC610091FD8267D00C2588E /* FakePhotosInputDataProvider.swift */; };
CDC6100D1FD8376000C2588E /* PhotosInputCellProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC6100C1FD8376000C2588E /* PhotosInputCellProviderTests.swift */; };
CDE15F43205993FB005D86DD /* PhotosInputCameraPickerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE15F42205993FB005D86DD /* PhotosInputCameraPickerTests.swift */; };
D6E3AEF625D6DF6000819A56 /* MessageViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E3AEF525D6DF6000819A56 /* MessageViewModelTests.swift */; };
D6E3AEFC25D6E01900819A56 /* FakeMessageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E3AEFB25D6E01900819A56 /* FakeMessageModel.swift */; };
D6E3AF0125D6E6C500819A56 /* FakeMessageContentFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E3AF0025D6E6C500819A56 /* FakeMessageContentFactory.swift */; };
DE51010D21B5670A009BC61C /* InputContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE51010C21B5670A009BC61C /* InputContainerView.swift */; };
E376C10524F8160B00069ECA /* PhotosInputPermissionsRequester.swift in Sources */ = {isa = PBXBuildFile; fileRef = E376C10324F8160B00069ECA /* PhotosInputPermissionsRequester.swift */; };
E3E0D60322EA1AA2006E2053 /* Comparable+Clamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3E0D60222EA1AA2006E2053 /* Comparable+Clamp.swift */; };
Expand Down Expand Up @@ -287,6 +290,9 @@
CDC610091FD8267D00C2588E /* FakePhotosInputDataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakePhotosInputDataProvider.swift; sourceTree = "<group>"; };
CDC6100C1FD8376000C2588E /* PhotosInputCellProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotosInputCellProviderTests.swift; sourceTree = "<group>"; };
CDE15F42205993FB005D86DD /* PhotosInputCameraPickerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotosInputCameraPickerTests.swift; sourceTree = "<group>"; };
D6E3AEF525D6DF6000819A56 /* MessageViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageViewModelTests.swift; sourceTree = "<group>"; };
D6E3AEFB25D6E01900819A56 /* FakeMessageModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeMessageModel.swift; sourceTree = "<group>"; };
D6E3AF0025D6E6C500819A56 /* FakeMessageContentFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeMessageContentFactory.swift; sourceTree = "<group>"; };
DE51010C21B5670A009BC61C /* InputContainerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputContainerView.swift; sourceTree = "<group>"; };
E376C10324F8160B00069ECA /* PhotosInputPermissionsRequester.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotosInputPermissionsRequester.swift; sourceTree = "<group>"; };
E3E0D60222EA1AA2006E2053 /* Comparable+Clamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Comparable+Clamp.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -690,12 +696,15 @@
EAAFBCEC2302D8C8004F553D /* CompoundMessagePresenterTests.swift */,
EAAFBCEA2302BBD3004F553D /* DefaultMessageContentPresenterTests.swift */,
EAAFBCF02302DBAF004F553D /* FakeMessageInteractionHandler.swift */,
D6E3AEFB25D6E01900819A56 /* FakeMessageModel.swift */,
EAAFBCEE2302DB22004F553D /* FakeViewModelBuilder.swift */,
EA75254F2302E94B0069D1AF /* MessageModel+Helpers.swift */,
D6E3AEF525D6DF6000819A56 /* MessageViewModelTests.swift */,
EA7527972302FD790069D1AF /* StubCompoundBubbleViewStyle.swift */,
EA7527992302FD980069D1AF /* StubMessageCollectionViewCellStyle.swift */,
EA75279B23031FE60069D1AF /* TestableCompoundMessagePresenter.swift */,
EA7525512302FB930069D1AF /* TestHelpers.swift */,
D6E3AF0025D6E6C500819A56 /* FakeMessageContentFactory.swift */,
);
path = BaseMessage;
sourceTree = "<group>";
Expand Down Expand Up @@ -1029,6 +1038,7 @@
files = (
C3C0CC861BFE49700052747C /* ChatInputItemTests.swift in Sources */,
CDC6100B1FD8268200C2588E /* FakePhotosInputDataProvider.swift in Sources */,
D6E3AEFC25D6E01900819A56 /* FakeMessageModel.swift in Sources */,
EAAFBCEB2302BBD3004F553D /* DefaultMessageContentPresenterTests.swift in Sources */,
EAAFBCEF2302DB22004F553D /* FakeViewModelBuilder.swift in Sources */,
EA75279A2302FD980069D1AF /* StubMessageCollectionViewCellStyle.swift in Sources */,
Expand All @@ -1043,6 +1053,7 @@
C35FE3C51C0331CF00D42980 /* TextMessagePresenterTests.swift in Sources */,
55ABA5731FC74E0400923302 /* UIEdgeInets+AdditionsTests.swift in Sources */,
C3C0CC8A1BFE49700052747C /* PhotosChatInputItemTests.swift in Sources */,
D6E3AF0125D6E6C500819A56 /* FakeMessageContentFactory.swift in Sources */,
EA7525502302E94B0069D1AF /* MessageModel+Helpers.swift in Sources */,
EAAFBCED2302D8C8004F553D /* CompoundMessagePresenterTests.swift in Sources */,
55ABA5691FC7498700923302 /* CGRect+AdditionsTests.swift in Sources */,
Expand All @@ -1054,6 +1065,7 @@
E3E0D60522EA1AEE006E2053 /* Comparable+ClampTests.swift in Sources */,
55ABA5591FC73B5600923302 /* CGSize+AdditionsTests.swift in Sources */,
C3C0CC871BFE49700052747C /* ChatInputItemViewTests.swift in Sources */,
D6E3AEF625D6DF6000819A56 /* MessageViewModelTests.swift in Sources */,
CDC6100D1FD8376000C2588E /* PhotosInputCellProviderTests.swift in Sources */,
C35FE3C81C033E7800D42980 /* PhotoMessagePresenterTests.swift in Sources */,
C3EFA6B01C03607A0063CE22 /* BaseMessagePresenterTests.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public protocol MessageViewModelProtocol: class { // why class? https://gist.git
var date: String { get }
var status: MessageViewModelStatus { get }
var avatarImage: Observable<UIImage?> { get set }
var messageContentTransferStatus: TransferStatus? { get set }
var canReply: Bool { get }
func willBeShown() // Optional
func wasHidden() // Optional
Expand Down Expand Up @@ -95,6 +96,14 @@ extension DecoratedMessageViewModelProtocol {
return self.messageViewModel.date
}

public var messageContentTransferStatus: TransferStatus? {
get {
return nil
}
set {
}
}

public var status: MessageViewModelStatus {
return self.messageViewModel.status
}
Expand Down Expand Up @@ -124,8 +133,15 @@ open class MessageViewModel: MessageViewModelProtocol {
open var decorationAttributes: BaseMessageDecorationAttributes
open var isUserInteractionEnabled: Bool = true

public var messageContentTransferStatus: TransferStatus?

open var status: MessageViewModelStatus {
return self.messageModel.status.viewModelStatus()
let deliveryStatus = self.messageModel.status.viewModelStatus()
guard let contentLoadStatus = self.messageContentTransferStatus else { return deliveryStatus }
if contentLoadStatus == .failed {
return .failed
}
return deliveryStatus
}

open lazy var date: String = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,13 +258,7 @@ open class BaseMessageCollectionViewCell<BubbleViewType>: UICollectionViewCell,
if self.isUpdating { return }
guard let viewModel = self.messageViewModel, let style = self.baseStyle else { return }
self.bubbleView.isUserInteractionEnabled = viewModel.isUserInteractionEnabled
if self.shouldShowFailedIcon {
self.failedButton.setImage(self.baseStyle.failedIcon, for: .normal)
self.failedButton.setImage(self.baseStyle.failedIconHighlighted, for: .highlighted)
self.failedButton.alpha = 1
} else {
self.failedButton.alpha = 0
}
self.updateFailedIconState()
self.accessoryTimestampView.attributedText = style.attributedStringForDate(viewModel.date)
self.updateSelectionIndicator(with: style)

Expand All @@ -281,6 +275,21 @@ open class BaseMessageCollectionViewCell<BubbleViewType>: UICollectionViewCell,
self.layoutIfNeeded()
}

public func updateFailedIconState() {
let oldAlpha = self.failedButton.alpha
if self.shouldShowFailedIcon {
self.failedButton.setImage(self.baseStyle.failedIcon, for: .normal)
self.failedButton.setImage(self.baseStyle.failedIconHighlighted, for: .highlighted)
self.failedButton.alpha = 1
} else {
self.failedButton.alpha = 0
}
if oldAlpha != self.failedButton.alpha {
// to recalculate bubble offsets
self.setNeedsLayout()
}
}

private func observeAvatar() {
guard self.viewContext != .sizing else { return }
guard let viewModel = self.messageViewModel else { return }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,7 @@ open class CompoundMessagePresenter<ViewModelBuilderT, InteractionHandlerT>
let currentIds = contentFactories.map { $0.identifier }
self.contentFactories = contentFactories

self.contentPresenters = self.contentFactories.compactMap {
var presenter = $0.createContentPresenter(forModel: self.messageModel)
presenter.delegate = self
return presenter
}
self.contentPresenters = self.contentFactories.compactMap(self.createContentPresenter)

self.menuPresenter = self.contentFactories.lazy.compactMap { $0.createMenuPresenter(forModel: self.messageModel) }.first

Expand Down Expand Up @@ -264,6 +260,20 @@ open class CompoundMessagePresenter<ViewModelBuilderT, InteractionHandlerT>
self.menuPresenter?.performMenuControllerAction(action)
}

open override func onCellFailedButtonTapped(_ failedButtonView: UIView) {
if self.messageModel.status == .failed {
super.onCellFailedButtonTapped(failedButtonView)
} else {
for presenter in self.contentPresenters {
guard let failablePresenter = presenter as? FailableMessageContentPresenterProtocol,
failablePresenter.contentTransferStatus?.value == .failed else {
continue
}
failablePresenter.handleFailedIconTap()
}
}
}

// MARK: - ChatItemSpotlighting

override open func spotlight() {
Expand Down Expand Up @@ -298,4 +308,36 @@ open class CompoundMessagePresenter<ViewModelBuilderT, InteractionHandlerT>
}
}
}

private func createContentPresenter(using factory: AnyMessageContentFactory<ModelT>) -> MessageContentPresenterProtocol {
var presenter = factory.createContentPresenter(forModel: self.messageModel)
presenter.delegate = self
if let failablePresenter = presenter as? FailableMessageContentPresenterProtocol {
failablePresenter.contentTransferStatus?.observe(self, closure: { [weak self] (_, _) in
self?.handleContentTransferStatusUpdate()
})
}
return presenter
}

private func handleContentTransferStatusUpdate() {
let aggregatedContentStatus = self.contentPresenters.compactMap { $0 as? FailableMessageContentPresenterProtocol }
.reduce(TransferStatus.success, { (result, presenter) in
guard let status = presenter.contentTransferStatus?.value else { return result }
switch status {
case .failed:
return .failed
case .transfering:
return result == .success ? status : result
case .idle, .success:
return result
}
})

let currentContentStatus = self.messageViewModel.messageContentTransferStatus
guard currentContentStatus != aggregatedContentStatus else { return }

self.messageViewModel.messageContentTransferStatus = aggregatedContentStatus
self.cell?.updateFailedIconState()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ public final class DefaultMessageContentPresenter<MessageType, ViewType: UIView>
public func contentWillBeShown() { self.onContentWillBeShown?(self.message, self.view) }
public func contentWasHidden() { self.onContentWasHidden?(self.message, self.view) }
public func contentWasTapped_deprecated() { self.onContentWasTapped_deprecated?(self.message, self.view) }
public func handleFailedIconTap() {}

public func bindToView(with viewReference: ViewReference) {
self.viewReference = viewReference
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ public protocol MessageContentPresenterProtocol {
func updateMessage(_ newMessage: Any)
}

public protocol FailableMessageContentPresenterProtocol: MessageContentPresenterProtocol {
var contentTransferStatus: Observable<TransferStatus>? { get }
func handleFailedIconTap()
}

public extension MessageContentPresenterProtocol {
var supportsMessageUpdating: Bool { return false }
var contentTransferStatus: TransferStatus? { nil }
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,78 @@ final class CompoundMessagePresenterTests: XCTestCase {
XCTAssertEqual(0, presenter.invokedUpdateContentCount)
XCTAssertEqual(1, presenter.invokedUpdateExistingContentPresentersCount)
}

func test_GivenFailableContentPresenter_WhenItsContentIsFailedToLoad_ThenViewModelStatusUpdatedToFailed() throws {
let contentTransferStatus = Observable<TransferStatus>(.idle)
let (presenter, viewModel) = try self.makeRealPresenter(contentTransferStatus: contentTransferStatus)
contentTransferStatus.value = .failed
XCTAssertEqual(viewModel.messageContentTransferStatus, .failed)
// to avoid the warning about not used variable
_ = presenter
}

func test_GivenFailableContentPresenter_WhenItsContentTurnsToFailedToSuccess_ThenViewModelStatusUpdatedToSuccess() throws {
let contentTransferStatus = Observable<TransferStatus>(.failed)
let (presenter, viewModel) = try self.makeRealPresenter(contentTransferStatus: contentTransferStatus)
contentTransferStatus.value = .success
XCTAssertEqual(viewModel.messageContentTransferStatus, .success)
// to avoid the warning about not used variable
_ = presenter
}

func test_GivenContentPresenterWithFailedContentAndMessageWithFailedDeliveryStauts_WhenFailIconTapped_ThenInteractionHandlerCalled() throws {
let fakeMessage = MessageModel(uid: "111",
senderId: "123",
type: "text",
isIncoming: true,
date: Date(timeIntervalSince1970: 0),
status: .failed)
let interactionHandler = FakeMessageInteractionHandler.niceMock()
let (presenter, _) = try self.makeRealPresenter(
message: fakeMessage,
interactionHandler: interactionHandler,
contentTransferStatus: .init(.failed)
)

presenter.onCellFailedButtonTapped(UIView())
XCTAssert(interactionHandler._userDidTapOnFailIcon.wasCalled)
}

func test_GivenContentPresenterWithFailedContentAndMessageWithSuccessDeliveryStauts_WhenFailIconTapped_ThenContentPresenterCalled() throws {
let contentPresenter = FakeMessageContentPresenter()
let (presenter, _) = try self.makeRealPresenter(
contentPresenter: contentPresenter,
contentTransferStatus: .init(.failed)
)

presenter.onCellFailedButtonTapped(UIView())
XCTAssert(contentPresenter.wasHandleFailedIconTapCalled)
}

private func makeRealPresenter(
message: MessageModel = TestHelpers.makeMessage(withId: "123"),
contentPresenter: FakeMessageContentPresenter = FakeMessageContentPresenter(),
interactionHandler: FakeMessageInteractionHandler? = nil,
contentTransferStatus: Observable<TransferStatus> = .init(.success)
) throws -> (CompoundMessagePresenter<FakeViewModelBuilder, FakeMessageInteractionHandler>, MessageViewModel) {
let viewModelBuilder = TestHelpers.makeFakeViewModelBuilder()
contentPresenter.contentTransferStatus = contentTransferStatus

let contentFactory = FakeMessageContentFactory<MessageModel>()
contentFactory.fakeContentPresenter = contentPresenter
let viewModel = try XCTUnwrap(viewModelBuilder.stubbedCreateViewModelResult)

let presenter = CompoundMessagePresenter<FakeViewModelBuilder, FakeMessageInteractionHandler>(
messageModel: message,
viewModelBuilder: viewModelBuilder,
interactionHandler: interactionHandler,
contentFactories: [.init(contentFactory)],
sizingCell: CompoundMessageCollectionViewCell(frame: .zero),
baseCellStyle: StubMessageCollectionViewCellStyle(),
compoundCellStyle: StubCompoundBubbleViewStyle(),
cache: Cache<CompoundBubbleLayoutProvider.Configuration, CompoundBubbleLayoutProvider>(),
accessibilityIdentifier: nil
)
return (presenter, viewModel)
}
}
Loading

0 comments on commit ec04592

Please sign in to comment.