197 lines
8.0 KiB
Swift
197 lines
8.0 KiB
Swift
/*
|
|
Copyright 2020-2024 New Vector Ltd.
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only
|
|
Please see LICENSE in the repository root for full details.
|
|
*/
|
|
|
|
import UIKit
|
|
import MatrixSDK
|
|
import SwiftUI
|
|
|
|
@objc protocol SizableBaseRoomCellType: BaseRoomCellProtocol {
|
|
static func sizingViewHeightHashValue(from bubbleCellData: MXKRoomBubbleCellData) -> Int
|
|
}
|
|
|
|
/// `SizableBaseRoomCell` allows a cell using Auto Layout that inherits from this class to automatically return the height of the cell and cache the result.
|
|
@objcMembers
|
|
class SizableBaseRoomCell: BaseRoomCell, SizableBaseRoomCellType {
|
|
|
|
// MARK: - Constants
|
|
|
|
private static let sizingViewHeightStore = SizingViewHeightStore()
|
|
private static var sizingViews: [String: SizableBaseRoomCell] = [:]
|
|
private static let sizingReactionsView = RoomReactionsView()
|
|
|
|
private static let reactionsViewSizer = RoomReactionsViewSizer()
|
|
private static let reactionsViewModelBuilder = RoomReactionsViewModelBuilder()
|
|
|
|
private static let urlPreviewViewSizer = URLPreviewViewSizer()
|
|
private var contentVC: UIViewController?
|
|
|
|
private class var sizingView: SizableBaseRoomCell {
|
|
let sizingView: SizableBaseRoomCell
|
|
|
|
let reuseIdentifier: String = self.defaultReuseIdentifier()
|
|
|
|
if let cachedSizingView = self.sizingViews[reuseIdentifier] {
|
|
sizingView = cachedSizingView
|
|
} else {
|
|
sizingView = self.createSizingView()
|
|
self.sizingViews[reuseIdentifier] = sizingView
|
|
}
|
|
return sizingView
|
|
}
|
|
|
|
// MARK: - Overrides
|
|
|
|
override class func height(for cellData: MXKCellData!, withMaximumWidth maxWidth: CGFloat) -> CGFloat {
|
|
guard let cellData = cellData else {
|
|
return 0
|
|
}
|
|
|
|
guard let roomBubbleCellData = cellData as? MXKRoomBubbleCellData else {
|
|
return 0
|
|
}
|
|
|
|
return self.height(for: roomBubbleCellData, fitting: maxWidth)
|
|
}
|
|
|
|
override func prepareForReuse() {
|
|
cleanContentVC()
|
|
|
|
super.prepareForReuse()
|
|
}
|
|
|
|
// MARK - SizableBaseRoomCellType
|
|
|
|
// Each sublcass should override this method, to indicate a unique identifier for a view height.
|
|
// This means that the value should change if there is some data that modify the cell height.
|
|
class func sizingViewHeightHashValue(from bubbleCellData: MXKRoomBubbleCellData) -> Int {
|
|
// TODO: Improve default hash value computation:
|
|
// - Implement RoomBubbleCellData hash
|
|
// - Handle reactions
|
|
return bubbleCellData.hashValue
|
|
}
|
|
|
|
// MARK: - Private
|
|
|
|
class func createSizingView() -> SizableBaseRoomCell {
|
|
return self.init(style: .default, reuseIdentifier: self.defaultReuseIdentifier())
|
|
}
|
|
|
|
private class func height(for roomBubbleCellData: MXKRoomBubbleCellData, fitting width: CGFloat) -> CGFloat {
|
|
// FIXME: Size cache is disabled for the moment waiting for a better default `sizingViewHeightHashValue` implementation.
|
|
|
|
// let height: CGFloat
|
|
//
|
|
// let sizingViewHeight = self.findOrCreateSizingViewHeight(from: roomBubbleCellData)
|
|
//
|
|
// if let cachedHeight = sizingViewHeight.heights[width] {
|
|
// height = cachedHeight
|
|
// } else {
|
|
// height = self.contentViewHeight(for: roomBubbleCellData, fitting: width)
|
|
// sizingViewHeight.heights[width] = height
|
|
// }
|
|
//
|
|
// return height
|
|
|
|
return self.contentViewHeight(for: roomBubbleCellData, fitting: width)
|
|
}
|
|
|
|
private static func findOrCreateSizingViewHeight(from bubbleData: MXKRoomBubbleCellData) -> SizingViewHeight {
|
|
let bubbleDataHashValue = self.sizingViewHeightHashValue(from: bubbleData)
|
|
return self.sizingViewHeightStore.findOrCreateSizingViewHeight(from: bubbleDataHashValue)
|
|
}
|
|
|
|
private static func contentViewHeight(for cellData: MXKCellData, fitting width: CGFloat) -> CGFloat {
|
|
let sizingView = self.sizingView
|
|
|
|
sizingView.didEndDisplay()
|
|
|
|
sizingView.render(cellData)
|
|
|
|
sizingView.setNeedsLayout()
|
|
sizingView.layoutIfNeeded()
|
|
|
|
if let contentVC = sizingView.contentVC as? UIHostingController<AnyView> {
|
|
contentVC.view.invalidateIntrinsicContentSize()
|
|
}
|
|
|
|
let fittingSize = CGSize(width: width, height: UIView.layoutFittingCompressedSize.height)
|
|
var height = sizingView.systemLayoutSizeFitting(fittingSize).height
|
|
|
|
// Add read receipt height if needed
|
|
if let roomBubbleCellData = cellData as? RoomBubbleCellData, let readReceipts = roomBubbleCellData.readReceipts, readReceipts.count > 0, sizingView is RoomCellReadReceiptsDisplayable {
|
|
height+=PlainRoomCellLayoutConstants.readReceiptsViewHeight
|
|
}
|
|
|
|
// Add reactions view height if needed
|
|
if sizingView is RoomCellReactionsDisplayable,
|
|
let roomBubbleCellData = cellData as? RoomBubbleCellData,
|
|
let reactionsViewModel = self.reactionsViewModelBuilder.buildForFirstVisibleComponent(of: roomBubbleCellData) {
|
|
|
|
let reactionWidth = sizingView.roomCellContentView?.reactionsContentView.frame.width ?? roomBubbleCellData.maxTextViewWidth
|
|
|
|
let reactionsHeight = self.reactionsViewSizer.height(for: reactionsViewModel, fittingWidth: reactionWidth)
|
|
height+=reactionsHeight
|
|
}
|
|
|
|
// Add thread summary view height if needed
|
|
if sizingView is RoomCellThreadSummaryDisplayable,
|
|
let roomBubbleCellData = cellData as? RoomBubbleCellData,
|
|
roomBubbleCellData.hasThreadRoot {
|
|
|
|
let bottomMargin = sizingView.roomCellContentView?.threadSummaryContentViewBottomConstraint.constant ?? 0
|
|
|
|
height += PlainRoomCellLayoutConstants.threadSummaryViewHeight
|
|
height += bottomMargin
|
|
}
|
|
|
|
// Add URL preview view height if needed
|
|
if sizingView is RoomCellURLPreviewDisplayable,
|
|
let roomBubbleCellData = cellData as? RoomBubbleCellData, let firstBubbleComponent =
|
|
roomBubbleCellData.getFirstBubbleComponentWithDisplay(), firstBubbleComponent.showURLPreview, let urlPreviewData = firstBubbleComponent.urlPreviewData as? URLPreviewData {
|
|
|
|
let urlPreviewMaxWidth = sizingView.roomCellContentView?.urlPreviewContentView.frame.width ?? roomBubbleCellData.maxTextViewWidth
|
|
|
|
let urlPreviewHeight = self.urlPreviewViewSizer.height(for: urlPreviewData, fittingWidth: urlPreviewMaxWidth)
|
|
height+=urlPreviewHeight
|
|
}
|
|
|
|
// Add read marker view height if needed
|
|
// Note: We cannot check if readMarkerView property is set here. Extra non needed height can be added
|
|
if sizingView is RoomCellReadMarkerDisplayable,
|
|
let roomBubbleCellData = cellData as? RoomBubbleCellData, let firstBubbleComponent =
|
|
roomBubbleCellData.getFirstBubbleComponentWithDisplay(),
|
|
let eventId = firstBubbleComponent.event.eventId, let room = roomBubbleCellData.mxSession.room(withRoomId: roomBubbleCellData.roomId), let readMarkerEventId = room.accountData.readMarkerEventId, eventId == readMarkerEventId {
|
|
|
|
height+=PlainRoomCellLayoutConstants.readMarkerViewHeight
|
|
}
|
|
|
|
return height
|
|
}
|
|
|
|
private func cleanContentVC() {
|
|
contentVC?.removeFromParent()
|
|
contentVC?.view.removeFromSuperview()
|
|
contentVC?.didMove(toParent: nil)
|
|
contentVC = nil
|
|
}
|
|
|
|
// MARK: - Public
|
|
|
|
func addContentViewController(_ controller: UIViewController, on contentView: UIView) {
|
|
controller.view.invalidateIntrinsicContentSize()
|
|
|
|
cleanContentVC()
|
|
|
|
let parent = vc_parentViewController
|
|
parent?.addChild(controller)
|
|
contentView.vc_addSubViewMatchingParent(controller.view)
|
|
controller.didMove(toParent: parent)
|
|
|
|
contentVC = controller
|
|
}
|
|
}
|