mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
1013 lines
43 KiB
Swift
1013 lines
43 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Display
|
|
import ComponentFlow
|
|
import PagerComponent
|
|
import TelegramPresentationData
|
|
import TelegramCore
|
|
import Postbox
|
|
import MultiAnimationRenderer
|
|
import AnimationCache
|
|
import AccountContext
|
|
import LottieAnimationCache
|
|
import AnimatedStickerNode
|
|
import TelegramAnimatedStickerNode
|
|
import SwiftSignalKit
|
|
import ShimmerEffect
|
|
import PagerComponent
|
|
import SoftwareVideo
|
|
import AVFoundation
|
|
import PhotoResources
|
|
//import ContextUI
|
|
import ShimmerEffect
|
|
|
|
private class GifVideoLayer: AVSampleBufferDisplayLayer {
|
|
private let context: AccountContext
|
|
private let file: TelegramMediaFile?
|
|
|
|
private var frameManager: SoftwareVideoLayerFrameManager?
|
|
|
|
private var thumbnailDisposable: Disposable?
|
|
|
|
private var playbackTimestamp: Double = 0.0
|
|
private var playbackTimer: SwiftSignalKit.Timer?
|
|
|
|
var started: (() -> Void)?
|
|
|
|
var shouldBeAnimating: Bool = false {
|
|
didSet {
|
|
if self.shouldBeAnimating == oldValue {
|
|
return
|
|
}
|
|
|
|
if self.shouldBeAnimating {
|
|
self.playbackTimer?.invalidate()
|
|
let startTimestamp = self.playbackTimestamp + CFAbsoluteTimeGetCurrent()
|
|
self.playbackTimer = SwiftSignalKit.Timer(timeout: 1.0 / 30.0, repeat: true, completion: { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
let timestamp = CFAbsoluteTimeGetCurrent() - startTimestamp
|
|
strongSelf.frameManager?.tick(timestamp: timestamp)
|
|
strongSelf.playbackTimestamp = timestamp
|
|
}, queue: .mainQueue())
|
|
self.playbackTimer?.start()
|
|
} else {
|
|
self.playbackTimer?.invalidate()
|
|
self.playbackTimer = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
init(context: AccountContext, file: TelegramMediaFile?, synchronousLoad: Bool) {
|
|
self.context = context
|
|
self.file = file
|
|
|
|
super.init()
|
|
|
|
self.videoGravity = .resizeAspectFill
|
|
|
|
if let file = self.file {
|
|
if let dimensions = file.dimensions {
|
|
self.thumbnailDisposable = (mediaGridMessageVideo(postbox: context.account.postbox, videoReference: .savedGif(media: file), synchronousLoad: synchronousLoad, nilForEmptyResult: true)
|
|
|> deliverOnMainQueue).start(next: { [weak self] transform in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
let boundingSize = CGSize(width: 93.0, height: 93.0)
|
|
let imageSize = dimensions.cgSize.aspectFilled(boundingSize)
|
|
|
|
if let image = transform(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets(), resizeMode: .fill(.clear)))?.generateImage() {
|
|
Queue.mainQueue().async {
|
|
if let strongSelf = self {
|
|
strongSelf.contents = image.cgImage
|
|
strongSelf.setupVideo()
|
|
strongSelf.started?()
|
|
}
|
|
}
|
|
} else {
|
|
strongSelf.setupVideo()
|
|
}
|
|
})
|
|
} else {
|
|
self.setupVideo()
|
|
}
|
|
}
|
|
}
|
|
|
|
override init(layer: Any) {
|
|
preconditionFailure()
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
deinit {
|
|
self.thumbnailDisposable?.dispose()
|
|
}
|
|
|
|
private func setupVideo() {
|
|
guard let file = self.file else {
|
|
return
|
|
}
|
|
let frameManager = SoftwareVideoLayerFrameManager(account: self.context.account, fileReference: .savedGif(media: file), layerHolder: nil, layer: self)
|
|
self.frameManager = frameManager
|
|
frameManager.started = { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
let _ = strongSelf
|
|
}
|
|
frameManager.start()
|
|
}
|
|
}
|
|
|
|
public final class GifPagerContentComponent: Component {
|
|
public typealias EnvironmentType = (EntityKeyboardChildEnvironment, PagerComponentChildEnvironment)
|
|
|
|
public enum Subject: Equatable {
|
|
case recent
|
|
case trending
|
|
case emojiSearch(String)
|
|
}
|
|
|
|
public final class InputInteraction {
|
|
public let performItemAction: (Item, UIView, CGRect) -> Void
|
|
public let openGifContextMenu: (Item, UIView, CGRect, ContextGesture, Bool) -> Void
|
|
public let loadMore: (String) -> Void
|
|
public let openSearch: () -> Void
|
|
|
|
public init(
|
|
performItemAction: @escaping (Item, UIView, CGRect) -> Void,
|
|
openGifContextMenu: @escaping (Item, UIView, CGRect, ContextGesture, Bool) -> Void,
|
|
loadMore: @escaping (String) -> Void,
|
|
openSearch: @escaping () -> Void
|
|
) {
|
|
self.performItemAction = performItemAction
|
|
self.openGifContextMenu = openGifContextMenu
|
|
self.loadMore = loadMore
|
|
self.openSearch = openSearch
|
|
}
|
|
}
|
|
|
|
public final class Item: Equatable {
|
|
public let file: FileMediaReference
|
|
public let contextResult: (ChatContextResultCollection, ChatContextResult)?
|
|
|
|
public init(file: FileMediaReference, contextResult: (ChatContextResultCollection, ChatContextResult)?) {
|
|
self.file = file
|
|
self.contextResult = contextResult
|
|
}
|
|
|
|
public static func ==(lhs: Item, rhs: Item) -> Bool {
|
|
if lhs === rhs {
|
|
return true
|
|
}
|
|
if lhs.file.media.fileId != rhs.file.media.fileId {
|
|
return false
|
|
}
|
|
if (lhs.contextResult == nil) != (rhs.contextResult != nil) {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
}
|
|
|
|
public let context: AccountContext
|
|
public let inputInteraction: InputInteraction
|
|
public let subject: Subject
|
|
public let items: [Item]
|
|
public let isLoading: Bool
|
|
public let loadMoreToken: String?
|
|
public let displaySearchWithPlaceholder: String?
|
|
public let searchInitiallyHidden: Bool
|
|
|
|
public init(
|
|
context: AccountContext,
|
|
inputInteraction: InputInteraction,
|
|
subject: Subject,
|
|
items: [Item],
|
|
isLoading: Bool,
|
|
loadMoreToken: String?,
|
|
displaySearchWithPlaceholder: String?,
|
|
searchInitiallyHidden: Bool
|
|
) {
|
|
self.context = context
|
|
self.inputInteraction = inputInteraction
|
|
self.subject = subject
|
|
self.items = items
|
|
self.isLoading = isLoading
|
|
self.loadMoreToken = loadMoreToken
|
|
self.displaySearchWithPlaceholder = displaySearchWithPlaceholder
|
|
self.searchInitiallyHidden = searchInitiallyHidden
|
|
}
|
|
|
|
public static func ==(lhs: GifPagerContentComponent, rhs: GifPagerContentComponent) -> Bool {
|
|
if lhs.context !== rhs.context {
|
|
return false
|
|
}
|
|
if lhs.inputInteraction !== rhs.inputInteraction {
|
|
return false
|
|
}
|
|
if lhs.subject != rhs.subject {
|
|
return false
|
|
}
|
|
if lhs.items != rhs.items {
|
|
return false
|
|
}
|
|
if lhs.isLoading != rhs.isLoading {
|
|
return false
|
|
}
|
|
if lhs.loadMoreToken != rhs.loadMoreToken {
|
|
return false
|
|
}
|
|
if lhs.displaySearchWithPlaceholder != rhs.displaySearchWithPlaceholder {
|
|
return false
|
|
}
|
|
if lhs.searchInitiallyHidden != rhs.searchInitiallyHidden {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
public final class View: ContextControllerSourceView, PagerContentViewWithBackground, UIScrollViewDelegate {
|
|
private struct ItemGroupDescription: Equatable {
|
|
let hasTitle: Bool
|
|
let itemCount: Int
|
|
}
|
|
|
|
private struct ItemGroupLayout: Equatable {
|
|
let frame: CGRect
|
|
let itemTopOffset: CGFloat
|
|
let itemCount: Int
|
|
}
|
|
|
|
private struct ItemLayout: Equatable {
|
|
let width: CGFloat
|
|
let containerInsets: UIEdgeInsets
|
|
let itemCount: Int
|
|
let itemSize: CGFloat
|
|
let horizontalSpacing: CGFloat
|
|
let verticalSpacing: CGFloat
|
|
let itemsPerRow: Int
|
|
let contentSize: CGSize
|
|
|
|
var searchInsets: UIEdgeInsets
|
|
var searchHeight: CGFloat
|
|
|
|
init(width: CGFloat, containerInsets: UIEdgeInsets, itemCount: Int) {
|
|
self.width = width
|
|
self.containerInsets = containerInsets
|
|
self.itemCount = itemCount
|
|
self.horizontalSpacing = 1.0
|
|
self.verticalSpacing = 1.0
|
|
|
|
self.searchHeight = 54.0
|
|
self.searchInsets = UIEdgeInsets(top: max(0.0, containerInsets.top + 1.0), left: containerInsets.left, bottom: 0.0, right: containerInsets.right)
|
|
|
|
let defaultItemSize: CGFloat = 120.0
|
|
|
|
let itemHorizontalSpace = width - self.containerInsets.left - self.containerInsets.right
|
|
var itemsPerRow = Int(floor((itemHorizontalSpace) / (defaultItemSize)))
|
|
itemsPerRow = max(3, itemsPerRow)
|
|
|
|
self.itemsPerRow = itemsPerRow
|
|
|
|
self.itemSize = floor((itemHorizontalSpace - self.horizontalSpacing * CGFloat(itemsPerRow - 1)) / CGFloat(itemsPerRow))
|
|
|
|
let numRowsInGroup = (itemCount + (self.itemsPerRow - 1)) / self.itemsPerRow
|
|
self.contentSize = CGSize(width: width, height: self.containerInsets.top + self.containerInsets.bottom + CGFloat(numRowsInGroup) * self.itemSize + CGFloat(max(0, numRowsInGroup - 1)) * self.verticalSpacing)
|
|
}
|
|
|
|
func frame(at index: Int) -> CGRect {
|
|
let row = index / self.itemsPerRow
|
|
let column = index % self.itemsPerRow
|
|
|
|
var rect = CGRect(
|
|
origin: CGPoint(
|
|
x: self.containerInsets.left + CGFloat(column) * (self.itemSize + self.horizontalSpacing),
|
|
y: self.containerInsets.top + CGFloat(row) * (self.itemSize + self.verticalSpacing)
|
|
),
|
|
size: CGSize(
|
|
width: self.itemSize,
|
|
height: self.itemSize
|
|
)
|
|
)
|
|
|
|
if column == self.itemsPerRow - 1 && index < self.itemCount - 1 {
|
|
rect.size.width = self.width - self.containerInsets.right - rect.minX
|
|
}
|
|
|
|
return rect
|
|
}
|
|
|
|
func visibleItems(for rect: CGRect) -> Range<Int>? {
|
|
let offsetRect = rect.offsetBy(dx: -self.containerInsets.left, dy: -containerInsets.top)
|
|
var minVisibleRow = Int(floor((offsetRect.minY - self.verticalSpacing) / (self.itemSize + self.verticalSpacing)))
|
|
minVisibleRow = max(0, minVisibleRow)
|
|
let maxVisibleRow = Int(ceil((offsetRect.maxY - self.verticalSpacing) / (self.itemSize + self.verticalSpacing)))
|
|
|
|
let minVisibleIndex = minVisibleRow * self.itemsPerRow
|
|
let maxVisibleIndex = (maxVisibleRow + 1) * self.itemsPerRow - 1
|
|
|
|
if maxVisibleIndex >= minVisibleIndex {
|
|
return minVisibleIndex ..< (maxVisibleIndex + 1)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
fileprivate enum ItemKey: Hashable {
|
|
case media(MediaId)
|
|
case placeholder(Int)
|
|
}
|
|
|
|
fileprivate final class ItemLayer: GifVideoLayer {
|
|
let item: Item?
|
|
|
|
private var disposable: Disposable?
|
|
private var fetchDisposable: Disposable?
|
|
|
|
private var isInHierarchyValue: Bool = false
|
|
public var isVisibleForAnimations: Bool = false {
|
|
didSet {
|
|
if self.isVisibleForAnimations != oldValue {
|
|
self.updatePlayback()
|
|
}
|
|
}
|
|
}
|
|
private(set) var displayPlaceholder: Bool = false
|
|
let onUpdateDisplayPlaceholder: (Bool, Double) -> Void
|
|
|
|
init(
|
|
item: Item?,
|
|
context: AccountContext,
|
|
groupId: String,
|
|
attemptSynchronousLoad: Bool,
|
|
onUpdateDisplayPlaceholder: @escaping (Bool, Double) -> Void
|
|
) {
|
|
self.item = item
|
|
self.onUpdateDisplayPlaceholder = onUpdateDisplayPlaceholder
|
|
|
|
super.init(context: context, file: item?.file.media, synchronousLoad: attemptSynchronousLoad)
|
|
|
|
if item == nil {
|
|
self.updateDisplayPlaceholder(displayPlaceholder: true, duration: 0.0)
|
|
}
|
|
|
|
self.started = { [weak self] in
|
|
let _ = self
|
|
//self?.updateDisplayPlaceholder(displayPlaceholder: false, duration: 0.2)
|
|
}
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
deinit {
|
|
self.disposable?.dispose()
|
|
self.fetchDisposable?.dispose()
|
|
}
|
|
|
|
override func action(forKey event: String) -> CAAction? {
|
|
if event == kCAOnOrderIn {
|
|
self.isInHierarchyValue = true
|
|
} else if event == kCAOnOrderOut {
|
|
self.isInHierarchyValue = false
|
|
}
|
|
self.updatePlayback()
|
|
return nullAction
|
|
}
|
|
|
|
private func updatePlayback() {
|
|
let shouldBePlaying = self.isInHierarchyValue && self.isVisibleForAnimations
|
|
|
|
self.shouldBeAnimating = shouldBePlaying
|
|
}
|
|
|
|
func updateDisplayPlaceholder(displayPlaceholder: Bool, duration: Double) {
|
|
if self.displayPlaceholder == displayPlaceholder {
|
|
return
|
|
}
|
|
self.displayPlaceholder = displayPlaceholder
|
|
self.onUpdateDisplayPlaceholder(displayPlaceholder, duration)
|
|
}
|
|
}
|
|
|
|
final class ItemPlaceholderView: UIView {
|
|
private let shimmerView: PortalSourceView?
|
|
private var placeholderView: PortalView?
|
|
|
|
init(shimmerView: PortalSourceView?) {
|
|
self.shimmerView = shimmerView
|
|
self.placeholderView = PortalView()
|
|
|
|
super.init(frame: CGRect())
|
|
|
|
self.clipsToBounds = true
|
|
|
|
if let placeholderView = self.placeholderView, let shimmerView = self.shimmerView {
|
|
placeholderView.view.clipsToBounds = true
|
|
self.addSubview(placeholderView.view)
|
|
shimmerView.addPortal(view: placeholderView)
|
|
}
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func update(size: CGSize) {
|
|
if let placeholderView = self.placeholderView {
|
|
placeholderView.view.frame = CGRect(origin: CGPoint(), size: size)
|
|
}
|
|
}
|
|
}
|
|
|
|
public final class ContentScrollLayer: CALayer {
|
|
public var mirrorLayer: CALayer?
|
|
|
|
override public init() {
|
|
super.init()
|
|
}
|
|
|
|
override public init(layer: Any) {
|
|
super.init(layer: layer)
|
|
}
|
|
|
|
required public init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override public var position: CGPoint {
|
|
get {
|
|
return super.position
|
|
} set(value) {
|
|
if let mirrorLayer = self.mirrorLayer {
|
|
mirrorLayer.position = value
|
|
}
|
|
super.position = value
|
|
}
|
|
}
|
|
|
|
override public var bounds: CGRect {
|
|
get {
|
|
return super.bounds
|
|
} set(value) {
|
|
if let mirrorLayer = self.mirrorLayer {
|
|
mirrorLayer.bounds = value
|
|
}
|
|
super.bounds = value
|
|
}
|
|
}
|
|
|
|
override public func add(_ animation: CAAnimation, forKey key: String?) {
|
|
if let mirrorLayer = self.mirrorLayer {
|
|
mirrorLayer.add(animation, forKey: key)
|
|
}
|
|
|
|
super.add(animation, forKey: key)
|
|
}
|
|
|
|
override public func removeAllAnimations() {
|
|
if let mirrorLayer = self.mirrorLayer {
|
|
mirrorLayer.removeAllAnimations()
|
|
}
|
|
|
|
super.removeAllAnimations()
|
|
}
|
|
|
|
override public func removeAnimation(forKey: String) {
|
|
if let mirrorLayer = self.mirrorLayer {
|
|
mirrorLayer.removeAnimation(forKey: forKey)
|
|
}
|
|
|
|
super.removeAnimation(forKey: forKey)
|
|
}
|
|
}
|
|
|
|
private final class ContentScrollView: UIScrollView, PagerExpandableScrollView {
|
|
override static var layerClass: AnyClass {
|
|
return ContentScrollLayer.self
|
|
}
|
|
|
|
private let mirrorView: UIView
|
|
|
|
init(mirrorView: UIView) {
|
|
self.mirrorView = mirrorView
|
|
|
|
super.init(frame: CGRect())
|
|
|
|
(self.layer as? ContentScrollLayer)?.mirrorLayer = mirrorView.layer
|
|
self.canCancelContentTouches = true
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func touchesShouldCancel(in view: UIView) -> Bool {
|
|
return true
|
|
}
|
|
}
|
|
|
|
private let shimmerHostView: PortalSourceView
|
|
private let standaloneShimmerEffect: StandaloneShimmerEffect
|
|
|
|
private let backgroundView: BlurredBackgroundView
|
|
private var vibrancyEffectView: UIVisualEffectView?
|
|
private let mirrorContentScrollView: UIView
|
|
private let scrollView: ContentScrollView
|
|
|
|
private let placeholdersContainerView: UIView
|
|
private var visibleSearchHeader: EmojiSearchHeaderView?
|
|
private var visibleItemPlaceholderViews: [ItemKey: ItemPlaceholderView] = [:]
|
|
private var visibleItemLayers: [ItemKey: ItemLayer] = [:]
|
|
private var ignoreScrolling: Bool = false
|
|
|
|
private var component: GifPagerContentComponent?
|
|
private var pagerEnvironment: PagerComponentChildEnvironment?
|
|
private var theme: PresentationTheme?
|
|
private var itemLayout: ItemLayout?
|
|
|
|
private var currentLoadMoreToken: String?
|
|
|
|
override init(frame: CGRect) {
|
|
self.backgroundView = BlurredBackgroundView(color: nil)
|
|
|
|
self.shimmerHostView = PortalSourceView()
|
|
self.standaloneShimmerEffect = StandaloneShimmerEffect()
|
|
|
|
self.placeholdersContainerView = UIView()
|
|
|
|
self.mirrorContentScrollView = UIView()
|
|
self.mirrorContentScrollView.layer.anchorPoint = CGPoint()
|
|
self.mirrorContentScrollView.clipsToBounds = true
|
|
self.scrollView = ContentScrollView(mirrorView: self.mirrorContentScrollView)
|
|
self.scrollView.layer.anchorPoint = CGPoint()
|
|
|
|
super.init(frame: frame)
|
|
|
|
self.addSubview(self.backgroundView)
|
|
|
|
self.shimmerHostView.alpha = 0.0
|
|
self.addSubview(self.shimmerHostView)
|
|
|
|
self.scrollView.delaysContentTouches = false
|
|
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
|
self.scrollView.contentInsetAdjustmentBehavior = .never
|
|
}
|
|
if #available(iOS 13.0, *) {
|
|
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
|
|
}
|
|
self.scrollView.showsVerticalScrollIndicator = true
|
|
self.scrollView.showsHorizontalScrollIndicator = false
|
|
self.scrollView.scrollsToTop = false
|
|
self.scrollView.delegate = self
|
|
self.addSubview(self.scrollView)
|
|
|
|
self.scrollView.addSubview(self.placeholdersContainerView)
|
|
|
|
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
|
|
|
self.isMultipleTouchEnabled = false
|
|
|
|
self.useSublayerTransformForActivation = false
|
|
self.shouldBegin = { [weak self] point in
|
|
guard let strongSelf = self else {
|
|
return false
|
|
}
|
|
strongSelf.targetLayerForActivationProgress = nil
|
|
if let (_, itemLayer) = strongSelf.itemLayer(atPoint: point) {
|
|
strongSelf.targetLayerForActivationProgress = itemLayer
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
self.activated = { [weak self] gesture, location in
|
|
guard let strongSelf = self, let component = strongSelf.component else {
|
|
gesture.cancel()
|
|
return
|
|
}
|
|
guard let (item, itemLayer) = strongSelf.itemLayer(atPoint: location) else {
|
|
gesture.cancel()
|
|
return
|
|
}
|
|
let rect = strongSelf.scrollView.convert(itemLayer.frame, to: strongSelf)
|
|
component.inputInteraction.openGifContextMenu(item, strongSelf, rect, gesture, component.subject == .recent)
|
|
}
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
private func openGifContextMenu(item: Item, sourceView: UIView, sourceRect: CGRect, gesture: ContextGesture, isSaved: Bool) {
|
|
guard let component = self.component else {
|
|
return
|
|
}
|
|
component.inputInteraction.openGifContextMenu(item, sourceView, sourceRect, gesture, isSaved)
|
|
}
|
|
|
|
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
|
if case .ended = recognizer.state {
|
|
if let component = self.component, let item = self.item(atPoint: recognizer.location(in: self)), let itemView = self.visibleItemLayers[.media(item.file.media.fileId)] {
|
|
component.inputInteraction.performItemAction(item, self, self.scrollView.convert(itemView.frame, to: self))
|
|
}
|
|
}
|
|
}
|
|
|
|
private func item(atPoint point: CGPoint) -> Item? {
|
|
let localPoint = self.convert(point, to: self.scrollView)
|
|
|
|
for (_, itemLayer) in self.visibleItemLayers {
|
|
if itemLayer.frame.contains(localPoint) {
|
|
return itemLayer.item
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
private func itemLayer(atPoint point: CGPoint) -> (Item, ItemLayer)? {
|
|
let localPoint = self.convert(point, to: self.scrollView)
|
|
|
|
for (_, itemLayer) in self.visibleItemLayers {
|
|
if itemLayer.frame.contains(localPoint) {
|
|
if let item = itemLayer.item {
|
|
return (item, itemLayer)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
private var previousScrollingOffset: CGFloat?
|
|
|
|
public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
|
if let presentation = scrollView.layer.presentation() {
|
|
scrollView.bounds = presentation.bounds
|
|
scrollView.layer.removeAllAnimations()
|
|
}
|
|
}
|
|
|
|
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
if self.ignoreScrolling {
|
|
return
|
|
}
|
|
|
|
self.updateVisibleItems(attemptSynchronousLoads: false)
|
|
|
|
self.updateScrollingOffset(transition: .immediate)
|
|
|
|
if scrollView.contentOffset.y >= scrollView.contentSize.height - scrollView.bounds.height - 100.0 {
|
|
if let component = self.component, let loadMoreToken = component.loadMoreToken, self.currentLoadMoreToken != loadMoreToken {
|
|
self.currentLoadMoreToken = loadMoreToken
|
|
component.inputInteraction.loadMore(loadMoreToken)
|
|
}
|
|
}
|
|
}
|
|
|
|
public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
|
if velocity.y != 0.0 {
|
|
targetContentOffset.pointee.y = self.snappedContentOffset(proposedOffset: targetContentOffset.pointee.y)
|
|
}
|
|
}
|
|
|
|
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
|
if !decelerate {
|
|
self.snapScrollingOffsetToInsets()
|
|
}
|
|
}
|
|
|
|
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
|
self.snapScrollingOffsetToInsets()
|
|
}
|
|
|
|
private func updateScrollingOffset(transition: Transition) {
|
|
let isInteracting = scrollView.isDragging || scrollView.isDecelerating
|
|
if let previousScrollingOffsetValue = self.previousScrollingOffset {
|
|
let currentBounds = scrollView.bounds
|
|
let offsetToTopEdge = max(0.0, currentBounds.minY - 0.0)
|
|
let offsetToBottomEdge = max(0.0, scrollView.contentSize.height - currentBounds.maxY)
|
|
|
|
let relativeOffset = scrollView.contentOffset.y - previousScrollingOffsetValue
|
|
self.pagerEnvironment?.onChildScrollingUpdate(PagerComponentChildEnvironment.ContentScrollingUpdate(
|
|
relativeOffset: relativeOffset,
|
|
absoluteOffsetToTopEdge: offsetToTopEdge,
|
|
absoluteOffsetToBottomEdge: offsetToBottomEdge,
|
|
isReset: false,
|
|
isInteracting: isInteracting,
|
|
transition: transition
|
|
))
|
|
self.previousScrollingOffset = scrollView.contentOffset.y
|
|
}
|
|
self.previousScrollingOffset = scrollView.contentOffset.y
|
|
}
|
|
|
|
private func snappedContentOffset(proposedOffset: CGFloat) -> CGFloat {
|
|
guard let pagerEnvironment = self.pagerEnvironment else {
|
|
return proposedOffset
|
|
}
|
|
|
|
var proposedOffset = proposedOffset
|
|
let bounds = self.bounds
|
|
if proposedOffset + bounds.height > self.scrollView.contentSize.height - pagerEnvironment.containerInsets.bottom {
|
|
proposedOffset = self.scrollView.contentSize.height - bounds.height
|
|
}
|
|
if proposedOffset < pagerEnvironment.containerInsets.top {
|
|
proposedOffset = 0.0
|
|
}
|
|
|
|
return proposedOffset
|
|
}
|
|
|
|
private func snapScrollingOffsetToInsets() {
|
|
let transition = Transition(animation: .curve(duration: 0.4, curve: .spring))
|
|
|
|
var currentBounds = self.scrollView.bounds
|
|
currentBounds.origin.y = self.snappedContentOffset(proposedOffset: currentBounds.minY)
|
|
transition.setBounds(view: self.scrollView, bounds: currentBounds)
|
|
|
|
self.updateScrollingOffset(transition: transition)
|
|
}
|
|
|
|
private func updateVisibleItems(attemptSynchronousLoads: Bool) {
|
|
guard let component = self.component, let itemLayout = self.itemLayout else {
|
|
return
|
|
}
|
|
|
|
var validIds = Set<ItemKey>()
|
|
|
|
var searchInset: CGFloat = 0.0
|
|
if let _ = component.displaySearchWithPlaceholder {
|
|
searchInset += itemLayout.searchHeight
|
|
}
|
|
|
|
if let itemRange = itemLayout.visibleItems(for: self.scrollView.bounds) {
|
|
for index in itemRange.lowerBound ..< itemRange.upperBound {
|
|
var item: Item?
|
|
let itemId: ItemKey
|
|
if index < component.items.count {
|
|
item = component.items[index]
|
|
itemId = .media(component.items[index].file.media.fileId)
|
|
} else if component.isLoading || component.loadMoreToken != nil {
|
|
itemId = .placeholder(index)
|
|
} else {
|
|
continue
|
|
}
|
|
|
|
if !component.isLoading {
|
|
if let placeholderView = self.visibleItemPlaceholderViews.removeValue(forKey: .placeholder(index)) {
|
|
self.visibleItemPlaceholderViews[itemId] = placeholderView
|
|
}
|
|
}
|
|
|
|
validIds.insert(itemId)
|
|
|
|
let itemFrame = itemLayout.frame(at: index).offsetBy(dx: 0.0, dy: searchInset)
|
|
|
|
let itemTransition: Transition = .immediate
|
|
var updateItemLayerPlaceholder = false
|
|
|
|
let itemLayer: ItemLayer
|
|
if let current = self.visibleItemLayers[itemId] {
|
|
itemLayer = current
|
|
} else {
|
|
updateItemLayerPlaceholder = true
|
|
|
|
itemLayer = ItemLayer(
|
|
item: item,
|
|
context: component.context,
|
|
groupId: "savedGif",
|
|
attemptSynchronousLoad: attemptSynchronousLoads,
|
|
onUpdateDisplayPlaceholder: { [weak self] displayPlaceholder, duration in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if displayPlaceholder {
|
|
if let itemLayer = strongSelf.visibleItemLayers[itemId] {
|
|
let placeholderView: ItemPlaceholderView
|
|
if let current = strongSelf.visibleItemPlaceholderViews[itemId] {
|
|
placeholderView = current
|
|
} else {
|
|
placeholderView = ItemPlaceholderView(shimmerView: strongSelf.shimmerHostView)
|
|
strongSelf.visibleItemPlaceholderViews[itemId] = placeholderView
|
|
strongSelf.placeholdersContainerView.addSubview(placeholderView)
|
|
}
|
|
placeholderView.frame = itemLayer.frame
|
|
placeholderView.update(size: placeholderView.bounds.size)
|
|
|
|
strongSelf.updateShimmerIfNeeded()
|
|
}
|
|
} else {
|
|
if let placeholderView = strongSelf.visibleItemPlaceholderViews[itemId] {
|
|
strongSelf.visibleItemPlaceholderViews.removeValue(forKey: itemId)
|
|
if duration > 0.0 {
|
|
if let itemLayer = strongSelf.visibleItemLayers[itemId] {
|
|
itemLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18)
|
|
}
|
|
|
|
placeholderView.alpha = 0.0
|
|
placeholderView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, completion: { [weak self, weak placeholderView] _ in
|
|
placeholderView?.removeFromSuperview()
|
|
self?.updateShimmerIfNeeded()
|
|
})
|
|
} else {
|
|
placeholderView.removeFromSuperview()
|
|
strongSelf.updateShimmerIfNeeded()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
)
|
|
self.scrollView.layer.addSublayer(itemLayer)
|
|
self.visibleItemLayers[itemId] = itemLayer
|
|
}
|
|
|
|
let itemPosition = CGPoint(x: itemFrame.midX, y: itemFrame.midY)
|
|
let itemBounds = CGRect(origin: CGPoint(), size: itemFrame.size)
|
|
|
|
itemTransition.setFrame(layer: itemLayer, frame: itemFrame)
|
|
itemLayer.isVisibleForAnimations = true
|
|
|
|
if let placeholderView = self.visibleItemPlaceholderViews[itemId] {
|
|
if placeholderView.layer.position != itemPosition || placeholderView.layer.bounds != itemBounds {
|
|
itemTransition.setFrame(view: placeholderView, frame: itemFrame)
|
|
placeholderView.update(size: itemFrame.size)
|
|
}
|
|
}
|
|
|
|
if updateItemLayerPlaceholder {
|
|
if itemLayer.displayPlaceholder {
|
|
itemLayer.onUpdateDisplayPlaceholder(true, 0.0)
|
|
} else {
|
|
itemLayer.onUpdateDisplayPlaceholder(false, 0.2)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var removedIds: [ItemKey] = []
|
|
for (id, itemLayer) in self.visibleItemLayers {
|
|
if !validIds.contains(id) {
|
|
removedIds.append(id)
|
|
itemLayer.removeFromSuperlayer()
|
|
|
|
if let view = self.visibleItemPlaceholderViews.removeValue(forKey: id) {
|
|
view.removeFromSuperview()
|
|
}
|
|
}
|
|
}
|
|
for id in removedIds {
|
|
self.visibleItemLayers.removeValue(forKey: id)
|
|
}
|
|
}
|
|
|
|
private func updateShimmerIfNeeded() {
|
|
if self.placeholdersContainerView.subviews.isEmpty {
|
|
self.standaloneShimmerEffect.layer = nil
|
|
} else {
|
|
self.standaloneShimmerEffect.layer = self.shimmerHostView.layer
|
|
}
|
|
}
|
|
|
|
public func pagerUpdateBackground(backgroundFrame: CGRect, transition: Transition) {
|
|
guard let theme = self.theme else {
|
|
return
|
|
}
|
|
if theme.overallDarkAppearance {
|
|
if let vibrancyEffectView = self.vibrancyEffectView {
|
|
self.vibrancyEffectView = nil
|
|
vibrancyEffectView.removeFromSuperview()
|
|
}
|
|
} else {
|
|
if self.vibrancyEffectView == nil {
|
|
let style: UIBlurEffect.Style
|
|
style = .extraLight
|
|
let blurEffect = UIBlurEffect(style: style)
|
|
let vibrancyEffect = UIVibrancyEffect(blurEffect: blurEffect)
|
|
let vibrancyEffectView = UIVisualEffectView(effect: vibrancyEffect)
|
|
self.vibrancyEffectView = vibrancyEffectView
|
|
self.backgroundView.addSubview(vibrancyEffectView)
|
|
vibrancyEffectView.contentView.addSubview(self.mirrorContentScrollView)
|
|
}
|
|
}
|
|
self.backgroundView.updateColor(color: theme.chat.inputMediaPanel.backgroundColor, enableBlur: true, forceKeepBlur: false, transition: transition.containedViewLayoutTransition)
|
|
transition.setFrame(view: self.backgroundView, frame: backgroundFrame)
|
|
self.backgroundView.update(size: backgroundFrame.size, transition: transition.containedViewLayoutTransition)
|
|
|
|
if let vibrancyEffectView = self.vibrancyEffectView {
|
|
transition.setFrame(view: vibrancyEffectView, frame: CGRect(origin: CGPoint(x: 0.0, y: -backgroundFrame.minY), size: CGSize(width: backgroundFrame.width, height: backgroundFrame.height + backgroundFrame.minY)))
|
|
}
|
|
}
|
|
|
|
func update(component: GifPagerContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
|
var contentReset = false
|
|
if let previousComponent = self.component, previousComponent.subject != component.subject {
|
|
contentReset = true
|
|
self.currentLoadMoreToken = nil
|
|
}
|
|
|
|
let keyboardChildEnvironment = environment[EntityKeyboardChildEnvironment.self].value
|
|
|
|
self.component = component
|
|
self.theme = keyboardChildEnvironment.theme
|
|
|
|
let pagerEnvironment = environment[PagerComponentChildEnvironment.self].value
|
|
self.pagerEnvironment = pagerEnvironment
|
|
|
|
transition.setFrame(view: self.shimmerHostView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
|
|
|
let shimmerBackgroundColor = keyboardChildEnvironment.theme.chat.inputPanel.primaryTextColor.withMultipliedAlpha(0.08)
|
|
let shimmerForegroundColor = keyboardChildEnvironment.theme.list.itemBlocksBackgroundColor.withMultipliedAlpha(0.15)
|
|
self.standaloneShimmerEffect.update(background: shimmerBackgroundColor, foreground: shimmerForegroundColor)
|
|
|
|
let itemLayout = ItemLayout(
|
|
width: availableSize.width,
|
|
containerInsets: UIEdgeInsets(top: pagerEnvironment.containerInsets.top, left: pagerEnvironment.containerInsets.left, bottom: pagerEnvironment.containerInsets.bottom, right: pagerEnvironment.containerInsets.right),
|
|
itemCount: component.items.count
|
|
)
|
|
self.itemLayout = itemLayout
|
|
|
|
self.ignoreScrolling = true
|
|
let scrollOriginY: CGFloat = 0.0
|
|
let scrollSize = CGSize(width: availableSize.width, height: availableSize.height)
|
|
|
|
transition.setPosition(view: self.scrollView, position: CGPoint(x: 0.0, y: scrollOriginY))
|
|
self.scrollView.bounds = CGRect(origin: self.scrollView.bounds.origin, size: scrollSize)
|
|
|
|
if self.scrollView.contentSize != itemLayout.contentSize {
|
|
self.scrollView.contentSize = itemLayout.contentSize
|
|
}
|
|
if self.scrollView.scrollIndicatorInsets != pagerEnvironment.containerInsets {
|
|
self.scrollView.scrollIndicatorInsets = pagerEnvironment.containerInsets
|
|
}
|
|
|
|
if contentReset {
|
|
self.scrollView.setContentOffset(CGPoint(), animated: false)
|
|
}
|
|
|
|
self.previousScrollingOffset = self.scrollView.contentOffset.y
|
|
self.ignoreScrolling = false
|
|
|
|
if let displaySearchWithPlaceholder = component.displaySearchWithPlaceholder {
|
|
let visibleSearchHeader: EmojiSearchHeaderView
|
|
if let current = self.visibleSearchHeader {
|
|
visibleSearchHeader = current
|
|
} else {
|
|
visibleSearchHeader = EmojiSearchHeaderView(activated: { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.component?.inputInteraction.openSearch()
|
|
}, deactivated: { _ in
|
|
}, updateQuery: {_, _ in
|
|
})
|
|
self.visibleSearchHeader = visibleSearchHeader
|
|
self.scrollView.addSubview(visibleSearchHeader)
|
|
self.mirrorContentScrollView.addSubview(visibleSearchHeader.tintContainerView)
|
|
}
|
|
|
|
let searchHeaderFrame = CGRect(origin: CGPoint(x: itemLayout.searchInsets.left, y: itemLayout.searchInsets.top), size: CGSize(width: itemLayout.width - itemLayout.searchInsets.left - itemLayout.searchInsets.right, height: itemLayout.searchHeight))
|
|
visibleSearchHeader.update(theme: keyboardChildEnvironment.theme, strings: keyboardChildEnvironment.strings, text: displaySearchWithPlaceholder, useOpaqueTheme: false, isActive: false, size: searchHeaderFrame.size, canFocus: false, transition: transition)
|
|
transition.setFrame(view: visibleSearchHeader, frame: searchHeaderFrame, completion: { [weak self] completed in
|
|
guard let strongSelf = self, completed, let visibleSearchHeader = strongSelf.visibleSearchHeader else {
|
|
return
|
|
}
|
|
|
|
if visibleSearchHeader.superview != strongSelf.scrollView {
|
|
strongSelf.scrollView.addSubview(visibleSearchHeader)
|
|
strongSelf.mirrorContentScrollView.addSubview(visibleSearchHeader.tintContainerView)
|
|
}
|
|
})
|
|
} else {
|
|
if let visibleSearchHeader = self.visibleSearchHeader {
|
|
self.visibleSearchHeader = nil
|
|
visibleSearchHeader.removeFromSuperview()
|
|
visibleSearchHeader.tintContainerView.removeFromSuperview()
|
|
}
|
|
}
|
|
|
|
self.updateVisibleItems(attemptSynchronousLoads: true)
|
|
|
|
return availableSize
|
|
}
|
|
}
|
|
|
|
public func makeView() -> View {
|
|
return View(frame: CGRect())
|
|
}
|
|
|
|
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
|
}
|
|
}
|