Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Chatto/Chatto.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
A410D4BD26C570B100A48342 /* ChatMessagesViewControllerHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = A410D4BC26C570B100A48342 /* ChatMessagesViewControllerHelpers.swift */; };
A42C66DE273BC387006B032A /* TimingChatInputBarAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A42C66DD273BC387006B032A /* TimingChatInputBarAnimation.swift */; };
A42C66E0273BC3B1006B032A /* SpringChatInputBarAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A42C66DF273BC3B1006B032A /* SpringChatInputBarAnimation.swift */; };
A441C27C2787442C00988636 /* ChatCollectionViewLayoutModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A441C27B2787442C00988636 /* ChatCollectionViewLayoutModelFactory.swift */; };
A441C27E2787446C00988636 /* ChatCollectionItemsDiffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A441C27D2787446C00988636 /* ChatCollectionItemsDiffer.swift */; };
A452A8C12705DAED00DCC8D5 /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = A452A8BF2705DAED00DCC8D5 /* Observable.swift */; };
A4724CDB27355829006B9562 /* ChatPanGestureRecogniserHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4724CDA27355829006B9562 /* ChatPanGestureRecogniserHandler.swift */; };
A4C03CFE26CE431D00360526 /* ChatInputBarPresenterProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4C03CFD26CE431D00360526 /* ChatInputBarPresenterProtocol.swift */; };
Expand Down Expand Up @@ -69,6 +71,8 @@
A410D4BC26C570B100A48342 /* ChatMessagesViewControllerHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessagesViewControllerHelpers.swift; sourceTree = "<group>"; };
A42C66DD273BC387006B032A /* TimingChatInputBarAnimation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimingChatInputBarAnimation.swift; sourceTree = "<group>"; };
A42C66DF273BC3B1006B032A /* SpringChatInputBarAnimation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpringChatInputBarAnimation.swift; sourceTree = "<group>"; };
A441C27B2787442C00988636 /* ChatCollectionViewLayoutModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatCollectionViewLayoutModelFactory.swift; sourceTree = "<group>"; };
A441C27D2787446C00988636 /* ChatCollectionItemsDiffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatCollectionItemsDiffer.swift; sourceTree = "<group>"; };
A452A8BF2705DAED00DCC8D5 /* Observable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = "<group>"; };
A4724CDA27355829006B9562 /* ChatPanGestureRecogniserHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatPanGestureRecogniserHandler.swift; sourceTree = "<group>"; };
A4C03CFD26CE431D00360526 /* ChatInputBarPresenterProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInputBarPresenterProtocol.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -224,6 +228,8 @@
A4F7666226BD417600008F24 /* ChatMessagesViewController.swift */,
A4F7666526BD4FA400008F24 /* ChatMessageCollectionAdapter.swift */,
A4F7666726BD59EF00008F24 /* ChatMessagesViewModel.swift */,
A441C27B2787442C00988636 /* ChatCollectionViewLayoutModelFactory.swift */,
A441C27D2787446C00988636 /* ChatCollectionItemsDiffer.swift */,
);
path = ChatMessages;
sourceTree = "<group>";
Expand Down Expand Up @@ -440,6 +446,8 @@
A42C66DE273BC387006B032A /* TimingChatInputBarAnimation.swift in Sources */,
C36281E71BF0F196004D6BCE /* UICollectionView+Scrolling.swift in Sources */,
26D653F2251043BD007BC13C /* ReplyFeedbackGenerator.swift in Sources */,
A441C27E2787446C00988636 /* ChatCollectionItemsDiffer.swift in Sources */,
A441C27C2787442C00988636 /* ChatCollectionViewLayoutModelFactory.swift in Sources */,
3565429D203DB99300B29DA1 /* ChatLayoutConfiguration.swift in Sources */,
C36281EB1BF0F62F004D6BCE /* DummyChatItemPresenter.swift in Sources */,
C3C7C3981CAC4BAC00A49929 /* ChatCollectionViewLayout.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
//
// Copyright (c) Bumble, 2021-present. All rights reserved.
//

import CoreGraphics

final class ChatCollectionItemsDiffer {

struct Diff {
let changes: CollectionChanges
let itemCompanionCollection: ChatItemCompanionCollection
let layoutModel: ChatCollectionViewLayoutModel
}

private let chatItemsDecorator: ChatItemsDecoratorProtocol
private let chatItemPresenterFactory: ChatItemPresenterFactoryProtocol
private let chatCollectionViewLayoutModelFactory: ChatCollectionViewLayoutModelFactoryProtocol

init(
chatItemsDecorator: ChatItemsDecoratorProtocol,
chatItemPresenterFactory: ChatItemPresenterFactoryProtocol,
chatCollectionViewLayoutModelFactory: ChatCollectionViewLayoutModelFactoryProtocol
) {
self.chatItemsDecorator = chatItemsDecorator
self.chatItemPresenterFactory = chatItemPresenterFactory
self.chatCollectionViewLayoutModelFactory = chatCollectionViewLayoutModelFactory
}

func calculateChanges(
newItems: [ChatItemProtocol],
oldItems: ChatItemCompanionCollection,
collectionViewWidth: CGFloat
) -> Diff {

let newDecoratedItems = self.chatItemsDecorator.decorateItems(newItems)
let changes = generateChanges(
oldCollection: oldItems.map(HashableItem.init),
newCollection: newDecoratedItems.map(HashableItem.init)
)
let itemCompanionCollection = self.createCompanionCollection(fromChatItems: newDecoratedItems, previousCompanionCollection: oldItems)
let layoutModel = self.chatCollectionViewLayoutModelFactory.createLayoutModel(itemCompanionCollection, collectionViewWidth: collectionViewWidth)

return Diff(
changes: changes,
itemCompanionCollection: itemCompanionCollection,
layoutModel: layoutModel
)
}

private func createCompanionCollection(fromChatItems newItems: [DecoratedChatItem], previousCompanionCollection oldItems: ChatItemCompanionCollection) -> ChatItemCompanionCollection {
return ChatItemCompanionCollection(items: newItems.map { (decoratedChatItem) -> ChatItemCompanion in

/*
We use an assumption, that message having a specific messageId never changes its type.
If such changes has to be supported, then generation of changes has to suppport reloading items.
Otherwise, updateVisibleCells may try to update the existing cells with new presenters which aren't able to work with another types.
*/

