mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-22 22:25:57 +00:00
Monoforums
This commit is contained in:
@@ -0,0 +1,603 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import MergeLists
|
||||
import ComponentDisplayAdapters
|
||||
|
||||
public final class AsyncListComponent: Component {
|
||||
public protocol ItemView: UIView {
|
||||
func isReorderable(at point: CGPoint) -> Bool
|
||||
}
|
||||
|
||||
public final class OverlayContainerView: UIView {
|
||||
public override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.layer.anchorPoint = CGPoint()
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public func updatePosition(position: CGPoint, transition: ComponentTransition) {
|
||||
let previousPosition: CGPoint
|
||||
var forceUpdate = false
|
||||
if self.layer.animation(forKey: "positionUpdate") != nil, let presentation = self.layer.presentation() {
|
||||
forceUpdate = true
|
||||
previousPosition = presentation.position
|
||||
|
||||
if !transition.animation.isImmediate {
|
||||
self.layer.removeAnimation(forKey: "positionUpdate")
|
||||
}
|
||||
} else {
|
||||
previousPosition = self.layer.position
|
||||
}
|
||||
|
||||
if previousPosition != position || forceUpdate {
|
||||
self.center = position
|
||||
if case let .curve(duration, curve) = transition.animation {
|
||||
self.layer.animate(
|
||||
from: NSValue(cgPoint: CGPoint(x: previousPosition.x - position.x, y: previousPosition.y - position.y)),
|
||||
to: NSValue(cgPoint: CGPoint()),
|
||||
keyPath: "position",
|
||||
duration: duration,
|
||||
delay: 0.0,
|
||||
curve: curve,
|
||||
removeOnCompletion: true,
|
||||
additive: true,
|
||||
completion: nil,
|
||||
key: "positionUpdate"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class ResetScrollingRequest: Equatable {
|
||||
let requestId: Int
|
||||
let id: AnyHashable
|
||||
|
||||
init(requestId: Int, id: AnyHashable) {
|
||||
self.requestId = requestId
|
||||
self.id = id
|
||||
}
|
||||
|
||||
static func ==(lhs: ResetScrollingRequest, rhs: ResetScrollingRequest) -> Bool {
|
||||
if lhs === rhs {
|
||||
return true
|
||||
}
|
||||
if lhs.requestId != rhs.requestId {
|
||||
return false
|
||||
}
|
||||
if lhs.id != rhs.id {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public final class ExternalState {
|
||||
public struct Value: Equatable {
|
||||
var resetScrollingRequest: ResetScrollingRequest?
|
||||
|
||||
public static func ==(lhs: Value, rhs: Value) -> Bool {
|
||||
if lhs.resetScrollingRequest != rhs.resetScrollingRequest {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public private(set) var value: Value = Value()
|
||||
private var nextId: Int = 0
|
||||
|
||||
public init() {
|
||||
|
||||
}
|
||||
|
||||
public func resetScrolling(id: AnyHashable) {
|
||||
let requestId = self.nextId
|
||||
self.nextId += 1
|
||||
self.value.resetScrollingRequest = ResetScrollingRequest(requestId: requestId, id: id)
|
||||
}
|
||||
}
|
||||
|
||||
public enum Direction {
|
||||
case vertical
|
||||
case horizontal
|
||||
}
|
||||
|
||||
public final class VisibleItem {
|
||||
public let item: AnyComponentWithIdentity<Empty>
|
||||
public let frame: CGRect
|
||||
|
||||
init(item: AnyComponentWithIdentity<Empty>, frame: CGRect) {
|
||||
self.item = item
|
||||
self.frame = frame
|
||||
}
|
||||
}
|
||||
|
||||
public final class VisibleItems: Sequence, IteratorProtocol {
|
||||
private let view: AsyncListComponent.View
|
||||
private var index: Int = 0
|
||||
private let indices: [(Int, CGRect)]
|
||||
|
||||
init(view: AsyncListComponent.View, direction: Direction) {
|
||||
self.view = view
|
||||
var indices: [(Int, CGRect)] = []
|
||||
view.listNode.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? ListItemNodeImpl, let index = itemNode.index {
|
||||
var itemFrame = itemNode.frame
|
||||
itemFrame.origin.y -= itemNode.transitionOffset
|
||||
if let animation = itemNode.animationForKey("height") {
|
||||
if let height = animation.to as? CGFloat {
|
||||
itemFrame.size.height = height
|
||||
}
|
||||
}
|
||||
|
||||
if case .horizontal = direction {
|
||||
itemFrame = CGRect(origin: CGPoint(x: itemFrame.minY, y: itemFrame.minX), size: CGSize(width: itemFrame.height, height: itemFrame.width))
|
||||
}
|
||||
|
||||
indices.append((index, itemFrame))
|
||||
}
|
||||
}
|
||||
indices.sort(by: { $0.0 < $1.0 })
|
||||
self.indices = indices
|
||||
}
|
||||
|
||||
public func next() -> VisibleItem? {
|
||||
if self.index >= self.indices.count {
|
||||
return nil
|
||||
}
|
||||
let index = self.index
|
||||
self.index += 1
|
||||
|
||||
if let component = self.view.component {
|
||||
let (itemIndex, itemFrame) = self.indices[index]
|
||||
return VisibleItem(item: component.items[itemIndex], frame: itemFrame)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public let externalState: ExternalState
|
||||
public let externalStateValue: ExternalState.Value
|
||||
public let items: [AnyComponentWithIdentity<Empty>]
|
||||
public let itemSetId: AnyHashable // Changing itemSetId supresses update animations
|
||||
public let direction: Direction
|
||||
public let insets: UIEdgeInsets
|
||||
public let reorderItems: ((Int, Int) -> Bool)?
|
||||
public let onVisibleItemsUpdated: ((VisibleItems, ComponentTransition) -> Void)?
|
||||
|
||||
public init(
|
||||
externalState: ExternalState,
|
||||
items: [AnyComponentWithIdentity<Empty>],
|
||||
itemSetId: AnyHashable,
|
||||
direction: Direction,
|
||||
insets: UIEdgeInsets,
|
||||
reorderItems: ((Int, Int) -> Bool)? = nil,
|
||||
onVisibleItemsUpdated: ((VisibleItems, ComponentTransition) -> Void)? = nil
|
||||
) {
|
||||
self.externalState = externalState
|
||||
self.externalStateValue = externalState.value
|
||||
self.items = items
|
||||
self.itemSetId = itemSetId
|
||||
self.direction = direction
|
||||
self.insets = insets
|
||||
self.reorderItems = reorderItems
|
||||
self.onVisibleItemsUpdated = onVisibleItemsUpdated
|
||||
}
|
||||
|
||||
public static func ==(lhs: AsyncListComponent, rhs: AsyncListComponent) -> Bool {
|
||||
if lhs.externalState !== rhs.externalState {
|
||||
return false
|
||||
}
|
||||
if lhs.items != rhs.items {
|
||||
return false
|
||||
}
|
||||
if lhs.itemSetId != rhs.itemSetId {
|
||||
return false
|
||||
}
|
||||
if lhs.direction != rhs.direction {
|
||||
return false
|
||||
}
|
||||
if lhs.insets != rhs.insets {
|
||||
return false
|
||||
}
|
||||
if (lhs.reorderItems == nil) != (rhs.reorderItems == nil) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private struct ItemEntry: Comparable, Identifiable {
|
||||
let contents: AnyComponentWithIdentity<Empty>
|
||||
let index: Int
|
||||
|
||||
var id: AnyHashable {
|
||||
return self.contents.id
|
||||
}
|
||||
|
||||
var stableId: AnyHashable {
|
||||
return self.id
|
||||
}
|
||||
|
||||
static func ==(lhs: ItemEntry, rhs: ItemEntry) -> Bool {
|
||||
if lhs.contents != rhs.contents {
|
||||
return false
|
||||
}
|
||||
if lhs.index != rhs.index {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static func <(lhs: ItemEntry, rhs: ItemEntry) -> Bool {
|
||||
return lhs.index < rhs.index
|
||||
}
|
||||
|
||||
func item(parentView: AsyncListComponent.View?, direction: Direction) -> ListViewItem {
|
||||
return ListItemImpl(parentView: parentView, contents: self.contents, direction: direction)
|
||||
}
|
||||
}
|
||||
|
||||
private final class ListItemImpl: ListViewItem {
|
||||
weak var parentView: AsyncListComponent.View?
|
||||
let contents: AnyComponentWithIdentity<Empty>
|
||||
let direction: Direction
|
||||
|
||||
let selectable: Bool = false
|
||||
|
||||
init(parentView: AsyncListComponent.View?, contents: AnyComponentWithIdentity<Empty>, direction: Direction) {
|
||||
self.parentView = parentView
|
||||
self.contents = contents
|
||||
self.direction = direction
|
||||
}
|
||||
|
||||
func nodeConfiguredForParams(
|
||||
async: @escaping (@escaping () -> Void) -> Void,
|
||||
params: ListViewItemLayoutParams,
|
||||
synchronousLoads: Bool,
|
||||
previousItem: ListViewItem?,
|
||||
nextItem: ListViewItem?,
|
||||
completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void
|
||||
) {
|
||||
async {
|
||||
let impl: () -> Void = {
|
||||
let node = ListItemNodeImpl()
|
||||
let (nodeLayout, apply) = node.asyncLayout()(self, params)
|
||||
node.insets = nodeLayout.insets
|
||||
node.contentSize = nodeLayout.contentSize
|
||||
|
||||
Queue.mainQueue().async {
|
||||
completion(node, {
|
||||
return (nil, { _ in
|
||||
apply(false)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if Thread.isMainThread {
|
||||
impl()
|
||||
} else {
|
||||
assert(false)
|
||||
Queue.mainQueue().async {
|
||||
impl()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
assert(node() is ListItemNodeImpl)
|
||||
if let nodeValue = node() as? ListItemNodeImpl {
|
||||
let layout = nodeValue.asyncLayout()
|
||||
async {
|
||||
let impl: () -> Void = {
|
||||
let (nodeLayout, apply) = layout(self, params)
|
||||
Queue.mainQueue().async {
|
||||
completion(nodeLayout, { _ in
|
||||
apply(animation.isAnimated)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if Thread.isMainThread {
|
||||
impl()
|
||||
} else {
|
||||
assert(false)
|
||||
Queue.mainQueue().async {
|
||||
impl()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class ListItemNodeImpl: ListViewItemNode {
|
||||
private let contentsView = ComponentView<Empty>()
|
||||
private(set) var item: ListItemImpl?
|
||||
|
||||
init() {
|
||||
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
|
||||
}
|
||||
|
||||
deinit {
|
||||
}
|
||||
|
||||
override func isReorderable(at point: CGPoint) -> Bool {
|
||||
if let itemView = self.contentsView.view as? ItemView {
|
||||
return itemView.isReorderable(at: self.view.convert(point, to: itemView))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override func snapshotForReordering() -> UIView? {
|
||||
return self.view.snapshotView(afterScreenUpdates: false)
|
||||
}
|
||||
|
||||
func asyncLayout() -> (ListItemImpl, ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (Bool) -> Void) {
|
||||
return { item, params in
|
||||
let containerSize: CGSize
|
||||
switch item.direction {
|
||||
case .vertical:
|
||||
containerSize = CGSize(width: params.width, height: 100000.0)
|
||||
case .horizontal:
|
||||
containerSize = CGSize(width: 100000.0, height: params.width)
|
||||
}
|
||||
|
||||
let contentsSize = self.contentsView.update(
|
||||
transition: .immediate,
|
||||
component: item.contents.component,
|
||||
environment: {},
|
||||
containerSize: containerSize
|
||||
)
|
||||
|
||||
let mappedContentsSize: CGSize
|
||||
switch item.direction {
|
||||
case .vertical:
|
||||
mappedContentsSize = CGSize(width: params.width, height: contentsSize.height)
|
||||
case .horizontal:
|
||||
mappedContentsSize = CGSize(width: params.width, height: contentsSize.width)
|
||||
}
|
||||
|
||||
let itemLayout = ListViewItemNodeLayout(contentSize: mappedContentsSize, insets: UIEdgeInsets())
|
||||
return (itemLayout, { animated in
|
||||
self.item = item
|
||||
|
||||
switch item.direction {
|
||||
case .vertical:
|
||||
self.layer.sublayerTransform = CATransform3DIdentity
|
||||
case .horizontal:
|
||||
self.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
|
||||
}
|
||||
|
||||
let contentsFrame = CGRect(origin: CGPoint(), size: contentsSize)
|
||||
|
||||
if let contentsComponentView = self.contentsView.view {
|
||||
if contentsComponentView.superview == nil {
|
||||
self.view.addSubview(contentsComponentView)
|
||||
}
|
||||
contentsComponentView.center = CGPoint(x: mappedContentsSize.width * 0.5, y: mappedContentsSize.height * 0.5)
|
||||
contentsComponentView.bounds = CGRect(origin: CGPoint(), size: contentsFrame.size)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
|
||||
super.animateInsertion(currentTimestamp, duration: duration, options: options)
|
||||
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
super.animateRemoved(currentTimestamp, duration: duration)
|
||||
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
|
||||
super.animateAdded(currentTimestamp, duration: duration)
|
||||
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
let listNode: ListView
|
||||
|
||||
private var externalStateValue: ExternalState.Value?
|
||||
private var isUpdating: Bool = false
|
||||
private(set) var component: AsyncListComponent?
|
||||
|
||||
private var currentEntries: [ItemEntry] = []
|
||||
|
||||
private var ignoreUpdateVisibleItems: Bool = false
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
self.listNode = ListView()
|
||||
self.listNode.useMainQueueTransactions = true
|
||||
self.listNode.scroller.delaysContentTouches = false
|
||||
self.listNode.reorderedItemHasShadow = false
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.listNode.view)
|
||||
|
||||
self.listNode.onContentsUpdated = { [weak self] transition in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.updateVisibleItems(transition: ComponentTransition(transition))
|
||||
}
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
|
||||
public func stopScrolling() {
|
||||
self.listNode.stopScrolling()
|
||||
}
|
||||
|
||||
private func updateVisibleItems(transition: ComponentTransition) {
|
||||
if self.ignoreUpdateVisibleItems {
|
||||
return
|
||||
}
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
if let onVisibleItemsUpdated = component.onVisibleItemsUpdated {
|
||||
onVisibleItemsUpdated(VisibleItems(view: self, direction: component.direction), transition)
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: AsyncListComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
let previousComponent = self.component
|
||||
self.component = component
|
||||
|
||||
let listSize: CGSize
|
||||
let listInsets: UIEdgeInsets
|
||||
switch component.direction {
|
||||
case .vertical:
|
||||
self.listNode.transform = CATransform3DIdentity
|
||||
listSize = CGSize(width: availableSize.width, height: availableSize.height)
|
||||
listInsets = component.insets
|
||||
case .horizontal:
|
||||
self.listNode.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
|
||||
listSize = CGSize(width: availableSize.height, height: availableSize.width)
|
||||
listInsets = UIEdgeInsets(top: component.insets.left, left: component.insets.top, bottom: component.insets.right, right: component.insets.bottom)
|
||||
}
|
||||
|
||||
var updateSizeAndInsets = ListViewUpdateSizeAndInsets(
|
||||
size: listSize,
|
||||
insets: listInsets,
|
||||
duration: 0.0,
|
||||
curve: .Default(duration: nil)
|
||||
)
|
||||
|
||||
var animateTransition = false
|
||||
var transactionOptions: ListViewDeleteAndInsertOptions = []
|
||||
|
||||
if !transition.animation.isImmediate, let previousComponent {
|
||||
if previousComponent.itemSetId == component.itemSetId {
|
||||
transactionOptions.insert(.AnimateInsertion)
|
||||
}
|
||||
animateTransition = true
|
||||
|
||||
switch transition.animation {
|
||||
case .none:
|
||||
break
|
||||
case let .curve(duration, curve):
|
||||
updateSizeAndInsets.duration = duration
|
||||
switch curve {
|
||||
case .linear, .easeInOut:
|
||||
updateSizeAndInsets.curve = .Default(duration: duration)
|
||||
case .spring:
|
||||
updateSizeAndInsets.curve = .Spring(duration: duration)
|
||||
case let .custom(a, b, c, d):
|
||||
updateSizeAndInsets.curve = .Custom(duration: duration, a, b, c, d)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var entries: [ItemEntry] = []
|
||||
for item in component.items {
|
||||
entries.append(ItemEntry(
|
||||
contents: item,
|
||||
index: entries.count
|
||||
))
|
||||
}
|
||||
|
||||
var scrollToItem: ListViewScrollToItem?
|
||||
if let resetScrollingRequest = component.externalStateValue.resetScrollingRequest, previousComponent?.externalStateValue.resetScrollingRequest != component.externalStateValue.resetScrollingRequest {
|
||||
//TODO:release calculate direction hint
|
||||
if let index = entries.firstIndex(where: { $0.id == resetScrollingRequest.id }) {
|
||||
scrollToItem = ListViewScrollToItem(
|
||||
index: index,
|
||||
position: .visible,
|
||||
animated: animateTransition,
|
||||
curve: updateSizeAndInsets.curve,
|
||||
directionHint: .Down
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
self.ignoreUpdateVisibleItems = true
|
||||
|
||||
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: self.currentEntries, rightList: entries)
|
||||
self.currentEntries = entries
|
||||
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
|
||||
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(parentView: self, direction: component.direction), directionHint: .Down) }
|
||||
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(parentView: self, direction: component.direction), directionHint: nil) }
|
||||
|
||||
transactionOptions.insert(.Synchronous)
|
||||
|
||||
self.listNode.transaction(
|
||||
deleteIndices: deletions,
|
||||
insertIndicesAndItems: insertions,
|
||||
updateIndicesAndItems: updates,
|
||||
options: transactionOptions,
|
||||
scrollToItem: scrollToItem,
|
||||
updateSizeAndInsets: updateSizeAndInsets,
|
||||
stationaryItemRange: nil,
|
||||
updateOpaqueState: nil,
|
||||
completion: { _ in }
|
||||
)
|
||||
|
||||
let mappedListFrame: CGRect
|
||||
switch component.direction {
|
||||
case .vertical:
|
||||
mappedListFrame = CGRect(origin: CGPoint(x: availableSize.width * 0.5, y: availableSize.height * 0.5), size: listSize)
|
||||
case .horizontal:
|
||||
mappedListFrame = CGRect(origin: CGPoint(x: availableSize.width * 0.5, y: availableSize.height * 0.5), size: listSize)
|
||||
}
|
||||
self.listNode.position = mappedListFrame.origin
|
||||
self.listNode.bounds = CGRect(origin: CGPoint(), size: mappedListFrame.size)
|
||||
|
||||
self.listNode.reorderItem = { [weak self] fromIndex, toIndex, _ in
|
||||
guard let self, let component = self.component else {
|
||||
return .single(false)
|
||||
}
|
||||
guard let reorderItems = component.reorderItems else {
|
||||
return .single(false)
|
||||
}
|
||||
|
||||
if reorderItems(fromIndex, toIndex) {
|
||||
return .single(true)
|
||||
} else {
|
||||
return .single(false)
|
||||
}
|
||||
}
|
||||
|
||||
self.ignoreUpdateVisibleItems = false
|
||||
|
||||
self.updateVisibleItems(transition: transition)
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user