184 lines
7.7 KiB
Swift
184 lines
7.7 KiB
Swift
//
|
|
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
extension ConversationViewController {
|
|
|
|
/// The visble content rect in the collection view's coordinate space
|
|
/// This rect does not include displayed cells occluded by content inset
|
|
@objc var visibleContentRect: CGRect {
|
|
let collectionViewBounds = collectionView.bounds
|
|
let insetBounds = collectionViewBounds.inset(by: collectionView.adjustedContentInset)
|
|
return insetBounds
|
|
}
|
|
|
|
/// The index path of the last item in the collection view's visible rect
|
|
@objc
|
|
public var firstVisibleIndexPath: IndexPath? {
|
|
// For people looking at this in the future, UICollectionView has a very similar looking
|
|
// property: -indexPathsForVisibleItems. Why aren't we using that?
|
|
//
|
|
// That property *almost* gives us what we want, but UIKit ordering isn't favorable. That property
|
|
// gets updated after -scrollViewDidScroll: returns. But sometimes we want to know what cells are visible
|
|
// with the updated -contentOffset in -scrollViewDidScroll:. So instead, we'll just see what layoutAttributes
|
|
// are now in the collection view's visible content rect. This should be safe, since it's computed from the
|
|
// already updated -contentOffset.
|
|
let visibleLayoutAttributes = layout.layoutAttributesForElements(in: visibleContentRect) ?? []
|
|
|
|
let firstVisibleIndexPath = visibleLayoutAttributes
|
|
.map { $0.indexPath }
|
|
.min { $0.row < $1.row }
|
|
|
|
if let firstVisibleIndexPath = firstVisibleIndexPath {
|
|
owsAssertDebug(percentOfIndexPathVisibleAboveBottom(firstVisibleIndexPath) > 0)
|
|
}
|
|
return firstVisibleIndexPath
|
|
}
|
|
|
|
/// The index path of the last item in the collection view's visible rect
|
|
@objc
|
|
public var lastVisibleIndexPath: IndexPath? {
|
|
// For people looking at this in the future, UICollectionView has a very similar looking
|
|
// property: -indexPathsForVisibleItems. Why aren't we using that?
|
|
//
|
|
// That property *almost* gives us what we want, but UIKit ordering isn't favorable. That property
|
|
// gets updated after -scrollViewDidScroll: returns. But sometimes we want to know what cells are visible
|
|
// with the updated -contentOffset in -scrollViewDidScroll:. So instead, we'll just see what layoutAttributes
|
|
// are now in the collection view's visible content rect. This should be safe, since it's computed from the
|
|
// already updated -contentOffset.
|
|
let visibleLayoutAttributes = layout.layoutAttributesForElements(in: visibleContentRect) ?? []
|
|
|
|
let lastVisibleIndexPath = visibleLayoutAttributes
|
|
.map { $0.indexPath }
|
|
.max { $0.row < $1.row }
|
|
|
|
if let lastVisibleIndexPath = lastVisibleIndexPath {
|
|
owsAssertDebug(percentOfIndexPathVisibleAboveBottom(lastVisibleIndexPath) > 0)
|
|
}
|
|
return lastVisibleIndexPath
|
|
}
|
|
|
|
@objc
|
|
var lastVisibleSortId: UInt64 {
|
|
guard let lastVisibleIndexPath = lastVisibleIndexPath else { return 0 }
|
|
return firstRenderItemReferenceWithSortId(atOrBeforeIndexPath: lastVisibleIndexPath)?.sortId ?? 0
|
|
}
|
|
|
|
@objc
|
|
var lastIndexPathInLoadedWindow: IndexPath? {
|
|
guard !renderItems.isEmpty else { return nil }
|
|
return IndexPath(row: renderItems.count - 1, section: 0)
|
|
}
|
|
|
|
@objc
|
|
var lastSortIdInLoadedWindow: UInt64 {
|
|
guard let lastIndexPath = lastIndexPathInLoadedWindow else { return 0 }
|
|
return firstRenderItemReferenceWithSortId(atOrBeforeIndexPath: lastIndexPath)?.sortId ?? 0
|
|
}
|
|
|
|
@objc
|
|
func saveLastVisibleSortIdAndOnScreenPercentage() {
|
|
AssertIsOnMainThread()
|
|
|
|
guard hasAppearedAndHasAppliedFirstLoad else {
|
|
return
|
|
}
|
|
guard !isMeasuringKeyboardHeight else {
|
|
return
|
|
}
|
|
|
|
var newValue: TSThread.LastVisibleInteraction?
|
|
if let lastVisibleIndexPath = lastVisibleIndexPath,
|
|
let reference = firstRenderItemReferenceWithSortId(atOrBeforeIndexPath: lastVisibleIndexPath) {
|
|
|
|
let onScreenPercentage = percentOfIndexPathVisibleAboveBottom(reference.indexPath)
|
|
|
|
newValue = TSThread.LastVisibleInteraction(sortId: reference.sortId,
|
|
onScreenPercentage: onScreenPercentage)
|
|
}
|
|
|
|
let thread = self.thread
|
|
let oldValue = databaseStorage.read { transaction in
|
|
thread.lastVisibleInteraction(transaction: transaction)
|
|
}
|
|
|
|
guard oldValue != newValue else {
|
|
return
|
|
}
|
|
|
|
databaseStorage.asyncWrite { transaction in
|
|
thread.setLastVisibleInteraction(newValue, transaction: transaction)
|
|
}
|
|
}
|
|
|
|
#if TESTABLE_BUILD
|
|
@objc
|
|
func logFirstAndLastVisibleItems() {
|
|
AssertIsOnMainThread()
|
|
|
|
if let firstVisibleIndexPath = firstVisibleIndexPath,
|
|
let reference = firstRenderItemReferenceWithSortId(atOrBeforeIndexPath: firstVisibleIndexPath) {
|
|
|
|
let onScreenPercentage = percentOfIndexPathVisibleAboveBottom(reference.indexPath)
|
|
let bodyText: String = (reference.interaction as? TSMessage)?.body ?? "none"
|
|
Logger.verbose("---- first visible item: sortId: \(reference.sortId), indexPath: \(reference.indexPath), bodyText: \(bodyText), onScreenPercentage: \(onScreenPercentage), ")
|
|
}
|
|
|
|
if let lastVisibleIndexPath = lastVisibleIndexPath,
|
|
let reference = firstRenderItemReferenceWithSortId(atOrBeforeIndexPath: lastVisibleIndexPath) {
|
|
|
|
let onScreenPercentage = percentOfIndexPathVisibleAboveBottom(reference.indexPath)
|
|
let bodyText: String = (reference.interaction as? TSMessage)?.body ?? "none"
|
|
Logger.verbose("---- last visible item: sortId: \(reference.sortId), indexPath: \(reference.indexPath), bodyText: \(bodyText), onScreenPercentage: \(onScreenPercentage)")
|
|
}
|
|
|
|
}
|
|
#endif
|
|
|
|
private func percentOfIndexPathVisibleAboveBottom(_ indexPath: IndexPath) -> CGFloat {
|
|
|
|
// If we don't have layout attributes, it's not visible
|
|
guard let attributes = layout.layoutAttributesForItem(at: indexPath) else { return 0.0 }
|
|
|
|
// Map everything to view controller's coordinate space
|
|
let cellFrameInPrimaryCoordinateSpace = view.convert(attributes.frame, from: collectionView)
|
|
let contentFrameInPrimaryCoordinateSpace = view.convert(visibleContentRect, from: collectionView)
|
|
|
|
// Distance between top edge of cell's frame and the bottom of the content frame
|
|
let heightAboveBottom = contentFrameInPrimaryCoordinateSpace.maxY - cellFrameInPrimaryCoordinateSpace.minY
|
|
return CGFloatClamp01(heightAboveBottom / cellFrameInPrimaryCoordinateSpace.height)
|
|
}
|
|
|
|
struct RenderItemReference {
|
|
let renderItem: CVRenderItem
|
|
let indexPath: IndexPath
|
|
|
|
var interaction: TSInteraction { renderItem.interaction }
|
|
var sortId: UInt64 { interaction.sortId }
|
|
}
|
|
|
|
private func firstRenderItemReferenceWithSortId(atOrBeforeIndexPath indexPath: IndexPath) -> RenderItemReference? {
|
|
AssertIsOnMainThread()
|
|
|
|
var matchingIndexPath = indexPath
|
|
|
|
while matchingIndexPath.row >= 0,
|
|
matchingIndexPath.row < renderItems.count,
|
|
let renderItem = renderItem(forIndex: matchingIndexPath.row) {
|
|
guard !renderItem.interaction.isDynamicInteraction() else {
|
|
guard matchingIndexPath.row > 0 else {
|
|
return nil
|
|
}
|
|
matchingIndexPath.row -= 1
|
|
continue
|
|
}
|
|
|
|
return RenderItemReference(renderItem: renderItem, indexPath: matchingIndexPath)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|