let presenter: ChatItemPresenterProtocol = {
guard let oldChatItemCompanion = oldItems[decoratedChatItem.uid] ?? oldItems[decoratedChatItem.chatItem.uid],
oldChatItemCompanion.chatItem.type == decoratedChatItem.chatItem.type,
oldChatItemCompanion.presenter.isItemUpdateSupported else {
return self.chatItemPresenterFactory.createChatItemPresenter(decoratedChatItem.chatItem)
}

oldChatItemCompanion.presenter.update(with: decoratedChatItem.chatItem)
return oldChatItemCompanion.presenter
}()

return ChatItemCompanion(uid: decoratedChatItem.uid, chatItem: decoratedChatItem.chatItem, presenter: presenter, decorationAttributes: decoratedChatItem.decorationAttributes)
})
}
}

private struct HashableItem: Hashable {
private let uid: String
private let type: String

init(_ decoratedChatItem: DecoratedChatItem) {
self.uid = decoratedChatItem.uid
self.type = decoratedChatItem.chatItem.type
}

init(_ chatItemCompanion: ChatItemCompanion) {
self.uid = chatItemCompanion.uid
self.type = chatItemCompanion.chatItem.type
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//
// Copyright (c) Bumble, 2021-present. All rights reserved.
//

import CoreGraphics
import Foundation

public protocol ChatCollectionViewLayoutModelFactoryProtocol {
func createLayoutModel(_ items: ChatItemCompanionCollection, collectionViewWidth: CGFloat) -> ChatCollectionViewLayoutModel
}

public final class ChatCollectionViewLayoutModelFactory: ChatCollectionViewLayoutModelFactoryProtocol {

public init() { }

public func createLayoutModel(_ items: ChatItemCompanionCollection, collectionViewWidth: CGFloat) -> ChatCollectionViewLayoutModel {
// swiftlint:disable:next nesting
typealias IntermediateItemLayoutData = (height: CGFloat?, bottomMargin: CGFloat)
typealias ItemLayoutData = (height: CGFloat, bottomMargin: CGFloat)
// swiftlint:disable:previous nesting

func createLayoutModel(intermediateLayoutData: [IntermediateItemLayoutData]) -> ChatCollectionViewLayoutModel {
let layoutData = intermediateLayoutData.map { (intermediateLayoutData: IntermediateItemLayoutData) -> ItemLayoutData in
return (height: intermediateLayoutData.height!, bottomMargin: intermediateLayoutData.bottomMargin)
}
return ChatCollectionViewLayoutModel.createModel(collectionViewWidth, itemsLayoutData: layoutData)
}

let isInBackground = !Thread.isMainThread
var intermediateLayoutData = [IntermediateItemLayoutData]()
var itemsForMainThread = [(index: Int, itemCompanion: ChatItemCompanion)]()

for (index, itemCompanion) in items.enumerated() {
var height: CGFloat?
let bottomMargin: CGFloat = itemCompanion.decorationAttributes?.bottomMargin ?? 0
if !isInBackground || itemCompanion.presenter.canCalculateHeightInBackground {
height = itemCompanion.presenter.heightForCell(maximumWidth: collectionViewWidth, decorationAttributes: itemCompanion.decorationAttributes)
} else {
itemsForMainThread.append((index: index, itemCompanion: itemCompanion))
}
intermediateLayoutData.append((height: height, bottomMargin: bottomMargin))
}

if itemsForMainThread.count > 0 {
DispatchQueue.main.sync {
for (index, itemCompanion) in itemsForMainThread {
let height = itemCompanion.presenter.heightForCell(
maximumWidth: collectionViewWidth,
decorationAttributes: itemCompanion.decorationAttributes
)
intermediateLayoutData[index].height = height
}
}
}
return createLayoutModel(intermediateLayoutData: intermediateLayoutData)
}
}
Loading