mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-28 19:05:49 +00:00
261 lines
11 KiB
Swift
261 lines
11 KiB
Swift
import Foundation
|
|
import Display
|
|
import UIKit
|
|
import ComponentFlow
|
|
import SwiftSignalKit
|
|
import TelegramCore
|
|
import AccountContext
|
|
import ReactionSelectionNode
|
|
|
|
final class MessageListComponent: Component {
|
|
struct Item: Equatable {
|
|
let id: AnyHashable
|
|
let peer: EnginePeer
|
|
let text: String
|
|
let entities: [MessageTextEntity]
|
|
}
|
|
|
|
class SendActionTransition {
|
|
public let textSnapshotView: UIView
|
|
public let globalFrame: CGRect
|
|
public let cornerRadius: CGFloat
|
|
|
|
init(textSnapshotView: UIView, globalFrame: CGRect, cornerRadius: CGFloat) {
|
|
self.textSnapshotView = textSnapshotView
|
|
self.globalFrame = globalFrame
|
|
self.cornerRadius = cornerRadius
|
|
}
|
|
}
|
|
|
|
private let context: AccountContext
|
|
private let items: [Item]
|
|
private let availableReactions: [ReactionItem]?
|
|
private let sendActionTransition: SendActionTransition?
|
|
|
|
init(
|
|
context: AccountContext,
|
|
items: [Item],
|
|
availableReactions: [ReactionItem]?,
|
|
sendActionTransition: SendActionTransition?
|
|
) {
|
|
self.context = context
|
|
self.items = items
|
|
self.availableReactions = availableReactions
|
|
self.sendActionTransition = sendActionTransition
|
|
}
|
|
|
|
static func == (lhs: MessageListComponent, rhs: MessageListComponent) -> Bool {
|
|
if lhs.context !== rhs.context {
|
|
return false
|
|
}
|
|
if lhs.items != rhs.items {
|
|
return false
|
|
}
|
|
if (lhs.availableReactions ?? []).isEmpty != (rhs.availableReactions ?? []).isEmpty {
|
|
return false
|
|
}
|
|
if lhs.sendActionTransition !== rhs.sendActionTransition {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
private final class ScrollView: UIScrollView {
|
|
override func touchesShouldCancel(in view: UIView) -> Bool {
|
|
return true
|
|
}
|
|
}
|
|
|
|
final class View: UIView, UIScrollViewDelegate {
|
|
private let scrollView: ScrollView
|
|
|
|
private var component: MessageListComponent?
|
|
private weak var state: EmptyComponentState?
|
|
private var isUpdating = false
|
|
|
|
private var nextSendActionTransition: MessageListComponent.SendActionTransition?
|
|
private var itemViews: [AnyHashable: ComponentView<Empty>] = [:]
|
|
|
|
private let topInset: CGFloat = 8.0
|
|
private let bottomInset: CGFloat = 8.0
|
|
private let itemSpacing: CGFloat = 6.0
|
|
|
|
private var ignoreScrolling: Bool = false
|
|
|
|
override init(frame: CGRect) {
|
|
self.scrollView = ScrollView()
|
|
|
|
super.init(frame: frame)
|
|
|
|
self.scrollView.delaysContentTouches = false
|
|
self.scrollView.canCancelContentTouches = true
|
|
self.scrollView.contentInsetAdjustmentBehavior = .never
|
|
if #available(iOS 13.0, *) {
|
|
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
|
|
}
|
|
self.scrollView.showsVerticalScrollIndicator = false
|
|
self.scrollView.showsHorizontalScrollIndicator = false
|
|
self.scrollView.alwaysBounceHorizontal = false
|
|
self.scrollView.alwaysBounceVertical = true
|
|
self.scrollView.scrollsToTop = false
|
|
self.scrollView.delegate = self
|
|
self.scrollView.clipsToBounds = false
|
|
self.scrollView.transform = CGAffineTransform(scaleX: 1.0, y: -1.0)
|
|
|
|
self.addSubview(self.scrollView)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
|
|
}
|
|
|
|
private func isAtBottom(tolerance: CGFloat = 1.0) -> Bool {
|
|
let bottomY = -self.scrollView.adjustedContentInset.top
|
|
return self.scrollView.contentOffset.y <= bottomY + tolerance
|
|
}
|
|
|
|
private func scrollToBottom(animated: Bool) {
|
|
let targetY = -self.scrollView.adjustedContentInset.top
|
|
if animated {
|
|
self.scrollView.setContentOffset(CGPoint(x: 0, y: targetY), animated: true)
|
|
} else {
|
|
self.scrollView.contentOffset = CGPoint(x: 0, y: targetY)
|
|
}
|
|
}
|
|
|
|
func update(component: MessageListComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
self.component = component
|
|
self.state = state
|
|
|
|
if let _ = component.sendActionTransition {
|
|
self.nextSendActionTransition = component.sendActionTransition
|
|
}
|
|
|
|
let originalTransition = transition
|
|
transition.setFrame(view: self.scrollView, frame: CGRect(origin: .zero, size: availableSize))
|
|
|
|
let previousContentHeight = self.scrollView.contentSize.height
|
|
let wasAtBottom = self.isAtBottom(tolerance: 1.0)
|
|
|
|
let maxWidth: CGFloat = 300.0
|
|
|
|
var measured: [(id: AnyHashable, size: CGSize, item: MessageListComponent.Item, itemTransition: ComponentTransition)] = []
|
|
measured.reserveCapacity(component.items.count)
|
|
|
|
for item in component.items {
|
|
var itemTransition = transition
|
|
let key = item.id
|
|
let container = self.itemViews[key] ?? {
|
|
itemTransition = .immediate
|
|
let v = ComponentView<Empty>()
|
|
self.itemViews[key] = v
|
|
return v
|
|
}()
|
|
|
|
let size = container.update(
|
|
transition: transition,
|
|
component: AnyComponent(MessageItemComponent(
|
|
context: component.context,
|
|
peer: item.peer,
|
|
text: item.text,
|
|
entities: item.entities,
|
|
availableReactions: component.availableReactions
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: maxWidth, height: .greatestFiniteMagnitude)
|
|
)
|
|
measured.append((id: key, size: size, item: item, itemTransition: itemTransition))
|
|
}
|
|
|
|
let itemsHeight: CGFloat = measured.reduce(0) { $0 + $1.size.height } +
|
|
CGFloat(max(0, measured.count - 1)) * self.itemSpacing
|
|
let contentHeight = self.topInset + itemsHeight + self.bottomInset
|
|
|
|
var y = self.bottomInset
|
|
|
|
var validKeys = Set<AnyHashable>()
|
|
for (index, entry) in measured.enumerated() {
|
|
validKeys.insert(entry.id)
|
|
if let itemView = self.itemViews[entry.id]?.view {
|
|
var customAnimation = false
|
|
if entry.item.peer.id == component.context.account.peerId, let _ = self.nextSendActionTransition {
|
|
customAnimation = true
|
|
}
|
|
let itemFrame = CGRect(
|
|
origin: CGPoint(x: floor((availableSize.width - entry.size.width) / 2.0), y: y),
|
|
size: entry.size
|
|
)
|
|
|
|
if itemView.superview == nil {
|
|
if !originalTransition.animation.isImmediate && !customAnimation {
|
|
originalTransition.animateAlpha(view: itemView, from: 0.0, to: 1.0)
|
|
originalTransition.animateScale(view: itemView, from: 0.01, to: 1.0)
|
|
}
|
|
if customAnimation, let nextSendActionTransition = self.nextSendActionTransition {
|
|
self.nextSendActionTransition = nil
|
|
itemView.frame = itemFrame
|
|
if let itemView = itemView as? MessageItemComponent.View {
|
|
itemView.isHidden = true
|
|
Queue.mainQueue().justDispatch {
|
|
itemView.animateFrom(globalFrame: nextSendActionTransition.globalFrame, cornerRadius: nextSendActionTransition.cornerRadius, textSnapshotView: nextSendActionTransition.textSnapshotView, transition: originalTransition)
|
|
itemView.isHidden = false
|
|
}
|
|
}
|
|
}
|
|
self.scrollView.addSubview(itemView)
|
|
}
|
|
entry.itemTransition.setFrame(view: itemView, frame: itemFrame)
|
|
}
|
|
y += entry.size.height
|
|
if index != measured.count - 1 { y += self.itemSpacing }
|
|
}
|
|
|
|
let finalContentHeight = max(availableSize.height, contentHeight)
|
|
self.scrollView.contentSize = CGSize(width: availableSize.width, height: finalContentHeight)
|
|
|
|
let delta = self.scrollView.contentSize.height - previousContentHeight
|
|
if !wasAtBottom && abs(delta) > .ulpOfOne {
|
|
self.scrollView.contentOffset.y += delta
|
|
} else if wasAtBottom {
|
|
self.scrollToBottom(animated: false)
|
|
}
|
|
|
|
if self.itemViews.count > validKeys.count {
|
|
let toRemove = self.itemViews.keys.filter { !validKeys.contains($0) }
|
|
for key in toRemove {
|
|
if let itemView = self.itemViews[key]?.view {
|
|
if transition.animation.isImmediate {
|
|
itemView.removeFromSuperview()
|
|
} else {
|
|
transition.setAlpha(view: itemView, alpha: 0.0, completion: { _ in
|
|
itemView.removeFromSuperview()
|
|
})
|
|
transition.setScale(view: itemView, scale: 0.01)
|
|
}
|
|
}
|
|
self.itemViews.removeValue(forKey: key)
|
|
}
|
|
}
|
|
|
|
if wasAtBottom {
|
|
self.scrollToBottom(animated: false)
|
|
}
|
|
|
|
return availableSize
|
|
}
|
|
}
|
|
|
|
|
|
func makeView() -> View {
|
|
return View()
|
|
}
|
|
|
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
|
}
|
|
}
|