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 userLocation: MediaResourceUserLocation 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, userLocation: MediaResourceUserLocation, file: TelegramMediaFile?, synchronousLoad: Bool) { self.context = context self.userLocation = userLocation 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, userLocation: userLocation, 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, userLocation: self.userLocation, userContentType: .other, 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? { 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, userLocation: .other, 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) { 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() 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, 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, hasSearchItems: true, 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, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } }