Swiftgram/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift
2022-09-05 03:57:37 +04:00

5479 lines
267 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 VideoAnimationCache
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import SwiftSignalKit
import ShimmerEffect
import PagerComponent
import StickerResources
import AppBundle
import UndoUI
import AudioToolbox
import SolidRoundedButtonComponent
import EmojiTextAttachmentView
private let premiumBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat List/PeerPremiumIcon"), color: .white)
private let featuredBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/PanelBadgeAdd"), color: .white)
private let lockedBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/PanelBadgeLock"), color: .white)
private let staticEmojiMapping: [(EmojiPagerContentComponent.StaticEmojiSegment, [String])] = {
guard let path = getAppBundle().path(forResource: "emoji1016", ofType: "txt") else {
return []
}
guard let string = try? String(contentsOf: URL(fileURLWithPath: path)) else {
return []
}
var result: [(EmojiPagerContentComponent.StaticEmojiSegment, [String])] = []
let orderedSegments = EmojiPagerContentComponent.StaticEmojiSegment.allCases
let segments = string.components(separatedBy: "\n\n")
for i in 0 ..< min(segments.count, orderedSegments.count) {
let list = segments[i].components(separatedBy: " ")
result.append((orderedSegments[i], list))
}
return result
}()
private final class WarpView: UIView {
private final class WarpPartView: UIView {
let cloneView: PortalView
init?(contentView: PortalSourceView) {
guard let cloneView = PortalView(matchPosition: false) else {
return nil
}
self.cloneView = cloneView
super.init(frame: CGRect())
self.layer.anchorPoint = CGPoint(x: 0.5, y: 0.0)
self.clipsToBounds = true
self.addSubview(cloneView.view)
contentView.addPortal(view: cloneView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(containerSize: CGSize, rect: CGRect, transition: Transition) {
transition.setFrame(view: self.cloneView.view, frame: CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: CGSize(width: containerSize.width, height: containerSize.height)))
}
}
let contentView: PortalSourceView
private let clippingView: UIView
private var warpViews: [WarpPartView] = []
private let warpMaskContainer: UIView
private let warpMaskGradientLayer: SimpleGradientLayer
override init(frame: CGRect) {
self.contentView = PortalSourceView()
self.clippingView = UIView()
self.warpMaskContainer = UIView()
self.warpMaskGradientLayer = SimpleGradientLayer()
self.warpMaskContainer.layer.mask = self.warpMaskGradientLayer
super.init(frame: frame)
self.clippingView.addSubview(self.contentView)
self.clippingView.clipsToBounds = true
self.addSubview(self.clippingView)
self.addSubview(self.warpMaskContainer)
for _ in 0 ..< 8 {
if let warpView = WarpPartView(contentView: self.contentView) {
self.warpViews.append(warpView)
self.warpMaskContainer.addSubview(warpView)
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(size: CGSize, topInset: CGFloat, warpHeight: CGFloat, theme: PresentationTheme, transition: Transition) {
transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(), size: size))
let allItemsHeight = warpHeight * 0.5
for i in 0 ..< self.warpViews.count {
let itemHeight = warpHeight / CGFloat(self.warpViews.count)
let itemFraction = CGFloat(i + 1) / CGFloat(self.warpViews.count)
let _ = itemHeight
let da = CGFloat.pi * 0.5 / CGFloat(self.warpViews.count)
let alpha = CGFloat.pi * 0.5 - itemFraction * CGFloat.pi * 0.5
let endPoint = CGPoint(x: cos(alpha), y: sin(alpha))
let prevAngle = alpha + da
let prevPt = CGPoint(x: cos(prevAngle), y: sin(prevAngle))
var angle: CGFloat
angle = -atan2(endPoint.y - prevPt.y, endPoint.x - prevPt.x)
let itemLengthVector = CGPoint(x: endPoint.x - prevPt.x, y: endPoint.y - prevPt.y)
let itemLength = sqrt(itemLengthVector.x * itemLengthVector.x + itemLengthVector.y * itemLengthVector.y) * warpHeight * 0.5
let _ = itemLength
var transform: CATransform3D
transform = CATransform3DIdentity
transform.m34 = 1.0 / 240.0
transform = CATransform3DTranslate(transform, 0.0, prevPt.x * allItemsHeight, (1.0 - prevPt.y) * allItemsHeight)
transform = CATransform3DRotate(transform, angle, 1.0, 0.0, 0.0)
let positionY = size.height - allItemsHeight + 4.0 + CGFloat(i) * itemLength
let rect = CGRect(origin: CGPoint(x: 0.0, y: positionY), size: CGSize(width: size.width, height: itemLength))
transition.setPosition(view: self.warpViews[i], position: CGPoint(x: rect.midX, y: 4.0))
transition.setBounds(view: self.warpViews[i], bounds: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: itemLength)))
transition.setTransform(view: self.warpViews[i], transform: transform)
self.warpViews[i].update(containerSize: size, rect: rect, transition: transition)
}
let frame = CGRect(origin: CGPoint(x: 0.0, y: -topInset), size: CGSize(width: size.width, height: topInset + size.height - 21.0))
transition.setPosition(view: self.clippingView, position: frame.center)
transition.setBounds(view: self.clippingView, bounds: CGRect(origin: CGPoint(x: 0.0, y: -topInset), size: frame.size))
transition.setFrame(view: self.warpMaskContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: size.height - allItemsHeight), size: CGSize(width: size.width, height: allItemsHeight)))
var locations: [NSNumber] = []
var colors: [CGColor] = []
let numStops = 6
for i in 0 ..< numStops {
let step = CGFloat(i) / CGFloat(numStops - 1)
locations.append(step as NSNumber)
colors.append(UIColor.black.withAlphaComponent(1.0 - step * step).cgColor)
}
let gradientHeight: CGFloat = 6.0
self.warpMaskGradientLayer.startPoint = CGPoint(x: 0.0, y: (allItemsHeight - gradientHeight) / allItemsHeight)
self.warpMaskGradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0)
self.warpMaskGradientLayer.locations = locations
self.warpMaskGradientLayer.colors = colors
self.warpMaskGradientLayer.type = .axial
transition.setFrame(layer: self.warpMaskGradientLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: allItemsHeight)))
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return self.contentView.hitTest(point, with: event)
}
}
public struct EmojiComponentReactionItem {
public var reaction: MessageReaction.Reaction
public var file: TelegramMediaFile
public init(reaction: MessageReaction.Reaction, file: TelegramMediaFile) {
self.reaction = reaction
self.file = file
}
}
public final class EntityKeyboardAnimationData: Equatable {
public enum Id: Hashable {
case file(MediaId)
case stickerPackThumbnail(ItemCollectionId)
}
public enum ItemType {
case still
case lottie
case video
var animationCacheAnimationType: AnimationCacheAnimationType {
switch self {
case .still:
return .still
case .lottie:
return .lottie
case .video:
return .video
}
}
}
public let id: Id
public let type: ItemType
public let resource: MediaResourceReference
public let dimensions: CGSize
public let immediateThumbnailData: Data?
public let isReaction: Bool
public init(id: Id, type: ItemType, resource: MediaResourceReference, dimensions: CGSize, immediateThumbnailData: Data?, isReaction: Bool) {
self.id = id
self.type = type
self.resource = resource
self.dimensions = dimensions
self.immediateThumbnailData = immediateThumbnailData
self.isReaction = isReaction
}
public convenience init(file: TelegramMediaFile, isReaction: Bool = false) {
let type: ItemType
if file.isVideoSticker || file.isVideoEmoji {
type = .video
} else if file.isAnimatedSticker {
type = .lottie
} else {
type = .still
}
self.init(id: .file(file.fileId), type: type, resource: .standalone(resource: file.resource), dimensions: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), immediateThumbnailData: file.immediateThumbnailData, isReaction: isReaction)
}
public static func ==(lhs: EntityKeyboardAnimationData, rhs: EntityKeyboardAnimationData) -> Bool {
if lhs === rhs {
return true
}
if lhs.resource.resource.id != rhs.resource.resource.id {
return false
}
if lhs.dimensions != rhs.dimensions {
return false
}
if lhs.type != rhs.type {
return false
}
if lhs.immediateThumbnailData != rhs.immediateThumbnailData {
return false
}
if lhs.isReaction != rhs.isReaction {
return false
}
return true
}
}
public class PassthroughLayer: CALayer {
public var mirrorLayer: CALayer?
override init() {
super.init()
}
override init(layer: Any) {
super.init(layer: layer)
}
required 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 var opacity: Float {
get {
return super.opacity
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.opacity = value
}
super.opacity = value
}
}
override public var sublayerTransform: CATransform3D {
get {
return super.sublayerTransform
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.sublayerTransform = value
}
super.sublayerTransform = value
}
}
override public var transform: CATransform3D {
get {
return super.transform
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.transform = value
}
super.transform = 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)
}
}
open class PassthroughView: UIView {
override public static var layerClass: AnyClass {
return PassthroughLayer.self
}
public let passthroughView: UIView
override public init(frame: CGRect) {
self.passthroughView = UIView()
super.init(frame: frame)
(self.layer as? PassthroughLayer)?.mirrorLayer = self.passthroughView.layer
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private class PassthroughShapeLayer: CAShapeLayer {
var mirrorLayer: CAShapeLayer?
override init() {
super.init()
}
override init(layer: Any) {
super.init(layer: layer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var position: CGPoint {
get {
return super.position
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.position = value
}
super.position = value
}
}
override var bounds: CGRect {
get {
return super.bounds
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.bounds = value
}
super.bounds = value
}
}
override var opacity: Float {
get {
return super.opacity
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.opacity = value
}
super.opacity = value
}
}
override var sublayerTransform: CATransform3D {
get {
return super.sublayerTransform
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.sublayerTransform = value
}
super.sublayerTransform = value
}
}
override var transform: CATransform3D {
get {
return super.transform
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.transform = value
}
super.transform = value
}
}
override var path: CGPath? {
get {
return super.path
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.path = value
}
super.path = value
}
}
override var fillColor: CGColor? {
get {
return super.fillColor
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.fillColor = value
}
super.fillColor = value
}
}
override var fillRule: CAShapeLayerFillRule {
get {
return super.fillRule
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.fillRule = value
}
super.fillRule = value
}
}
override var strokeColor: CGColor? {
get {
return super.strokeColor
} set(value) {
/*if let mirrorLayer = self.mirrorLayer {
mirrorLayer.strokeColor = value
}*/
super.strokeColor = value
}
}
override var strokeStart: CGFloat {
get {
return super.strokeStart
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.strokeStart = value
}
super.strokeStart = value
}
}
override var strokeEnd: CGFloat {
get {
return super.strokeEnd
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.strokeEnd = value
}
super.strokeEnd = value
}
}
override var lineWidth: CGFloat {
get {
return super.lineWidth
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.lineWidth = value
}
super.lineWidth = value
}
}
override var miterLimit: CGFloat {
get {
return super.miterLimit
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.miterLimit = value
}
super.miterLimit = value
}
}
override var lineCap: CAShapeLayerLineCap {
get {
return super.lineCap
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.lineCap = value
}
super.lineCap = value
}
}
override var lineJoin: CAShapeLayerLineJoin {
get {
return super.lineJoin
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.lineJoin = value
}
super.lineJoin = value
}
}
override var lineDashPhase: CGFloat {
get {
return super.lineDashPhase
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.lineDashPhase = value
}
super.lineDashPhase = value
}
}
override var lineDashPattern: [NSNumber]? {
get {
return super.lineDashPattern
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.lineDashPattern = value
}
super.lineDashPattern = value
}
}
override func add(_ animation: CAAnimation, forKey key: String?) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.add(animation, forKey: key)
}
super.add(animation, forKey: key)
}
override func removeAllAnimations() {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.removeAllAnimations()
}
super.removeAllAnimations()
}
override func removeAnimation(forKey: String) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.removeAnimation(forKey: forKey)
}
super.removeAnimation(forKey: forKey)
}
}
private final class PremiumBadgeView: UIView {
private var badge: EmojiPagerContentComponent.View.ItemLayer.Badge?
let contentLayer: SimpleLayer
private let overlayColorLayer: SimpleLayer
private let iconLayer: SimpleLayer
init() {
self.contentLayer = SimpleLayer()
self.contentLayer.contentsGravity = .resize
self.contentLayer.masksToBounds = true
self.overlayColorLayer = SimpleLayer()
self.overlayColorLayer.masksToBounds = true
self.iconLayer = SimpleLayer()
super.init(frame: CGRect())
self.layer.addSublayer(self.contentLayer)
self.layer.addSublayer(self.overlayColorLayer)
self.layer.addSublayer(self.iconLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(transition: Transition, badge: EmojiPagerContentComponent.View.ItemLayer.Badge, backgroundColor: UIColor, size: CGSize) {
if self.badge != badge {
self.badge = badge
switch badge {
case .premium:
self.iconLayer.contents = premiumBadgeIcon?.cgImage
case .featured:
self.iconLayer.contents = featuredBadgeIcon?.cgImage
case .locked:
self.iconLayer.contents = lockedBadgeIcon?.cgImage
}
}
let iconInset: CGFloat
switch badge {
case .premium:
iconInset = 2.0
case .featured:
iconInset = 0.0
case .locked:
iconInset = 0.0
}
self.overlayColorLayer.backgroundColor = backgroundColor.cgColor
transition.setFrame(layer: self.contentLayer, frame: CGRect(origin: CGPoint(), size: size))
transition.setCornerRadius(layer: self.contentLayer, cornerRadius: min(size.width / 2.0, size.height / 2.0))
transition.setFrame(layer: self.overlayColorLayer, frame: CGRect(origin: CGPoint(), size: size))
transition.setCornerRadius(layer: self.overlayColorLayer, cornerRadius: min(size.width / 2.0, size.height / 2.0))
transition.setFrame(layer: self.iconLayer, frame: CGRect(origin: CGPoint(), size: size).insetBy(dx: iconInset, dy: iconInset))
}
}
private final class GroupHeaderActionButton: UIButton {
private var currentTextLayout: (string: String, color: UIColor, constrainedWidth: CGFloat, size: CGSize)?
private let backgroundLayer: SimpleLayer
private let textLayer: SimpleLayer
private let pressed: () -> Void
init(pressed: @escaping () -> Void) {
self.pressed = pressed
self.backgroundLayer = SimpleLayer()
self.backgroundLayer.masksToBounds = true
self.textLayer = SimpleLayer()
super.init(frame: CGRect())
self.layer.addSublayer(self.backgroundLayer)
self.layer.addSublayer(self.textLayer)
self.addTarget(self, action: #selector(self.onPressed), for: .touchUpInside)
}
required init(coder: NSCoder) {
preconditionFailure()
}
@objc private func onPressed() {
self.pressed()
}
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
self.alpha = 0.6
return super.beginTracking(touch, with: event)
}
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
let alpha = self.alpha
self.alpha = 1.0
self.layer.animateAlpha(from: alpha, to: 1.0, duration: 0.25)
super.endTracking(touch, with: event)
}
override func cancelTracking(with event: UIEvent?) {
let alpha = self.alpha
self.alpha = 1.0
self.layer.animateAlpha(from: alpha, to: 1.0, duration: 0.25)
super.cancelTracking(with: event)
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
let alpha = self.alpha
self.alpha = 1.0
self.layer.animateAlpha(from: alpha, to: 1.0, duration: 0.25)
super.touchesCancelled(touches, with: event)
}
func update(theme: PresentationTheme, title: String) -> CGSize {
let textConstrainedWidth: CGFloat = 100.0
let color = theme.list.itemCheckColors.foregroundColor
self.backgroundLayer.backgroundColor = theme.list.itemCheckColors.fillColor.cgColor
let textSize: CGSize
if let currentTextLayout = self.currentTextLayout, currentTextLayout.string == title, currentTextLayout.color == color, currentTextLayout.constrainedWidth == textConstrainedWidth {
textSize = currentTextLayout.size
} else {
let font: UIFont = Font.semibold(15.0)
let string = NSAttributedString(string: title.uppercased(), font: font, textColor: color)
let stringBounds = string.boundingRect(with: CGSize(width: textConstrainedWidth, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
textSize = CGSize(width: ceil(stringBounds.width), height: ceil(stringBounds.height))
self.textLayer.contents = generateImage(textSize, opaque: false, scale: 0.0, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
string.draw(in: stringBounds)
UIGraphicsPopContext()
})?.cgImage
self.currentTextLayout = (title, color, textConstrainedWidth, textSize)
}
let size = CGSize(width: textSize.width + 16.0 * 2.0, height: 28.0)
let textFrame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: floor((size.height - textSize.height) / 2.0)), size: textSize)
self.textLayer.frame = textFrame
self.backgroundLayer.frame = CGRect(origin: CGPoint(), size: size)
self.backgroundLayer.cornerRadius = min(size.width, size.height) / 2.0
return size
}
}
private final class GroupHeaderLayer: UIView {
override static var layerClass: AnyClass {
return PassthroughLayer.self
}
private let actionPressed: () -> Void
private let performItemAction: (EmojiPagerContentComponent.Item, UIView, CGRect, CALayer) -> Void
private let textLayer: SimpleLayer
private let tintTextLayer: SimpleLayer
private var subtitleLayer: SimpleLayer?
private var tintSubtitleLayer: SimpleLayer?
private var lockIconLayer: SimpleLayer?
private var tintLockIconLayer: SimpleLayer?
private(set) var clearIconLayer: SimpleLayer?
private var tintClearIconLayer: SimpleLayer?
private var separatorLayer: SimpleLayer?
private var tintSeparatorLayer: SimpleLayer?
private var actionButton: GroupHeaderActionButton?
private var groupEmbeddedView: GroupEmbeddedView?
private var theme: PresentationTheme?
private var currentTextLayout: (string: String, color: UIColor, constrainedWidth: CGFloat, size: CGSize)?
private var currentSubtitleLayout: (string: String, color: UIColor, constrainedWidth: CGFloat, size: CGSize)?
let tintContentLayer: SimpleLayer
init(actionPressed: @escaping () -> Void, performItemAction: @escaping (EmojiPagerContentComponent.Item, UIView, CGRect, CALayer) -> Void) {
self.actionPressed = actionPressed
self.performItemAction = performItemAction
self.textLayer = SimpleLayer()
self.tintTextLayer = SimpleLayer()
self.tintContentLayer = SimpleLayer()
super.init(frame: CGRect())
self.layer.addSublayer(self.textLayer)
self.tintContentLayer.addSublayer(self.tintTextLayer)
(self.layer as? PassthroughLayer)?.mirrorLayer = self.tintContentLayer
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(
context: AccountContext,
theme: PresentationTheme,
layoutType: EmojiPagerContentComponent.ItemLayoutType,
hasTopSeparator: Bool,
actionButtonTitle: String?,
title: String,
subtitle: String?,
isPremiumLocked: Bool,
hasClear: Bool,
embeddedItems: [EmojiPagerContentComponent.Item]?,
constrainedSize: CGSize,
insets: UIEdgeInsets,
cache: AnimationCache,
renderer: MultiAnimationRenderer,
attemptSynchronousLoad: Bool
) -> (size: CGSize, centralContentWidth: CGFloat) {
var themeUpdated = false
if self.theme !== theme {
self.theme = theme
themeUpdated = true
}
let needsVibrancy = !theme.overallDarkAppearance
let textOffsetY: CGFloat
if hasTopSeparator {
textOffsetY = 9.0
} else {
textOffsetY = 0.0
}
let color: UIColor
let needsTintText: Bool
if subtitle != nil {
color = theme.chat.inputPanel.primaryTextColor
needsTintText = false
} else {
color = theme.chat.inputMediaPanel.panelContentVibrantOverlayColor
needsTintText = true
}
let subtitleColor = theme.chat.inputMediaPanel.panelContentVibrantOverlayColor
let titleHorizontalOffset: CGFloat
if isPremiumLocked {
titleHorizontalOffset = 10.0 + 2.0
} else {
titleHorizontalOffset = 0.0
}
var actionButtonSize: CGSize?
if let actionButtonTitle = actionButtonTitle {
let actionButton: GroupHeaderActionButton
if let current = self.actionButton {
actionButton = current
} else {
actionButton = GroupHeaderActionButton(pressed: self.actionPressed)
self.actionButton = actionButton
self.addSubview(actionButton)
}
actionButtonSize = actionButton.update(theme: theme, title: actionButtonTitle)
} else {
if let actionButton = self.actionButton {
self.actionButton = nil
actionButton.removeFromSuperview()
}
}
var textConstrainedWidth = constrainedSize.width - titleHorizontalOffset - 10.0
if let actionButtonSize = actionButtonSize {
textConstrainedWidth -= actionButtonSize.width - 8.0
}
let textSize: CGSize
if let currentTextLayout = self.currentTextLayout, currentTextLayout.string == title, currentTextLayout.color == color, currentTextLayout.constrainedWidth == textConstrainedWidth {
textSize = currentTextLayout.size
} else {
let font: UIFont
let stringValue: String
if subtitle == nil {
font = Font.medium(13.0)
stringValue = title.uppercased()
} else {
font = Font.semibold(16.0)
stringValue = title
}
let string = NSAttributedString(string: stringValue, font: font, textColor: color)
let whiteString = NSAttributedString(string: stringValue, font: font, textColor: .white)
let stringBounds = string.boundingRect(with: CGSize(width: textConstrainedWidth, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
textSize = CGSize(width: ceil(stringBounds.width), height: ceil(stringBounds.height))
self.textLayer.contents = generateImage(textSize, opaque: false, scale: 0.0, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
string.draw(in: stringBounds)
UIGraphicsPopContext()
})?.cgImage
self.tintTextLayer.contents = generateImage(textSize, opaque: false, scale: 0.0, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
whiteString.draw(in: stringBounds)
UIGraphicsPopContext()
})?.cgImage
self.tintTextLayer.isHidden = !needsVibrancy
self.currentTextLayout = (title, color, textConstrainedWidth, textSize)
}
let textFrame: CGRect
if subtitle == nil {
textFrame = CGRect(origin: CGPoint(x: titleHorizontalOffset + floor((constrainedSize.width - titleHorizontalOffset - textSize.width) / 2.0), y: textOffsetY), size: textSize)
} else {
textFrame = CGRect(origin: CGPoint(x: titleHorizontalOffset, y: textOffsetY), size: textSize)
}
self.textLayer.frame = textFrame
self.tintTextLayer.frame = textFrame
self.tintTextLayer.isHidden = !needsTintText
if isPremiumLocked {
let lockIconLayer: SimpleLayer
if let current = self.lockIconLayer {
lockIconLayer = current
} else {
lockIconLayer = SimpleLayer()
self.lockIconLayer = lockIconLayer
self.layer.addSublayer(lockIconLayer)
}
if let image = PresentationResourcesChat.chatEntityKeyboardLock(theme, color: color) {
let imageSize = image.size
lockIconLayer.contents = image.cgImage
lockIconLayer.frame = CGRect(origin: CGPoint(x: textFrame.minX - imageSize.width - 3.0, y: 2.0 + UIScreenPixel), size: imageSize)
} else {
lockIconLayer.contents = nil
}
let tintLockIconLayer: SimpleLayer
if let current = self.tintLockIconLayer {
tintLockIconLayer = current
} else {
tintLockIconLayer = SimpleLayer()
self.tintLockIconLayer = tintLockIconLayer
self.tintContentLayer.addSublayer(tintLockIconLayer)
}
if let image = PresentationResourcesChat.chatEntityKeyboardLock(theme, color: .white) {
tintLockIconLayer.contents = image.cgImage
tintLockIconLayer.frame = lockIconLayer.frame
tintLockIconLayer.isHidden = !needsVibrancy
} else {
tintLockIconLayer.contents = nil
}
} else {
if let lockIconLayer = self.lockIconLayer {
self.lockIconLayer = nil
lockIconLayer.removeFromSuperlayer()
}
if let tintLockIconLayer = self.tintLockIconLayer {
self.tintLockIconLayer = nil
tintLockIconLayer.removeFromSuperlayer()
}
}
let subtitleSize: CGSize
if let subtitle = subtitle {
var updateSubtitleContents: UIImage?
var updateTintSubtitleContents: UIImage?
if let currentSubtitleLayout = self.currentSubtitleLayout, currentSubtitleLayout.string == subtitle, currentSubtitleLayout.color == subtitleColor, currentSubtitleLayout.constrainedWidth == textConstrainedWidth {
subtitleSize = currentSubtitleLayout.size
} else {
let string = NSAttributedString(string: subtitle, font: Font.regular(15.0), textColor: subtitleColor)
let whiteString = NSAttributedString(string: subtitle, font: Font.regular(15.0), textColor: .white)
let stringBounds = string.boundingRect(with: CGSize(width: textConstrainedWidth, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
subtitleSize = CGSize(width: ceil(stringBounds.width), height: ceil(stringBounds.height))
updateSubtitleContents = generateImage(subtitleSize, opaque: false, scale: 0.0, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
string.draw(in: stringBounds)
UIGraphicsPopContext()
})
updateTintSubtitleContents = generateImage(subtitleSize, opaque: false, scale: 0.0, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
whiteString.draw(in: stringBounds)
UIGraphicsPopContext()
})
self.currentSubtitleLayout = (subtitle, subtitleColor, textConstrainedWidth, subtitleSize)
}
let subtitleLayer: SimpleLayer
if let current = self.subtitleLayer {
subtitleLayer = current
} else {
subtitleLayer = SimpleLayer()
self.subtitleLayer = subtitleLayer
self.layer.addSublayer(subtitleLayer)
}
if let updateSubtitleContents = updateSubtitleContents {
subtitleLayer.contents = updateSubtitleContents.cgImage
}
let tintSubtitleLayer: SimpleLayer
if let current = self.tintSubtitleLayer {
tintSubtitleLayer = current
} else {
tintSubtitleLayer = SimpleLayer()
self.tintSubtitleLayer = tintSubtitleLayer
self.tintContentLayer.addSublayer(tintSubtitleLayer)
}
tintSubtitleLayer.isHidden = !needsVibrancy
if let updateTintSubtitleContents = updateTintSubtitleContents {
tintSubtitleLayer.contents = updateTintSubtitleContents.cgImage
}
let subtitleFrame = CGRect(origin: CGPoint(x: 0.0, y: textFrame.maxY + 1.0), size: subtitleSize)
subtitleLayer.frame = subtitleFrame
tintSubtitleLayer.frame = subtitleFrame
} else {
subtitleSize = CGSize()
if let subtitleLayer = self.subtitleLayer {
self.subtitleLayer = nil
subtitleLayer.removeFromSuperlayer()
}
}
var clearWidth: CGFloat = 0.0
if hasClear {
var updateImage = themeUpdated
let clearIconLayer: SimpleLayer
if let current = self.clearIconLayer {
clearIconLayer = current
} else {
updateImage = true
clearIconLayer = SimpleLayer()
self.clearIconLayer = clearIconLayer
self.layer.addSublayer(clearIconLayer)
}
let tintClearIconLayer: SimpleLayer
if let current = self.tintClearIconLayer {
tintClearIconLayer = current
} else {
updateImage = true
tintClearIconLayer = SimpleLayer()
self.tintClearIconLayer = tintClearIconLayer
self.tintContentLayer.addSublayer(tintClearIconLayer)
}
tintClearIconLayer.isHidden = !needsVibrancy
var clearSize = clearIconLayer.bounds.size
if updateImage, let image = PresentationResourcesChat.chatInputMediaPanelGridDismissImage(theme, color: theme.chat.inputMediaPanel.panelContentVibrantOverlayColor) {
clearSize = image.size
clearIconLayer.contents = image.cgImage
}
if updateImage, let image = PresentationResourcesChat.chatInputMediaPanelGridDismissImage(theme, color: .white) {
tintClearIconLayer.contents = image.cgImage
}
clearIconLayer.frame = CGRect(origin: CGPoint(x: constrainedSize.width - clearSize.width, y: floorToScreenPixels((textSize.height - clearSize.height) / 2.0)), size: clearSize)
tintClearIconLayer.frame = clearIconLayer.frame
clearWidth = 4.0 + clearSize.width
} else {
if let clearIconLayer = self.clearIconLayer {
self.clearIconLayer = nil
clearIconLayer.removeFromSuperlayer()
}
if let tintClearIconLayer = self.tintClearIconLayer {
self.tintClearIconLayer = nil
tintClearIconLayer.removeFromSuperlayer()
}
}
var size: CGSize
size = CGSize(width: constrainedSize.width, height: constrainedSize.height)
if let embeddedItems = embeddedItems {
let groupEmbeddedView: GroupEmbeddedView
if let current = self.groupEmbeddedView {
groupEmbeddedView = current
} else {
groupEmbeddedView = GroupEmbeddedView(performItemAction: self.performItemAction)
self.groupEmbeddedView = groupEmbeddedView
self.addSubview(groupEmbeddedView)
}
let groupEmbeddedViewSize = CGSize(width: constrainedSize.width + insets.left + insets.right, height: 36.0)
groupEmbeddedView.frame = CGRect(origin: CGPoint(x: -insets.left, y: size.height - groupEmbeddedViewSize.height), size: groupEmbeddedViewSize)
groupEmbeddedView.update(
context: context,
theme: theme,
insets: insets,
size: groupEmbeddedViewSize,
items: embeddedItems,
cache: cache,
renderer: renderer,
attemptSynchronousLoad: attemptSynchronousLoad
)
} else {
if let groupEmbeddedView = self.groupEmbeddedView {
self.groupEmbeddedView = nil
groupEmbeddedView.removeFromSuperview()
}
}
if let actionButtonSize = actionButtonSize, let actionButton = self.actionButton {
actionButton.frame = CGRect(origin: CGPoint(x: size.width - actionButtonSize.width, y: textFrame.minY + 3.0), size: actionButtonSize)
}
if hasTopSeparator {
let separatorLayer: SimpleLayer
if let current = self.separatorLayer {
separatorLayer = current
} else {
separatorLayer = SimpleLayer()
self.separatorLayer = separatorLayer
self.layer.addSublayer(separatorLayer)
}
separatorLayer.backgroundColor = theme.chat.inputMediaPanel.panelContentVibrantOverlayColor.cgColor
separatorLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: UIScreenPixel))
let tintSeparatorLayer: SimpleLayer
if let current = self.tintSeparatorLayer {
tintSeparatorLayer = current
} else {
tintSeparatorLayer = SimpleLayer()
self.tintSeparatorLayer = tintSeparatorLayer
self.tintContentLayer.addSublayer(tintSeparatorLayer)
}
tintSeparatorLayer.backgroundColor = UIColor.white.cgColor
tintSeparatorLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: UIScreenPixel))
tintSeparatorLayer.isHidden = !needsVibrancy
} else {
if let separatorLayer = self.separatorLayer {
self.separatorLayer = separatorLayer
separatorLayer.removeFromSuperlayer()
}
if let tintSeparatorLayer = self.tintSeparatorLayer {
self.tintSeparatorLayer = tintSeparatorLayer
tintSeparatorLayer.removeFromSuperlayer()
}
}
return (size, titleHorizontalOffset + textSize.width + clearWidth)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return super.hitTest(point, with: event)
}
func tapGesture(_ recognizer: UITapGestureRecognizer) -> Bool {
if let groupEmbeddedView = self.groupEmbeddedView {
return groupEmbeddedView.tapGesture(recognizer)
} else {
return false
}
}
}
private final class GroupEmbeddedView: UIScrollView, UIScrollViewDelegate, PagerExpandableScrollView {
private struct ItemLayout {
var itemSize: CGFloat
var itemSpacing: CGFloat
var sideInset: CGFloat
var itemCount: Int
var contentSize: CGSize
init(height: CGFloat, sideInset: CGFloat, itemCount: Int) {
self.itemSize = 30.0
self.itemSpacing = 20.0
self.sideInset = sideInset
self.itemCount = itemCount
self.contentSize = CGSize(width: self.sideInset * 2.0 + CGFloat(self.itemCount) * self.itemSize + CGFloat(self.itemCount - 1) * self.itemSpacing, height: height)
}
func frame(at index: Int) -> CGRect {
return CGRect(origin: CGPoint(x: sideInset + CGFloat(index) * (self.itemSize + self.itemSpacing), y: floor((self.contentSize.height - self.itemSize) / 2.0)), size: CGSize(width: self.itemSize, height: self.itemSize))
}
func visibleItems(for rect: CGRect) -> Range<Int>? {
let offsetRect = rect.offsetBy(dx: -self.sideInset, dy: 0.0)
var minVisibleIndex = Int(floor((offsetRect.minX - self.itemSpacing) / (self.itemSize + self.itemSpacing)))
minVisibleIndex = max(0, minVisibleIndex)
var maxVisibleIndex = Int(ceil((offsetRect.maxX - self.itemSpacing) / (self.itemSize + self.itemSpacing)))
maxVisibleIndex = min(maxVisibleIndex, self.itemCount - 1)
if minVisibleIndex <= maxVisibleIndex {
return minVisibleIndex ..< (maxVisibleIndex + 1)
} else {
return nil
}
}
}
private let performItemAction: (EmojiPagerContentComponent.Item, UIView, CGRect, CALayer) -> Void
private var visibleItemLayers: [EmojiPagerContentComponent.View.ItemLayer.Key: EmojiPagerContentComponent.View.ItemLayer] = [:]
private var ignoreScrolling: Bool = false
private var context: AccountContext?
private var theme: PresentationTheme?
private var cache: AnimationCache?
private var renderer: MultiAnimationRenderer?
private var currentInsets: UIEdgeInsets?
private var currentSize: CGSize?
private var items: [EmojiPagerContentComponent.Item]?
private var itemLayout: ItemLayout?
init(performItemAction: @escaping (EmojiPagerContentComponent.Item, UIView, CGRect, CALayer) -> Void) {
self.performItemAction = performItemAction
super.init(frame: CGRect())
self.delaysContentTouches = false
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.contentInsetAdjustmentBehavior = .never
}
if #available(iOS 13.0, *) {
self.automaticallyAdjustsScrollIndicatorInsets = false
}
self.showsVerticalScrollIndicator = true
self.showsHorizontalScrollIndicator = false
self.delegate = self
self.clipsToBounds = true
self.scrollsToTop = false
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func tapGesture(_ recognizer: UITapGestureRecognizer) -> Bool {
guard let itemLayout = self.itemLayout else {
return false
}
if case .ended = recognizer.state {
let point = recognizer.location(in: self)
for (_, itemLayer) in self.visibleItemLayers {
if itemLayer.frame.inset(by: UIEdgeInsets(top: 6.0, left: itemLayout.itemSpacing, bottom: 6.0, right: itemLayout.itemSpacing)).contains(point) {
self.performItemAction(itemLayer.item, self, itemLayer.frame, itemLayer)
return true
}
}
}
return false
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.ignoreScrolling {
self.updateVisibleItems(transition: .immediate, attemptSynchronousLoad: false)
}
}
private func updateVisibleItems(transition: Transition, attemptSynchronousLoad: Bool) {
guard let context = self.context, let theme = self.theme, let itemLayout = self.itemLayout, let items = self.items, let cache = self.cache, let renderer = self.renderer else {
return
}
var validIds = Set<EmojiPagerContentComponent.View.ItemLayer.Key>()
if let itemRange = itemLayout.visibleItems(for: self.bounds) {
for index in itemRange.lowerBound ..< itemRange.upperBound {
let item = items[index]
let itemId = EmojiPagerContentComponent.View.ItemLayer.Key(
groupId: AnyHashable(0),
itemId: item.content.id
)
validIds.insert(itemId)
let itemLayer: EmojiPagerContentComponent.View.ItemLayer
if let current = self.visibleItemLayers[itemId] {
itemLayer = current
} else {
itemLayer = EmojiPagerContentComponent.View.ItemLayer(
item: item,
context: context,
attemptSynchronousLoad: attemptSynchronousLoad,
content: item.content,
cache: cache,
renderer: renderer,
placeholderColor: .clear,
blurredBadgeColor: .clear,
accentIconColor: theme.list.itemAccentColor,
pointSize: CGSize(width: 32.0, height: 32.0),
onUpdateDisplayPlaceholder: { _, _ in
}
)
self.visibleItemLayers[itemId] = itemLayer
self.layer.addSublayer(itemLayer)
}
let itemFrame = itemLayout.frame(at: index)
itemLayer.frame = itemFrame
itemLayer.isVisibleForAnimations = true
}
}
var removedIds: [EmojiPagerContentComponent.View.ItemLayer.Key] = []
for (id, itemLayer) in self.visibleItemLayers {
if !validIds.contains(id) {
removedIds.append(id)
itemLayer.removeFromSuperlayer()
}
}
for id in removedIds {
self.visibleItemLayers.removeValue(forKey: id)
}
}
func update(
context: AccountContext,
theme: PresentationTheme,
insets: UIEdgeInsets,
size: CGSize,
items: [EmojiPagerContentComponent.Item],
cache: AnimationCache,
renderer: MultiAnimationRenderer,
attemptSynchronousLoad: Bool
) {
if self.theme === theme && self.currentInsets == insets && self.currentSize == size && self.items == items {
return
}
self.context = context
self.theme = theme
self.currentInsets = insets
self.currentSize = size
self.items = items
self.cache = cache
self.renderer = renderer
let itemLayout = ItemLayout(height: size.height, sideInset: insets.left, itemCount: items.count)
self.itemLayout = itemLayout
self.ignoreScrolling = true
if itemLayout.contentSize != self.contentSize {
self.contentSize = itemLayout.contentSize
}
self.ignoreScrolling = false
self.updateVisibleItems(transition: .immediate, attemptSynchronousLoad: attemptSynchronousLoad)
}
}
private final class GroupExpandActionButton: UIButton {
override static var layerClass: AnyClass {
return PassthroughLayer.self
}
let tintContainerLayer: SimpleLayer
private var currentTextLayout: (string: String, color: UIColor, constrainedWidth: CGFloat, size: CGSize)?
private let backgroundLayer: SimpleLayer
private let tintBackgroundLayer: SimpleLayer
private let textLayer: SimpleLayer
private let pressed: () -> Void
init(pressed: @escaping () -> Void) {
self.pressed = pressed
self.tintContainerLayer = SimpleLayer()
self.backgroundLayer = SimpleLayer()
self.backgroundLayer.masksToBounds = true
self.tintBackgroundLayer = SimpleLayer()
self.tintBackgroundLayer.masksToBounds = true
self.textLayer = SimpleLayer()
super.init(frame: CGRect())
(self.layer as? PassthroughLayer)?.mirrorLayer = self.tintContainerLayer
self.layer.addSublayer(self.backgroundLayer)
self.layer.addSublayer(self.textLayer)
self.addTarget(self, action: #selector(self.onPressed), for: .touchUpInside)
}
required init(coder: NSCoder) {
preconditionFailure()
}
@objc private func onPressed() {
self.pressed()
}
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
self.alpha = 0.6
return super.beginTracking(touch, with: event)
}
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
let alpha = self.alpha
self.alpha = 1.0
self.layer.animateAlpha(from: alpha, to: 1.0, duration: 0.25)
super.endTracking(touch, with: event)
}
override func cancelTracking(with event: UIEvent?) {
let alpha = self.alpha
self.alpha = 1.0
self.layer.animateAlpha(from: alpha, to: 1.0, duration: 0.25)
super.cancelTracking(with: event)
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
let alpha = self.alpha
self.alpha = 1.0
self.layer.animateAlpha(from: alpha, to: 1.0, duration: 0.25)
super.touchesCancelled(touches, with: event)
}
func update(theme: PresentationTheme, title: String, useOpaqueTheme: Bool) -> CGSize {
let textConstrainedWidth: CGFloat = 100.0
let color = theme.list.itemCheckColors.foregroundColor
if useOpaqueTheme {
self.backgroundLayer.backgroundColor = theme.chat.inputMediaPanel.panelContentControlVibrantOverlayColor.cgColor
} else {
self.backgroundLayer.backgroundColor = theme.chat.inputMediaPanel.panelContentControlOpaqueOverlayColor.cgColor
}
self.tintContainerLayer.backgroundColor = UIColor.white.cgColor
let textSize: CGSize
if let currentTextLayout = self.currentTextLayout, currentTextLayout.string == title, currentTextLayout.color == color, currentTextLayout.constrainedWidth == textConstrainedWidth {
textSize = currentTextLayout.size
} else {
let font: UIFont = Font.semibold(13.0)
let string = NSAttributedString(string: title.uppercased(), font: font, textColor: color)
let stringBounds = string.boundingRect(with: CGSize(width: textConstrainedWidth, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
textSize = CGSize(width: ceil(stringBounds.width), height: ceil(stringBounds.height))
self.textLayer.contents = generateImage(textSize, opaque: false, scale: 0.0, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
string.draw(in: stringBounds)
UIGraphicsPopContext()
})?.cgImage
self.currentTextLayout = (title, color, textConstrainedWidth, textSize)
}
var sideInset: CGFloat = 10.0
if textSize.width > 24.0 {
sideInset = 6.0
}
let size = CGSize(width: textSize.width + sideInset * 2.0, height: 28.0)
let textFrame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: floor((size.height - textSize.height) / 2.0)), size: textSize)
self.textLayer.frame = textFrame
self.backgroundLayer.frame = CGRect(origin: CGPoint(), size: size)
self.tintBackgroundLayer.frame = CGRect(origin: CGPoint(), size: size)
self.backgroundLayer.cornerRadius = min(size.width, size.height) / 2.0
self.tintContainerLayer.cornerRadius = min(size.width, size.height) / 2.0
return size
}
}
public protocol EmojiContentPeekBehavior: AnyObject {
func setGestureRecognizerEnabled(view: UIView, isEnabled: Bool, itemAtPoint: @escaping (CGPoint) -> (AnyHashable, EmojiPagerContentComponent.View.ItemLayer, TelegramMediaFile)?)
}
public final class EmojiPagerContentComponent: Component {
public typealias EnvironmentType = (EntityKeyboardChildEnvironment, PagerComponentChildEnvironment)
public final class ContentAnimation {
public enum AnimationType {
case generic
case groupExpanded(id: AnyHashable)
case groupInstalled(id: AnyHashable)
}
public let type: AnimationType
public init(type: AnimationType) {
self.type = type
}
}
public final class SynchronousLoadBehavior {
public let isDisabled: Bool
public init(isDisabled: Bool) {
self.isDisabled = isDisabled
}
}
public struct CustomLayout: Equatable {
public var itemsPerRow: Int
public var itemSize: CGFloat
public var sideInset: CGFloat
public var itemSpacing: CGFloat
public init(
itemsPerRow: Int,
itemSize: CGFloat,
sideInset: CGFloat,
itemSpacing: CGFloat
) {
self.itemsPerRow = itemsPerRow
self.itemSize = itemSize
self.sideInset = sideInset
self.itemSpacing = itemSpacing
}
}
public final class ExternalBackground {
public let effectContainerView: UIView?
public init(
effectContainerView: UIView?
) {
self.effectContainerView = effectContainerView
}
}
public final class InputInteractionHolder {
public var inputInteraction: InputInteraction?
public init() {
}
}
public final class InputInteraction {
public let performItemAction: (AnyHashable, Item, UIView, CGRect, CALayer, Bool) -> Void
public let deleteBackwards: () -> Void
public let openStickerSettings: () -> Void
public let openFeatured: () -> Void
public let addGroupAction: (AnyHashable, Bool) -> Void
public let clearGroup: (AnyHashable) -> Void
public let pushController: (ViewController) -> Void
public let presentController: (ViewController) -> Void
public let presentGlobalOverlayController: (ViewController) -> Void
public let navigationController: () -> NavigationController?
public let sendSticker: ((FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?, [ItemCollectionId]) -> Void)?
public let chatPeerId: PeerId?
public let peekBehavior: EmojiContentPeekBehavior?
public let customLayout: CustomLayout?
public let externalBackground: ExternalBackground?
public let useOpaqueTheme: Bool
public init(
performItemAction: @escaping (AnyHashable, Item, UIView, CGRect, CALayer, Bool) -> Void,
deleteBackwards: @escaping () -> Void,
openStickerSettings: @escaping () -> Void,
openFeatured: @escaping () -> Void,
addGroupAction: @escaping (AnyHashable, Bool) -> Void,
clearGroup: @escaping (AnyHashable) -> Void,
pushController: @escaping (ViewController) -> Void,
presentController: @escaping (ViewController) -> Void,
presentGlobalOverlayController: @escaping (ViewController) -> Void,
navigationController: @escaping () -> NavigationController?,
sendSticker: ((FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?, [ItemCollectionId]) -> Void)?,
chatPeerId: PeerId?,
peekBehavior: EmojiContentPeekBehavior?,
customLayout: CustomLayout?,
externalBackground: ExternalBackground?,
useOpaqueTheme: Bool
) {
self.performItemAction = performItemAction
self.deleteBackwards = deleteBackwards
self.openStickerSettings = openStickerSettings
self.openFeatured = openFeatured
self.addGroupAction = addGroupAction
self.clearGroup = clearGroup
self.pushController = pushController
self.presentController = presentController
self.presentGlobalOverlayController = presentGlobalOverlayController
self.navigationController = navigationController
self.sendSticker = sendSticker
self.chatPeerId = chatPeerId
self.peekBehavior = peekBehavior
self.customLayout = customLayout
self.externalBackground = externalBackground
self.useOpaqueTheme = useOpaqueTheme
}
}
public enum StaticEmojiSegment: Int32, CaseIterable {
case people = 0
case animalsAndNature = 1
case foodAndDrink = 2
case activityAndSport = 3
case travelAndPlaces = 4
case objects = 5
case symbols = 6
case flags = 7
}
public enum ItemContent: Equatable {
public enum Id: Hashable {
case animation(EntityKeyboardAnimationData.Id)
case staticEmoji(String)
case icon(Icon)
}
public enum Icon: Equatable {
case premiumStar
}
case animation(EntityKeyboardAnimationData)
case staticEmoji(String)
case icon(Icon)
public var id: Id {
switch self {
case let .animation(animation):
return .animation(animation.id)
case let .staticEmoji(value):
return .staticEmoji(value)
case let .icon(icon):
return .icon(icon)
}
}
}
public final class Item: Equatable {
public enum Icon: Equatable {
case none
case locked
case premium
}
public let animationData: EntityKeyboardAnimationData?
public let content: ItemContent
public let itemFile: TelegramMediaFile?
public let subgroupId: Int32?
public let icon: Icon
public let accentTint: Bool
public init(
animationData: EntityKeyboardAnimationData?,
content: ItemContent,
itemFile: TelegramMediaFile?,
subgroupId: Int32?,
icon: Icon,
accentTint: Bool
) {
self.animationData = animationData
self.content = content
self.itemFile = itemFile
self.subgroupId = subgroupId
self.icon = icon
self.accentTint = accentTint
}
public static func ==(lhs: Item, rhs: Item) -> Bool {
if lhs === rhs {
return true
}
if lhs.animationData?.resource.resource.id != rhs.animationData?.resource.resource.id {
return false
}
if lhs.content != rhs.content {
return false
}
if lhs.itemFile?.fileId != rhs.itemFile?.fileId {
return false
}
if lhs.subgroupId != rhs.subgroupId {
return false
}
if lhs.icon != rhs.icon {
return false
}
if lhs.accentTint != rhs.accentTint {
return false
}
return true
}
}
public final class ItemGroup: Equatable {
public let supergroupId: AnyHashable
public let groupId: AnyHashable
public let title: String?
public let subtitle: String?
public let actionButtonTitle: String?
public let isFeatured: Bool
public let isPremiumLocked: Bool
public let isEmbedded: Bool
public let hasClear: Bool
public let collapsedLineCount: Int?
public let displayPremiumBadges: Bool
public let headerItem: EntityKeyboardAnimationData?
public let items: [Item]
public init(
supergroupId: AnyHashable,
groupId: AnyHashable,
title: String?,
subtitle: String?,
actionButtonTitle: String?,
isFeatured: Bool,
isPremiumLocked: Bool,
isEmbedded: Bool,
hasClear: Bool,
collapsedLineCount: Int?,
displayPremiumBadges: Bool,
headerItem: EntityKeyboardAnimationData?,
items: [Item]
) {
self.supergroupId = supergroupId
self.groupId = groupId
self.title = title
self.subtitle = subtitle
self.actionButtonTitle = actionButtonTitle
self.isFeatured = isFeatured
self.isPremiumLocked = isPremiumLocked
self.isEmbedded = isEmbedded
self.hasClear = hasClear
self.collapsedLineCount = collapsedLineCount
self.displayPremiumBadges = displayPremiumBadges
self.headerItem = headerItem
self.items = items
}
public static func ==(lhs: ItemGroup, rhs: ItemGroup) -> Bool {
if lhs === rhs {
return true
}
if lhs.supergroupId != rhs.supergroupId {
return false
}
if lhs.groupId != rhs.groupId {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.subtitle != rhs.subtitle {
return false
}
if lhs.actionButtonTitle != rhs.actionButtonTitle {
return false
}
if lhs.isFeatured != rhs.isFeatured {
return false
}
if lhs.isPremiumLocked != rhs.isPremiumLocked {
return false
}
if lhs.isEmbedded != rhs.isEmbedded {
return false
}
if lhs.hasClear != rhs.hasClear {
return false
}
if lhs.collapsedLineCount != rhs.collapsedLineCount {
return false
}
if lhs.displayPremiumBadges != rhs.displayPremiumBadges {
return false
}
if lhs.headerItem != rhs.headerItem {
return false
}
if lhs.items != rhs.items {
return false
}
return true
}
}
public enum ItemLayoutType {
case compact
case detailed
}
public let id: AnyHashable
public let context: AccountContext
public let avatarPeer: EnginePeer?
public let animationCache: AnimationCache
public let animationRenderer: MultiAnimationRenderer
public let inputInteractionHolder: InputInteractionHolder
public let itemGroups: [ItemGroup]
public let itemLayoutType: ItemLayoutType
public let warpContentsOnEdges: Bool
public let enableLongPress: Bool
public let selectedItems: Set<MediaId>
public init(
id: AnyHashable,
context: AccountContext,
avatarPeer: EnginePeer?,
animationCache: AnimationCache,
animationRenderer: MultiAnimationRenderer,
inputInteractionHolder: InputInteractionHolder,
itemGroups: [ItemGroup],
itemLayoutType: ItemLayoutType,
warpContentsOnEdges: Bool,
enableLongPress: Bool,
selectedItems: Set<MediaId>
) {
self.id = id
self.context = context
self.avatarPeer = avatarPeer
self.animationCache = animationCache
self.animationRenderer = animationRenderer
self.inputInteractionHolder = inputInteractionHolder
self.itemGroups = itemGroups
self.itemLayoutType = itemLayoutType
self.warpContentsOnEdges = warpContentsOnEdges
self.enableLongPress = enableLongPress
self.selectedItems = selectedItems
}
public func withUpdatedItemGroups(_ itemGroups: [ItemGroup]) -> EmojiPagerContentComponent {
return EmojiPagerContentComponent(
id: self.id,
context: self.context,
avatarPeer: self.avatarPeer,
animationCache: self.animationCache,
animationRenderer: self.animationRenderer,
inputInteractionHolder: self.inputInteractionHolder,
itemGroups: itemGroups,
itemLayoutType: self.itemLayoutType,
warpContentsOnEdges: self.warpContentsOnEdges,
enableLongPress: self.enableLongPress,
selectedItems: self.selectedItems
)
}
public static func ==(lhs: EmojiPagerContentComponent, rhs: EmojiPagerContentComponent) -> Bool {
if lhs === rhs {
return true
}
if lhs.id != rhs.id {
return false
}
if lhs.context !== rhs.context {
return false
}
if lhs.avatarPeer != rhs.avatarPeer {
return false
}
if lhs.animationCache !== rhs.animationCache {
return false
}
if lhs.animationRenderer !== rhs.animationRenderer {
return false
}
if lhs.inputInteractionHolder !== rhs.inputInteractionHolder {
return false
}
if lhs.itemGroups != rhs.itemGroups {
return false
}
if lhs.itemLayoutType != rhs.itemLayoutType {
return false
}
if lhs.warpContentsOnEdges != rhs.warpContentsOnEdges {
return false
}
if lhs.enableLongPress != rhs.enableLongPress {
return false
}
if lhs.selectedItems != rhs.selectedItems {
return false
}
return true
}
public final class Tag {
public let id: AnyHashable
public init(id: AnyHashable) {
self.id = id
}
}
public final class View: UIView, UIScrollViewDelegate, PagerContentViewWithBackground, ComponentTaggedView {
private struct ItemGroupDescription: Equatable {
let supergroupId: AnyHashable
let groupId: AnyHashable
let hasTitle: Bool
let isPremiumLocked: Bool
let isFeatured: Bool
let itemCount: Int
let isEmbedded: Bool
let collapsedLineCount: Int?
}
private struct ItemGroupLayout: Equatable {
let frame: CGRect
let supergroupId: AnyHashable
let groupId: AnyHashable
let headerHeight: CGFloat
let itemTopOffset: CGFloat
let itemCount: Int
let collapsedItemIndex: Int?
let collapsedItemText: String?
}
private struct ItemLayout: Equatable {
var layoutType: ItemLayoutType
var width: CGFloat
var headerInsets: UIEdgeInsets
var itemInsets: UIEdgeInsets
var curveNearBounds: Bool
var itemGroupLayouts: [ItemGroupLayout]
var itemDefaultHeaderHeight: CGFloat
var itemFeaturedHeaderHeight: CGFloat
var nativeItemSize: CGFloat
let visibleItemSize: CGFloat
let playbackItemSize: CGFloat
var horizontalSpacing: CGFloat
var verticalSpacing: CGFloat
var verticalGroupDefaultSpacing: CGFloat
var verticalGroupFeaturedSpacing: CGFloat
var itemsPerRow: Int
var contentSize: CGSize
var premiumButtonInset: CGFloat
var premiumButtonHeight: CGFloat
init(layoutType: ItemLayoutType, width: CGFloat, containerInsets: UIEdgeInsets, itemGroups: [ItemGroupDescription], expandedGroupIds: Set<AnyHashable>, curveNearBounds: Bool, customLayout: CustomLayout?) {
self.layoutType = layoutType
self.width = width
self.premiumButtonInset = 6.0
self.premiumButtonHeight = 50.0
self.curveNearBounds = curveNearBounds
let minItemsPerRow: Int
let minSpacing: CGFloat
let itemInsets: UIEdgeInsets
switch layoutType {
case .compact:
minItemsPerRow = 8
self.nativeItemSize = 40.0
self.playbackItemSize = 48.0
self.verticalSpacing = 9.0
if width >= 420.0 {
itemInsets = UIEdgeInsets(top: containerInsets.top, left: containerInsets.left + 5.0, bottom: containerInsets.bottom, right: containerInsets.right + 5.0)
minSpacing = 2.0
} else {
itemInsets = UIEdgeInsets(top: containerInsets.top, left: containerInsets.left + 7.0, bottom: containerInsets.bottom, right: containerInsets.right + 7.0)
minSpacing = 9.0
}
self.headerInsets = UIEdgeInsets(top: containerInsets.top, left: containerInsets.left + 16.0, bottom: containerInsets.bottom, right: containerInsets.right + 16.0)
self.itemDefaultHeaderHeight = 24.0
self.itemFeaturedHeaderHeight = self.itemDefaultHeaderHeight
case .detailed:
minItemsPerRow = 5
self.nativeItemSize = 70.0
self.playbackItemSize = 96.0
self.verticalSpacing = 2.0
minSpacing = 12.0
self.itemDefaultHeaderHeight = 24.0
self.itemFeaturedHeaderHeight = 60.0
itemInsets = UIEdgeInsets(top: containerInsets.top, left: containerInsets.left + 10.0, bottom: containerInsets.bottom, right: containerInsets.right + 10.0)
self.headerInsets = UIEdgeInsets(top: containerInsets.top, left: containerInsets.left + 16.0, bottom: containerInsets.bottom, right: containerInsets.right + 16.0)
}
self.verticalGroupDefaultSpacing = 18.0
self.verticalGroupFeaturedSpacing = 15.0
if let customLayout = customLayout {
self.itemsPerRow = customLayout.itemsPerRow
self.nativeItemSize = customLayout.itemSize
self.visibleItemSize = customLayout.itemSize
self.verticalSpacing = 9.0
self.itemInsets = UIEdgeInsets(top: containerInsets.top, left: containerInsets.left + customLayout.sideInset, bottom: containerInsets.bottom, right: containerInsets.right + customLayout.sideInset)
self.horizontalSpacing = customLayout.itemSpacing
} else {
self.itemInsets = itemInsets
let itemHorizontalSpace = width - self.itemInsets.left - self.itemInsets.right
self.itemsPerRow = max(minItemsPerRow, Int((itemHorizontalSpace + minSpacing) / (self.nativeItemSize + minSpacing)))
let proposedItemSize = floor((itemHorizontalSpace - minSpacing * (CGFloat(self.itemsPerRow) - 1.0)) / CGFloat(self.itemsPerRow))
self.visibleItemSize = proposedItemSize < self.nativeItemSize ? proposedItemSize : self.nativeItemSize
self.horizontalSpacing = floorToScreenPixels((itemHorizontalSpace - self.visibleItemSize * CGFloat(self.itemsPerRow)) / CGFloat(self.itemsPerRow - 1))
}
let actualContentWidth = self.visibleItemSize * CGFloat(self.itemsPerRow) + self.horizontalSpacing * CGFloat(self.itemsPerRow - 1)
self.itemInsets.left = floorToScreenPixels((width - actualContentWidth) / 2.0)
self.itemInsets.right = self.itemInsets.left
var verticalGroupOrigin: CGFloat = self.itemInsets.top
self.itemGroupLayouts = []
for itemGroup in itemGroups {
var itemTopOffset: CGFloat = 0.0
var headerHeight: CGFloat = 0.0
var groupSpacing = self.verticalGroupDefaultSpacing
if itemGroup.hasTitle {
if itemGroup.isFeatured {
headerHeight = self.itemFeaturedHeaderHeight
groupSpacing = self.verticalGroupFeaturedSpacing
} else {
headerHeight = self.itemDefaultHeaderHeight
}
}
if itemGroup.isEmbedded {
headerHeight += 32.0
groupSpacing -= 4.0
}
itemTopOffset += headerHeight
var numRowsInGroup: Int
if itemGroup.isEmbedded {
numRowsInGroup = 0
} else {
numRowsInGroup = (itemGroup.itemCount + (self.itemsPerRow - 1)) / self.itemsPerRow
}
var collapsedItemIndex: Int?
var collapsedItemText: String?
let visibleItemCount: Int
if itemGroup.isEmbedded {
visibleItemCount = 0
} else if let collapsedLineCount = itemGroup.collapsedLineCount, !expandedGroupIds.contains(itemGroup.groupId) {
let maxLines: Int = collapsedLineCount
if numRowsInGroup > maxLines {
visibleItemCount = self.itemsPerRow * maxLines - 1
collapsedItemIndex = visibleItemCount
collapsedItemText = "+\(itemGroup.itemCount - visibleItemCount)"
} else {
visibleItemCount = itemGroup.itemCount
}
} else {
visibleItemCount = itemGroup.itemCount
}
if !itemGroup.isEmbedded {
numRowsInGroup = (visibleItemCount + (self.itemsPerRow - 1)) / self.itemsPerRow
}
var groupContentSize = CGSize(width: width, height: itemTopOffset + CGFloat(numRowsInGroup) * self.visibleItemSize + CGFloat(max(0, numRowsInGroup - 1)) * self.verticalSpacing)
if (itemGroup.isPremiumLocked || itemGroup.isFeatured), case .compact = layoutType {
groupContentSize.height += self.premiumButtonInset + self.premiumButtonHeight
}
self.itemGroupLayouts.append(ItemGroupLayout(
frame: CGRect(origin: CGPoint(x: 0.0, y: verticalGroupOrigin), size: groupContentSize),
supergroupId: itemGroup.supergroupId,
groupId: itemGroup.groupId,
headerHeight: headerHeight,
itemTopOffset: itemTopOffset,
itemCount: visibleItemCount,
collapsedItemIndex: collapsedItemIndex,
collapsedItemText: collapsedItemText
))
verticalGroupOrigin += groupContentSize.height + groupSpacing
}
verticalGroupOrigin += self.itemInsets.bottom
self.contentSize = CGSize(width: width, height: verticalGroupOrigin)
}
func frame(groupIndex: Int, itemIndex: Int) -> CGRect {
let groupLayout = self.itemGroupLayouts[groupIndex]
let row = itemIndex / self.itemsPerRow
let column = itemIndex % self.itemsPerRow
return CGRect(
origin: CGPoint(
x: self.itemInsets.left + CGFloat(column) * (self.visibleItemSize + self.horizontalSpacing),
y: groupLayout.frame.minY + groupLayout.itemTopOffset + CGFloat(row) * (self.visibleItemSize + self.verticalSpacing)
),
size: CGSize(
width: self.visibleItemSize,
height: self.visibleItemSize
)
)
}
func visibleItems(for rect: CGRect) -> [(supergroupId: AnyHashable, groupId: AnyHashable, groupIndex: Int, groupItems: Range<Int>?)] {
var result: [(supergroupId: AnyHashable, groupId: AnyHashable, groupIndex: Int, groupItems: Range<Int>?)] = []
for groupIndex in 0 ..< self.itemGroupLayouts.count {
let group = self.itemGroupLayouts[groupIndex]
if !rect.intersects(group.frame) {
continue
}
let offsetRect = rect.offsetBy(dx: -self.itemInsets.left, dy: -group.frame.minY - group.itemTopOffset)
var minVisibleRow = Int(floor((offsetRect.minY - self.verticalSpacing) / (self.visibleItemSize + self.verticalSpacing)))
minVisibleRow = max(0, minVisibleRow)
let maxVisibleRow = Int(ceil((offsetRect.maxY - self.verticalSpacing) / (self.visibleItemSize + self.verticalSpacing)))
let minVisibleIndex = minVisibleRow * self.itemsPerRow
let maxVisibleIndex = min(group.itemCount - 1, (maxVisibleRow + 1) * self.itemsPerRow - 1)
result.append((
supergroupId: group.supergroupId,
groupId: group.groupId,
groupIndex: groupIndex,
groupItems: maxVisibleIndex >= minVisibleIndex ? (minVisibleIndex ..< (maxVisibleIndex + 1)) : nil
))
}
return result
}
}
public final class ItemPlaceholderView: UIView {
private let shimmerView: PortalSourceView?
private var placeholderView: PortalView?
private let placeholderMaskLayer: SimpleLayer
private var placeholderImageView: UIImageView?
public init(
context: AccountContext,
dimensions: CGSize?,
immediateThumbnailData: Data?,
shimmerView: PortalSourceView?,
color: UIColor,
size: CGSize
) {
self.shimmerView = shimmerView
self.placeholderMaskLayer = SimpleLayer()
super.init(frame: CGRect())
if let shimmerView = self.shimmerView, let placeholderView = PortalView() {
self.placeholderView = placeholderView
placeholderView.view.clipsToBounds = true
placeholderView.view.layer.mask = self.placeholderMaskLayer
self.addSubview(placeholderView.view)
shimmerView.addPortal(view: placeholderView)
}
let useDirectContent = self.placeholderView == nil
Queue.concurrentDefaultQueue().async { [weak self] in
if let image = generateStickerPlaceholderImage(data: immediateThumbnailData, size: size, scale: min(2.0, UIScreenScale), imageSize: dimensions ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: useDirectContent ? color : .black) {
Queue.mainQueue().async {
guard let strongSelf = self else {
return
}
if useDirectContent {
strongSelf.layer.contents = image.cgImage
} else {
strongSelf.placeholderMaskLayer.contents = image.cgImage
}
}
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func update(size: CGSize) {
if let placeholderView = self.placeholderView {
placeholderView.view.frame = CGRect(origin: CGPoint(), size: size)
}
self.placeholderMaskLayer.frame = CGRect(origin: CGPoint(), size: size)
}
}
public final class ItemLayer: MultiAnimationRenderTarget {
public struct Key: Hashable {
var groupId: AnyHashable
var itemId: ItemContent.Id
public init(
groupId: AnyHashable,
itemId: ItemContent.Id
) {
self.groupId = groupId
self.itemId = itemId
}
}
enum Badge {
case premium
case locked
case featured
}
public let item: Item
private let content: ItemContent
private let placeholderColor: UIColor
let pixelSize: CGSize
private let size: CGSize
private var disposable: Disposable?
private var fetchDisposable: Disposable?
private var premiumBadgeView: PremiumBadgeView?
private var badge: Badge?
private var validSize: CGSize?
private var isInHierarchyValue: Bool = false
public var isVisibleForAnimations: Bool = false {
didSet {
if self.isVisibleForAnimations != oldValue {
self.updatePlayback()
}
}
}
public private(set) var displayPlaceholder: Bool = false
public let onUpdateDisplayPlaceholder: (Bool, Double) -> Void
public init(
item: Item,
context: AccountContext,
attemptSynchronousLoad: Bool,
content: ItemContent,
cache: AnimationCache,
renderer: MultiAnimationRenderer,
placeholderColor: UIColor,
blurredBadgeColor: UIColor,
accentIconColor: UIColor,
pointSize: CGSize,
onUpdateDisplayPlaceholder: @escaping (Bool, Double) -> Void
) {
self.item = item
self.content = content
self.placeholderColor = placeholderColor
self.onUpdateDisplayPlaceholder = onUpdateDisplayPlaceholder
let scale = min(2.0, UIScreenScale)
let pixelSize = CGSize(width: pointSize.width * scale, height: pointSize.height * scale)
self.pixelSize = pixelSize
self.size = CGSize(width: pixelSize.width / scale, height: pixelSize.height / scale)
super.init()
switch content {
case let .animation(animationData):
let loadAnimation: () -> Void = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.disposable = renderer.add(target: strongSelf, cache: cache, itemId: animationData.resource.resource.id.stringRepresentation, unique: false, size: pixelSize, fetch: animationCacheFetchFile(context: context, resource: animationData.resource, type: animationData.type.animationCacheAnimationType, keyframeOnly: pixelSize.width >= 120.0))
}
if attemptSynchronousLoad {
if !renderer.loadFirstFrameSynchronously(target: self, cache: cache, itemId: animationData.resource.resource.id.stringRepresentation, size: pixelSize) {
self.updateDisplayPlaceholder(displayPlaceholder: true)
self.fetchDisposable = renderer.loadFirstFrame(target: self, cache: cache, itemId: animationData.resource.resource.id.stringRepresentation, size: pixelSize, fetch: animationCacheFetchFile(context: context, resource: animationData.resource, type: animationData.type.animationCacheAnimationType, keyframeOnly: true), completion: { [weak self] success, isFinal in
if !isFinal {
if !success {
Queue.mainQueue().async {
guard let strongSelf = self else {
return
}
strongSelf.updateDisplayPlaceholder(displayPlaceholder: true)
}
}
return
}
Queue.mainQueue().async {
loadAnimation()
if !success {
guard let strongSelf = self else {
return
}
strongSelf.updateDisplayPlaceholder(displayPlaceholder: true)
}
}
})
} else {
loadAnimation()
}
} else {
self.fetchDisposable = renderer.loadFirstFrame(target: self, cache: cache, itemId: animationData.resource.resource.id.stringRepresentation, size: pixelSize, fetch: animationCacheFetchFile(context: context, resource: animationData.resource, type: animationData.type.animationCacheAnimationType, keyframeOnly: true), completion: { [weak self] success, isFinal in
if !isFinal {
if !success {
Queue.mainQueue().async {
guard let strongSelf = self else {
return
}
strongSelf.updateDisplayPlaceholder(displayPlaceholder: true)
}
}
return
}
Queue.mainQueue().async {
loadAnimation()
if !success {
guard let strongSelf = self else {
return
}
strongSelf.updateDisplayPlaceholder(displayPlaceholder: true)
}
}
})
}
case let .staticEmoji(staticEmoji):
let image = generateImage(pointSize, opaque: false, scale: min(UIScreenScale, 3.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
let preScaleFactor: CGFloat = 1.0
let scaledSize = CGSize(width: floor(size.width * preScaleFactor), height: floor(size.height * preScaleFactor))
let scaleFactor = scaledSize.width / size.width
context.scaleBy(x: 1.0 / scaleFactor, y: 1.0 / scaleFactor)
let string = NSAttributedString(string: staticEmoji, font: Font.regular(floor(32.0 * scaleFactor)), textColor: .black)
let boundingRect = string.boundingRect(with: scaledSize, options: .usesLineFragmentOrigin, context: nil)
UIGraphicsPushContext(context)
string.draw(at: CGPoint(x: floor((scaledSize.width - boundingRect.width) / 2.0 + boundingRect.minX), y: floor((scaledSize.height - boundingRect.height) / 2.0 + boundingRect.minY)))
UIGraphicsPopContext()
})
self.contents = image?.cgImage
case let .icon(icon):
let image = generateImage(pointSize, opaque: false, scale: min(UIScreenScale, 3.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
switch icon {
case .premiumStar:
if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: accentIconColor) {
let imageSize = image.size.aspectFitted(CGSize(width: size.width - 6.0, height: size.height - 6.0))
image.draw(in: CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.height - imageSize.height) / 2.0)), size: imageSize))
}
}
UIGraphicsPopContext()
})
self.contents = image?.cgImage
}
}
override public init(layer: Any) {
guard let layer = layer as? ItemLayer else {
preconditionFailure()
}
self.item = layer.item
self.content = layer.content
self.placeholderColor = layer.placeholderColor
self.size = layer.size
self.pixelSize = layer.pixelSize
self.onUpdateDisplayPlaceholder = { _, _ in }
super.init(layer: layer)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.disposable?.dispose()
self.fetchDisposable?.dispose()
}
public override func action(forKey event: String) -> CAAction? {
if event == kCAOnOrderIn {
self.isInHierarchyValue = true
} else if event == kCAOnOrderOut {
self.isInHierarchyValue = false
}
self.updatePlayback()
return nullAction
}
func update(transition: Transition, size: CGSize, badge: Badge?, blurredBadgeColor: UIColor, blurredBadgeBackgroundColor: UIColor) {
if self.badge != badge || self.validSize != size {
self.badge = badge
self.validSize = size
if let badge = badge {
var badgeTransition = transition
let premiumBadgeView: PremiumBadgeView
if let current = self.premiumBadgeView {
premiumBadgeView = current
} else {
badgeTransition = .immediate
premiumBadgeView = PremiumBadgeView()
self.premiumBadgeView = premiumBadgeView
self.addSublayer(premiumBadgeView.layer)
}
let badgeDiameter = min(16.0, floor(size.height * 0.5))
let badgeSize = CGSize(width: badgeDiameter, height: badgeDiameter)
badgeTransition.setFrame(view: premiumBadgeView, frame: CGRect(origin: CGPoint(x: size.width - badgeSize.width, y: size.height - badgeSize.height), size: badgeSize))
premiumBadgeView.update(transition: badgeTransition, badge: badge, backgroundColor: blurredBadgeColor, size: badgeSize)
self.blurredRepresentationBackgroundColor = blurredBadgeBackgroundColor
self.blurredRepresentationTarget = premiumBadgeView.contentLayer
} else {
if let premiumBadgeView = self.premiumBadgeView {
self.premiumBadgeView = nil
premiumBadgeView.removeFromSuperview()
self.blurredRepresentationBackgroundColor = nil
self.blurredRepresentationTarget = nil
}
}
}
}
private func updatePlayback() {
let shouldBePlaying = self.isInHierarchyValue && self.isVisibleForAnimations
self.shouldBeAnimating = shouldBePlaying
}
public override func updateDisplayPlaceholder(displayPlaceholder: Bool) {
if self.displayPlaceholder == displayPlaceholder {
return
}
self.displayPlaceholder = displayPlaceholder
self.onUpdateDisplayPlaceholder(displayPlaceholder, 0.0)
}
public override func transitionToContents(_ contents: AnyObject, didLoop: Bool) {
self.contents = contents
if self.displayPlaceholder {
self.displayPlaceholder = false
self.onUpdateDisplayPlaceholder(false, 0.2)
self.animateAlpha(from: 0.0, to: 1.0, duration: 0.18)
}
}
}
private final class GroupBorderLayer: PassthroughShapeLayer {
let tintContainerLayer: CAShapeLayer
override init() {
self.tintContainerLayer = CAShapeLayer()
super.init()
self.mirrorLayer = self.tintContainerLayer
}
override func action(forKey event: String) -> CAAction? {
return nullAction
}
override init(layer: Any) {
self.tintContainerLayer = CAShapeLayer()
super.init(layer: layer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private final class ItemSelectionLayer: PassthroughLayer {
let tintContainerLayer: SimpleLayer
override init() {
self.tintContainerLayer = SimpleLayer()
super.init()
self.mirrorLayer = self.tintContainerLayer
}
override func action(forKey event: String) -> CAAction? {
return nullAction
}
override init(layer: Any) {
self.tintContainerLayer = SimpleLayer()
super.init(layer: layer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private final class ContentScrollLayer: CALayer {
var mirrorLayer: CALayer?
override init() {
super.init()
}
override init(layer: Any) {
super.init(layer: layer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var position: CGPoint {
get {
return super.position
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.position = value
}
super.position = value
}
}
override var bounds: CGRect {
get {
return super.bounds
} set(value) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.bounds = value
}
super.bounds = value
}
}
override func add(_ animation: CAAnimation, forKey key: String?) {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.add(animation, forKey: key)
}
super.add(animation, forKey: key)
}
override func removeAllAnimations() {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.removeAllAnimations()
}
super.removeAllAnimations()
}
override 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
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private enum VisualItemKey: Hashable {
case item(id: ItemLayer.Key)
case header(groupId: AnyHashable)
case groupExpandButton(groupId: AnyHashable)
case groupActionButton(groupId: AnyHashable)
}
private let shimmerHostView: PortalSourceView?
private let standaloneShimmerEffect: StandaloneShimmerEffect?
private let backgroundView: BlurredBackgroundView
private var vibrancyEffectView: UIVisualEffectView?
public private(set) var mirrorContentClippingView: UIView?
private let mirrorContentScrollView: UIView
private var warpView: WarpView?
private var mirrorContentWarpView: WarpView?
private let scrollView: ContentScrollView
private var scrollGradientLayer: SimpleGradientLayer?
private let boundsChangeTrackerLayer = SimpleLayer()
private var effectiveVisibleSize: CGSize = CGSize()
private let placeholdersContainerView: UIView
private var visibleItemPlaceholderViews: [ItemLayer.Key: ItemPlaceholderView] = [:]
private var visibleItemSelectionLayers: [ItemLayer.Key: ItemSelectionLayer] = [:]
private var visibleItemLayers: [ItemLayer.Key: ItemLayer] = [:]
private var visibleGroupHeaders: [AnyHashable: GroupHeaderLayer] = [:]
private var visibleGroupBorders: [AnyHashable: GroupBorderLayer] = [:]
private var visibleGroupPremiumButtons: [AnyHashable: ComponentView<Empty>] = [:]
private var visibleGroupExpandActionButtons: [AnyHashable: GroupExpandActionButton] = [:]
private var expandedGroupIds: Set<AnyHashable> = Set()
private var ignoreScrolling: Bool = false
private var keepTopPanelVisibleUntilScrollingInput: Bool = false
private var component: EmojiPagerContentComponent?
private weak var state: EmptyComponentState?
private var pagerEnvironment: PagerComponentChildEnvironment?
private var keyboardChildEnvironment: EntityKeyboardChildEnvironment?
private var activeItemUpdated: ActionSlot<(AnyHashable, AnyHashable?, Transition)>?
private var itemLayout: ItemLayout?
private var longTapRecognizer: UILongPressGestureRecognizer?
override init(frame: CGRect) {
self.backgroundView = BlurredBackgroundView(color: nil)
if ProcessInfo.processInfo.processorCount > 2 {
self.shimmerHostView = PortalSourceView()
self.standaloneShimmerEffect = StandaloneShimmerEffect()
} else {
self.shimmerHostView = nil
self.standaloneShimmerEffect = nil
}
self.mirrorContentScrollView = UIView()
self.mirrorContentScrollView.layer.anchorPoint = CGPoint()
self.mirrorContentScrollView.clipsToBounds = false
self.scrollView = ContentScrollView(mirrorView: self.mirrorContentScrollView)
self.scrollView.layer.anchorPoint = CGPoint()
self.placeholdersContainerView = UIView()
super.init(frame: frame)
self.addSubview(self.backgroundView)
if let shimmerHostView = self.shimmerHostView {
shimmerHostView.alpha = 0.0
self.addSubview(shimmerHostView)
}
self.boundsChangeTrackerLayer.opacity = 0.0
self.layer.addSublayer(self.boundsChangeTrackerLayer)
self.boundsChangeTrackerLayer.didEnterHierarchy = { [weak self] in
self?.standaloneShimmerEffect?.updateLayer()
}
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.delegate = self
self.scrollView.clipsToBounds = false
self.scrollView.scrollsToTop = false
self.addSubview(self.scrollView)
self.scrollView.addSubview(self.placeholdersContainerView)
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
let longTapRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.longPressGesture(_:)))
longTapRecognizer.minimumPressDuration = 0.2
self.longTapRecognizer = longTapRecognizer
self.addGestureRecognizer(longTapRecognizer)
longTapRecognizer.isEnabled = false
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func updateIsWarpEnabled(isEnabled: Bool) {
if isEnabled {
if self.warpView == nil {
let warpView = WarpView(frame: CGRect())
self.warpView = warpView
self.insertSubview(warpView, aboveSubview: self.scrollView)
warpView.contentView.addSubview(self.scrollView)
}
if self.mirrorContentWarpView == nil {
let mirrorContentWarpView = WarpView(frame: CGRect())
self.mirrorContentWarpView = mirrorContentWarpView
mirrorContentWarpView.contentView.addSubview(self.mirrorContentScrollView)
}
} else {
if let warpView = self.warpView {
self.warpView = nil
self.insertSubview(self.scrollView, aboveSubview: warpView)
warpView.removeFromSuperview()
}
if let mirrorContentWarpView = self.mirrorContentWarpView {
self.mirrorContentWarpView = nil
if let mirrorContentClippingView = self.mirrorContentClippingView {
mirrorContentClippingView.addSubview(self.mirrorContentScrollView)
} else if let vibrancyEffectView = self.vibrancyEffectView {
vibrancyEffectView.contentView.addSubview(self.mirrorContentScrollView)
}
mirrorContentWarpView.removeFromSuperview()
}
}
}
public func matches(tag: Any) -> Bool {
if let tag = tag as? Tag {
if tag.id == self.component?.id {
return true
}
}
return false
}
public func animateIn(fromLocation: CGPoint) {
let scrollLocation = self.convert(fromLocation, to: self.scrollView)
for (key, itemLayer) in self.visibleItemLayers {
let distanceVector = CGPoint(x: scrollLocation.x - itemLayer.position.x, y: scrollLocation.y - itemLayer.position.y)
let distance = sqrt(distanceVector.x * distanceVector.x + distanceVector.y * distanceVector.y)
let distanceNorm = min(1.0, max(0.0, distance / self.bounds.width))
let delay = 0.05 + (distanceNorm) * 0.3
let t = itemLayer.transform
let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
itemLayer.animateScale(from: 0.01, to: currentScale, duration: 0.18, delay: delay, timingFunction: kCAMediaTimingFunctionSpring)
if let itemSelectionLayer = self.visibleItemSelectionLayers[key] {
itemSelectionLayer.animateScale(from: 0.01, to: 1.0, duration: 0.18, delay: delay, timingFunction: kCAMediaTimingFunctionSpring)
}
}
}
public func animateInReactionSelection(sourceItems: [MediaId: (frame: CGRect, frameIndex: Int, placeholder: UIImage)]) {
guard let component = self.component, let itemLayout = self.itemLayout else {
return
}
for (key, itemLayer) in self.visibleItemLayers {
guard case let .animation(animationData) = itemLayer.item.content else {
continue
}
guard let file = itemLayer.item.itemFile else {
continue
}
if let sourceItem = sourceItems[file.fileId] {
itemLayer.animatePosition(from: CGPoint(x: sourceItem.frame.center.x - itemLayer.position.x, y: 0.0), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
if let itemSelectionLayer = self.visibleItemSelectionLayers[key] {
itemSelectionLayer.animatePosition(from: CGPoint(x: sourceItem.frame.center.x - itemLayer.position.x, y: 0.0), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
itemSelectionLayer.animate(from: (min(sourceItem.frame.width, sourceItem.frame.height) * 0.5) as NSNumber, to: 8.0 as NSNumber, keyPath: "cornerRadius", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.3)
}
component.animationRenderer.setFrameIndex(itemId: animationData.resource.resource.id.stringRepresentation, size: itemLayer.pixelSize, frameIndex: sourceItem.frameIndex, placeholder: sourceItem.placeholder)
} else {
let distance = itemLayer.position.y - itemLayout.frame(groupIndex: 0, itemIndex: 0).midY
let maxDistance = self.bounds.height
let clippedDistance = max(0.0, min(distance, maxDistance))
let distanceNorm = clippedDistance / maxDistance
let delay = listViewAnimationCurveSystem(distanceNorm) * 0.1
itemLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: delay)
let t = itemLayer.transform
let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
itemLayer.animateSpring(from: 0.01 as NSNumber, to: currentScale as NSNumber, keyPath: "transform.scale", duration: 0.6, delay: delay)
if let itemSelectionLayer = self.visibleItemSelectionLayers[key] {
itemSelectionLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: delay)
itemSelectionLayer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.6, delay: delay)
}
}
}
for (_, groupHeader) in self.visibleGroupHeaders {
let distance = groupHeader.layer.position.y - itemLayout.frame(groupIndex: 0, itemIndex: 0).midY
let maxDistance = self.bounds.height
let clippedDistance = max(0.0, min(distance, maxDistance))
let distanceNorm = clippedDistance / maxDistance
let delay = listViewAnimationCurveSystem(distanceNorm) * 0.16
groupHeader.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: delay)
groupHeader.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, delay: delay)
groupHeader.tintContentLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: delay)
groupHeader.tintContentLayer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, delay: delay)
}
}
public func layerForItem(groupId: AnyHashable, item: EmojiPagerContentComponent.Item) -> CALayer? {
let itemKey = EmojiPagerContentComponent.View.ItemLayer.Key(groupId: groupId, itemId: item.content.id)
if let itemLayer = self.visibleItemLayers[itemKey] {
return itemLayer
} else {
return nil
}
}
public func scrollToItemGroup(id supergroupId: AnyHashable, subgroupId: Int32?) {
guard let component = self.component, let pagerEnvironment = self.pagerEnvironment, let itemLayout = self.itemLayout else {
return
}
for groupIndex in 0 ..< itemLayout.itemGroupLayouts.count {
let group = itemLayout.itemGroupLayouts[groupIndex]
var subgroupItemIndex: Int?
if group.supergroupId == supergroupId {
if let subgroupId = subgroupId {
inner: for itemGroup in component.itemGroups {
if itemGroup.supergroupId == supergroupId {
for i in 0 ..< itemGroup.items.count {
if itemGroup.items[i].subgroupId == subgroupId {
subgroupItemIndex = i
break
}
}
break inner
}
}
}
let wasIgnoringScrollingEvents = self.ignoreScrolling
self.ignoreScrolling = true
self.scrollView.setContentOffset(self.scrollView.contentOffset, animated: false)
self.keepTopPanelVisibleUntilScrollingInput = true
let anchorFrame: CGRect
if let subgroupItemIndex = subgroupItemIndex {
anchorFrame = itemLayout.frame(groupIndex: groupIndex, itemIndex: subgroupItemIndex)
} else {
anchorFrame = group.frame
}
var scrollPosition = anchorFrame.minY + floor(-itemLayout.verticalGroupDefaultSpacing / 2.0) - pagerEnvironment.containerInsets.top
if scrollPosition > self.scrollView.contentSize.height - self.scrollView.bounds.height {
scrollPosition = self.scrollView.contentSize.height - self.scrollView.bounds.height
}
if scrollPosition < 0.0 {
scrollPosition = 0.0
}
let offsetDirectionSign: Double = scrollPosition < self.scrollView.bounds.minY ? -1.0 : 1.0
var previousVisibleLayers: [ItemLayer.Key: (CALayer, CGRect)] = [:]
for (id, layer) in self.visibleItemLayers {
previousVisibleLayers[id] = (layer, layer.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY))
}
var previousVisibleItemSelectionLayers: [ItemLayer.Key: (CALayer, CGRect)] = [:]
for (id, layer) in self.visibleItemSelectionLayers {
previousVisibleItemSelectionLayers[id] = (layer, layer.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY))
}
var previousVisiblePlaceholderViews: [ItemLayer.Key: (UIView, CGRect)] = [:]
for (id, view) in self.visibleItemPlaceholderViews {
previousVisiblePlaceholderViews[id] = (view, view.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY))
}
var previousVisibleGroupHeaders: [AnyHashable: (GroupHeaderLayer, CGRect)] = [:]
for (id, view) in self.visibleGroupHeaders {
if !self.scrollView.bounds.intersects(view.frame) {
continue
}
previousVisibleGroupHeaders[id] = (view, view.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY))
}
var previousVisibleGroupBorders: [AnyHashable: (GroupBorderLayer, CGRect)] = [:]
for (id, layer) in self.visibleGroupBorders {
previousVisibleGroupBorders[id] = (layer, layer.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY))
}
var previousVisibleGroupPremiumButtons: [AnyHashable: (UIView, CGRect)] = [:]
for (id, view) in self.visibleGroupPremiumButtons {
if let view = view.view {
previousVisibleGroupPremiumButtons[id] = (view, view.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY))
}
}
var previousVisibleGroupExpandActionButtons: [AnyHashable: (GroupExpandActionButton, CGRect)] = [:]
for (id, view) in self.visibleGroupExpandActionButtons {
previousVisibleGroupExpandActionButtons[id] = (view, view.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY))
}
self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: scrollPosition), size: self.scrollView.bounds.size)
self.ignoreScrolling = wasIgnoringScrollingEvents
self.updateVisibleItems(transition: .immediate, attemptSynchronousLoads: true, previousItemPositions: nil, updatedItemPositions: nil)
var commonItemOffset: CGFloat?
var previousVisibleBoundingRect: CGRect?
for (id, layerAndFrame) in previousVisibleLayers {
if let layer = self.visibleItemLayers[id] {
if commonItemOffset == nil {
let visibleFrame = layer.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY)
commonItemOffset = layerAndFrame.1.minY - visibleFrame.minY
}
break
} else {
if let previousVisibleBoundingRectValue = previousVisibleBoundingRect {
previousVisibleBoundingRect = layerAndFrame.1.union(previousVisibleBoundingRectValue)
} else {
previousVisibleBoundingRect = layerAndFrame.1
}
}
}
for (id, viewAndFrame) in previousVisiblePlaceholderViews {
if let view = self.visibleItemPlaceholderViews[id] {
if commonItemOffset == nil {
let visibleFrame = view.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY)
commonItemOffset = viewAndFrame.1.minY - visibleFrame.minY
}
break
} else {
if let previousVisibleBoundingRectValue = previousVisibleBoundingRect {
previousVisibleBoundingRect = viewAndFrame.1.union(previousVisibleBoundingRectValue)
} else {
previousVisibleBoundingRect = viewAndFrame.1
}
}
}
for (id, layerAndFrame) in previousVisibleGroupHeaders {
if let view = self.visibleGroupHeaders[id] {
if commonItemOffset == nil, self.scrollView.bounds.intersects(view.frame) {
let visibleFrame = view.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY)
commonItemOffset = layerAndFrame.1.minY - visibleFrame.minY
}
break
} else {
if let previousVisibleBoundingRectValue = previousVisibleBoundingRect {
previousVisibleBoundingRect = layerAndFrame.1.union(previousVisibleBoundingRectValue)
} else {
previousVisibleBoundingRect = layerAndFrame.1
}
}
}
/*for (id, layerAndFrame) in previousVisibleGroupBorders {
if let layer = self.visibleGroupBorders[id] {
if commonItemOffset == nil, self.scrollView.bounds.intersects(layer.frame) {
let visibleFrame = layer.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY)
commonItemOffset = layerAndFrame.1.minY - visibleFrame.minY
}
break
} else {
if let previousVisibleBoundingRectValue = previousVisibleBoundingRect {
previousVisibleBoundingRect = layerAndFrame.1.union(previousVisibleBoundingRectValue)
} else {
previousVisibleBoundingRect = layerAndFrame.1
}
}
}*/
for (id, viewAndFrame) in previousVisibleGroupPremiumButtons {
if let view = self.visibleGroupPremiumButtons[id]?.view, self.scrollView.bounds.intersects(view.frame) {
if commonItemOffset == nil {
let visibleFrame = view.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY)
commonItemOffset = viewAndFrame.1.minY - visibleFrame.minY
}
break
} else {
if let previousVisibleBoundingRectValue = previousVisibleBoundingRect {
previousVisibleBoundingRect = viewAndFrame.1.union(previousVisibleBoundingRectValue)
} else {
previousVisibleBoundingRect = viewAndFrame.1
}
}
}
for (id, viewAndFrame) in previousVisibleGroupExpandActionButtons {
if let view = self.visibleGroupExpandActionButtons[id], self.scrollView.bounds.intersects(view.frame) {
if commonItemOffset == nil {
let visibleFrame = view.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY)
commonItemOffset = viewAndFrame.1.minY - visibleFrame.minY
}
break
} else {
if let previousVisibleBoundingRectValue = previousVisibleBoundingRect {
previousVisibleBoundingRect = viewAndFrame.1.union(previousVisibleBoundingRectValue)
} else {
previousVisibleBoundingRect = viewAndFrame.1
}
}
}
let duration = 0.4
let timingFunction = kCAMediaTimingFunctionSpring
if let commonItemOffset = commonItemOffset {
for (_, layer) in self.visibleItemLayers {
layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true)
}
for (id, layerAndFrame) in previousVisibleLayers {
if self.visibleItemLayers[id] != nil {
continue
}
let layer = layerAndFrame.0
self.scrollView.layer.addSublayer(layer)
layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, additive: true, completion: { [weak layer] _ in
layer?.removeFromSuperlayer()
})
}
for (_, view) in self.visibleItemPlaceholderViews {
view.layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true)
}
for (id, viewAndFrame) in previousVisiblePlaceholderViews {
if self.visibleItemPlaceholderViews[id] != nil {
continue
}
let view = viewAndFrame.0
self.placeholdersContainerView.addSubview(view)
view.layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, additive: true, completion: { [weak view] _ in
view?.removeFromSuperview()
})
}
for (_, view) in self.visibleGroupHeaders {
view.layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true)
}
for (id, viewAndFrame) in previousVisibleGroupHeaders {
if self.visibleGroupHeaders[id] != nil {
continue
}
let view = viewAndFrame.0
self.scrollView.addSubview(view)
let tintContentLayer = view.tintContentLayer
self.mirrorContentScrollView.layer.addSublayer(tintContentLayer)
view.layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, additive: true, completion: { [weak view, weak tintContentLayer] _ in
view?.removeFromSuperview()
tintContentLayer?.removeFromSuperlayer()
})
}
for (_, layer) in self.visibleGroupBorders {
layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true)
}
for (id, layerAndFrame) in previousVisibleGroupBorders {
if self.visibleGroupBorders[id] != nil {
continue
}
let layer = layerAndFrame.0
self.scrollView.layer.addSublayer(layer)
let tintContainerLayer = layer.tintContainerLayer
self.mirrorContentScrollView.layer.addSublayer(tintContainerLayer)
layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, additive: true, completion: { [weak layer, weak tintContainerLayer] _ in
layer?.removeFromSuperlayer()
tintContainerLayer?.removeFromSuperlayer()
})
}
for (_, view) in self.visibleGroupPremiumButtons {
if let view = view.view {
view.layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true)
}
}
for (id, viewAndFrame) in previousVisibleGroupPremiumButtons {
if self.visibleGroupPremiumButtons[id] != nil {
continue
}
let view = viewAndFrame.0
self.scrollView.addSubview(view)
view.layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, additive: true, completion: { [weak view] _ in
view?.removeFromSuperview()
})
}
for (_, view) in self.visibleGroupExpandActionButtons {
view.layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true)
}
for (id, viewAndFrame) in previousVisibleGroupExpandActionButtons {
if self.visibleGroupExpandActionButtons[id] != nil {
continue
}
let view = viewAndFrame.0
self.scrollView.addSubview(view)
let tintContainerLayer = view.tintContainerLayer
self.mirrorContentScrollView.layer.addSublayer(tintContainerLayer)
view.layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, additive: true, completion: { [weak view, weak tintContainerLayer] _ in
view?.removeFromSuperview()
tintContainerLayer?.removeFromSuperlayer()
})
}
} else if let previousVisibleBoundingRect = previousVisibleBoundingRect {
var updatedVisibleBoundingRect: CGRect?
for (_, layer) in self.visibleItemLayers {
let frame = layer.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY)
if let updatedVisibleBoundingRectValue = updatedVisibleBoundingRect {
updatedVisibleBoundingRect = frame.union(updatedVisibleBoundingRectValue)
} else {
updatedVisibleBoundingRect = frame
}
}
for (_, view) in self.visibleItemPlaceholderViews {
let frame = view.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY)
if let updatedVisibleBoundingRectValue = updatedVisibleBoundingRect {
updatedVisibleBoundingRect = frame.union(updatedVisibleBoundingRectValue)
} else {
updatedVisibleBoundingRect = frame
}
}
for (_, view) in self.visibleGroupHeaders {
if !self.scrollView.bounds.intersects(view.frame) {
continue
}
let frame = view.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY)
if let updatedVisibleBoundingRectValue = updatedVisibleBoundingRect {
updatedVisibleBoundingRect = frame.union(updatedVisibleBoundingRectValue)
} else {
updatedVisibleBoundingRect = frame
}
}
for (_, view) in self.visibleGroupPremiumButtons {
if let view = view.view {
if !self.scrollView.bounds.intersects(view.frame) {
continue
}
let frame = view.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY)
if let updatedVisibleBoundingRectValue = updatedVisibleBoundingRect {
updatedVisibleBoundingRect = frame.union(updatedVisibleBoundingRectValue)
} else {
updatedVisibleBoundingRect = frame
}
}
}
for (_, view) in self.visibleGroupExpandActionButtons {
if !self.scrollView.bounds.intersects(view.frame) {
continue
}
let frame = view.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY)
if let updatedVisibleBoundingRectValue = updatedVisibleBoundingRect {
updatedVisibleBoundingRect = frame.union(updatedVisibleBoundingRectValue)
} else {
updatedVisibleBoundingRect = frame
}
}
if let updatedVisibleBoundingRect = updatedVisibleBoundingRect {
var commonItemOffset = updatedVisibleBoundingRect.height * offsetDirectionSign
if previousVisibleBoundingRect.intersects(updatedVisibleBoundingRect) {
if offsetDirectionSign < 0.0 {
commonItemOffset = previousVisibleBoundingRect.minY - updatedVisibleBoundingRect.maxY
} else {
commonItemOffset = previousVisibleBoundingRect.maxY - updatedVisibleBoundingRect.minY
}
}
for (_, layer) in self.visibleItemLayers {
layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true)
}
for (_, layer) in self.visibleItemSelectionLayers {
layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true)
}
for (id, layerAndFrame) in previousVisibleLayers {
if self.visibleItemLayers[id] != nil {
continue
}
let layer = layerAndFrame.0
layer.frame = layerAndFrame.1.offsetBy(dx: 0.0, dy: self.scrollView.bounds.minY)
self.scrollView.layer.addSublayer(layer)
layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -commonItemOffset), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, additive: true, completion: { [weak layer] _ in
layer?.removeFromSuperlayer()
})
}
for (id, layerAndFrame) in previousVisibleItemSelectionLayers {
if self.visibleItemSelectionLayers[id] != nil {
continue
}
let layer = layerAndFrame.0
layer.frame = layerAndFrame.1.offsetBy(dx: 0.0, dy: self.scrollView.bounds.minY)
self.scrollView.layer.addSublayer(layer)
layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -commonItemOffset), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, additive: true, completion: { [weak layer] _ in
layer?.removeFromSuperlayer()
})
}
for (_, view) in self.visibleItemPlaceholderViews {
view.layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true)
}
for (id, viewAndFrame) in previousVisiblePlaceholderViews {
if self.visibleItemPlaceholderViews[id] != nil {
continue
}
let view = viewAndFrame.0
view.frame = viewAndFrame.1.offsetBy(dx: 0.0, dy: self.scrollView.bounds.minY)
self.placeholdersContainerView.addSubview(view)
view.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -commonItemOffset), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, additive: true, completion: { [weak view] _ in
view?.removeFromSuperview()
})
}
for (_, view) in self.visibleGroupHeaders {
if !self.scrollView.bounds.intersects(view.frame) {
continue
}
view.layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true)
}
for (id, viewAndFrame) in previousVisibleGroupHeaders {
if self.visibleGroupHeaders[id] != nil {
continue
}
let view = viewAndFrame.0
view.frame = viewAndFrame.1.offsetBy(dx: 0.0, dy: self.scrollView.bounds.minY)
self.scrollView.addSubview(view)
let tintContentLayer = view.tintContentLayer
self.mirrorContentScrollView.layer.addSublayer(tintContentLayer)
view.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -commonItemOffset), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, additive: true, completion: { [weak view, weak tintContentLayer] _ in
view?.removeFromSuperview()
tintContentLayer?.removeFromSuperlayer()
})
}
for (_, layer) in self.visibleGroupBorders {
if !self.scrollView.bounds.intersects(layer.frame) {
continue
}
layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true)
}
for (id, layerAndFrame) in previousVisibleGroupBorders {
if self.visibleGroupBorders[id] != nil {
continue
}
let layer = layerAndFrame.0
layer.frame = layerAndFrame.1.offsetBy(dx: 0.0, dy: self.scrollView.bounds.minY)
self.scrollView.layer.addSublayer(layer)
let tintContainerLayer = layer.tintContainerLayer
self.mirrorContentScrollView.layer.addSublayer(tintContainerLayer)
layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -commonItemOffset), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, additive: true, completion: { [weak layer, weak tintContainerLayer] _ in
layer?.removeFromSuperlayer()
tintContainerLayer?.removeFromSuperlayer()
})
}
for (_, view) in self.visibleGroupPremiumButtons {
if let view = view.view {
view.layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true)
}
}
for (id, viewAndFrame) in previousVisibleGroupPremiumButtons {
if self.visibleGroupPremiumButtons[id] != nil {
continue
}
let view = viewAndFrame.0
view.frame = viewAndFrame.1.offsetBy(dx: 0.0, dy: self.scrollView.bounds.minY)
self.scrollView.addSubview(view)
view.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -commonItemOffset), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, additive: true, completion: { [weak view] _ in
view?.removeFromSuperview()
})
}
for (_, view) in self.visibleGroupExpandActionButtons {
view.layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true)
}
for (id, viewAndFrame) in previousVisibleGroupExpandActionButtons {
if self.visibleGroupExpandActionButtons[id] != nil {
continue
}
let view = viewAndFrame.0
view.frame = viewAndFrame.1.offsetBy(dx: 0.0, dy: self.scrollView.bounds.minY)
self.scrollView.addSubview(view)
let tintContainerLayer = view.tintContainerLayer
self.mirrorContentScrollView.layer.addSublayer(tintContainerLayer)
view.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -commonItemOffset), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, additive: true, completion: { [weak view, weak tintContainerLayer] _ in
view?.removeFromSuperview()
tintContainerLayer?.removeFromSuperlayer()
})
}
}
}
}
}
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
guard let component = self.component else {
return
}
if case .ended = recognizer.state {
let locationInScrollView = recognizer.location(in: self.scrollView)
outer: for (id, groupHeader) in self.visibleGroupHeaders {
if groupHeader.frame.insetBy(dx: -10.0, dy: -6.0).contains(locationInScrollView) {
let groupHeaderPoint = self.scrollView.convert(locationInScrollView, to: groupHeader)
if let clearIconLayer = groupHeader.clearIconLayer, clearIconLayer.frame.insetBy(dx: -4.0, dy: -4.0).contains(groupHeaderPoint) {
component.inputInteractionHolder.inputInteraction?.clearGroup(id)
return
} else {
if groupHeader.tapGesture(recognizer) {
return
}
}
}
}
var foundItem = false
var foundExactItem = false
if let (item, itemKey) = self.item(atPoint: recognizer.location(in: self)), let itemLayer = self.visibleItemLayers[itemKey] {
foundExactItem = true
foundItem = true
if !itemLayer.displayPlaceholder {
component.inputInteractionHolder.inputInteraction?.performItemAction(itemKey.groupId, item, self, self.scrollView.convert(itemLayer.frame, to: self), itemLayer, false)
}
}
if !foundExactItem {
if let (item, itemKey) = self.item(atPoint: recognizer.location(in: self), extendedHitRange: true), let itemLayer = self.visibleItemLayers[itemKey] {
foundItem = true
if !itemLayer.displayPlaceholder {
component.inputInteractionHolder.inputInteraction?.performItemAction(itemKey.groupId, item, self, self.scrollView.convert(itemLayer.frame, to: self), itemLayer, false)
}
}
}
let _ = foundItem
}
}
private let longPressDuration: Double = 0.5
private var longPressItem: EmojiPagerContentComponent.View.ItemLayer.Key?
private var hapticFeedback: HapticFeedback?
private var continuousHaptic: AnyObject?
private var longPressTimer: SwiftSignalKit.Timer?
@objc private func longPressGesture(_ recognizer: UILongPressGestureRecognizer) {
switch recognizer.state {
case .began:
let point = recognizer.location(in: self)
guard let item = self.item(atPoint: point), let itemLayer = self.visibleItemLayers[item.1] else {
return
}
switch item.0.content {
case .animation:
break
default:
return
}
self.longPressItem = item.1
if #available(iOS 13.0, *) {
self.continuousHaptic = try? ContinuousHaptic(duration: longPressDuration)
}
if self.hapticFeedback == nil {
self.hapticFeedback = HapticFeedback()
}
let _ = itemLayer
//let transition = Transition(animation: .curve(duration: longPressDuration, curve: .easeInOut))
//transition.setScale(layer: itemLayer, scale: 1.3)
self.longPressTimer?.invalidate()
self.longPressTimer = SwiftSignalKit.Timer(timeout: longPressDuration, repeat: false, completion: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.longTapRecognizer?.state = .ended
}, queue: .mainQueue())
self.longPressTimer?.start()
case .changed:
let point = recognizer.location(in: self)
if let longPressItem = self.longPressItem, let item = self.item(atPoint: point), longPressItem == item.1 {
} else {
self.longTapRecognizer?.state = .cancelled
}
case .cancelled:
self.longPressTimer?.invalidate()
self.continuousHaptic = nil
if let itemKey = self.longPressItem {
self.longPressItem = nil
if let itemLayer = self.visibleItemLayers[itemKey] {
let transition = Transition(animation: .curve(duration: 0.3, curve: .spring))
transition.setScale(layer: itemLayer, scale: 1.0)
}
}
case .ended:
self.longPressTimer?.invalidate()
self.continuousHaptic = nil
if let itemKey = self.longPressItem {
self.longPressItem = nil
if let component = self.component, let itemLayer = self.visibleItemLayers[itemKey] {
component.inputInteractionHolder.inputInteraction?.performItemAction(itemKey.groupId, itemLayer.item, self, self.scrollView.convert(itemLayer.frame, to: self), itemLayer, true)
} else {
if let itemLayer = self.visibleItemLayers[itemKey] {
let transition = Transition(animation: .curve(duration: 0.3, curve: .spring))
transition.setScale(layer: itemLayer, scale: 1.0)
}
}
}
default:
break
}
}
private func item(atPoint point: CGPoint, extendedHitRange: Bool = false) -> (Item, ItemLayer.Key)? {
let localPoint = self.convert(point, to: self.scrollView)
var closestItem: (key: ItemLayer.Key, distance: CGFloat)?
for (key, itemLayer) in self.visibleItemLayers {
if extendedHitRange {
let position = CGPoint(x: itemLayer.frame.midX, y: itemLayer.frame.midY)
let distance = CGPoint(x: localPoint.x - position.x, y: localPoint.y - position.y)
let distance2 = distance.x * distance.x + distance.y * distance.y
if distance2 > pow(max(itemLayer.bounds.width, itemLayer.bounds.height), 2.0) {
continue
}
if let closestItemValue = closestItem {
if closestItemValue.distance > distance2 {
closestItem = (key, distance2)
}
} else {
closestItem = (key, distance2)
}
} else {
if itemLayer.frame.contains(localPoint) {
return (itemLayer.item, key)
}
}
}
if let key = closestItem?.key {
if let itemLayer = self.visibleItemLayers[key] {
return (itemLayer.item, key)
}
}
return nil
}
private struct ScrollingOffsetState: Equatable {
var value: CGFloat
var isDraggingOrDecelerating: Bool
}
private var previousScrollingOffset: ScrollingOffsetState?
public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
if self.keepTopPanelVisibleUntilScrollingInput {
self.keepTopPanelVisibleUntilScrollingInput = false
self.updateScrollingOffset(isReset: true, transition: .immediate)
}
if let presentation = scrollView.layer.presentation() {
scrollView.bounds = presentation.bounds
scrollView.layer.removeAllAnimations()
}
}
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
if self.ignoreScrolling {
return
}
self.updateVisibleItems(transition: .immediate, attemptSynchronousLoads: false, previousItemPositions: nil, updatedItemPositions: nil)
self.updateScrollingOffset(isReset: false, transition: .immediate)
}
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(isReset: Bool, transition: Transition) {
guard let component = self.component else {
return
}
let isInteracting = scrollView.isDragging || scrollView.isDecelerating
if let previousScrollingOffsetValue = self.previousScrollingOffset, !self.keepTopPanelVisibleUntilScrollingInput {
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.value
if case .detailed = component.itemLayoutType {
self.pagerEnvironment?.onChildScrollingUpdate(PagerComponentChildEnvironment.ContentScrollingUpdate(
relativeOffset: relativeOffset,
absoluteOffsetToTopEdge: offsetToTopEdge,
absoluteOffsetToBottomEdge: offsetToBottomEdge,
isReset: isReset,
isInteracting: isInteracting,
transition: transition
))
}
}
self.previousScrollingOffset = ScrollingOffsetState(value: scrollView.contentOffset.y, isDraggingOrDecelerating: isInteracting)
}
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(isReset: false, transition: transition)
}
private func updateVisibleItems(transition: Transition, attemptSynchronousLoads: Bool, previousItemPositions: [VisualItemKey: CGPoint]?, previousAbsoluteItemPositions: [VisualItemKey: CGPoint]? = nil, updatedItemPositions: [VisualItemKey: CGPoint]?, hintDisappearingGroupFrame: (groupId: AnyHashable, frame: CGRect)? = nil) {
guard let component = self.component, let pagerEnvironment = self.pagerEnvironment, let keyboardChildEnvironment = self.keyboardChildEnvironment, let itemLayout = self.itemLayout else {
return
}
let useOpaqueTheme = component.inputInteractionHolder.inputInteraction?.useOpaqueTheme ?? false
var topVisibleGroupId: AnyHashable?
var topVisibleSubgroupId: AnyHashable?
var validIds = Set<ItemLayer.Key>()
var validGroupHeaderIds = Set<AnyHashable>()
var validGroupBorderIds = Set<AnyHashable>()
var validGroupPremiumButtonIds = Set<AnyHashable>()
var validGroupExpandActionButtons = Set<AnyHashable>()
let effectiveVisibleBounds = CGRect(origin: self.scrollView.bounds.origin, size: self.effectiveVisibleSize)
let topVisibleDetectionBounds = effectiveVisibleBounds.offsetBy(dx: 0.0, dy: pagerEnvironment.containerInsets.top)
let contentAnimation = transition.userData(ContentAnimation.self)
var transitionHintInstalledGroupId: AnyHashable?
var transitionHintExpandedGroupId: AnyHashable?
if let contentAnimation = contentAnimation {
switch contentAnimation.type {
case let .groupInstalled(groupId):
transitionHintInstalledGroupId = groupId
case let .groupExpanded(groupId):
transitionHintExpandedGroupId = groupId
default:
break
}
}
for groupItems in itemLayout.visibleItems(for: effectiveVisibleBounds) {
let itemGroup = component.itemGroups[groupItems.groupIndex]
let itemGroupLayout = itemLayout.itemGroupLayouts[groupItems.groupIndex]
var assignTopVisibleSubgroupId = false
if topVisibleGroupId == nil && itemGroupLayout.frame.intersects(topVisibleDetectionBounds) {
topVisibleGroupId = groupItems.supergroupId
assignTopVisibleSubgroupId = true
}
var headerCentralContentWidth: CGFloat?
var headerSizeUpdated = false
if let title = itemGroup.title {
validGroupHeaderIds.insert(itemGroup.groupId)
let groupHeaderView: GroupHeaderLayer
var groupHeaderTransition = transition
if let current = self.visibleGroupHeaders[itemGroup.groupId] {
groupHeaderView = current
} else {
groupHeaderTransition = .immediate
let groupId = itemGroup.groupId
groupHeaderView = GroupHeaderLayer(
actionPressed: { [weak self] in
guard let strongSelf = self, let component = strongSelf.component else {
return
}
component.inputInteractionHolder.inputInteraction?.addGroupAction(groupId, false)
},
performItemAction: { [weak self] item, view, rect, layer in
guard let strongSelf = self, let component = strongSelf.component else {
return
}
component.inputInteractionHolder.inputInteraction?.performItemAction(groupId, item, view, rect, layer, false)
}
)
self.visibleGroupHeaders[itemGroup.groupId] = groupHeaderView
self.scrollView.addSubview(groupHeaderView)
self.mirrorContentScrollView.layer.addSublayer(groupHeaderView.tintContentLayer)
}
var actionButtonTitle: String?
if case .detailed = itemLayout.layoutType, itemGroup.isFeatured {
actionButtonTitle = itemGroup.actionButtonTitle
}
let hasTopSeparator = false
let (groupHeaderSize, centralContentWidth) = groupHeaderView.update(
context: component.context,
theme: keyboardChildEnvironment.theme,
layoutType: itemLayout.layoutType,
hasTopSeparator: hasTopSeparator,
actionButtonTitle: actionButtonTitle,
title: title,
subtitle: itemGroup.subtitle,
isPremiumLocked: itemGroup.isPremiumLocked,
hasClear: itemGroup.hasClear,
embeddedItems: itemGroup.isEmbedded ? itemGroup.items : nil,
constrainedSize: CGSize(width: itemLayout.contentSize.width - itemLayout.headerInsets.left - itemLayout.headerInsets.right, height: itemGroupLayout.headerHeight),
insets: itemLayout.headerInsets,
cache: component.animationCache,
renderer: component.animationRenderer,
attemptSynchronousLoad: attemptSynchronousLoads
)
if groupHeaderView.bounds.size != groupHeaderSize {
headerSizeUpdated = true
}
headerCentralContentWidth = centralContentWidth
let groupHeaderFrame = CGRect(origin: CGPoint(x: floor((itemLayout.contentSize.width - groupHeaderSize.width) / 2.0), y: itemGroupLayout.frame.minY + 1.0), size: groupHeaderSize)
groupHeaderView.bounds = CGRect(origin: CGPoint(), size: groupHeaderFrame.size)
groupHeaderTransition.setPosition(view: groupHeaderView, position: CGPoint(x: groupHeaderFrame.midX, y: groupHeaderFrame.midY))
}
let groupBorderRadius: CGFloat = 16.0
if itemGroup.isPremiumLocked && !itemGroup.isFeatured && !itemGroup.isEmbedded && !itemLayout.curveNearBounds {
validGroupBorderIds.insert(itemGroup.groupId)
let groupBorderLayer: GroupBorderLayer
var groupBorderTransition = transition
if let current = self.visibleGroupBorders[itemGroup.groupId] {
groupBorderLayer = current
} else {
groupBorderTransition = .immediate
groupBorderLayer = GroupBorderLayer()
self.visibleGroupBorders[itemGroup.groupId] = groupBorderLayer
self.scrollView.layer.insertSublayer(groupBorderLayer, at: 0)
self.mirrorContentScrollView.layer.addSublayer(groupBorderLayer.tintContainerLayer)
groupBorderLayer.strokeColor = keyboardChildEnvironment.theme.chat.inputMediaPanel.panelContentVibrantOverlayColor.cgColor
groupBorderLayer.tintContainerLayer.strokeColor = UIColor.white.cgColor
groupBorderLayer.lineWidth = 1.6
groupBorderLayer.lineCap = .round
groupBorderLayer.fillColor = nil
}
let groupBorderHorizontalInset: CGFloat = itemLayout.itemInsets.left - 4.0
let groupBorderVerticalTopOffset: CGFloat = 8.0
let groupBorderVerticalInset: CGFloat = 6.0
let groupBorderFrame = CGRect(origin: CGPoint(x: groupBorderHorizontalInset, y: itemGroupLayout.frame.minY + groupBorderVerticalTopOffset), size: CGSize(width: itemLayout.width - groupBorderHorizontalInset * 2.0, height: itemGroupLayout.frame.size.height - groupBorderVerticalTopOffset + groupBorderVerticalInset))
if groupBorderLayer.bounds.size != groupBorderFrame.size || headerSizeUpdated {
let headerWidth: CGFloat
if let headerCentralContentWidth = headerCentralContentWidth {
headerWidth = headerCentralContentWidth + 14.0
} else {
headerWidth = 0.0
}
let path = CGMutablePath()
let radius = groupBorderRadius
path.move(to: CGPoint(x: floor((groupBorderFrame.width - headerWidth) / 2.0), y: 0.0))
path.addLine(to: CGPoint(x: radius, y: 0.0))
path.addArc(tangent1End: CGPoint(x: 0.0, y: 0.0), tangent2End: CGPoint(x: 0.0, y: radius), radius: radius)
path.addLine(to: CGPoint(x: 0.0, y: groupBorderFrame.height - radius))
path.addArc(tangent1End: CGPoint(x: 0.0, y: groupBorderFrame.height), tangent2End: CGPoint(x: radius, y: groupBorderFrame.height), radius: radius)
path.addLine(to: CGPoint(x: groupBorderFrame.width - radius, y: groupBorderFrame.height))
path.addArc(tangent1End: CGPoint(x: groupBorderFrame.width, y: groupBorderFrame.height), tangent2End: CGPoint(x: groupBorderFrame.width, y: groupBorderFrame.height - radius), radius: radius)
path.addLine(to: CGPoint(x: groupBorderFrame.width, y: radius))
path.addArc(tangent1End: CGPoint(x: groupBorderFrame.width, y: 0.0), tangent2End: CGPoint(x: groupBorderFrame.width - radius, y: 0.0), radius: radius)
path.addLine(to: CGPoint(x: floor((groupBorderFrame.width - headerWidth) / 2.0) + headerWidth, y: 0.0))
let pathLength = (2.0 * groupBorderFrame.width + 2.0 * groupBorderFrame.height - 8.0 * radius + 2.0 * .pi * radius) - headerWidth
var numberOfDashes = Int(floor(pathLength / 6.0))
if numberOfDashes % 2 == 0 {
numberOfDashes -= 1
}
let wholeLength = 6.0 * CGFloat(numberOfDashes)
let remainingLength = pathLength - wholeLength
let dashSpace = remainingLength / CGFloat(numberOfDashes)
groupBorderTransition.setShapeLayerPath(layer: groupBorderLayer, path: path)
groupBorderTransition.setShapeLayerLineDashPattern(layer: groupBorderLayer, pattern: [(5.0 + dashSpace) as NSNumber, (7.0 + dashSpace) as NSNumber])
}
groupBorderTransition.setFrame(layer: groupBorderLayer, frame: groupBorderFrame)
}
if (itemGroup.isPremiumLocked || itemGroup.isFeatured), !itemGroup.isEmbedded, case .compact = itemLayout.layoutType {
let groupPremiumButtonMeasuringFrame = CGRect(origin: CGPoint(x: itemLayout.itemInsets.left, y: itemGroupLayout.frame.maxY - 50.0 + 1.0), size: CGSize(width: 100.0, height: 50.0))
if effectiveVisibleBounds.intersects(groupPremiumButtonMeasuringFrame) {
validGroupPremiumButtonIds.insert(itemGroup.groupId)
let groupPremiumButton: ComponentView<Empty>
var groupPremiumButtonTransition = transition
var animateButtonIn = false
if let current = self.visibleGroupPremiumButtons[itemGroup.groupId] {
groupPremiumButton = current
} else {
groupPremiumButtonTransition = .immediate
animateButtonIn = !transition.animation.isImmediate
groupPremiumButton = ComponentView<Empty>()
self.visibleGroupPremiumButtons[itemGroup.groupId] = groupPremiumButton
}
let groupId = itemGroup.groupId
let isPremiumLocked = itemGroup.isPremiumLocked
let title: String
let backgroundColor: UIColor
let backgroundColors: [UIColor]
let foregroundColor: UIColor
let animationName: String?
let gloss: Bool
if itemGroup.isPremiumLocked {
title = keyboardChildEnvironment.strings.EmojiInput_UnlockPack(itemGroup.title ?? "Emoji").string
backgroundColors = [
UIColor(rgb: 0x0077ff),
UIColor(rgb: 0x6b93ff),
UIColor(rgb: 0x8878ff),
UIColor(rgb: 0xe46ace)
]
backgroundColor = backgroundColors[0]
foregroundColor = .white
animationName = "premium_unlock"
gloss = true
} else {
title = keyboardChildEnvironment.strings.EmojiInput_AddPack(itemGroup.title ?? "Emoji").string
backgroundColors = []
backgroundColor = keyboardChildEnvironment.theme.list.itemCheckColors.fillColor
foregroundColor = keyboardChildEnvironment.theme.list.itemCheckColors.foregroundColor
animationName = nil
gloss = false
}
let groupPremiumButtonSize = groupPremiumButton.update(
transition: groupPremiumButtonTransition,
component: AnyComponent(SolidRoundedButtonComponent(
title: title,
theme: SolidRoundedButtonComponent.Theme(
backgroundColor: backgroundColor,
backgroundColors: backgroundColors,
foregroundColor: foregroundColor
),
font: .bold,
fontSize: 17.0,
height: 50.0,
cornerRadius: groupBorderRadius,
gloss: gloss,
animationName: animationName,
iconPosition: .right,
iconSpacing: 4.0,
action: { [weak self] in
guard let strongSelf = self, let component = strongSelf.component else {
return
}
component.inputInteractionHolder.inputInteraction?.addGroupAction(groupId, isPremiumLocked)
}
)),
environment: {},
containerSize: CGSize(width: itemLayout.width - itemLayout.itemInsets.left - itemLayout.itemInsets.right, height: itemLayout.premiumButtonHeight)
)
let groupPremiumButtonFrame = CGRect(origin: CGPoint(x: itemLayout.itemInsets.left, y: itemGroupLayout.frame.maxY - groupPremiumButtonSize.height + 1.0), size: groupPremiumButtonSize)
if let view = groupPremiumButton.view {
if view.superview == nil {
self.scrollView.addSubview(view)
}
if animateButtonIn, !transition.animation.isImmediate {
if let previousItemPosition = previousItemPositions?[.groupActionButton(groupId: itemGroup.groupId)], transitionHintInstalledGroupId != itemGroup.groupId, transitionHintExpandedGroupId != itemGroup.groupId {
groupPremiumButtonTransition = transition
view.center = previousItemPosition
}
}
groupPremiumButtonTransition.setFrame(view: view, frame: groupPremiumButtonFrame)
if animateButtonIn, !transition.animation.isImmediate {
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
transition.animateScale(view: view, from: 0.01, to: 1.0)
}
}
}
}
if !itemGroup.isEmbedded, let collapsedItemIndex = itemGroupLayout.collapsedItemIndex, let collapsedItemText = itemGroupLayout.collapsedItemText {
validGroupExpandActionButtons.insert(itemGroup.groupId)
let groupId = itemGroup.groupId
var animateButtonIn = false
var groupExpandActionButtonTransition = transition
let groupExpandActionButton: GroupExpandActionButton
if let current = self.visibleGroupExpandActionButtons[itemGroup.groupId] {
groupExpandActionButton = current
} else {
groupExpandActionButtonTransition = .immediate
animateButtonIn = !transition.animation.isImmediate
groupExpandActionButton = GroupExpandActionButton(pressed: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.expandGroup(groupId: groupId)
})
self.visibleGroupExpandActionButtons[itemGroup.groupId] = groupExpandActionButton
self.scrollView.addSubview(groupExpandActionButton)
self.mirrorContentScrollView.layer.addSublayer(groupExpandActionButton.tintContainerLayer)
}
if animateButtonIn, !transition.animation.isImmediate {
if let previousItemPosition = previousItemPositions?[.groupExpandButton(groupId: itemGroup.groupId)], transitionHintInstalledGroupId != itemGroup.groupId, transitionHintExpandedGroupId != itemGroup.groupId {
groupExpandActionButtonTransition = transition
groupExpandActionButton.center = previousItemPosition
}
}
let baseItemFrame = itemLayout.frame(groupIndex: groupItems.groupIndex, itemIndex: collapsedItemIndex)
let buttonSize = groupExpandActionButton.update(theme: keyboardChildEnvironment.theme, title: collapsedItemText, useOpaqueTheme: useOpaqueTheme)
let buttonFrame = CGRect(origin: CGPoint(x: baseItemFrame.minX + floor((baseItemFrame.width - buttonSize.width) / 2.0), y: baseItemFrame.minY + floor((baseItemFrame.height - buttonSize.height) / 2.0)), size: buttonSize)
groupExpandActionButtonTransition.setFrame(view: groupExpandActionButton, frame: buttonFrame)
}
if !itemGroup.isEmbedded, let groupItemRange = groupItems.groupItems {
for index in groupItemRange.lowerBound ..< groupItemRange.upperBound {
let item = itemGroup.items[index]
if assignTopVisibleSubgroupId {
if let subgroupId = item.subgroupId {
topVisibleSubgroupId = AnyHashable(subgroupId)
}
}
let itemId = ItemLayer.Key(
groupId: itemGroup.groupId,
itemId: item.content.id
)
validIds.insert(itemId)
let itemDimensions: CGSize = item.animationData?.dimensions ?? CGSize(width: 512.0, height: 512.0)
let itemNativeFitSize = itemDimensions.aspectFitted(CGSize(width: itemLayout.nativeItemSize, height: itemLayout.nativeItemSize))
let itemVisibleFitSize = itemDimensions.aspectFitted(CGSize(width: itemLayout.visibleItemSize, height: itemLayout.visibleItemSize))
let itemPlaybackSize = itemDimensions.aspectFitted(CGSize(width: itemLayout.playbackItemSize, height: itemLayout.playbackItemSize))
var animateItemIn = false
var updateItemLayerPlaceholder = false
var itemTransition = transition
let itemLayer: ItemLayer
if let current = self.visibleItemLayers[itemId] {
itemLayer = current
} else {
updateItemLayerPlaceholder = true
itemTransition = .immediate
animateItemIn = !transition.animation.isImmediate
let pointSize: CGSize
if case .staticEmoji = item.content {
pointSize = itemVisibleFitSize
} else {
pointSize = itemPlaybackSize
}
let placeholderColor = keyboardChildEnvironment.theme.chat.inputPanel.primaryTextColor.withMultipliedAlpha(0.1)
itemLayer = ItemLayer(
item: item,
context: component.context,
attemptSynchronousLoad: attemptSynchronousLoads,
content: item.content,
cache: component.animationCache,
renderer: component.animationRenderer,
placeholderColor: placeholderColor,
blurredBadgeColor: keyboardChildEnvironment.theme.chat.inputPanel.panelBackgroundColor.withMultipliedAlpha(0.5),
accentIconColor: keyboardChildEnvironment.theme.list.itemAccentColor,
pointSize: pointSize,
onUpdateDisplayPlaceholder: { [weak self] displayPlaceholder, duration in
guard let strongSelf = self else {
return
}
if displayPlaceholder, let animationData = item.animationData {
if let itemLayer = strongSelf.visibleItemLayers[itemId] {
let placeholderView: ItemPlaceholderView
if let current = strongSelf.visibleItemPlaceholderViews[itemId] {
placeholderView = current
} else {
placeholderView = ItemPlaceholderView(
context: component.context,
dimensions: animationData.dimensions,
immediateThumbnailData: animationData.immediateThumbnailData,
shimmerView: strongSelf.shimmerHostView,
color: placeholderColor,
size: itemNativeFitSize
)
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 {
placeholderView.layer.opacity = 0.0
placeholderView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, completion: { [weak self, weak placeholderView] _ in
guard let strongSelf = self else {
return
}
placeholderView?.removeFromSuperview()
strongSelf.updateShimmerIfNeeded()
})
} else {
placeholderView.removeFromSuperview()
strongSelf.updateShimmerIfNeeded()
}
}
}
}
)
self.scrollView.layer.addSublayer(itemLayer)
self.visibleItemLayers[itemId] = itemLayer
}
var itemFrame = itemLayout.frame(groupIndex: groupItems.groupIndex, itemIndex: index)
let baseItemFrame = itemFrame
itemFrame.origin.x += floor((itemFrame.width - itemVisibleFitSize.width) / 2.0)
itemFrame.origin.y += floor((itemFrame.height - itemVisibleFitSize.height) / 2.0)
itemFrame.size = itemVisibleFitSize
let itemBounds = CGRect(origin: CGPoint(), size: itemFrame.size)
itemTransition.setBounds(layer: itemLayer, bounds: CGRect(origin: CGPoint(), size: itemFrame.size))
if animateItemIn, !transition.animation.isImmediate {
if let previousItemPosition = previousItemPositions?[.item(id: itemId)], transitionHintInstalledGroupId != itemId.groupId, transitionHintExpandedGroupId != itemId.groupId {
itemTransition = transition
itemLayer.position = previousItemPosition
} else {
if transitionHintInstalledGroupId == itemId.groupId || transitionHintExpandedGroupId == itemId.groupId {
itemLayer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4)
itemLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
} else {
itemLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
}
let itemPosition = CGPoint(x: itemFrame.midX, y: itemFrame.midY)
itemTransition.setPosition(layer: itemLayer, position: itemPosition)
var badge: ItemLayer.Badge?
if itemGroup.displayPremiumBadges, let file = item.itemFile, file.isPremiumSticker {
badge = .premium
} else {
switch item.icon {
case .none:
break
case .locked:
badge = .locked
case .premium:
badge = .premium
}
}
itemLayer.update(transition: transition, size: itemFrame.size, badge: badge, blurredBadgeColor: UIColor(white: 0.0, alpha: 0.1), blurredBadgeBackgroundColor: keyboardChildEnvironment.theme.list.plainBackgroundColor)
if item.accentTint {
itemLayer.layerTintColor = keyboardChildEnvironment.theme.list.itemAccentColor.cgColor
} else {
itemLayer.layerTintColor = nil
}
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)
}
} else if updateItemLayerPlaceholder {
if itemLayer.displayPlaceholder {
itemLayer.onUpdateDisplayPlaceholder(true, 0.0)
}
}
if let itemFile = item.itemFile, component.selectedItems.contains(itemFile.fileId) {
let itemSelectionLayer: ItemSelectionLayer
if let current = self.visibleItemSelectionLayers[itemId] {
itemSelectionLayer = current
} else {
itemSelectionLayer = ItemSelectionLayer()
itemSelectionLayer.cornerRadius = 8.0
itemSelectionLayer.tintContainerLayer.cornerRadius = 8.0
self.scrollView.layer.insertSublayer(itemSelectionLayer, below: itemLayer)
self.mirrorContentScrollView.layer.addSublayer(itemSelectionLayer.tintContainerLayer)
self.visibleItemSelectionLayers[itemId] = itemSelectionLayer
}
if item.accentTint {
itemSelectionLayer.backgroundColor = keyboardChildEnvironment.theme.list.itemAccentColor.withMultipliedAlpha(0.1).cgColor
itemSelectionLayer.tintContainerLayer.backgroundColor = UIColor.clear.cgColor
} else {
if useOpaqueTheme {
itemSelectionLayer.backgroundColor = keyboardChildEnvironment.theme.chat.inputMediaPanel.panelContentControlOpaqueSelectionColor.cgColor
itemSelectionLayer.tintContainerLayer.backgroundColor = UIColor.clear.cgColor
} else {
itemSelectionLayer.backgroundColor = keyboardChildEnvironment.theme.chat.inputMediaPanel.panelContentControlVibrantSelectionColor.cgColor
itemSelectionLayer.tintContainerLayer.backgroundColor = UIColor(white: 1.0, alpha: 0.2).cgColor
}
}
itemSelectionLayer.frame = baseItemFrame
itemLayer.transform = CATransform3DMakeScale(0.8, 0.8, 1.0)
} else {
itemLayer.transform = CATransform3DIdentity
}
if animateItemIn, !transition.animation.isImmediate, let contentAnimation = contentAnimation, case .groupExpanded(id: itemGroup.groupId) = contentAnimation.type, let placeholderView = self.visibleItemPlaceholderViews[itemId] {
placeholderView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4)
placeholderView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
}
itemLayer.isVisibleForAnimations = true
}
}
}
var removedPlaceholerViews = false
var removedIds: [ItemLayer.Key] = []
for (id, itemLayer) in self.visibleItemLayers {
if !validIds.contains(id) {
removedIds.append(id)
let itemSelectionLayer = self.visibleItemSelectionLayers[id]
if !transition.animation.isImmediate {
if let hintDisappearingGroupFrame = hintDisappearingGroupFrame, hintDisappearingGroupFrame.groupId == id.groupId {
if let previousAbsolutePosition = previousAbsoluteItemPositions?[.item(id: id)] {
itemLayer.position = self.convert(previousAbsolutePosition, to: self.scrollView)
transition.setPosition(layer: itemLayer, position: CGPoint(x: hintDisappearingGroupFrame.frame.midX, y: hintDisappearingGroupFrame.frame.minY + 20.0))
}
itemLayer.opacity = 0.0
itemLayer.animateScale(from: 1.0, to: 0.01, duration: 0.16)
itemLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.16, completion: { [weak itemLayer] _ in
itemLayer?.removeFromSuperlayer()
})
if let itemSelectionLayer = itemSelectionLayer {
itemSelectionLayer.opacity = 0.0
itemSelectionLayer.animateScale(from: 1.0, to: 0.01, duration: 0.16)
itemSelectionLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.16, completion: { [weak itemSelectionLayer] _ in
itemSelectionLayer?.removeFromSuperlayer()
})
let itemSelectionTintContainerLayer = itemSelectionLayer.tintContainerLayer
itemSelectionTintContainerLayer.opacity = 0.0
itemSelectionTintContainerLayer.animateScale(from: 1.0, to: 0.01, duration: 0.16)
itemSelectionTintContainerLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.16, completion: { [weak itemSelectionTintContainerLayer] _ in
itemSelectionTintContainerLayer?.removeFromSuperlayer()
})
}
} else if let position = updatedItemPositions?[.item(id: id)], transitionHintInstalledGroupId != id.groupId {
transition.setPosition(layer: itemLayer, position: position, completion: { [weak itemLayer] _ in
itemLayer?.removeFromSuperlayer()
})
if let itemSelectionLayer = itemSelectionLayer {
let itemSelectionTintContainerLayer = itemSelectionLayer.tintContainerLayer
transition.setPosition(layer: itemSelectionLayer, position: position, completion: { [weak itemSelectionLayer, weak itemSelectionTintContainerLayer] _ in
itemSelectionLayer?.removeFromSuperlayer()
itemSelectionTintContainerLayer?.removeFromSuperlayer()
})
}
} else {
itemLayer.opacity = 0.0
itemLayer.animateScale(from: 1.0, to: 0.01, duration: 0.2)
itemLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak itemLayer] _ in
itemLayer?.removeFromSuperlayer()
})
if let itemSelectionLayer = itemSelectionLayer {
itemSelectionLayer.opacity = 0.0
itemSelectionLayer.animateScale(from: 1.0, to: 0.01, duration: 0.2)
itemSelectionLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak itemSelectionLayer] _ in
itemSelectionLayer?.removeFromSuperlayer()
})
let itemSelectionTintContainerLayer = itemSelectionLayer.tintContainerLayer
itemSelectionTintContainerLayer.opacity = 0.0
itemSelectionTintContainerLayer.animateScale(from: 1.0, to: 0.01, duration: 0.2)
itemSelectionTintContainerLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak itemSelectionTintContainerLayer] _ in
itemSelectionTintContainerLayer?.removeFromSuperlayer()
})
}
}
} else {
itemLayer.removeFromSuperlayer()
if let itemSelectionLayer = itemSelectionLayer {
itemSelectionLayer.removeFromSuperlayer()
itemSelectionLayer.tintContainerLayer.removeFromSuperlayer()
}
}
}
}
for id in removedIds {
self.visibleItemLayers.removeValue(forKey: id)
self.visibleItemSelectionLayers.removeValue(forKey: id)
if let view = self.visibleItemPlaceholderViews.removeValue(forKey: id) {
view.removeFromSuperview()
removedPlaceholerViews = true
}
}
var removedItemSelectionLayerIds: [ItemLayer.Key] = []
for (id, itemSelectionLayer) in self.visibleItemSelectionLayers {
var fileId: MediaId?
switch id.itemId {
case let .animation(id):
switch id {
case let .file(fileIdValue):
fileId = fileIdValue
default:
break
}
default:
break
}
if let fileId = fileId, component.selectedItems.contains(fileId) {
} else {
itemSelectionLayer.removeFromSuperlayer()
removedItemSelectionLayerIds.append(id)
}
}
for id in removedItemSelectionLayerIds {
self.visibleItemSelectionLayers.removeValue(forKey: id)
}
var removedGroupHeaderIds: [AnyHashable] = []
for (id, groupHeaderLayer) in self.visibleGroupHeaders {
if !validGroupHeaderIds.contains(id) {
removedGroupHeaderIds.append(id)
if !transition.animation.isImmediate {
var isAnimatingDisappearance = false
if let hintDisappearingGroupFrame = hintDisappearingGroupFrame, hintDisappearingGroupFrame.groupId == id, let previousAbsolutePosition = previousAbsoluteItemPositions?[VisualItemKey.header(groupId: id)] {
groupHeaderLayer.center = self.convert(previousAbsolutePosition, to: self.scrollView)
transition.setPosition(layer: groupHeaderLayer.layer, position: CGPoint(x: hintDisappearingGroupFrame.frame.midX, y: hintDisappearingGroupFrame.frame.minY + 20.0))
isAnimatingDisappearance = true
}
let tintContentLayer = groupHeaderLayer.tintContentLayer
if !isAnimatingDisappearance, let position = updatedItemPositions?[.header(groupId: id)] {
transition.setPosition(layer: groupHeaderLayer.layer, position: position, completion: { [weak groupHeaderLayer, weak tintContentLayer] _ in
groupHeaderLayer?.removeFromSuperview()
tintContentLayer?.removeFromSuperlayer()
})
} else {
groupHeaderLayer.alpha = 0.0
groupHeaderLayer.layer.animateScale(from: 1.0, to: 0.5, duration: 0.16)
groupHeaderLayer.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.16, completion: { [weak groupHeaderLayer, weak tintContentLayer] _ in
groupHeaderLayer?.removeFromSuperview()
tintContentLayer?.removeFromSuperlayer()
})
}
} else {
groupHeaderLayer.removeFromSuperview()
groupHeaderLayer.tintContentLayer.removeFromSuperlayer()
}
}
}
for id in removedGroupHeaderIds {
self.visibleGroupHeaders.removeValue(forKey: id)
}
var removedGroupBorderIds: [AnyHashable] = []
for (id, groupBorderLayer) in self.visibleGroupBorders {
if !validGroupBorderIds.contains(id) {
removedGroupBorderIds.append(id)
groupBorderLayer.removeFromSuperlayer()
groupBorderLayer.tintContainerLayer.removeFromSuperlayer()
}
}
for id in removedGroupBorderIds {
self.visibleGroupBorders.removeValue(forKey: id)
}
var removedGroupPremiumButtonIds: [AnyHashable] = []
for (id, groupPremiumButton) in self.visibleGroupPremiumButtons {
if !validGroupPremiumButtonIds.contains(id), let buttonView = groupPremiumButton.view {
if !transition.animation.isImmediate {
var isAnimatingDisappearance = false
if let position = updatedItemPositions?[.groupActionButton(groupId: id)], position.y > buttonView.center.y {
} else if let hintDisappearingGroupFrame = hintDisappearingGroupFrame, hintDisappearingGroupFrame.groupId == id, let previousAbsolutePosition = previousAbsoluteItemPositions?[VisualItemKey.groupActionButton(groupId: id)] {
buttonView.center = self.convert(previousAbsolutePosition, to: self.scrollView)
transition.setPosition(layer: buttonView.layer, position: CGPoint(x: hintDisappearingGroupFrame.frame.midX, y: hintDisappearingGroupFrame.frame.minY + 20.0))
isAnimatingDisappearance = true
}
if !isAnimatingDisappearance, let position = updatedItemPositions?[.groupActionButton(groupId: id)] {
buttonView.alpha = 0.0
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.16, completion: { [weak buttonView] _ in
buttonView?.removeFromSuperview()
})
transition.setPosition(layer: buttonView.layer, position: position)
} else {
buttonView.alpha = 0.0
if transitionHintExpandedGroupId == id || hintDisappearingGroupFrame?.groupId == id {
buttonView.layer.animateScale(from: 1.0, to: 0.5, duration: 0.16)
}
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.16, completion: { [weak buttonView] _ in
buttonView?.removeFromSuperview()
})
}
} else {
removedGroupPremiumButtonIds.append(id)
buttonView.removeFromSuperview()
}
}
}
for id in removedGroupPremiumButtonIds {
self.visibleGroupPremiumButtons.removeValue(forKey: id)
}
var removedGroupExpandActionButtonIds: [AnyHashable] = []
for (id, button) in self.visibleGroupExpandActionButtons {
if !validGroupExpandActionButtons.contains(id) {
removedGroupExpandActionButtonIds.append(id)
if !transition.animation.isImmediate {
var isAnimatingDisappearance = false
if self.visibleGroupHeaders[id] == nil, let hintDisappearingGroupFrame = hintDisappearingGroupFrame, hintDisappearingGroupFrame.groupId == id, let previousAbsolutePosition = previousAbsoluteItemPositions?[.groupExpandButton(groupId: id)] {
button.center = self.convert(previousAbsolutePosition, to: self.scrollView)
button.tintContainerLayer.position = button.center
transition.setPosition(layer: button.layer, position: CGPoint(x: hintDisappearingGroupFrame.frame.midX, y: hintDisappearingGroupFrame.frame.minY + 20.0))
isAnimatingDisappearance = true
}
let tintContainerLayer = button.tintContainerLayer
if !isAnimatingDisappearance, let position = updatedItemPositions?[.groupExpandButton(groupId: id)] {
transition.setPosition(layer: button.layer, position: position, completion: { [weak button, weak tintContainerLayer] _ in
button?.removeFromSuperview()
tintContainerLayer?.removeFromSuperlayer()
})
} else {
button.alpha = 0.0
if transitionHintExpandedGroupId == id || hintDisappearingGroupFrame?.groupId == id {
button.layer.animateScale(from: 1.0, to: 0.5, duration: 0.16)
}
button.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.16, completion: { [weak button, weak tintContainerLayer] _ in
button?.removeFromSuperview()
tintContainerLayer?.removeFromSuperlayer()
})
}
} else {
button.removeFromSuperview()
button.tintContainerLayer.removeFromSuperlayer()
}
}
}
for id in removedGroupExpandActionButtonIds {
self.visibleGroupExpandActionButtons.removeValue(forKey: id)
}
if removedPlaceholerViews {
self.updateShimmerIfNeeded()
}
if itemLayout.curveNearBounds {
} else {
if let scrollGradientLayer = self.scrollGradientLayer {
self.scrollGradientLayer = nil
scrollGradientLayer.removeFromSuperlayer()
}
}
if let topVisibleGroupId = topVisibleGroupId {
self.activeItemUpdated?.invoke((topVisibleGroupId, topVisibleSubgroupId, .immediate))
}
}
private func updateShimmerIfNeeded() {
if let standaloneShimmerEffect = self.standaloneShimmerEffect, let shimmerHostView = self.shimmerHostView {
if self.placeholdersContainerView.subviews.isEmpty {
standaloneShimmerEffect.layer = nil
} else {
standaloneShimmerEffect.layer = shimmerHostView.layer
}
}
}
private func expandGroup(groupId: AnyHashable) {
self.expandedGroupIds.insert(groupId)
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(ContentAnimation(type: .groupExpanded(id: groupId))))
}
public func pagerUpdateBackground(backgroundFrame: CGRect, transition: Transition) {
guard let component = self.component, let keyboardChildEnvironment = self.keyboardChildEnvironment else {
return
}
if let externalBackground = component.inputInteractionHolder.inputInteraction?.externalBackground, let effectContainerView = externalBackground.effectContainerView {
let mirrorContentClippingView: UIView
if let current = self.mirrorContentClippingView {
mirrorContentClippingView = current
} else {
mirrorContentClippingView = UIView()
mirrorContentClippingView.clipsToBounds = true
self.mirrorContentClippingView = mirrorContentClippingView
if let mirrorContentWarpView = self.mirrorContentWarpView {
mirrorContentClippingView.addSubview(mirrorContentWarpView)
} else {
mirrorContentClippingView.addSubview(self.mirrorContentScrollView)
}
}
let clippingFrame = CGRect(origin: CGPoint(x: 0.0, y: 42.0), size: CGSize(width: backgroundFrame.width, height: backgroundFrame.height))
transition.setPosition(view: mirrorContentClippingView, position: clippingFrame.center)
transition.setBounds(view: mirrorContentClippingView, bounds: CGRect(origin: CGPoint(x: 0.0, y: 42.0), size: clippingFrame.size))
if mirrorContentClippingView.superview !== effectContainerView {
effectContainerView.addSubview(mirrorContentClippingView)
}
} else if keyboardChildEnvironment.theme.overallDarkAppearance || component.warpContentsOnEdges {
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)
}
}
if component.warpContentsOnEdges {
self.backgroundView.isHidden = true
} else {
self.backgroundView.isHidden = false
}
self.backgroundView.updateColor(color: keyboardChildEnvironment.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: EmojiPagerContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
let previousComponent = self.component
self.component = component
self.state = state
component.inputInteractionHolder.inputInteraction?.peekBehavior?.setGestureRecognizerEnabled(view: self, isEnabled: component.itemLayoutType == .detailed, itemAtPoint: { [weak self] point in
guard let strongSelf = self else {
return nil
}
guard let item = strongSelf.item(atPoint: point), let itemLayer = strongSelf.visibleItemLayers[item.1], let file = item.0.itemFile else {
return nil
}
if itemLayer.displayPlaceholder {
return nil
}
return (item.1.groupId, itemLayer, file)
})
let keyboardChildEnvironment = environment[EntityKeyboardChildEnvironment.self].value
let pagerEnvironment = environment[PagerComponentChildEnvironment.self].value
self.keyboardChildEnvironment = keyboardChildEnvironment
self.activeItemUpdated = keyboardChildEnvironment.getContentActiveItemUpdated(component.id)
self.pagerEnvironment = pagerEnvironment
self.updateIsWarpEnabled(isEnabled: component.warpContentsOnEdges)
if let longTapRecognizer = self.longTapRecognizer {
longTapRecognizer.isEnabled = component.enableLongPress
}
if let shimmerHostView = self.shimmerHostView {
transition.setFrame(view: shimmerHostView, frame: CGRect(origin: CGPoint(), size: availableSize))
}
if let standaloneShimmerEffect = self.standaloneShimmerEffect {
let shimmerBackgroundColor = keyboardChildEnvironment.theme.chat.inputPanel.primaryTextColor.withMultipliedAlpha(0.08)
let shimmerForegroundColor = keyboardChildEnvironment.theme.list.itemBlocksBackgroundColor.withMultipliedAlpha(0.15)
standaloneShimmerEffect.update(background: shimmerBackgroundColor, foreground: shimmerForegroundColor)
}
var previousItemPositions: [VisualItemKey: CGPoint]?
var calculateUpdatedItemPositions = false
var updatedItemPositions: [VisualItemKey: CGPoint]?
let contentAnimation = transition.userData(ContentAnimation.self)
var transitionHintInstalledGroupId: AnyHashable?
var transitionHintExpandedGroupId: AnyHashable?
if let contentAnimation = contentAnimation {
switch contentAnimation.type {
case let .groupInstalled(groupId):
transitionHintInstalledGroupId = groupId
case let .groupExpanded(groupId):
transitionHintExpandedGroupId = groupId
default:
break
}
}
let _ = transitionHintExpandedGroupId
var hintDisappearingGroupFrame: (groupId: AnyHashable, frame: CGRect)?
var previousAbsoluteItemPositions: [VisualItemKey: CGPoint] = [:]
var anchorItems: [ItemLayer.Key: CGRect] = [:]
if let previousComponent = previousComponent, let previousItemLayout = self.itemLayout, previousComponent.itemGroups != component.itemGroups {
if !transition.animation.isImmediate {
var previousItemPositionsValue: [VisualItemKey: CGPoint] = [:]
for groupIndex in 0 ..< previousComponent.itemGroups.count {
let itemGroup = previousComponent.itemGroups[groupIndex]
for itemIndex in 0 ..< itemGroup.items.count {
let item = itemGroup.items[itemIndex]
let itemKey: ItemLayer.Key
itemKey = ItemLayer.Key(
groupId: itemGroup.groupId,
itemId: item.content.id
)
let itemFrame = previousItemLayout.frame(groupIndex: groupIndex, itemIndex: itemIndex)
previousItemPositionsValue[.item(id: itemKey)] = CGPoint(x: itemFrame.midX, y: itemFrame.midY)
}
}
previousItemPositions = previousItemPositionsValue
calculateUpdatedItemPositions = true
}
let effectiveVisibleBounds = CGRect(origin: self.scrollView.bounds.origin, size: self.effectiveVisibleSize)
let topVisibleDetectionBounds = effectiveVisibleBounds
for (key, itemLayer) in self.visibleItemLayers {
if !topVisibleDetectionBounds.intersects(itemLayer.frame) {
continue
}
let absoluteFrame = self.scrollView.convert(itemLayer.frame, to: self)
if let transitionHintInstalledGroupId = transitionHintInstalledGroupId, transitionHintInstalledGroupId == key.groupId {
if let hintDisappearingGroupFrameValue = hintDisappearingGroupFrame {
hintDisappearingGroupFrame = (hintDisappearingGroupFrameValue.groupId, absoluteFrame.union(hintDisappearingGroupFrameValue.frame))
} else {
hintDisappearingGroupFrame = (key.groupId, absoluteFrame)
}
previousAbsoluteItemPositions[.item(id: key)] = CGPoint(x: absoluteFrame.midX, y: absoluteFrame.midY)
} else {
anchorItems[key] = absoluteFrame
}
}
for (id, groupHeader) in self.visibleGroupHeaders {
if !topVisibleDetectionBounds.intersects(groupHeader.frame) {
continue
}
let absoluteFrame = self.scrollView.convert(groupHeader.frame, to: self)
if let transitionHintInstalledGroupId = transitionHintInstalledGroupId, transitionHintInstalledGroupId == id {
if let hintDisappearingGroupFrameValue = hintDisappearingGroupFrame {
hintDisappearingGroupFrame = (hintDisappearingGroupFrameValue.groupId, absoluteFrame.union(hintDisappearingGroupFrameValue.frame))
} else {
hintDisappearingGroupFrame = (id, absoluteFrame)
}
previousAbsoluteItemPositions[.header(groupId: id)] = CGPoint(x: absoluteFrame.midX, y: absoluteFrame.midY)
}
}
for (id, button) in self.visibleGroupExpandActionButtons {
if !topVisibleDetectionBounds.intersects(button.frame) {
continue
}
let absoluteFrame = self.scrollView.convert(button.frame, to: self)
if let transitionHintInstalledGroupId = transitionHintInstalledGroupId, transitionHintInstalledGroupId == id {
if let hintDisappearingGroupFrameValue = hintDisappearingGroupFrame {
hintDisappearingGroupFrame = (hintDisappearingGroupFrameValue.groupId, absoluteFrame.union(hintDisappearingGroupFrameValue.frame))
} else {
hintDisappearingGroupFrame = (id, absoluteFrame)
}
previousAbsoluteItemPositions[.groupExpandButton(groupId: id)] = CGPoint(x: absoluteFrame.midX, y: absoluteFrame.midY)
}
}
for (id, button) in self.visibleGroupPremiumButtons {
guard let buttonView = button.view else {
continue
}
if !topVisibleDetectionBounds.intersects(buttonView.frame) {
continue
}
let absoluteFrame = self.scrollView.convert(buttonView.frame, to: self)
if let transitionHintInstalledGroupId = transitionHintInstalledGroupId, transitionHintInstalledGroupId == id {
if let hintDisappearingGroupFrameValue = hintDisappearingGroupFrame {
hintDisappearingGroupFrame = (hintDisappearingGroupFrameValue.groupId, absoluteFrame.union(hintDisappearingGroupFrameValue.frame))
} else {
hintDisappearingGroupFrame = (id, absoluteFrame)
}
previousAbsoluteItemPositions[.groupActionButton(groupId: id)] = CGPoint(x: absoluteFrame.midX, y: absoluteFrame.midY)
}
}
}
var itemGroups: [ItemGroupDescription] = []
for itemGroup in component.itemGroups {
itemGroups.append(ItemGroupDescription(
supergroupId: itemGroup.supergroupId,
groupId: itemGroup.groupId,
hasTitle: itemGroup.title != nil,
isPremiumLocked: itemGroup.isPremiumLocked,
isFeatured: itemGroup.isFeatured,
itemCount: itemGroup.items.count,
isEmbedded: itemGroup.isEmbedded,
collapsedLineCount: itemGroup.collapsedLineCount
))
}
var itemTransition = transition
let itemLayout = ItemLayout(
layoutType: component.itemLayoutType,
width: availableSize.width,
containerInsets: UIEdgeInsets(top: pagerEnvironment.containerInsets.top + 9.0, left: pagerEnvironment.containerInsets.left, bottom: 9.0 + pagerEnvironment.containerInsets.bottom, right: pagerEnvironment.containerInsets.right),
itemGroups: itemGroups,
expandedGroupIds: self.expandedGroupIds,
curveNearBounds: component.warpContentsOnEdges,
customLayout: component.inputInteractionHolder.inputInteraction?.customLayout
)
if let previousItemLayout = self.itemLayout {
if previousItemLayout.width != itemLayout.width {
itemTransition = .immediate
} else if transition.userData(ContentAnimation.self) == nil {
itemTransition = .immediate
}
} else {
itemTransition = .immediate
}
self.itemLayout = itemLayout
self.ignoreScrolling = true
transition.setPosition(view: self.scrollView, position: CGPoint())
let previousSize = self.scrollView.bounds.size
self.scrollView.bounds = CGRect(origin: self.scrollView.bounds.origin, size: availableSize)
let warpHeight: CGFloat = 50.0
if let warpView = self.warpView {
transition.setFrame(view: warpView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize))
warpView.update(size: CGSize(width: availableSize.width, height: availableSize.height), topInset: pagerEnvironment.containerInsets.top, warpHeight: warpHeight, theme: keyboardChildEnvironment.theme, transition: transition)
}
if let mirrorContentWarpView = self.mirrorContentWarpView {
transition.setFrame(view: mirrorContentWarpView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize))
mirrorContentWarpView.update(size: CGSize(width: availableSize.width, height: availableSize.height), topInset: pagerEnvironment.containerInsets.top, warpHeight: warpHeight, theme: keyboardChildEnvironment.theme, transition: transition)
}
if availableSize.height > previousSize.height || transition.animation.isImmediate {
self.boundsChangeTrackerLayer.removeAllAnimations()
self.boundsChangeTrackerLayer.bounds = self.scrollView.bounds
self.effectiveVisibleSize = self.scrollView.bounds.size
} else {
self.effectiveVisibleSize = CGSize(width: availableSize.width, height: max(self.effectiveVisibleSize.height, availableSize.height))
transition.setBounds(layer: self.boundsChangeTrackerLayer, bounds: self.scrollView.bounds, completion: { [weak self] completed in
guard let strongSelf = self else {
return
}
let effectiveVisibleSize = strongSelf.scrollView.bounds.size
if strongSelf.effectiveVisibleSize != effectiveVisibleSize {
strongSelf.effectiveVisibleSize = effectiveVisibleSize
strongSelf.updateVisibleItems(transition: .immediate, attemptSynchronousLoads: false, previousItemPositions: nil, updatedItemPositions: nil)
}
})
}
if self.scrollView.contentSize != itemLayout.contentSize {
self.scrollView.contentSize = itemLayout.contentSize
}
var scrollIndicatorInsets = pagerEnvironment.containerInsets
if self.warpView != nil {
scrollIndicatorInsets.bottom += 20.0
}
if self.scrollView.scrollIndicatorInsets != scrollIndicatorInsets {
self.scrollView.scrollIndicatorInsets = scrollIndicatorInsets
}
self.previousScrollingOffset = ScrollingOffsetState(value: scrollView.contentOffset.y, isDraggingOrDecelerating: scrollView.isDragging || scrollView.isDecelerating)
var animatedScrollOffset: CGFloat = 0.0
if !anchorItems.isEmpty {
let sortedAnchorItems: [(ItemLayer.Key, CGRect)] = anchorItems.sorted(by: { lhs, rhs in
if lhs.value.minY != rhs.value.minY {
return lhs.value.minY < rhs.value.minY
} else {
return lhs.value.minX < rhs.value.minX
}
})
outer: for i in 0 ..< component.itemGroups.count {
for anchorItem in sortedAnchorItems {
if component.itemGroups[i].groupId != anchorItem.0.groupId {
continue
}
for j in 0 ..< component.itemGroups[i].items.count {
let itemKey: ItemLayer.Key
itemKey = ItemLayer.Key(
groupId: component.itemGroups[i].groupId,
itemId: component.itemGroups[i].items[j].content.id
)
if itemKey == anchorItem.0 {
let itemFrame = itemLayout.frame(groupIndex: i, itemIndex: j)
var contentOffsetY = itemFrame.minY - anchorItem.1.minY
if contentOffsetY > self.scrollView.contentSize.height - self.scrollView.bounds.height {
contentOffsetY = self.scrollView.contentSize.height - self.scrollView.bounds.height
}
if contentOffsetY < 0.0 {
contentOffsetY = 0.0
}
let previousBounds = self.scrollView.bounds
self.scrollView.setContentOffset(CGPoint(x: 0.0, y: contentOffsetY), animated: false)
let scrollOffset = previousBounds.minY - contentOffsetY
transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: scrollOffset), to: CGPoint(), additive: true)
animatedScrollOffset = scrollOffset
break outer
}
}
}
}
}
self.ignoreScrolling = false
if calculateUpdatedItemPositions {
var updatedItemPositionsValue: [VisualItemKey: CGPoint] = [:]
for groupIndex in 0 ..< component.itemGroups.count {
let itemGroup = component.itemGroups[groupIndex]
let itemGroupLayout = itemLayout.itemGroupLayouts[groupIndex]
for itemIndex in 0 ..< itemGroup.items.count {
let item = itemGroup.items[itemIndex]
let itemKey: ItemLayer.Key
itemKey = ItemLayer.Key(
groupId: itemGroup.groupId,
itemId: item.content.id
)
let itemFrame = itemLayout.frame(groupIndex: groupIndex, itemIndex: itemIndex)
updatedItemPositionsValue[.item(id: itemKey)] = CGPoint(x: itemFrame.midX, y: itemFrame.midY)
}
let groupPremiumButtonFrame = CGRect(origin: CGPoint(x: itemLayout.itemInsets.left, y: itemGroupLayout.frame.maxY - itemLayout.premiumButtonHeight + 1.0), size: CGSize(width: itemLayout.width - itemLayout.itemInsets.left - itemLayout.itemInsets.right, height: itemLayout.premiumButtonHeight))
updatedItemPositionsValue[.groupActionButton(groupId: itemGroup.groupId)] = CGPoint(x: groupPremiumButtonFrame.midX, y: groupPremiumButtonFrame.midY)
}
updatedItemPositions = updatedItemPositionsValue
}
if let hintDisappearingGroupFrameValue = hintDisappearingGroupFrame {
hintDisappearingGroupFrame = (hintDisappearingGroupFrameValue.groupId, self.scrollView.convert(hintDisappearingGroupFrameValue.frame, from: self))
}
for (id, position) in previousAbsoluteItemPositions {
previousAbsoluteItemPositions[id] = position.offsetBy(dx: 0.0, dy: animatedScrollOffset)
}
var attemptSynchronousLoads = !(scrollView.isDragging || scrollView.isDecelerating)
if let synchronousLoadBehavior = transition.userData(SynchronousLoadBehavior.self) {
if synchronousLoadBehavior.isDisabled {
attemptSynchronousLoads = false
}
}
self.updateVisibleItems(transition: itemTransition, attemptSynchronousLoads: attemptSynchronousLoads, previousItemPositions: previousItemPositions, previousAbsoluteItemPositions: previousAbsoluteItemPositions, updatedItemPositions: updatedItemPositions, hintDisappearingGroupFrame: hintDisappearingGroupFrame)
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)
}
private static func hasPremium(context: AccountContext, chatPeerId: EnginePeer.Id?, premiumIfSavedMessages: Bool) -> Signal<Bool, NoError> {
let hasPremium: Signal<Bool, NoError>
if premiumIfSavedMessages, let chatPeerId = chatPeerId, chatPeerId == context.account.peerId {
hasPremium = .single(true)
} else {
hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> map { peer -> Bool in
guard case let .user(user) = peer else {
return false
}
return user.isPremium
}
|> distinctUntilChanged
}
return hasPremium
}
public static func emojiInputData(
context: AccountContext,
animationCache: AnimationCache,
animationRenderer: MultiAnimationRenderer,
isStandalone: Bool,
isStatusSelection: Bool,
isReactionSelection: Bool,
isQuickReactionSelection: Bool = false,
topReactionItems: [EmojiComponentReactionItem],
areUnicodeEmojiEnabled: Bool,
areCustomEmojiEnabled: Bool,
chatPeerId: EnginePeer.Id?,
selectedItems: Set<MediaId> = Set(),
topStatusTitle: String? = nil
) -> Signal<EmojiPagerContentComponent, NoError> {
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
let isPremiumDisabled = premiumConfiguration.isPremiumDisabled
let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings
var orderedItemListCollectionIds: [Int32] = []
orderedItemListCollectionIds.append(Namespaces.OrderedItemList.LocalRecentEmoji)
var iconStatusEmoji: Signal<[TelegramMediaFile], NoError> = .single([])
if isStatusSelection {
orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudFeaturedStatusEmoji)
orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudRecentStatusEmoji)
iconStatusEmoji = context.engine.stickers.loadedStickerPack(reference: .iconStatusEmoji, forceActualized: false)
|> map { result -> [TelegramMediaFile] in
switch result {
case let .result(_, items, _):
return items.map(\.file)
default:
return []
}
}
|> take(1)
} else if isReactionSelection {
orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudTopReactions)
orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudRecentReactions)
}
let availableReactions: Signal<AvailableReactions?, NoError>
if isReactionSelection {
availableReactions = context.engine.stickers.availableReactions()
} else {
availableReactions = .single(nil)
}
let emojiItems: Signal<EmojiPagerContentComponent, NoError> = combineLatest(
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: orderedItemListCollectionIds, namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000),
hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: true),
context.account.viewTracker.featuredEmojiPacks(),
availableReactions,
iconStatusEmoji
)
|> map { view, hasPremium, featuredEmojiPacks, availableReactions, iconStatusEmoji -> EmojiPagerContentComponent in
struct ItemGroup {
var supergroupId: AnyHashable
var id: AnyHashable
var title: String?
var subtitle: String?
var isPremiumLocked: Bool
var isFeatured: Bool
var collapsedLineCount: Int?
var isClearable: Bool
var headerItem: EntityKeyboardAnimationData?
var items: [EmojiPagerContentComponent.Item]
}
var itemGroups: [ItemGroup] = []
var itemGroupIndexById: [AnyHashable: Int] = [:]
var recentEmoji: OrderedItemListView?
var featuredStatusEmoji: OrderedItemListView?
var recentStatusEmoji: OrderedItemListView?
var topReactions: OrderedItemListView?
var recentReactions: OrderedItemListView?
for orderedView in view.orderedItemListsViews {
if orderedView.collectionId == Namespaces.OrderedItemList.LocalRecentEmoji {
recentEmoji = orderedView
} else if orderedView.collectionId == Namespaces.OrderedItemList.CloudFeaturedStatusEmoji {
featuredStatusEmoji = orderedView
} else if orderedView.collectionId == Namespaces.OrderedItemList.CloudRecentStatusEmoji {
recentStatusEmoji = orderedView
} else if orderedView.collectionId == Namespaces.OrderedItemList.CloudRecentReactions {
recentReactions = orderedView
} else if orderedView.collectionId == Namespaces.OrderedItemList.CloudTopReactions {
topReactions = orderedView
}
}
if isStatusSelection {
let resultItem = EmojiPagerContentComponent.Item(
animationData: nil,
content: .icon(.premiumStar),
itemFile: nil,
subgroupId: nil,
icon: .none,
accentTint: false
)
let groupId = "recent"
if let groupIndex = itemGroupIndexById[groupId] {
itemGroups[groupIndex].items.append(resultItem)
} else {
itemGroupIndexById[groupId] = itemGroups.count
//TODO:localize
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: topStatusTitle?.uppercased(), subtitle: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: 5, isClearable: false, headerItem: nil, items: [resultItem]))
}
var existingIds = Set<MediaId>()
for file in iconStatusEmoji {
if existingIds.contains(file.fileId) {
continue
}
existingIds.insert(file.fileId)
var accentTint = false
for attribute in file.attributes {
if case let .CustomEmoji(_, _, packReference) = attribute {
switch packReference {
case let .id(id, _):
if id == 773947703670341676 || id == 2964141614563343 {
accentTint = true
}
default:
break
}
}
}
let resultItem: EmojiPagerContentComponent.Item
let animationData = EntityKeyboardAnimationData(file: file)
resultItem = EmojiPagerContentComponent.Item(
animationData: animationData,
content: .animation(animationData),
itemFile: file,
subgroupId: nil,
icon: .none,
accentTint: accentTint
)
if let groupIndex = itemGroupIndexById[groupId] {
itemGroups[groupIndex].items.append(resultItem)
}
}
if let recentStatusEmoji = recentStatusEmoji {
for item in recentStatusEmoji.items {
guard let item = item.contents.get(RecentMediaItem.self) else {
continue
}
let file = item.media
if existingIds.contains(file.fileId) {
continue
}
existingIds.insert(file.fileId)
var accentTint = false
for attribute in file.attributes {
if case let .CustomEmoji(_, _, packReference) = attribute {
switch packReference {
case let .id(id, _):
if id == 773947703670341676 || id == 2964141614563343 {
accentTint = true
}
default:
break
}
}
}
let resultItem: EmojiPagerContentComponent.Item
let animationData = EntityKeyboardAnimationData(file: file)
resultItem = EmojiPagerContentComponent.Item(
animationData: animationData,
content: .animation(animationData),
itemFile: file,
subgroupId: nil,
icon: .none,
accentTint: accentTint
)
if let groupIndex = itemGroupIndexById[groupId] {
itemGroups[groupIndex].items.append(resultItem)
}
}
}
if let featuredStatusEmoji = featuredStatusEmoji {
for item in featuredStatusEmoji.items.prefix(7) {
guard let item = item.contents.get(RecentMediaItem.self) else {
continue
}
let file = item.media
if existingIds.contains(file.fileId) {
continue
}
existingIds.insert(file.fileId)
let resultItem: EmojiPagerContentComponent.Item
var accentTint = false
for attribute in file.attributes {
if case let .CustomEmoji(_, _, packReference) = attribute {
switch packReference {
case let .id(id, _):
if id == 773947703670341676 || id == 2964141614563343 {
accentTint = true
}
default:
break
}
}
}
let animationData = EntityKeyboardAnimationData(file: file)
resultItem = EmojiPagerContentComponent.Item(
animationData: animationData,
content: .animation(animationData),
itemFile: file,
subgroupId: nil,
icon: .none,
accentTint: accentTint
)
if let groupIndex = itemGroupIndexById[groupId] {
itemGroups[groupIndex].items.append(resultItem)
}
}
}
} else if isReactionSelection {
var existingIds = Set<MessageReaction.Reaction>()
var topReactionItems = topReactionItems
if topReactionItems.isEmpty {
if let topReactions = topReactions {
for item in topReactions.items {
guard let topReaction = item.contents.get(RecentReactionItem.self) else {
continue
}
switch topReaction.content {
case let .builtin(value):
if let reaction = availableReactions?.reactions.first(where: { $0.value == .builtin(value) }) {
topReactionItems.append(EmojiComponentReactionItem(reaction: .builtin(value), file: reaction.selectAnimation))
} else {
continue
}
case let .custom(file):
topReactionItems.append(EmojiComponentReactionItem(reaction: .custom(file.fileId.id), file: file))
}
}
}
}
let maxTopLineCount: Int
if hasPremium {
maxTopLineCount = 2
} else {
maxTopLineCount = 5
}
for reactionItem in topReactionItems {
if existingIds.contains(reactionItem.reaction) {
continue
}
existingIds.insert(reactionItem.reaction)
let icon: EmojiPagerContentComponent.Item.Icon
if !hasPremium, case .custom = reactionItem.reaction {
icon = .locked
} else {
icon = .none
}
let animationFile = reactionItem.file
let animationData = EntityKeyboardAnimationData(file: animationFile, isReaction: true)
let resultItem = EmojiPagerContentComponent.Item(
animationData: animationData,
content: .animation(animationData),
itemFile: animationFile,
subgroupId: nil,
icon: icon,
accentTint: false
)
let groupId = "recent"
if let groupIndex = itemGroupIndexById[groupId] {
itemGroups[groupIndex].items.append(resultItem)
if itemGroups[groupIndex].items.count >= 8 * maxTopLineCount {
break
}
} else {
itemGroupIndexById[groupId] = itemGroups.count
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: nil, isClearable: false, headerItem: nil, items: [resultItem]))
}
}
var hasRecent = false
if let recentReactions = recentReactions, !recentReactions.items.isEmpty {
hasRecent = true
}
let maxRecentLineCount: Int
if hasPremium {
maxRecentLineCount = 10
} else {
maxRecentLineCount = 5
}
//TODO:localize
let popularTitle = hasRecent ? "Recently Used" : "Popular"
if let availableReactions = availableReactions {
for reactionItem in availableReactions.reactions {
if !reactionItem.isEnabled {
continue
}
if existingIds.contains(reactionItem.value) {
continue
}
existingIds.insert(reactionItem.value)
let icon: EmojiPagerContentComponent.Item.Icon
if !hasPremium, case .custom = reactionItem.value {
icon = .locked
} else {
icon = .none
}
let animationFile = reactionItem.selectAnimation
let animationData = EntityKeyboardAnimationData(file: animationFile, isReaction: true)
let resultItem = EmojiPagerContentComponent.Item(
animationData: animationData,
content: .animation(animationData),
itemFile: animationFile,
subgroupId: nil,
icon: icon,
accentTint: false
)
if hasPremium {
let groupId = "popular"
if let groupIndex = itemGroupIndexById[groupId] {
itemGroups[groupIndex].items.append(resultItem)
} else {
itemGroupIndexById[groupId] = itemGroups.count
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: popularTitle, subtitle: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: nil, isClearable: hasRecent && !isQuickReactionSelection, headerItem: nil, items: [resultItem]))
}
} else {
let groupId = "recent"
if let groupIndex = itemGroupIndexById[groupId] {
itemGroups[groupIndex].items.append(resultItem)
if itemGroups[groupIndex].items.count >= maxRecentLineCount * 8 {
break
}
} else {
itemGroupIndexById[groupId] = itemGroups.count
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: nil, isClearable: false, headerItem: nil, items: [resultItem]))
}
}
}
}
if let recentReactions = recentReactions {
var popularInsertIndex = 0
for item in recentReactions.items {
guard let item = item.contents.get(RecentReactionItem.self) else {
continue
}
let animationFile: TelegramMediaFile
let icon: EmojiPagerContentComponent.Item.Icon
switch item.content {
case let .builtin(value):
if existingIds.contains(.builtin(value)) {
continue
}
existingIds.insert(.builtin(value))
if let availableReactions = availableReactions, let availableReaction = availableReactions.reactions.first(where: { $0.value == .builtin(value) }) {
if let centerAnimation = availableReaction.centerAnimation {
animationFile = centerAnimation
} else {
continue
}
} else {
continue
}
icon = .none
case let .custom(file):
if existingIds.contains(.custom(file.fileId.id)) {
continue
}
existingIds.insert(.custom(file.fileId.id))
animationFile = file
if !hasPremium {
icon = .locked
} else {
icon = .none
}
}
let animationData = EntityKeyboardAnimationData(file: animationFile, isReaction: true)
let resultItem = EmojiPagerContentComponent.Item(
animationData: animationData,
content: .animation(animationData),
itemFile: animationFile,
subgroupId: nil,
icon: icon,
accentTint: false
)
let groupId = "popular"
if let groupIndex = itemGroupIndexById[groupId] {
if itemGroups[groupIndex].items.count + 1 >= maxRecentLineCount * 8 {
break
}
itemGroups[groupIndex].items.insert(resultItem, at: popularInsertIndex)
popularInsertIndex += 1
} else {
itemGroupIndexById[groupId] = itemGroups.count
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: popularTitle, subtitle: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: nil, isClearable: hasRecent && !isQuickReactionSelection, headerItem: nil, items: [resultItem]))
}
}
}
}
if let recentEmoji = recentEmoji, !isReactionSelection, !isStatusSelection {
for item in recentEmoji.items {
guard let item = item.contents.get(RecentEmojiItem.self) else {
continue
}
if case let .file(file) = item.content, isPremiumDisabled, file.isPremiumEmoji {
continue
}
if !areCustomEmojiEnabled, case .file = item.content {
continue
}
let resultItem: EmojiPagerContentComponent.Item
switch item.content {
case let .file(file):
let animationData = EntityKeyboardAnimationData(file: file)
resultItem = EmojiPagerContentComponent.Item(
animationData: animationData,
content: .animation(animationData),
itemFile: file,
subgroupId: nil,
icon: .none,
accentTint: false
)
case let .text(text):
resultItem = EmojiPagerContentComponent.Item(
animationData: nil,
content: .staticEmoji(text),
itemFile: nil,
subgroupId: nil,
icon: .none,
accentTint: false
)
}
let groupId = "recent"
if let groupIndex = itemGroupIndexById[groupId] {
itemGroups[groupIndex].items.append(resultItem)
} else {
itemGroupIndexById[groupId] = itemGroups.count
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.Emoji_FrequentlyUsed, subtitle: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: nil, isClearable: true, headerItem: nil, items: [resultItem]))
}
}
}
if areUnicodeEmojiEnabled {
for (subgroupId, list) in staticEmojiMapping {
let groupId: AnyHashable = "static"
for emojiString in list {
let resultItem = EmojiPagerContentComponent.Item(
animationData: nil,
content: .staticEmoji(emojiString),
itemFile: nil,
subgroupId: subgroupId.rawValue,
icon: .none,
accentTint: false
)
if let groupIndex = itemGroupIndexById[groupId] {
itemGroups[groupIndex].items.append(resultItem)
} else {
itemGroupIndexById[groupId] = itemGroups.count
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.EmojiInput_SectionTitleEmoji, subtitle: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: nil, isClearable: false, headerItem: nil, items: [resultItem]))
}
}
}
}
var installedCollectionIds = Set<ItemCollectionId>()
for (id, _, _) in view.collectionInfos {
installedCollectionIds.insert(id)
}
if areCustomEmojiEnabled {
for entry in view.entries {
guard let item = entry.item as? StickerPackItem else {
continue
}
var icon: EmojiPagerContentComponent.Item.Icon = .none
if isReactionSelection, !hasPremium {
icon = .locked
}
let animationData = EntityKeyboardAnimationData(file: item.file)
let resultItem = EmojiPagerContentComponent.Item(
animationData: animationData,
content: .animation(animationData),
itemFile: item.file,
subgroupId: nil,
icon: icon,
accentTint: false
)
let supergroupId = entry.index.collectionId
let groupId: AnyHashable = supergroupId
let isPremiumLocked: Bool = item.file.isPremiumEmoji && !hasPremium
if isPremiumLocked && isPremiumDisabled {
continue
}
if let groupIndex = itemGroupIndexById[groupId] {
itemGroups[groupIndex].items.append(resultItem)
} else {
itemGroupIndexById[groupId] = itemGroups.count
var title = ""
var headerItem: EntityKeyboardAnimationData?
inner: for (id, info, _) in view.collectionInfos {
if id == entry.index.collectionId, let info = info as? StickerPackCollectionInfo {
title = info.title
if let thumbnail = info.thumbnail {
let type: EntityKeyboardAnimationData.ItemType
if item.file.isAnimatedSticker {
type = .lottie
} else if item.file.isVideoEmoji || item.file.isVideoSticker {
type = .video
} else {
type = .still
}
headerItem = EntityKeyboardAnimationData(
id: .stickerPackThumbnail(info.id),
type: type,
resource: .stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource),
dimensions: thumbnail.dimensions.cgSize,
immediateThumbnailData: info.immediateThumbnailData,
isReaction: false
)
}
break inner
}
}
itemGroups.append(ItemGroup(supergroupId: supergroupId, id: groupId, title: title, subtitle: nil, isPremiumLocked: isPremiumLocked, isFeatured: false, collapsedLineCount: nil, isClearable: false, headerItem: headerItem, items: [resultItem]))
}
}
if !isStandalone {
for featuredEmojiPack in featuredEmojiPacks {
if installedCollectionIds.contains(featuredEmojiPack.info.id) {
continue
}
for item in featuredEmojiPack.topItems {
let animationData = EntityKeyboardAnimationData(file: item.file)
let resultItem = EmojiPagerContentComponent.Item(
animationData: animationData,
content: .animation(animationData),
itemFile: item.file,
subgroupId: nil,
icon: .none,
accentTint: false
)
let supergroupId = featuredEmojiPack.info.id
let groupId: AnyHashable = supergroupId
let isPremiumLocked: Bool = item.file.isPremiumEmoji && !hasPremium
if isPremiumLocked && isPremiumDisabled {
continue
}
if let groupIndex = itemGroupIndexById[groupId] {
itemGroups[groupIndex].items.append(resultItem)
} else {
itemGroupIndexById[groupId] = itemGroups.count
var headerItem: EntityKeyboardAnimationData?
if let thumbnailFileId = featuredEmojiPack.info.thumbnailFileId, let file = featuredEmojiPack.topItems.first(where: { $0.file.fileId.id == thumbnailFileId }) {
headerItem = EntityKeyboardAnimationData(file: file.file)
} else if let thumbnail = featuredEmojiPack.info.thumbnail {
let info = featuredEmojiPack.info
let type: EntityKeyboardAnimationData.ItemType
if item.file.isAnimatedSticker {
type = .lottie
} else if item.file.isVideoEmoji || item.file.isVideoSticker {
type = .video
} else {
type = .still
}
headerItem = EntityKeyboardAnimationData(
id: .stickerPackThumbnail(info.id),
type: type,
resource: .stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource),
dimensions: thumbnail.dimensions.cgSize,
immediateThumbnailData: info.immediateThumbnailData,
isReaction: false
)
}
itemGroups.append(ItemGroup(supergroupId: supergroupId, id: groupId, title: featuredEmojiPack.info.title, subtitle: nil, isPremiumLocked: isPremiumLocked, isFeatured: true, collapsedLineCount: 3, isClearable: false, headerItem: headerItem, items: [resultItem]))
}
}
}
}
}
return EmojiPagerContentComponent(
id: "emoji",
context: context,
avatarPeer: nil,
animationCache: animationCache,
animationRenderer: animationRenderer,
inputInteractionHolder: EmojiPagerContentComponent.InputInteractionHolder(),
itemGroups: itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in
var headerItem = group.headerItem
if let groupId = group.id.base as? ItemCollectionId {
outer: for (id, info, _) in view.collectionInfos {
if id == groupId, let info = info as? StickerPackCollectionInfo {
if let thumbnailFileId = info.thumbnailFileId {
for item in group.items {
if let itemFile = item.itemFile, itemFile.fileId.id == thumbnailFileId {
headerItem = EntityKeyboardAnimationData(file: itemFile)
break outer
}
}
}
}
}
}
return EmojiPagerContentComponent.ItemGroup(
supergroupId: group.supergroupId,
groupId: group.id,
title: group.title,
subtitle: group.subtitle,
actionButtonTitle: nil,
isFeatured: group.isFeatured,
isPremiumLocked: group.isPremiumLocked,
isEmbedded: false,
hasClear: group.isClearable,
collapsedLineCount: group.collapsedLineCount,
displayPremiumBadges: false,
headerItem: headerItem,
items: group.items
)
},
itemLayoutType: .compact,
warpContentsOnEdges: isReactionSelection || isStatusSelection,
enableLongPress: (isReactionSelection && !isQuickReactionSelection) || isStatusSelection,
selectedItems: selectedItems
)
}
return emojiItems
}
}