247 lines
8.6 KiB
Swift
247 lines
8.6 KiB
Swift
//
|
|
// Copyright 2020 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import SignalServiceKit
|
|
|
|
enum CVLoadType: Equatable, CustomStringConvertible {
|
|
case loadInitialMapping(focusMessageIdOnOpen: String?,
|
|
scrollAction: CVScrollAction)
|
|
case loadSameLocation(scrollAction: CVScrollAction)
|
|
case loadOlder
|
|
case loadNewer
|
|
case loadNewest(scrollAction: CVScrollAction)
|
|
case loadPageAroundInteraction(interactionId: String,
|
|
scrollAction: CVScrollAction)
|
|
|
|
fileprivate var priority: UInt {
|
|
switch self {
|
|
case .loadInitialMapping:
|
|
// We can't do any other load until we do the initial mapping.
|
|
return 4
|
|
case .loadSameLocation:
|
|
return 0
|
|
case .loadOlder:
|
|
// The view is auto-loading.
|
|
return 1
|
|
case .loadNewer:
|
|
// The view is auto-loading.
|
|
return 1
|
|
case .loadNewest:
|
|
// The user explicitly requested this load.
|
|
return 2
|
|
case .loadPageAroundInteraction:
|
|
// The user explicitly requested this load.
|
|
return 3
|
|
}
|
|
}
|
|
|
|
var scrollAction: CVScrollAction {
|
|
switch self {
|
|
case .loadInitialMapping(_, let scrollAction):
|
|
return scrollAction
|
|
case .loadSameLocation(let scrollAction):
|
|
return scrollAction
|
|
case .loadOlder, .loadNewer:
|
|
return .none
|
|
case .loadNewest(let scrollAction):
|
|
return scrollAction
|
|
case .loadPageAroundInteraction(_, let scrollAction):
|
|
return scrollAction
|
|
}
|
|
}
|
|
|
|
// MARK: - CustomStringConvertible
|
|
|
|
public var description: String {
|
|
switch self {
|
|
case .loadInitialMapping:
|
|
return "loadInitialMapping"
|
|
case .loadSameLocation:
|
|
return "loadSameLocation"
|
|
case .loadOlder:
|
|
return "loadOlder"
|
|
case .loadNewer:
|
|
return "loadNewer"
|
|
case .loadNewest:
|
|
return "loadNewest"
|
|
case .loadPageAroundInteraction:
|
|
return "loadPageAroundInteraction"
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
struct CVLoadRequest {
|
|
public typealias RequestId = UInt
|
|
let requestId: RequestId
|
|
let loadType: CVLoadType
|
|
let updatedInteractionIds: Set<String>
|
|
let deletedInteractionIds: Set<String>
|
|
let canReuseInteractionModels: Bool
|
|
let canReuseComponentStates: Bool
|
|
let didReset: Bool
|
|
|
|
var isInitialLoad: Bool {
|
|
switch loadType {
|
|
case .loadInitialMapping:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
var scrollAction: CVScrollAction { loadType.scrollAction }
|
|
|
|
// Now that loads are async, there's some complexity.
|
|
//
|
|
// * Maybe two loads are enqueued before we can do either. We only want
|
|
// to do one load in this case that reflects the motive for both loads.
|
|
// * Load requests are stateful, e.g. "reload because interaction X changed".
|
|
// We need to collect such state.
|
|
// * Load requests might conflict: e.g. "load older" "load newer" or "load
|
|
// search result X", "load search result Y". We arbitrate.
|
|
// * After a load lands we often want to scroll to a given interaction.
|
|
// That's tricky; the user may have "cancelled" that scroll request with
|
|
// other UX interactions before the load began or while the load was in flight.
|
|
//
|
|
// `CVLoadRequest.Builder` will handle these responsibilties.
|
|
struct Builder {
|
|
private static let requestIdCounter = AtomicUInt(lock: .sharedGlobal)
|
|
let requestId = Self.requestIdCounter.increment()
|
|
|
|
// Has any load been requested?
|
|
private var shouldLoad = false
|
|
|
|
private var updatedInteractionIds = Set<String>()
|
|
private var deletedInteractionIds = Set<String>()
|
|
|
|
private var loadType: CVLoadType = .loadSameLocation(scrollAction: .none)
|
|
|
|
private mutating func tryToUpdateLoadType(_ newValue: CVLoadType) {
|
|
guard newValue.priority >= loadType.priority else {
|
|
return
|
|
}
|
|
|
|
if case .loadSameLocation = loadType,
|
|
case .loadSameLocation = newValue,
|
|
newValue.scrollAction == .none {
|
|
// Don't lose the scroll action:
|
|
//
|
|
// Don't replace and old .loadSameLocation with a scroll action
|
|
// with a new .loadSameLocation without a scroll action.
|
|
return
|
|
}
|
|
|
|
loadType = newValue
|
|
}
|
|
|
|
private var canReuseInteractionModels = true
|
|
private var canReuseComponentStates = true
|
|
private var didReset = false
|
|
|
|
mutating func reload(updatedInteractionIds: Set<String>,
|
|
deletedInteractionIds: Set<String>) {
|
|
AssertIsOnMainThread()
|
|
|
|
self.updatedInteractionIds.formUnion(updatedInteractionIds)
|
|
self.deletedInteractionIds.formUnion(deletedInteractionIds)
|
|
|
|
shouldLoad = true
|
|
}
|
|
|
|
mutating func loadInitialMapping(focusMessageIdOnOpen: String?) {
|
|
AssertIsOnMainThread()
|
|
|
|
// Configure for initial mapping.
|
|
let scrollAction = CVScrollAction(action: .initialPosition,
|
|
isAnimated: false)
|
|
tryToUpdateLoadType(.loadInitialMapping(focusMessageIdOnOpen: focusMessageIdOnOpen,
|
|
scrollAction: scrollAction))
|
|
shouldLoad = true
|
|
}
|
|
|
|
mutating func loadOlderItems() {
|
|
AssertIsOnMainThread()
|
|
|
|
tryToUpdateLoadType(.loadOlder)
|
|
shouldLoad = true
|
|
}
|
|
|
|
mutating func loadNewerItems() {
|
|
AssertIsOnMainThread()
|
|
|
|
tryToUpdateLoadType(.loadNewer)
|
|
shouldLoad = true
|
|
}
|
|
|
|
mutating func loadAndScrollToNewestItems(isAnimated: Bool) {
|
|
AssertIsOnMainThread()
|
|
|
|
let scrollAction = CVScrollAction(action: .bottomOfLoadWindow,
|
|
isAnimated: isAnimated)
|
|
tryToUpdateLoadType(.loadNewest(scrollAction: scrollAction))
|
|
shouldLoad = true
|
|
}
|
|
|
|
mutating func loadAndScrollToInteraction(interactionId: String,
|
|
onScreenPercentage: CGFloat,
|
|
alignment: ScrollAlignment,
|
|
isAnimated: Bool) {
|
|
AssertIsOnMainThread()
|
|
|
|
let scrollAction = CVScrollAction(action: .scrollTo(interactionId: interactionId,
|
|
onScreenPercentage: onScreenPercentage,
|
|
alignment: alignment),
|
|
isAnimated: isAnimated)
|
|
tryToUpdateLoadType(.loadPageAroundInteraction(interactionId: interactionId,
|
|
scrollAction: scrollAction))
|
|
shouldLoad = true
|
|
}
|
|
|
|
mutating func reload(scrollAction: CVScrollAction?) {
|
|
AssertIsOnMainThread()
|
|
|
|
if let scrollAction = scrollAction {
|
|
tryToUpdateLoadType(.loadSameLocation(scrollAction: scrollAction))
|
|
}
|
|
shouldLoad = true
|
|
}
|
|
|
|
mutating func reloadWithoutCaches() {
|
|
reload(canReuseInteractionModels: false, canReuseComponentStates: false, didReset: true)
|
|
}
|
|
|
|
mutating func reload(canReuseInteractionModels: Bool = true,
|
|
canReuseComponentStates: Bool = true,
|
|
didReset: Bool = false) {
|
|
AssertIsOnMainThread()
|
|
|
|
self.canReuseInteractionModels = self.canReuseInteractionModels && canReuseInteractionModels
|
|
self.canReuseComponentStates = self.canReuseComponentStates && canReuseComponentStates
|
|
self.didReset = self.didReset || didReset
|
|
|
|
shouldLoad = true
|
|
}
|
|
|
|
func build() -> CVLoadRequest? {
|
|
AssertIsOnMainThread()
|
|
|
|
guard shouldLoad else {
|
|
return nil
|
|
}
|
|
|
|
return CVLoadRequest(
|
|
requestId: requestId,
|
|
loadType: loadType,
|
|
updatedInteractionIds: updatedInteractionIds,
|
|
deletedInteractionIds: deletedInteractionIds,
|
|
canReuseInteractionModels: canReuseInteractionModels,
|
|
canReuseComponentStates: canReuseComponentStates,
|
|
didReset: didReset
|
|
)
|
|
}
|
|
}
|
|
}
|