Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,18 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ActivityIndicatorComponent",
module_name = "ActivityIndicatorComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/ComponentFlow:ComponentFlow",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,50 @@
import Foundation
import UIKit
import ComponentFlow
public final class ActivityIndicatorComponent: Component {
public let color: UIColor
public init(
color: UIColor
) {
self.color = color
}
public static func ==(lhs: ActivityIndicatorComponent, rhs: ActivityIndicatorComponent) -> Bool {
if lhs.color != rhs.color {
return false
}
return true
}
public final class View: UIActivityIndicatorView {
public init() {
super.init(style: UIActivityIndicatorView.Style.large)
}
required public init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: ActivityIndicatorComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
if component.color != self.color {
self.color = component.color
}
if !self.isAnimating {
self.startAnimating()
}
return CGSize(width: 22.0, height: 22.0)
}
}
public func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
@@ -0,0 +1,22 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AnimatedStickerComponent",
module_name = "AnimatedStickerComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer",
"//submodules/TelegramCore:TelegramCore",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,151 @@
import Foundation
import UIKit
import ComponentFlow
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import HierarchyTrackingLayer
import TelegramCore
public final class AnimatedStickerComponent: Component {
public struct Animation: Equatable {
public enum Source: Equatable {
case bundle(name: String)
case file(media: TelegramMediaFile)
}
public var source: Source
public var scale: CGFloat
public var loop: Bool
public init(source: Source, scale: CGFloat = 2.0, loop: Bool) {
self.source = source
self.scale = scale
self.loop = loop
}
}
public let account: Account
public let animation: Animation
public var tintColor: UIColor?
public let isAnimating: Bool
public let size: CGSize
public init(account: Account, animation: Animation, tintColor: UIColor? = nil, isAnimating: Bool = true, size: CGSize) {
self.account = account
self.animation = animation
self.tintColor = tintColor
self.isAnimating = isAnimating
self.size = size
}
public static func ==(lhs: AnimatedStickerComponent, rhs: AnimatedStickerComponent) -> Bool {
if lhs.account !== rhs.account {
return false
}
if lhs.animation != rhs.animation {
return false
}
if lhs.tintColor != rhs.tintColor {
return false
}
if lhs.isAnimating != rhs.isAnimating {
return false
}
if lhs.size != rhs.size {
return false
}
return true
}
public final class View: UIView {
private var component: AnimatedStickerComponent?
private var animationNode: AnimatedStickerNode?
private let hierarchyTrackingLayer: HierarchyTrackingLayer
private var isInHierarchy: Bool = false
override init(frame: CGRect) {
self.hierarchyTrackingLayer = HierarchyTrackingLayer()
super.init(frame: frame)
self.layer.addSublayer(self.hierarchyTrackingLayer)
self.hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.isInHierarchy = true
strongSelf.animationNode?.visibility = true
}
self.hierarchyTrackingLayer.didExitHierarchy = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.isInHierarchy = false
strongSelf.animationNode?.visibility = false
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: AnimatedStickerComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
if self.component?.animation != component.animation {
self.animationNode?.view.removeFromSuperview()
let animationNode = DefaultAnimatedStickerNodeImpl()
let source: AnimatedStickerNodeSource
switch component.animation.source {
case let .bundle(name):
source = AnimatedStickerNodeLocalFileSource(name: name)
case let .file(media):
source = AnimatedStickerResourceSource(account: component.account, resource: media.resource, fitzModifier: nil, isVideo: false)
}
var playbackMode: AnimatedStickerPlaybackMode = .still(.start)
if component.animation.loop {
playbackMode = .loop
} else if component.isAnimating {
playbackMode = .once
} else {
animationNode.autoplay = true
}
animationNode.setup(source: source, width: Int(component.size.width * component.animation.scale), height: Int(component.size.height * component.animation.scale), playbackMode: playbackMode, mode: .direct(cachePathPrefix: nil))
animationNode.visibility = self.isInHierarchy
self.animationNode = animationNode
self.addSubnode(animationNode)
}
self.animationNode?.setOverlayColor(component.tintColor, replace: true, animated: false)
if !component.animation.loop && component.isAnimating != self.component?.isAnimating {
if component.isAnimating {
let _ = self.animationNode?.playIfNeeded()
}
}
self.component = component
let animationSize = component.size
let size = CGSize(width: min(animationSize.width, availableSize.width), height: min(animationSize.height, availableSize.height))
if let animationNode = self.animationNode {
animationNode.frame = CGRect(origin: CGPoint(x: floor((size.width - animationSize.width) / 2.0), y: floor((size.height - animationSize.height) / 2.0)), size: animationSize)
animationNode.updateLayout(size: animationSize)
}
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
@@ -0,0 +1,20 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "BalancedTextComponent",
module_name = "BalancedTextComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/Markdown:Markdown",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,226 @@
import Foundation
import UIKit
import ComponentFlow
import Display
import Markdown
public final class BalancedTextComponent: Component {
public enum TextContent: Equatable {
case plain(NSAttributedString)
case markdown(text: String, attributes: MarkdownAttributes)
}
public let text: TextContent
public let balanced: Bool
public let horizontalAlignment: NSTextAlignment
public let verticalAlignment: TextVerticalAlignment
public let truncationType: CTLineTruncationType
public let maximumNumberOfLines: Int
public let lineSpacing: CGFloat
public let cutout: TextNodeCutout?
public let insets: UIEdgeInsets
public let tintColor: UIColor?
public let textShadowColor: UIColor?
public let textShadowBlur: CGFloat?
public let textStroke: (UIColor, CGFloat)?
public let highlightColor: UIColor?
public let highlightInset: UIEdgeInsets
public let highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)?
public let tapAction: (([NSAttributedString.Key: Any], Int) -> Void)?
public let longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)?
public init(
text: TextContent,
balanced: Bool = true,
horizontalAlignment: NSTextAlignment = .natural,
verticalAlignment: TextVerticalAlignment = .top,
truncationType: CTLineTruncationType = .end,
maximumNumberOfLines: Int = 1,
lineSpacing: CGFloat = 0.0,
cutout: TextNodeCutout? = nil,
insets: UIEdgeInsets = UIEdgeInsets(),
tintColor: UIColor? = nil,
textShadowColor: UIColor? = nil,
textShadowBlur: CGFloat? = nil,
textStroke: (UIColor, CGFloat)? = nil,
highlightColor: UIColor? = nil,
highlightInset: UIEdgeInsets = .zero,
highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? = nil,
tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil,
longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil
) {
self.text = text
self.balanced = balanced
self.horizontalAlignment = horizontalAlignment
self.verticalAlignment = verticalAlignment
self.truncationType = truncationType
self.maximumNumberOfLines = maximumNumberOfLines
self.lineSpacing = lineSpacing
self.cutout = cutout
self.insets = insets
self.tintColor = tintColor
self.textShadowColor = textShadowColor
self.textShadowBlur = textShadowBlur
self.textStroke = textStroke
self.highlightColor = highlightColor
self.highlightInset = highlightInset
self.highlightAction = highlightAction
self.tapAction = tapAction
self.longTapAction = longTapAction
}
public static func ==(lhs: BalancedTextComponent, rhs: BalancedTextComponent) -> Bool {
if lhs.text != rhs.text {
return false
}
if lhs.balanced != rhs.balanced {
return false
}
if lhs.horizontalAlignment != rhs.horizontalAlignment {
return false
}
if lhs.verticalAlignment != rhs.verticalAlignment {
return false
}
if lhs.truncationType != rhs.truncationType {
return false
}
if lhs.maximumNumberOfLines != rhs.maximumNumberOfLines {
return false
}
if lhs.lineSpacing != rhs.lineSpacing {
return false
}
if lhs.cutout != rhs.cutout {
return false
}
if lhs.insets != rhs.insets {
return false
}
if lhs.tintColor != rhs.tintColor {
return false
}
if let lhsTextShadowColor = lhs.textShadowColor, let rhsTextShadowColor = rhs.textShadowColor {
if !lhsTextShadowColor.isEqual(rhsTextShadowColor) {
return false
}
} else if (lhs.textShadowColor != nil) != (rhs.textShadowColor != nil) {
return false
}
if lhs.textShadowBlur != rhs.textShadowBlur {
return false
}
if let lhsTextStroke = lhs.textStroke, let rhsTextStroke = rhs.textStroke {
if !lhsTextStroke.0.isEqual(rhsTextStroke.0) {
return false
}
if lhsTextStroke.1 != rhsTextStroke.1 {
return false
}
} else if (lhs.textShadowColor != nil) != (rhs.textShadowColor != nil) {
return false
}
if let lhsHighlightColor = lhs.highlightColor, let rhsHighlightColor = rhs.highlightColor {
if !lhsHighlightColor.isEqual(rhsHighlightColor) {
return false
}
} else if (lhs.highlightColor != nil) != (rhs.highlightColor != nil) {
return false
}
if lhs.highlightInset != rhs.highlightInset {
return false
}
return true
}
public final class View: UIView {
private let textView: ImmediateTextView
override public init(frame: CGRect) {
self.textView = ImmediateTextView()
super.init(frame: frame)
self.addSubview(self.textView)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func attributeSubstring(name: String, index: Int) -> (String, String)? {
return self.textView.attributeSubstring(name: name, index: index)
}
public func update(component: BalancedTextComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
let attributedString: NSAttributedString
switch component.text {
case let .plain(string):
attributedString = string
case let .markdown(text, attributes):
attributedString = parseMarkdownIntoAttributedString(text, attributes: attributes)
}
self.textView.attributedText = attributedString
self.textView.maximumNumberOfLines = component.maximumNumberOfLines
self.textView.truncationType = component.truncationType
self.textView.textAlignment = component.horizontalAlignment
self.textView.verticalAlignment = component.verticalAlignment
self.textView.lineSpacing = component.lineSpacing
self.textView.cutout = component.cutout
self.textView.insets = component.insets
self.textView.textShadowColor = component.textShadowColor
self.textView.textShadowBlur = component.textShadowBlur
self.textView.textStroke = component.textStroke
self.textView.linkHighlightColor = component.highlightColor
self.textView.linkHighlightInset = component.highlightInset
self.textView.highlightAttributeAction = component.highlightAction
self.textView.tapAttributeAction = component.tapAction
self.textView.longTapAttributeAction = component.longTapAction
var bestSize: (availableWidth: CGFloat, info: TextNodeLayout)
let info = self.textView.updateLayoutFullInfo(availableSize)
bestSize = (availableSize.width, info)
if component.balanced && info.numberOfLines > 1 {
let measureIncrement = 8.0
var measureWidth = info.size.width
measureWidth -= measureIncrement
while measureWidth > 0.0 {
let otherInfo = self.textView.updateLayoutFullInfo(CGSize(width: measureWidth, height: availableSize.height))
if otherInfo.numberOfLines > bestSize.info.numberOfLines {
break
}
if (otherInfo.size.width - otherInfo.trailingLineWidth) < (bestSize.info.size.width - bestSize.info.trailingLineWidth) {
bestSize = (measureWidth, otherInfo)
}
measureWidth -= measureIncrement
}
let bestInfo = self.textView.updateLayoutFullInfo(CGSize(width: bestSize.availableWidth, height: availableSize.height))
bestSize = (availableSize.width, bestInfo)
}
if let tintColor = component.tintColor {
transition.setTintColor(layer: self.textView.layer, color: tintColor)
}
self.textView.frame = CGRect(origin: CGPoint(), size: bestSize.info.size)
return bestSize.info.size
}
}
public func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
@@ -0,0 +1,20 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "BlurredBackgroundComponent",
module_name = "BlurredBackgroundComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,62 @@
import Foundation
import UIKit
import ComponentFlow
import Display
import ComponentDisplayAdapters
public final class BlurredBackgroundComponent: Component {
public let color: UIColor
public let tintContainerView: UIView?
public let cornerRadius: CGFloat
public init(
color: UIColor,
tintContainerView: UIView? = nil,
cornerRadius: CGFloat = 0.0
) {
self.color = color
self.tintContainerView = tintContainerView
self.cornerRadius = cornerRadius
}
public static func ==(lhs: BlurredBackgroundComponent, rhs: BlurredBackgroundComponent) -> Bool {
if lhs.color != rhs.color {
return false
}
if lhs.tintContainerView !== rhs.tintContainerView {
return false
}
if lhs.cornerRadius != rhs.cornerRadius {
return false
}
return true
}
public final class View: BlurredBackgroundView {
private var tintContainerView: UIView?
private var vibrancyEffectView: UIVisualEffectView?
public func update(component: BlurredBackgroundComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
self.updateColor(color: component.color, forceKeepBlur: true, transition: transition.containedViewLayoutTransition)
self.update(size: availableSize, cornerRadius: component.cornerRadius, transition: transition.containedViewLayoutTransition)
if let tintContainerView = self.tintContainerView {
transition.setFrame(view: tintContainerView, frame: CGRect(origin: CGPoint(), size: availableSize))
}
if let vibrancyEffectView = self.vibrancyEffectView {
transition.setFrame(view: vibrancyEffectView, frame: CGRect(origin: CGPoint(), size: availableSize))
}
return availableSize
}
}
public func makeView() -> View {
return View(color: nil, enableBlur: true)
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
@@ -0,0 +1,20 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "BundleIconComponent",
module_name = "BundleIconComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/AppBundle:AppBundle",
"//submodules/Display:Display",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,108 @@
import Foundation
import UIKit
import ComponentFlow
import AppBundle
import Display
public final class BundleIconComponent: Component {
public let name: String
public let tintColor: UIColor?
public let maxSize: CGSize?
public let scaleFactor: CGFloat
public let shadowColor: UIColor?
public let shadowBlur: CGFloat
public let flipVertically: Bool
public init(name: String, tintColor: UIColor?, maxSize: CGSize? = nil, scaleFactor: CGFloat = 1.0, shadowColor: UIColor? = nil, shadowBlur: CGFloat = 0.0, flipVertically: Bool = false) {
self.name = name
self.tintColor = tintColor
self.maxSize = maxSize
self.scaleFactor = scaleFactor
self.shadowColor = shadowColor
self.shadowBlur = shadowBlur
self.flipVertically = flipVertically
}
public static func ==(lhs: BundleIconComponent, rhs: BundleIconComponent) -> Bool {
if lhs.name != rhs.name {
return false
}
if lhs.tintColor != rhs.tintColor {
return false
}
if lhs.maxSize != rhs.maxSize {
return false
}
if lhs.scaleFactor != rhs.scaleFactor {
return false
}
if lhs.shadowColor != rhs.shadowColor {
return false
}
if lhs.shadowBlur != rhs.shadowBlur {
return false
}
if lhs.flipVertically != rhs.flipVertically {
return false
}
return true
}
public final class View: UIImageView {
private var component: BundleIconComponent?
override init(frame: CGRect) {
super.init(frame: frame)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: BundleIconComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
if self.component?.name != component.name || self.component?.tintColor != component.tintColor || self.component?.shadowColor != component.shadowColor || self.component?.shadowBlur != component.shadowBlur || self.component?.flipVertically != component.flipVertically {
var image: UIImage?
if let tintColor = component.tintColor {
image = generateTintedImage(image: UIImage(bundleImageName: component.name), color: tintColor, backgroundColor: nil)
} else {
image = UIImage(bundleImageName: component.name)
}
if let imageValue = image, let shadowColor = component.shadowColor, component.shadowBlur != 0.0 {
image = generateImage(CGSize(width: imageValue.size.width + component.shadowBlur * 2.0, height: imageValue.size.height + component.shadowBlur * 2.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setShadow(offset: CGSize(), blur: component.shadowBlur, color: shadowColor.cgColor)
if let cgImage = imageValue.cgImage {
context.draw(cgImage, in: CGRect(origin: CGPoint(x: component.shadowBlur, y: component.shadowBlur), size: imageValue.size))
}
})
}
if component.flipVertically, let cgImage = image?.cgImage {
self.image = UIImage(cgImage: cgImage, scale: image?.scale ?? 0.0, orientation: .down)
} else {
self.image = image
}
}
self.component = component
var imageSize = self.image?.size ?? CGSize()
if let maxSize = component.maxSize {
imageSize = imageSize.aspectFitted(maxSize)
}
if component.scaleFactor != 1.0 {
imageSize.width = floor(imageSize.width * component.scaleFactor)
imageSize.height = floor(imageSize.height * component.scaleFactor)
}
return CGSize(width: min(imageSize.width, availableSize.width), height: min(imageSize.height, availableSize.height))
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
@@ -0,0 +1,19 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ComponentDisplayAdapters",
module_name = "ComponentDisplayAdapters",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,54 @@
import Foundation
import UIKit
import ComponentFlow
import Display
public extension ComponentTransition.Animation.Curve {
init(_ curve: ContainedViewLayoutTransitionCurve) {
switch curve {
case .linear:
self = .linear
case .easeInOut:
self = .easeInOut
case let .custom(a, b, c, d):
self = .custom(a, b, c, d)
case .customSpring:
self = .spring
case .spring:
self = .spring
}
}
var containedViewLayoutTransitionCurve: ContainedViewLayoutTransitionCurve {
switch self {
case .linear:
return .linear
case .easeInOut:
return .easeInOut
case .spring:
return .spring
case let .custom(a, b, c, d):
return .custom(a, b, c, d)
}
}
}
public extension ComponentTransition {
init(_ transition: ContainedViewLayoutTransition) {
switch transition {
case .immediate:
self.init(animation: .none)
case let .animated(duration, curve):
self.init(animation: .curve(duration: duration, curve: ComponentTransition.Animation.Curve(curve)))
}
}
var containedViewLayoutTransition: ContainedViewLayoutTransition {
switch self.animation {
case .none:
return .immediate
case let .curve(duration, curve):
return .animated(duration: duration, curve: curve.containedViewLayoutTransitionCurve)
}
}
}
@@ -0,0 +1,20 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "CreditCardInputComponent",
module_name = "CreditCardInputComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/Stripe:Stripe",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,172 @@
import Foundation
import UIKit
import ComponentFlow
import Display
import Stripe
public final class CreditCardInputComponent: Component {
public enum DataType {
case cardNumber
case expirationDate
}
public let dataType: DataType
public let text: String
public let textColor: UIColor
public let errorTextColor: UIColor
public let placeholder: String
public let placeholderColor: UIColor
public let updated: (String) -> Void
public init(
dataType: DataType,
text: String,
textColor: UIColor,
errorTextColor: UIColor,
placeholder: String,
placeholderColor: UIColor,
updated: @escaping (String) -> Void
) {
self.dataType = dataType
self.text = text
self.textColor = textColor
self.errorTextColor = errorTextColor
self.placeholder = placeholder
self.placeholderColor = placeholderColor
self.updated = updated
}
public static func ==(lhs: CreditCardInputComponent, rhs: CreditCardInputComponent) -> Bool {
if lhs.dataType != rhs.dataType {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.textColor != rhs.textColor {
return false
}
if lhs.errorTextColor != rhs.errorTextColor {
return false
}
if lhs.placeholder != rhs.placeholder {
return false
}
if lhs.placeholderColor != rhs.placeholderColor {
return false
}
return true
}
public final class View: UIView, STPFormTextFieldDelegate, UITextFieldDelegate {
private let textField: STPFormTextField
private var component: CreditCardInputComponent?
private let viewModel: STPPaymentCardTextFieldViewModel
override init(frame: CGRect) {
self.textField = STPFormTextField(frame: CGRect())
self.viewModel = STPPaymentCardTextFieldViewModel()
super.init(frame: frame)
self.textField.backgroundColor = .clear
self.textField.keyboardType = .phonePad
self.textField.formDelegate = self
self.textField.validText = true
self.addSubview(self.textField)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func textFieldChanged(_ textField: UITextField) {
self.component?.updated(self.textField.text ?? "")
}
public func formTextFieldDidBackspace(onEmpty formTextField: STPFormTextField) {
}
public func formTextField(_ formTextField: STPFormTextField, modifyIncomingTextChange input: NSAttributedString) -> NSAttributedString {
guard let component = self.component else {
return input
}
switch component.dataType {
case .cardNumber:
self.viewModel.cardNumber = input.string
return NSAttributedString(string: self.viewModel.cardNumber ?? "", attributes: self.textField.defaultTextAttributes)
case .expirationDate:
self.viewModel.rawExpiration = input.string
return NSAttributedString(string: self.viewModel.rawExpiration ?? "", attributes: self.textField.defaultTextAttributes)
}
}
public func formTextFieldTextDidChange(_ textField: STPFormTextField) {
guard let component = self.component else {
return
}
component.updated(self.textField.text ?? "")
let state: STPCardValidationState
switch component.dataType {
case .cardNumber:
state = self.viewModel.validationState(for: .number)
case .expirationDate:
state = self.viewModel.validationState(for: .expiration)
}
self.textField.validText = true
switch state {
case .invalid:
self.textField.validText = false
case .incomplete:
break
case .valid:
break
@unknown default:
break
}
}
func update(component: CreditCardInputComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
switch component.dataType {
case .cardNumber:
self.textField.autoFormattingBehavior = .cardNumbers
case .expirationDate:
self.textField.autoFormattingBehavior = .expiration
}
self.textField.font = UIFont.systemFont(ofSize: 17.0)
self.textField.defaultColor = component.textColor
self.textField.errorColor = .red
self.textField.placeholderColor = component.placeholderColor
if self.textField.text != component.text {
self.textField.text = component.text
}
self.textField.attributedPlaceholder = NSAttributedString(string: component.placeholder, font: self.textField.font, textColor: component.placeholderColor)
let size = CGSize(width: availableSize.width, height: 44.0)
transition.setFrame(view: self.textField, frame: CGRect(origin: CGPoint(), size: size), completion: nil)
self.component = component
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,19 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "PrefixSectionGroupComponent",
module_name = "PrefixSectionGroupComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,194 @@
import Foundation
import UIKit
import ComponentFlow
import Display
public final class PrefixSectionGroupComponent: Component {
public final class Item: Equatable {
public let prefix: AnyComponentWithIdentity<Empty>
public let content: AnyComponentWithIdentity<Empty>
public init(prefix: AnyComponentWithIdentity<Empty>, content: AnyComponentWithIdentity<Empty>) {
self.prefix = prefix
self.content = content
}
public static func ==(lhs: Item, rhs: Item) -> Bool {
if lhs.prefix != rhs.prefix {
return false
}
if lhs.content != rhs.content {
return false
}
return true
}
}
public let items: [Item]
public let backgroundColor: UIColor
public let separatorColor: UIColor
public init(
items: [Item],
backgroundColor: UIColor,
separatorColor: UIColor
) {
self.items = items
self.backgroundColor = backgroundColor
self.separatorColor = separatorColor
}
public static func ==(lhs: PrefixSectionGroupComponent, rhs: PrefixSectionGroupComponent) -> Bool {
if lhs.items != rhs.items {
return false
}
if lhs.backgroundColor != rhs.backgroundColor {
return false
}
if lhs.separatorColor != rhs.separatorColor {
return false
}
return true
}
public final class View: UIView {
private let backgroundView: UIView
private var itemViews: [AnyHashable: ComponentHostView<Empty>] = [:]
private var separatorViews: [UIView] = []
override init(frame: CGRect) {
self.backgroundView = UIView()
super.init(frame: frame)
self.addSubview(self.backgroundView)
self.backgroundView.layer.cornerRadius = 10.0
self.backgroundView.layer.masksToBounds = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: PrefixSectionGroupComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let spacing: CGFloat = 16.0
let sideInset: CGFloat = 16.0
self.backgroundView.backgroundColor = component.backgroundColor
var size = CGSize(width: availableSize.width, height: 0.0)
var validIds: [AnyHashable] = []
var maxPrefixSize = CGSize()
var prefixItemSizes: [CGSize] = []
for item in component.items {
validIds.append(item.prefix.id)
let itemView: ComponentHostView<Empty>
var itemTransition = transition
if let current = self.itemViews[item.prefix.id] {
itemView = current
} else {
itemTransition = transition.withAnimation(.none)
itemView = ComponentHostView<Empty>()
self.itemViews[item.prefix.id] = itemView
self.addSubview(itemView)
}
let itemSize = itemView.update(
transition: itemTransition,
component: item.prefix.component,
environment: {},
containerSize: CGSize(width: size.width, height: .greatestFiniteMagnitude)
)
prefixItemSizes.append(itemSize)
maxPrefixSize.width = max(maxPrefixSize.width, itemSize.width)
maxPrefixSize.height = max(maxPrefixSize.height, itemSize.height)
}
var maxContentSize = CGSize()
var contentItemSizes: [CGSize] = []
for item in component.items {
validIds.append(item.content.id)
let itemView: ComponentHostView<Empty>
var itemTransition = transition
if let current = self.itemViews[item.content.id] {
itemView = current
} else {
itemTransition = transition.withAnimation(.none)
itemView = ComponentHostView<Empty>()
self.itemViews[item.content.id] = itemView
self.addSubview(itemView)
}
let itemSize = itemView.update(
transition: itemTransition,
component: item.content.component,
environment: {},
containerSize: CGSize(width: size.width - maxPrefixSize.width - sideInset - spacing, height: .greatestFiniteMagnitude)
)
contentItemSizes.append(itemSize)
maxContentSize.width = max(maxContentSize.width, itemSize.width)
maxContentSize.height = max(maxContentSize.height, itemSize.height)
}
for i in 0 ..< component.items.count {
let itemSize = CGSize(width: size.width, height: max(prefixItemSizes[i].height, contentItemSizes[i].height))
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height), size: itemSize)
let prefixView = itemViews[component.items[i].prefix.id]!
let contentView = itemViews[component.items[i].content.id]!
prefixView.frame = CGRect(origin: CGPoint(x: sideInset, y: itemFrame.minY + floor((itemFrame.height - prefixItemSizes[i].height) / 2.0)), size: prefixItemSizes[i])
contentView.frame = CGRect(origin: CGPoint(x: itemFrame.minX + sideInset + maxPrefixSize.width + spacing, y: itemFrame.minY + floor((itemFrame.height - contentItemSizes[i].height) / 2.0)), size: contentItemSizes[i])
size.height += itemSize.height
if i != component.items.count - 1 {
let separatorView: UIView
if self.separatorViews.count > i {
separatorView = self.separatorViews[i]
} else {
separatorView = UIView()
self.separatorViews.append(separatorView)
self.addSubview(separatorView)
}
separatorView.backgroundColor = component.separatorColor
separatorView.frame = CGRect(origin: CGPoint(x: itemFrame.minX + sideInset, y: itemFrame.maxY), size: CGSize(width: itemFrame.width - sideInset, height: UIScreenPixel))
}
}
var removeIds: [AnyHashable] = []
for (id, itemView) in self.itemViews {
if !validIds.contains(id) {
removeIds.append(id)
itemView.removeFromSuperview()
}
}
for id in removeIds {
self.itemViews.removeValue(forKey: id)
}
if self.separatorViews.count > component.items.count - 1 {
for i in (component.items.count - 1) ..< self.separatorViews.count {
self.separatorViews[i].removeFromSuperview()
}
self.separatorViews.removeSubrange((component.items.count - 1) ..< self.separatorViews.count)
}
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size), completion: nil)
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,19 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "TextInputComponent",
module_name = "TextInputComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,86 @@
import Foundation
import UIKit
import ComponentFlow
import Display
public final class TextInputComponent: Component {
public let text: String
public let textColor: UIColor
public let placeholder: String
public let placeholderColor: UIColor
public let updated: (String) -> Void
public init(
text: String,
textColor: UIColor,
placeholder: String,
placeholderColor: UIColor,
updated: @escaping (String) -> Void
) {
self.text = text
self.textColor = textColor
self.placeholder = placeholder
self.placeholderColor = placeholderColor
self.updated = updated
}
public static func ==(lhs: TextInputComponent, rhs: TextInputComponent) -> Bool {
if lhs.text != rhs.text {
return false
}
if lhs.textColor != rhs.textColor {
return false
}
if lhs.placeholder != rhs.placeholder {
return false
}
if lhs.placeholderColor != rhs.placeholderColor {
return false
}
return true
}
public final class View: UITextField, UITextFieldDelegate {
private var component: TextInputComponent?
override init(frame: CGRect) {
super.init(frame: frame)
self.delegate = self
self.addTarget(self, action: #selector(self.textFieldChanged(_:)), for: .editingChanged)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func textFieldChanged(_ textField: UITextField) {
self.component?.updated(self.text ?? "")
}
func update(component: TextInputComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.font = UIFont.systemFont(ofSize: 17.0)
self.textColor = component.textColor
if self.text != component.text {
self.text = component.text
}
self.attributedPlaceholder = NSAttributedString(string: component.placeholder, font: self.font, textColor: component.placeholderColor)
let size = CGSize(width: availableSize.width, height: 44.0)
self.component = component
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,17 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "HierarchyTrackingLayer",
module_name = "HierarchyTrackingLayer",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,33 @@
import UIKit
private final class NullActionClass: NSObject, CAAction {
@objc public func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) {
}
}
private let nullAction = NullActionClass()
open class HierarchyTrackingLayer: CALayer {
public var didEnterHierarchy: (() -> Void)?
public var didExitHierarchy: (() -> Void)?
public var isInHierarchyUpdated: ((Bool) -> Void)?
public private(set) var isInHierarchy: Bool = false {
didSet {
if self.isInHierarchy != oldValue {
self.isInHierarchyUpdated?(self.isInHierarchy)
}
}
}
override open func action(forKey event: String) -> CAAction? {
if event == kCAOnOrderIn {
self.isInHierarchy = true
self.didEnterHierarchy?()
} else if event == kCAOnOrderOut {
self.isInHierarchy = false
self.didExitHierarchy?()
}
return nullAction
}
}
@@ -0,0 +1,23 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "LottieAnimationComponent",
module_name = "LottieAnimationComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/lottie-ios:Lottie",
"//submodules/AppBundle:AppBundle",
"//submodules/Display:Display",
"//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer",
"//submodules/GZip:GZip",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,331 @@
import Foundation
import ComponentFlow
import Lottie
import AppBundle
import HierarchyTrackingLayer
import UIKit
import Display
import GZip
public final class LottieAnimationComponent: Component {
public struct AnimationItem: Equatable {
public enum StillPosition {
case begin
case end
}
public enum Mode: Equatable {
case still(position: StillPosition)
case animating(loop: Bool)
case animateTransitionFromPrevious
}
public var name: String
public var mode: Mode
public var range: (CGFloat, CGFloat)?
public var speed: CGFloat
public var waitForCompletion: Bool
public init(name: String, mode: Mode, range: (CGFloat, CGFloat)? = nil, speed: CGFloat = 1.0, waitForCompletion: Bool = true) {
self.name = name
self.mode = mode
self.range = range
self.speed = speed
self.waitForCompletion = waitForCompletion
}
public static func == (lhs: LottieAnimationComponent.AnimationItem, rhs: LottieAnimationComponent.AnimationItem) -> Bool {
if lhs.name != rhs.name {
return false
}
if lhs.mode != rhs.mode {
return false
}
if lhs.speed != rhs.speed {
return false
}
if let lhsRange = lhs.range, let rhsRange = rhs.range, lhsRange != rhsRange {
return false
} else if (lhs.range == nil) != (rhs.range == nil) {
return false
}
return true
}
}
public let animation: AnimationItem
public let colors: [String: UIColor]
public let tag: AnyObject?
public let size: CGSize?
public init(animation: AnimationItem, colors: [String: UIColor], tag: AnyObject? = nil, size: CGSize?) {
self.animation = animation
self.colors = colors
self.tag = tag
self.size = size
}
public func tagged(_ tag: AnyObject?) -> LottieAnimationComponent {
return LottieAnimationComponent(
animation: self.animation,
colors: self.colors,
tag: tag,
size: self.size
)
}
public static func ==(lhs: LottieAnimationComponent, rhs: LottieAnimationComponent) -> Bool {
if lhs.animation != rhs.animation {
return false
}
if lhs.colors != rhs.colors {
return false
}
if lhs.tag !== rhs.tag {
return false
}
if lhs.size != rhs.size {
return false
}
return true
}
public final class View: UIView, ComponentTaggedView {
private var component: LottieAnimationComponent?
//private var colorCallbacks: [LOTColorValueCallback] = []
private var animationView: AnimationView?
private var didPlayToCompletion: Bool = false
private let hierarchyTrackingLayer: HierarchyTrackingLayer
private var currentCompletion: (() -> Void)?
override init(frame: CGRect) {
self.hierarchyTrackingLayer = HierarchyTrackingLayer()
super.init(frame: frame)
self.layer.addSublayer(self.hierarchyTrackingLayer)
self.hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in
guard let strongSelf = self, let animationView = strongSelf.animationView else {
return
}
if case .loop = animationView.loopMode {
animationView.play { _ in
self?.currentCompletion?()
}
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func matches(tag: Any) -> Bool {
if let component = self.component, let componentTag = component.tag {
let tag = tag as AnyObject
if componentTag === tag {
return true
}
}
return false
}
public func playOnce() {
guard let animationView = self.animationView, let component = self.component else {
return
}
animationView.stop()
animationView.loopMode = .playOnce
if let range = component.animation.range {
animationView.play(fromProgress: range.0, toProgress: range.1, completion: { [weak self] _ in
self?.currentCompletion?()
})
} else {
animationView.play { [weak self] _ in
self?.currentCompletion?()
}
}
}
func update(component: LottieAnimationComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
var updatePlayback = false
var updateColors = false
if let currentComponent = self.component, currentComponent.colors != component.colors {
updateColors = true
}
var animateSize = true
var updateComponent = true
if self.component?.animation != component.animation {
if let animationView = self.animationView {
if case .animateTransitionFromPrevious = component.animation.mode, !animationView.isAnimationPlaying, !self.didPlayToCompletion {
updateComponent = false
animationView.play { [weak self] _ in
self?.currentCompletion?()
}
}
}
if let animationView = self.animationView, animationView.isAnimationPlaying && component.animation.waitForCompletion {
updateComponent = false
self.currentCompletion = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.didPlayToCompletion = true
let _ = strongSelf.update(component: component, availableSize: availableSize, transition: transition)
}
animationView.loopMode = .playOnce
} else {
self.component = component
self.animationView?.removeFromSuperview()
self.didPlayToCompletion = false
self.currentCompletion = nil
var animation: Animation?
if let url = getAppBundle().url(forResource: component.animation.name, withExtension: "json"), let maybeAnimation = Animation.filepath(url.path) {
animation = maybeAnimation
} else if let url = getAppBundle().url(forResource: component.animation.name, withExtension: "tgs"), let data = try? Data(contentsOf: URL(fileURLWithPath: url.path)), let unpackedData = TGGUnzipData(data, 5 * 1024 * 1024) {
animation = try? Animation.from(data: unpackedData, strategy: .codable)
}
if let animation {
let view = AnimationView(animation: animation, configuration: LottieConfiguration(renderingEngine: .mainThread, decodingStrategy: .codable))
switch component.animation.mode {
case .still, .animateTransitionFromPrevious:
view.loopMode = .playOnce
case let .animating(loop):
if loop {
view.loopMode = .loop
} else {
view.loopMode = .playOnce
}
}
view.animationSpeed = component.animation.speed
view.backgroundColor = .clear
view.isOpaque = false
updateColors = true
self.animationView = view
self.addSubview(view)
animateSize = false
updatePlayback = true
}
}
}
if updateComponent {
self.component = component
}
if updateColors, let animationView = self.animationView {
if let value = component.colors["__allcolors__"] {
for keypath in animationView.allKeypaths(predicate: { $0.keys.last == "Colors" }) {
animationView.setValueProvider(GradientValueProvider([value.lottieColorValue, value.lottieColorValue]), keypath: AnimationKeypath(keypath: keypath))
}
for keypath in animationView.allKeypaths(predicate: { $0.keys.last == "Color" }) {
animationView.setValueProvider(ColorValueProvider(value.lottieColorValue), keypath: AnimationKeypath(keypath: keypath))
}
}
for (key, value) in component.colors {
if key == "__allcolors__" {
continue
}
animationView.setValueProvider(ColorValueProvider(value.lottieColorValue), keypath: AnimationKeypath(keypath: "\(key).Color"))
}
}
var animationSize = CGSize()
if let animationView = self.animationView, let animation = animationView.animation {
animationSize = animation.size
}
if let customSize = component.size {
animationSize = customSize
}
let size = CGSize(width: min(animationSize.width, availableSize.width), height: min(animationSize.height, availableSize.height))
if let animationView = self.animationView {
let animationFrame = CGRect(origin: CGPoint(x: floor((size.width - animationSize.width) / 2.0), y: floor((size.height - animationSize.height) / 2.0)), size: animationSize)
if animationView.frame != animationFrame {
if !transition.animation.isImmediate && animateSize && !animationView.frame.isEmpty && animationView.frame.size != animationFrame.size {
let previouosAnimationFrame = animationView.frame
if let snapshotView = animationView.snapshotView(afterScreenUpdates: false) {
snapshotView.frame = previouosAnimationFrame
animationView.superview?.insertSubview(snapshotView, belowSubview: animationView)
transition.setPosition(view: snapshotView, position: CGPoint(x: animationFrame.midX, y: animationFrame.midY))
snapshotView.bounds = CGRect(origin: CGPoint(), size: animationFrame.size)
let scaleFactor = previouosAnimationFrame.width / animationFrame.width
transition.animateScale(view: snapshotView, from: scaleFactor, to: 1.0)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
}
transition.setPosition(view: animationView, position: CGPoint(x: animationFrame.midX, y: animationFrame.midY))
transition.setBounds(view: animationView, bounds: CGRect(origin: CGPoint(), size: animationFrame.size))
transition.animateSublayerScale(view: animationView, from: previouosAnimationFrame.width / animationFrame.width, to: 1.0)
animationView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18)
} else if animationView.frame.size == animationFrame.size {
transition.setFrame(view: animationView, frame: animationFrame)
} else {
animationView.frame = animationFrame
}
}
if updatePlayback {
if case .animating = component.animation.mode {
if !animationView.isAnimationPlaying {
if let range = component.animation.range {
animationView.play(fromProgress: range.0, toProgress: range.1, completion: { [weak self] _ in
self?.currentCompletion?()
})
} else {
animationView.play { [weak self] _ in
self?.currentCompletion?()
}
}
}
} else {
if case let .still(position) = component.animation.mode {
switch position {
case .begin:
animationView.currentFrame = 0.0
case .end:
animationView.currentFrame = animationView.animation?.endFrame ?? 0.0
}
}
if animationView.isAnimationPlaying {
animationView.stop()
}
}
}
}
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
@@ -0,0 +1,18 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "MetalImageView",
module_name = "MetalImageView",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,252 @@
import Foundation
import UIKit
import Metal
import Display
private func alignUp(size: Int, align: Int) -> Int {
precondition(((align - 1) & align) == 0, "Align must be a power of two")
let alignmentMask = align - 1
return (size + alignmentMask) & ~alignmentMask
}
open class MetalImageLayer: CALayer {
fileprivate final class TextureStoragePool {
let width: Int
let height: Int
private var items: [TextureStorage.Content] = []
init(width: Int, height: Int) {
self.width = width
self.height = height
}
func recycle(content: TextureStorage.Content) {
if self.items.count < 4 {
self.items.append(content)
} else {
print("Warning: over-recycling texture storage")
}
}
func take() -> TextureStorage.Content? {
if self.items.isEmpty {
return nil
}
return self.items.removeLast()
}
}
fileprivate final class TextureStorage {
final class Content {
#if !targetEnvironment(simulator)
let buffer: MTLBuffer
#endif
let width: Int
let height: Int
let bytesPerRow: Int
let texture: MTLTexture
init?(device: MTLDevice, width: Int, height: Int) {
if #available(iOS 12.0, *) {
let bytesPerPixel = 4
let pixelRowAlignment = device.minimumLinearTextureAlignment(for: .bgra8Unorm)
let bytesPerRow = alignUp(size: width * bytesPerPixel, align: pixelRowAlignment)
self.width = width
self.height = height
self.bytesPerRow = bytesPerRow
#if targetEnvironment(simulator)
let textureDescriptor = MTLTextureDescriptor()
textureDescriptor.textureType = .type2D
textureDescriptor.pixelFormat = .bgra8Unorm
textureDescriptor.width = width
textureDescriptor.height = height
textureDescriptor.usage = [.renderTarget]
textureDescriptor.storageMode = .shared
guard let texture = device.makeTexture(descriptor: textureDescriptor) else {
return nil
}
#else
guard let buffer = device.makeBuffer(length: bytesPerRow * height, options: MTLResourceOptions.storageModeShared) else {
return nil
}
self.buffer = buffer
let textureDescriptor = MTLTextureDescriptor()
textureDescriptor.textureType = .type2D
textureDescriptor.pixelFormat = .bgra8Unorm
textureDescriptor.width = width
textureDescriptor.height = height
textureDescriptor.usage = [.renderTarget]
textureDescriptor.storageMode = buffer.storageMode
guard let texture = buffer.makeTexture(descriptor: textureDescriptor, offset: 0, bytesPerRow: bytesPerRow) else {
return nil
}
#endif
self.texture = texture
} else {
return nil
}
}
}
private weak var pool: TextureStoragePool?
let content: Content
private var isInvalidated: Bool = false
init(pool: TextureStoragePool, content: Content) {
self.pool = pool
self.content = content
}
deinit {
if !self.isInvalidated {
self.pool?.recycle(content: self.content)
}
}
func createCGImage() -> CGImage? {
if self.isInvalidated {
return nil
}
self.isInvalidated = true
#if targetEnvironment(simulator)
guard let data = NSMutableData(capacity: self.content.bytesPerRow * self.content.height) else {
return nil
}
data.length = self.content.bytesPerRow * self.content.height
self.content.texture.getBytes(data.mutableBytes, bytesPerRow: self.content.bytesPerRow, bytesPerImage: self.content.bytesPerRow * self.content.height, from: MTLRegion(origin: MTLOrigin(), size: MTLSize(width: self.content.width, height: self.content.height, depth: 1)), mipmapLevel: 0, slice: 0)
guard let dataProvider = CGDataProvider(data: data as CFData) else {
return nil
}
#else
let content = self.content
let pool = self.pool
guard let dataProvider = CGDataProvider(data: Data(bytesNoCopy: self.content.buffer.contents(), count: self.content.buffer.length, deallocator: .custom { [weak pool] _, _ in
guard let pool = pool else {
return
}
pool.recycle(content: content)
}) as CFData) else {
return nil
}
#endif
guard let image = CGImage(
width: Int(self.content.width),
height: Int(self.content.height),
bitsPerComponent: 8,
bitsPerPixel: 8 * 4,
bytesPerRow: self.content.bytesPerRow,
space: DeviceGraphicsContextSettings.shared.colorSpace,
bitmapInfo: DeviceGraphicsContextSettings.shared.transparentBitmapInfo,
provider: dataProvider,
decode: nil,
shouldInterpolate: true,
intent: .defaultIntent
) else {
return nil
}
return image
}
}
public final class Drawable {
private weak var renderer: Renderer?
fileprivate let textureStorage: TextureStorage
public var texture: MTLTexture {
return self.textureStorage.content.texture
}
fileprivate init(renderer: Renderer, textureStorage: TextureStorage) {
self.renderer = renderer
self.textureStorage = textureStorage
}
public func present(completion: @escaping () -> Void) {
self.renderer?.present(drawable: self)
completion()
}
}
public final class Renderer {
public var device: MTLDevice?
private var storagePool: TextureStoragePool?
public var imageUpdated: ((CGImage?) -> Void)?
public var drawableSize: CGSize = CGSize() {
didSet {
if self.drawableSize != oldValue {
if !self.drawableSize.width.isZero && !self.drawableSize.height.isZero {
self.storagePool = TextureStoragePool(width: Int(self.drawableSize.width), height: Int(self.drawableSize.height))
} else {
self.storagePool = nil
}
}
}
}
public func nextDrawable() -> Drawable? {
guard let device = self.device else {
return nil
}
guard let storagePool = self.storagePool else {
return nil
}
if let content = storagePool.take() {
return Drawable(renderer: self, textureStorage: TextureStorage(pool: storagePool, content: content))
} else {
guard let content = TextureStorage.Content(device: device, width: storagePool.width, height: storagePool.height) else {
return nil
}
return Drawable(renderer: self, textureStorage: TextureStorage(pool: storagePool, content: content))
}
}
fileprivate func present(drawable: Drawable) {
if let imageUpdated = self.imageUpdated {
imageUpdated(drawable.textureStorage.createCGImage())
}
}
}
public let renderer = Renderer()
override public init() {
super.init()
self.renderer.imageUpdated = { [weak self] image in
self?.contents = image
}
}
override public init(layer: Any) {
preconditionFailure()
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override open func action(forKey event: String) -> CAAction? {
return nullAction
}
}
open class MetalImageView: UIView {
public static override var layerClass: AnyClass {
return MetalImageLayer.self
}
}
@@ -0,0 +1,20 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "MultilineTextComponent",
module_name = "MultilineTextComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/Markdown:Markdown",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,194 @@
import Foundation
import UIKit
import ComponentFlow
import Display
import Markdown
public final class MultilineTextComponent: Component {
public enum TextContent: Equatable {
case plain(NSAttributedString)
case markdown(text: String, attributes: MarkdownAttributes)
}
public let text: TextContent
public let horizontalAlignment: NSTextAlignment
public let verticalAlignment: TextVerticalAlignment
public let truncationType: CTLineTruncationType
public let maximumNumberOfLines: Int
public let lineSpacing: CGFloat
public let cutout: TextNodeCutout?
public let insets: UIEdgeInsets
public let tintColor: UIColor?
public let textShadowColor: UIColor?
public let textShadowBlur: CGFloat?
public let textStroke: (UIColor, CGFloat)?
public let highlightColor: UIColor?
public let highlightInset: UIEdgeInsets
public let highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)?
public let tapAction: (([NSAttributedString.Key: Any], Int) -> Void)?
public let longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)?
public init(
text: TextContent,
horizontalAlignment: NSTextAlignment = .natural,
verticalAlignment: TextVerticalAlignment = .top,
truncationType: CTLineTruncationType = .end,
maximumNumberOfLines: Int = 1,
lineSpacing: CGFloat = 0.0,
cutout: TextNodeCutout? = nil,
insets: UIEdgeInsets = UIEdgeInsets(),
tintColor: UIColor? = nil,
textShadowColor: UIColor? = nil,
textShadowBlur: CGFloat? = nil,
textStroke: (UIColor, CGFloat)? = nil,
highlightColor: UIColor? = nil,
highlightInset: UIEdgeInsets = .zero,
highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? = nil,
tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil,
longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil
) {
self.text = text
self.horizontalAlignment = horizontalAlignment
self.verticalAlignment = verticalAlignment
self.truncationType = truncationType
self.maximumNumberOfLines = maximumNumberOfLines
self.lineSpacing = lineSpacing
self.cutout = cutout
self.insets = insets
self.tintColor = tintColor
self.textShadowColor = textShadowColor
self.textShadowBlur = textShadowBlur
self.textStroke = textStroke
self.highlightColor = highlightColor
self.highlightInset = highlightInset
self.highlightAction = highlightAction
self.tapAction = tapAction
self.longTapAction = longTapAction
}
public static func ==(lhs: MultilineTextComponent, rhs: MultilineTextComponent) -> Bool {
if lhs.text != rhs.text {
return false
}
if lhs.horizontalAlignment != rhs.horizontalAlignment {
return false
}
if lhs.verticalAlignment != rhs.verticalAlignment {
return false
}
if lhs.truncationType != rhs.truncationType {
return false
}
if lhs.maximumNumberOfLines != rhs.maximumNumberOfLines {
return false
}
if lhs.lineSpacing != rhs.lineSpacing {
return false
}
if lhs.cutout != rhs.cutout {
return false
}
if lhs.insets != rhs.insets {
return false
}
if lhs.tintColor != rhs.tintColor {
return false
}
if let lhsTextShadowColor = lhs.textShadowColor, let rhsTextShadowColor = rhs.textShadowColor {
if !lhsTextShadowColor.isEqual(rhsTextShadowColor) {
return false
}
} else if (lhs.textShadowColor != nil) != (rhs.textShadowColor != nil) {
return false
}
if lhs.textShadowBlur != rhs.textShadowBlur {
return false
}
if let lhsTextStroke = lhs.textStroke, let rhsTextStroke = rhs.textStroke {
if !lhsTextStroke.0.isEqual(rhsTextStroke.0) {
return false
}
if lhsTextStroke.1 != rhsTextStroke.1 {
return false
}
} else if (lhs.textShadowColor != nil) != (rhs.textShadowColor != nil) {
return false
}
if let lhsHighlightColor = lhs.highlightColor, let rhsHighlightColor = rhs.highlightColor {
if !lhsHighlightColor.isEqual(rhsHighlightColor) {
return false
}
} else if (lhs.highlightColor != nil) != (rhs.highlightColor != nil) {
return false
}
if lhs.highlightInset != rhs.highlightInset {
return false
}
return true
}
public final class View: ImmediateTextView {
public func update(component: MultilineTextComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
let attributedString: NSAttributedString
switch component.text {
case let .plain(string):
attributedString = string
case let .markdown(text, attributes):
attributedString = parseMarkdownIntoAttributedString(text, attributes: attributes)
}
//let previousText = self.attributedText?.string
self.attributedText = attributedString
self.maximumNumberOfLines = component.maximumNumberOfLines
self.truncationType = component.truncationType
self.textAlignment = component.horizontalAlignment
self.verticalAlignment = component.verticalAlignment
self.lineSpacing = component.lineSpacing
self.cutout = component.cutout
self.insets = component.insets
self.textShadowColor = component.textShadowColor
self.textShadowBlur = component.textShadowBlur
self.textStroke = component.textStroke
self.linkHighlightColor = component.highlightColor
self.linkHighlightInset = component.highlightInset
self.highlightAttributeAction = component.highlightAction
self.tapAttributeAction = component.tapAction
self.longTapAttributeAction = component.longTapAction
/*if case let .curve(duration, _) = transition.animation, let previousText = previousText, previousText != attributedString.string {
if let snapshotView = self.snapshotView(afterScreenUpdates: false) {
snapshotView.center = self.center
self.superview?.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
}
}*/
let size = self.updateLayout(availableSize)
if let tintColor = component.tintColor {
transition.setTintColor(layer: self.layer, color: tintColor)
} else {
self.layer.layerTintColor = nil
}
return size
}
}
public func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
@@ -0,0 +1,26 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "MultilineTextWithEntitiesComponent",
module_name = "MultilineTextWithEntitiesComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/Markdown:Markdown",
"//submodules/TextFormat:TextFormat",
"//submodules/AccountContext:AccountContext",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
"//submodules/TelegramUI/Components/TextNodeWithEntities:TextNodeWithEntities",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,343 @@
import Foundation
import UIKit
import ComponentFlow
import Display
import Markdown
import TextNodeWithEntities
import AccountContext
import AnimationCache
import MultiAnimationRenderer
public final class MultilineTextWithEntitiesComponent: Component {
public final class External {
public fileprivate(set) var layout: TextNodeLayout?
public init() {
}
}
public enum TextContent: Equatable {
case plain(NSAttributedString)
case markdown(text: String, attributes: MarkdownAttributes)
}
public let external: External?
public let context: AccountContext?
public let animationCache: AnimationCache?
public let animationRenderer: MultiAnimationRenderer?
public let placeholderColor: UIColor?
public let text: TextContent
public let horizontalAlignment: NSTextAlignment
public let verticalAlignment: TextVerticalAlignment
public let truncationType: CTLineTruncationType
public let maximumNumberOfLines: Int
public let lineSpacing: CGFloat
public let cutout: TextNodeCutout?
public let insets: UIEdgeInsets
public let spoilerColor: UIColor
public let textShadowColor: UIColor?
public let textStroke: (UIColor, CGFloat)?
public let highlightColor: UIColor?
public let highlightInset: UIEdgeInsets
public let handleSpoilers: Bool
public let manualVisibilityControl: Bool
public let resetAnimationsOnVisibilityChange: Bool
public let displaysAsynchronously: Bool
public let maxWidth: CGFloat?
public let highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)?
public let tapAction: (([NSAttributedString.Key: Any], Int) -> Void)?
public let longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)?
public init(
external: External? = nil,
context: AccountContext?,
animationCache: AnimationCache?,
animationRenderer: MultiAnimationRenderer?,
placeholderColor: UIColor?,
text: TextContent,
horizontalAlignment: NSTextAlignment = .natural,
verticalAlignment: TextVerticalAlignment = .top,
truncationType: CTLineTruncationType = .end,
maximumNumberOfLines: Int = 1,
lineSpacing: CGFloat = 0.0,
cutout: TextNodeCutout? = nil,
insets: UIEdgeInsets = UIEdgeInsets(),
spoilerColor: UIColor = .black,
textShadowColor: UIColor? = nil,
textStroke: (UIColor, CGFloat)? = nil,
highlightColor: UIColor? = nil,
highlightInset: UIEdgeInsets = .zero,
handleSpoilers: Bool = false,
manualVisibilityControl: Bool = false,
resetAnimationsOnVisibilityChange: Bool = false,
displaysAsynchronously: Bool = true,
maxWidth: CGFloat? = nil,
highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? = nil,
tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil,
longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil
) {
self.external = external
self.context = context
self.animationCache = animationCache
self.animationRenderer = animationRenderer
self.placeholderColor = placeholderColor
self.text = text
self.horizontalAlignment = horizontalAlignment
self.verticalAlignment = verticalAlignment
self.truncationType = truncationType
self.maximumNumberOfLines = maximumNumberOfLines
self.lineSpacing = lineSpacing
self.cutout = cutout
self.insets = insets
self.spoilerColor = spoilerColor
self.textShadowColor = textShadowColor
self.textStroke = textStroke
self.highlightColor = highlightColor
self.highlightInset = highlightInset
self.highlightAction = highlightAction
self.handleSpoilers = handleSpoilers
self.manualVisibilityControl = manualVisibilityControl
self.resetAnimationsOnVisibilityChange = resetAnimationsOnVisibilityChange
self.displaysAsynchronously = displaysAsynchronously
self.maxWidth = maxWidth
self.tapAction = tapAction
self.longTapAction = longTapAction
}
public static func ==(lhs: MultilineTextWithEntitiesComponent, rhs: MultilineTextWithEntitiesComponent) -> Bool {
if lhs.external !== rhs.external {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.horizontalAlignment != rhs.horizontalAlignment {
return false
}
if lhs.verticalAlignment != rhs.verticalAlignment {
return false
}
if lhs.truncationType != rhs.truncationType {
return false
}
if lhs.maximumNumberOfLines != rhs.maximumNumberOfLines {
return false
}
if lhs.lineSpacing != rhs.lineSpacing {
return false
}
if lhs.cutout != rhs.cutout {
return false
}
if lhs.insets != rhs.insets {
return false
}
if lhs.handleSpoilers != rhs.handleSpoilers {
return false
}
if lhs.manualVisibilityControl != rhs.manualVisibilityControl {
return false
}
if lhs.resetAnimationsOnVisibilityChange != rhs.resetAnimationsOnVisibilityChange {
return false
}
if lhs.displaysAsynchronously != rhs.displaysAsynchronously {
return false
}
if lhs.maxWidth != rhs.maxWidth {
return false
}
if let lhsTextShadowColor = lhs.textShadowColor, let rhsTextShadowColor = rhs.textShadowColor {
if !lhsTextShadowColor.isEqual(rhsTextShadowColor) {
return false
}
} else if (lhs.textShadowColor != nil) != (rhs.textShadowColor != nil) {
return false
}
if let lhsTextStroke = lhs.textStroke, let rhsTextStroke = rhs.textStroke {
if !lhsTextStroke.0.isEqual(rhsTextStroke.0) {
return false
}
if lhsTextStroke.1 != rhsTextStroke.1 {
return false
}
} else if (lhs.textShadowColor != nil) != (rhs.textShadowColor != nil) {
return false
}
if let lhsHighlightColor = lhs.highlightColor, let rhsHighlightColor = rhs.highlightColor {
if !lhsHighlightColor.isEqual(rhsHighlightColor) {
return false
}
} else if (lhs.highlightColor != nil) != (rhs.highlightColor != nil) {
return false
}
if lhs.highlightInset != rhs.highlightInset {
return false
}
return true
}
public final class View: UIView {
var spoilerTextNode: ImmediateTextNodeWithEntities?
let textNode: ImmediateTextNodeWithEntities
public override init(frame: CGRect) {
self.textNode = ImmediateTextNodeWithEntities()
super.init(frame: frame)
self.addSubview(self.textNode.view)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func attributes(at point: CGPoint) -> (Int, [NSAttributedString.Key: Any])? {
if let result = self.textNode.attributesAtPoint(CGPoint(x: point.x - self.textNode.frame.minX, y: point.y - self.textNode.frame.minY)) {
return result
}
return nil
}
public var isSpoilerConcealed: Bool {
if let dustNode = self.textNode.dustNode, !dustNode.isRevealed {
return true
}
return false
}
public var hasRTL: Bool {
return self.textNode.cachedLayout?.hasRTL ?? false
}
public func updateVisibility(_ isVisible: Bool) {
self.textNode.visibility = isVisible
}
public func update(component: MultilineTextWithEntitiesComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
self.textNode.displaysAsynchronously = component.displaysAsynchronously
let attributedString: NSAttributedString
switch component.text {
case let .plain(string):
attributedString = string
case let .markdown(text, attributes):
attributedString = parseMarkdownIntoAttributedString(text, attributes: attributes)
}
let previousText = self.textNode.attributedText?.string
self.textNode.attributedText = attributedString
self.textNode.maximumNumberOfLines = component.maximumNumberOfLines
self.textNode.truncationType = component.truncationType
self.textNode.textAlignment = component.horizontalAlignment
self.textNode.verticalAlignment = component.verticalAlignment
self.textNode.lineSpacing = component.lineSpacing
self.textNode.cutout = component.cutout
self.textNode.insets = component.insets
self.textNode.textShadowColor = component.textShadowColor
self.textNode.textStroke = component.textStroke
self.textNode.linkHighlightColor = component.highlightColor
self.textNode.linkHighlightInset = component.highlightInset
self.textNode.highlightAttributeAction = component.highlightAction
self.textNode.tapAttributeAction = component.tapAction
self.textNode.longTapAttributeAction = component.longTapAction
self.textNode.spoilerColor = component.spoilerColor
self.textNode.resetEmojiToFirstFrameAutomatically = component.resetAnimationsOnVisibilityChange
if case let .curve(duration, _) = transition.animation, let previousText = previousText, previousText != attributedString.string {
if let snapshotView = self.snapshotContentTree() {
snapshotView.center = self.center
self.superview?.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
}
}
if !component.manualVisibilityControl {
self.textNode.visibility = true
}
if let context = component.context, let animationCache = component.animationCache, let animationRenderer = component.animationRenderer, let placeholderColor = component.placeholderColor {
self.textNode.arguments = TextNodeWithEntities.Arguments(
context: context,
cache: animationCache,
renderer: animationRenderer,
placeholderColor: placeholderColor,
attemptSynchronous: false
)
}
var constrainedSize = availableSize
if let maxWidth = component.maxWidth {
constrainedSize.width = maxWidth
}
let layoutInfo = self.textNode.updateLayoutFullInfo(constrainedSize)
self.textNode.frame = CGRect(origin: .zero, size: layoutInfo.size)
if component.handleSpoilers {
let spoilerTextNode: ImmediateTextNodeWithEntities
if let current = self.spoilerTextNode {
spoilerTextNode = current
} else {
spoilerTextNode = ImmediateTextNodeWithEntities()
spoilerTextNode.alpha = 0.0
self.spoilerTextNode = spoilerTextNode
self.textNode.dustNode?.textNode = spoilerTextNode
}
spoilerTextNode.displaySpoilers = true
spoilerTextNode.displaySpoilerEffect = false
spoilerTextNode.attributedText = attributedString
spoilerTextNode.maximumNumberOfLines = component.maximumNumberOfLines
spoilerTextNode.truncationType = component.truncationType
spoilerTextNode.textAlignment = component.horizontalAlignment
spoilerTextNode.verticalAlignment = component.verticalAlignment
spoilerTextNode.lineSpacing = component.lineSpacing
spoilerTextNode.cutout = component.cutout
spoilerTextNode.insets = component.insets
spoilerTextNode.textShadowColor = component.textShadowColor
spoilerTextNode.textStroke = component.textStroke
spoilerTextNode.isUserInteractionEnabled = false
let size = spoilerTextNode.updateLayout(constrainedSize)
spoilerTextNode.frame = CGRect(origin: .zero, size: size)
if spoilerTextNode.view.superview == nil {
self.addSubview(spoilerTextNode.view)
}
} else if let spoilerTextNode = self.spoilerTextNode {
self.spoilerTextNode = nil
spoilerTextNode.view.removeFromSuperview()
self.textNode.dustNode?.textNode = nil
}
if let external = component.external {
external.layout = layoutInfo
}
return layoutInfo.size
}
}
public func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
@@ -0,0 +1,20 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "PagerComponent",
module_name = "PagerComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/DirectionalPanGesture:DirectionalPanGesture",
],
visibility = [
"//visibility:public",
],
)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,19 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ProgressIndicatorComponent",
module_name = "ProgressIndicatorComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/Display:Display",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,113 @@
import Foundation
import UIKit
import Display
import ComponentFlow
public final class ProgressIndicatorComponent: Component {
public let diameter: CGFloat
public let value: Double
public let backgroundColor: UIColor
public let foregroundColor: UIColor
public init(
diameter: CGFloat,
backgroundColor: UIColor,
foregroundColor: UIColor,
value: Double
) {
self.diameter = diameter
self.backgroundColor = backgroundColor
self.foregroundColor = foregroundColor
self.value = value
}
public static func ==(lhs: ProgressIndicatorComponent, rhs: ProgressIndicatorComponent) -> Bool {
if lhs.diameter != rhs.diameter {
return false
}
if lhs.backgroundColor != rhs.backgroundColor {
return false
}
if lhs.foregroundColor != rhs.foregroundColor {
return false
}
if lhs.value != rhs.value {
return false
}
return true
}
public final class View: UIView {
private var currentComponent: ProgressIndicatorComponent?
private let foregroundShapeLayer: SimpleShapeLayer
public init() {
self.foregroundShapeLayer = SimpleShapeLayer()
self.foregroundShapeLayer.isOpaque = false
self.foregroundShapeLayer.backgroundColor = nil
self.foregroundShapeLayer.fillColor = nil
self.foregroundShapeLayer.lineCap = .round
super.init(frame: CGRect())
let shapeLayer = self.layer as! CAShapeLayer
shapeLayer.isOpaque = false
shapeLayer.backgroundColor = nil
shapeLayer.fillColor = nil
self.layer.addSublayer(self.foregroundShapeLayer)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public static var layerClass: AnyClass {
return CAShapeLayer.self
}
func update(component: ProgressIndicatorComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
let lineWidth: CGFloat = 1.33
let size = CGSize(width: component.diameter, height: component.diameter)
let shapeLayer = self.layer as! CAShapeLayer
if self.currentComponent?.backgroundColor != component.backgroundColor {
shapeLayer.strokeColor = component.backgroundColor.cgColor
shapeLayer.lineWidth = lineWidth
}
if self.currentComponent?.foregroundColor != component.foregroundColor {
self.foregroundShapeLayer.strokeColor = component.foregroundColor.cgColor
self.foregroundShapeLayer.lineWidth = lineWidth
}
if self.currentComponent?.diameter != component.diameter {
let path = UIBezierPath(arcCenter: CGPoint(x: component.diameter / 2.0, y: component.diameter / 2.0), radius: component.diameter / 2.0, startAngle: -CGFloat.pi / 2.0, endAngle: 2.0 * CGFloat.pi - CGFloat.pi / 2.0, clockwise: true).cgPath
shapeLayer.path = path
self.foregroundShapeLayer.path = path
self.foregroundShapeLayer.frame = CGRect(origin: CGPoint(), size: size)
}
if self.currentComponent != nil {
let previousValue: CGFloat = self.foregroundShapeLayer.presentation()?.strokeEnd ?? self.foregroundShapeLayer.strokeEnd
self.foregroundShapeLayer.animate(from: CGFloat(previousValue) as NSNumber, to: CGFloat(component.value) as NSNumber, keyPath: "strokeEnd", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.12)
}
self.foregroundShapeLayer.strokeEnd = CGFloat(component.value)
self.currentComponent = component
return size
}
}
public func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
@@ -0,0 +1,33 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ReactionButtonListComponent",
module_name = "ReactionButtonListComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/AccountContext:AccountContext",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/WebPBinding:WebPBinding",
"//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode",
"//submodules/Components/ReactionImageComponent:ReactionImageComponent",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
"//submodules/TextFormat:TextFormat",
"//submodules/AppBundle",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,27 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ReactionImageComponent",
module_name = "ReactionImageComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/TelegramCore:TelegramCore",
"//submodules/AccountContext:AccountContext",
"//submodules/WebPBinding:WebPBinding",
"//submodules/rlottie:RLottieBinding",
"//submodules/GZip:GZip",
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,194 @@
import Foundation
import AsyncDisplayKit
import Display
import ComponentFlow
import SwiftSignalKit
import TelegramCore
import AccountContext
import TelegramPresentationData
import UIKit
import WebPBinding
import RLottieBinding
import GZip
import AnimationCache
import EmojiTextAttachmentView
public let sharedReactionStaticImage = Queue(name: "SharedReactionStaticImage", qos: .default)
public func reactionStaticImage(context: AccountContext, animation: TelegramMediaFile, pixelSize: CGSize, queue: Queue) -> Signal<EngineMediaResource.ResourceData, NoError> {
return context.engine.resources.custom(id: "\(animation.resource.id.stringRepresentation):reaction-static-\(pixelSize.width)x\(pixelSize.height)-v10", fetch: EngineMediaResource.Fetch {
return Signal { subscriber in
let fetchDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .image, reference: MediaResourceReference.standalone(resource: animation.resource)).start()
var customColor: UIColor?
if animation.isCustomTemplateEmoji {
customColor = nil
}
let fetchFrame = animationCacheFetchFile(context: context, userLocation: .other, userContentType: .sticker, resource: MediaResourceReference.standalone(resource: animation.resource), type: AnimationCacheAnimationType(file: animation), keyframeOnly: true, customColor: customColor)
class AnimationCacheItemWriterImpl: AnimationCacheItemWriter {
let queue: Queue
private let frameReceived: (UIImage) -> Void
init(queue: Queue, frameReceived: @escaping (UIImage) -> Void) {
self.queue = queue
self.frameReceived = frameReceived
}
var isCancelled: Bool {
return false
}
func add(with drawingBlock: (AnimationCacheItemDrawingSurface) -> Double?, proposedWidth: Int, proposedHeight: Int, insertKeyframe: Bool) {
if let renderContext = DrawingContext(size: CGSize(width: proposedWidth, height: proposedHeight), scale: 1.0, clear: true) {
let _ = drawingBlock(AnimationCacheItemDrawingSurface(
argb: renderContext.bytes.assumingMemoryBound(to: UInt8.self),
width: Int(renderContext.scaledSize.width),
height: Int(renderContext.scaledSize.height),
bytesPerRow: renderContext.bytesPerRow,
length: renderContext.length
))
if let image = renderContext.generateImage() {
self.frameReceived(image)
}
}
}
func finish() {
}
}
let innerWriter = AnimationCacheItemWriterImpl(queue: queue, frameReceived: { image in
guard let pngData = image.pngData() else {
return
}
let tempFile = EngineTempBox.shared.tempFile(fileName: "image.png")
guard let _ = try? pngData.write(to: URL(fileURLWithPath: tempFile.path)) else {
return
}
subscriber.putNext(.moveTempFile(file: tempFile))
subscriber.putCompletion()
})
let dataDisposable = fetchFrame(AnimationCacheFetchOptions(
size: pixelSize,
writer: innerWriter,
firstFrameOnly: true
))
/*let dataDisposable = context.account.postbox.mediaBox.resourceData(animation.resource).start(next: { data in
if !data.complete {
return
}
guard let data = try? Data(contentsOf: URL(fileURLWithPath: data.path)) else {
return
}
guard let unpackedData = TGGUnzipData(data, 5 * 1024 * 1024) else {
return
}
guard let instance = LottieInstance(data: unpackedData, fitzModifier: .none, colorReplacements: nil, cacheKey: "") else {
return
}
instance.renderFrame(with: Int32(instance.frameCount - 1), into: renderContext.bytes.assumingMemoryBound(to: UInt8.self), width: Int32(renderContext.size.width * renderContext.scale), height: Int32(renderContext.size.height * renderContext.scale), bytesPerRow: Int32(renderContext.bytesPerRow))
})*/
return ActionDisposable {
fetchDisposable.dispose()
dataDisposable.dispose()
}
}
})
}
public final class ReactionImageNode: ASDisplayNode {
private var disposable: Disposable?
private let size: CGSize
private let isAnimation: Bool
private let iconNode: ASImageNode
public init(context: AccountContext, availableReactions: AvailableReactions?, reaction: MessageReaction.Reaction, displayPixelSize: CGSize) {
self.iconNode = ASImageNode()
var file: TelegramMediaFile?
var animationFile: TelegramMediaFile?
if let availableReactions = availableReactions {
for availableReaction in availableReactions.reactions {
if availableReaction.value == reaction {
file = availableReaction.staticIcon._parse()
animationFile = availableReaction.centerAnimation?._parse()
break
}
}
}
if let animationFile = animationFile {
self.size = animationFile.dimensions?.cgSize ?? displayPixelSize
var displaySize = self.size.aspectFitted(displayPixelSize)
displaySize.width = floor(displaySize.width * 2.0)
displaySize.height = floor(displaySize.height * 2.0)
self.isAnimation = true
super.init()
self.disposable = (reactionStaticImage(context: context, animation: animationFile, pixelSize: CGSize(width: displaySize.width * UIScreenScale, height: displaySize.height * UIScreenScale), queue: sharedReactionStaticImage)
|> deliverOnMainQueue).start(next: { [weak self] data in
guard let strongSelf = self else {
return
}
if data.isComplete, let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)) {
if let image = UIImage(data: dataValue) {
strongSelf.iconNode.image = image
}
}
}).strict()
} else if let file = file {
self.size = file.dimensions?.cgSize ?? displayPixelSize
self.isAnimation = false
super.init()
self.disposable = (context.account.postbox.mediaBox.resourceData(file.resource)
|> deliverOnMainQueue).start(next: { [weak self] data in
guard let strongSelf = self else {
return
}
if data.complete, let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)) {
if let image = WebP.convert(fromWebP: dataValue) {
strongSelf.iconNode.image = image
}
}
}).strict()
} else {
self.size = displayPixelSize
self.isAnimation = false
super.init()
}
self.addSubnode(self.iconNode)
}
deinit {
self.disposable?.dispose()
}
public func update(size: CGSize) {
var imageSize = self.size.aspectFitted(size)
if self.isAnimation {
imageSize.width *= 2.0
imageSize.height *= 2.0
}
self.iconNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.height - imageSize.height) / 2.0)), size: imageSize)
}
}
@@ -0,0 +1,34 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ReactionListContextMenuContent",
module_name = "ReactionListContextMenuContent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/AccountContext:AccountContext",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/WebPBinding:WebPBinding",
"//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode",
"//submodules/ContextUI:ContextUI",
"//submodules/AvatarNode:AvatarNode",
"//submodules/Components/ReactionImageComponent:ReactionImageComponent",
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
"//submodules/TelegramUI/Components/EmojiStatusComponent:EmojiStatusComponent",
"//submodules/TextFormat:TextFormat",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,22 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "SheetComponent",
module_name = "SheetComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/Components/ViewControllerComponent:ViewControllerComponent",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/TelegramUI/Components/DynamicCornerRadiusView",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,605 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import ViewControllerComponent
import SwiftSignalKit
import DynamicCornerRadiusView
public final class SheetComponentEnvironment: Equatable {
public let isDisplaying: Bool
public let isCentered: Bool
public let hasInputHeight: Bool
public let regularMetricsSize: CGSize?
public let dismiss: (Bool) -> Void
public init(isDisplaying: Bool, isCentered: Bool, hasInputHeight: Bool, regularMetricsSize: CGSize?, dismiss: @escaping (Bool) -> Void) {
self.isDisplaying = isDisplaying
self.isCentered = isCentered
self.hasInputHeight = hasInputHeight
self.regularMetricsSize = regularMetricsSize
self.dismiss = dismiss
}
public static func ==(lhs: SheetComponentEnvironment, rhs: SheetComponentEnvironment) -> Bool {
if lhs.isDisplaying != rhs.isDisplaying {
return false
}
if lhs.isCentered != rhs.isCentered {
return false
}
if lhs.hasInputHeight != rhs.hasInputHeight {
return false
}
if lhs.regularMetricsSize != rhs.regularMetricsSize {
return false
}
return true
}
}
public let sheetComponentTag = GenericComponentViewTag()
public final class SheetComponent<ChildEnvironmentType: Sendable & Equatable>: Component {
public typealias EnvironmentType = (ChildEnvironmentType, SheetComponentEnvironment)
public class ExternalState {
public fileprivate(set) var contentHeight: CGFloat
public init() {
self.contentHeight = 0.0
}
}
public enum BackgroundColor: Equatable {
public enum BlurStyle: Equatable {
case light
case dark
}
case color(UIColor)
case blur(BlurStyle)
}
public enum Style: Equatable {
case glass
case legacy
}
public let content: AnyComponent<ChildEnvironmentType>
public let headerContent: AnyComponent<Empty>?
public let style: Style
public let backgroundColor: BackgroundColor
public let followContentSizeChanges: Bool
public let clipsContent: Bool
public let isScrollEnabled: Bool
public let hasDimView: Bool
public let autoAnimateOut: Bool
public let externalState: ExternalState?
public let animateOut: ActionSlot<Action<()>>
public let onPan: () -> Void
public let willDismiss: () -> Void
public init(
content: AnyComponent<ChildEnvironmentType>,
headerContent: AnyComponent<Empty>? = nil,
style: Style = .legacy,
backgroundColor: BackgroundColor,
followContentSizeChanges: Bool = false,
clipsContent: Bool = false,
isScrollEnabled: Bool = true,
hasDimView: Bool = true,
autoAnimateOut: Bool = true,
externalState: ExternalState? = nil,
animateOut: ActionSlot<Action<()>>,
onPan: @escaping () -> Void = {},
willDismiss: @escaping () -> Void = {}
) {
self.content = content
self.headerContent = headerContent
self.style = style
self.backgroundColor = backgroundColor
self.followContentSizeChanges = followContentSizeChanges
self.clipsContent = clipsContent
self.isScrollEnabled = isScrollEnabled
self.hasDimView = hasDimView
self.autoAnimateOut = autoAnimateOut
self.externalState = externalState
self.animateOut = animateOut
self.onPan = onPan
self.willDismiss = willDismiss
}
public static func ==(lhs: SheetComponent, rhs: SheetComponent) -> Bool {
if lhs.content != rhs.content {
return false
}
if lhs.headerContent != rhs.headerContent {
return false
}
if lhs.style != rhs.style {
return false
}
if lhs.backgroundColor != rhs.backgroundColor {
return false
}
if lhs.followContentSizeChanges != rhs.followContentSizeChanges {
return false
}
if lhs.isScrollEnabled != rhs.isScrollEnabled {
return false
}
if lhs.hasDimView != rhs.hasDimView {
return false
}
if lhs.autoAnimateOut != rhs.autoAnimateOut {
return false
}
if lhs.animateOut != rhs.animateOut {
return false
}
return true
}
private class ScrollView: UIScrollView {
var ignoreScroll = false
override func setContentOffset(_ contentOffset: CGPoint, animated: Bool) {
guard !self.ignoreScroll else {
return
}
if animated && abs(contentOffset.y - self.contentOffset.y) > 200.0 {
return
}
super.setContentOffset(contentOffset, animated: animated)
}
override func touchesShouldCancel(in view: UIView) -> Bool {
return true
}
}
public final class View: UIView, UIScrollViewDelegate, ComponentTaggedView {
public final class Tag {
public init() {
}
}
public func matches(tag: Any) -> Bool {
if let _ = tag as? Tag {
return true
}
return false
}
private var component: SheetComponent<ChildEnvironmentType>?
private let dimView: UIView
private let scrollView: ScrollView
private let backgroundView: SheetBackgroundView
private var effectView: UIVisualEffectView?
private let clipView: SheetBackgroundView
private let contentView: ComponentView<ChildEnvironmentType>
private var headerView: ComponentView<Empty>?
private var isAnimatingOut: Bool = false
private var previousIsDisplaying: Bool = false
private var dismiss: ((Bool) -> Void)?
private var keyboardWillShowObserver: AnyObject?
override init(frame: CGRect) {
self.dimView = UIView()
self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.4)
self.scrollView = ScrollView()
self.scrollView.delaysContentTouches = false
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.scrollView.contentInsetAdjustmentBehavior = .never
}
self.scrollView.showsVerticalScrollIndicator = false
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.alwaysBounceVertical = true
self.backgroundView = SheetBackgroundView()
self.clipView = SheetBackgroundView()
self.contentView = ComponentView<ChildEnvironmentType>()
super.init(frame: frame)
self.scrollView.delegate = self
self.addSubview(self.dimView)
self.scrollView.addSubview(self.backgroundView)
self.addSubview(self.scrollView)
self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimViewTapGesture(_:))))
self.keyboardWillShowObserver = NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: nil, using: { [weak self] _ in
if let strongSelf = self {
strongSelf.scrollView.ignoreScroll = true
Queue.mainQueue().after(0.1, {
strongSelf.scrollView.ignoreScroll = false
})
}
})
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
if let keyboardFrameChangeObserver = self.keyboardWillShowObserver {
NotificationCenter.default.removeObserver(keyboardFrameChangeObserver)
}
}
@objc private func dimViewTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.dismiss?(true)
}
}
public func dismissAnimated() {
self.dismiss?(true)
}
public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
self.component?.onPan()
}
private var scrollingOut = false
public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let contentOffset = (scrollView.contentOffset.y + scrollView.contentInset.top - scrollView.contentSize.height) * -1.0
let dismissalOffset = scrollView.contentSize.height - scrollView.contentInset.top + scrollView.contentSize.height
let delta = dismissalOffset - contentOffset
let initialVelocity = !delta.isZero ? velocity.y / delta : 0.0
let currentContentOffset = scrollView.contentOffset
targetContentOffset.pointee = currentContentOffset
if velocity.y > 300.0 {
self.component?.willDismiss()
self.animateOut(initialVelocity: initialVelocity, completion: {
self.dismiss?(false)
})
} else {
if contentOffset < scrollView.contentSize.height * 0.1 {
if contentOffset < 0.0 {
} else {
scrollView.setContentOffset(CGPoint(x: 0.0, y: scrollView.contentSize.height - scrollView.contentInset.top), animated: true)
}
} else {
self.component?.willDismiss()
self.animateOut(initialVelocity: initialVelocity, completion: {
self.dismiss?(false)
})
}
}
}
private var ignoreScrolling: Bool = false
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard !self.ignoreScrolling else {
return
}
let contentOffset = (scrollView.contentOffset.y + scrollView.contentInset.top - scrollView.contentSize.height) * -1.0
if contentOffset >= scrollView.contentSize.height {
self.dismiss?(false)
}
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let headerView = self.headerView?.view, headerView.bounds.contains(self.convert(point, to: headerView)) {
return super.hitTest(point, with: event)
}
if !self.backgroundView.bounds.contains(self.convert(point, to: self.backgroundView)) {
return self.dimView
}
return super.hitTest(point, with: event)
}
private func animateIn() {
self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
let targetPosition = self.scrollView.center
var offset: CGFloat = self.scrollView.contentSize.height
if self.isCentered {
offset = self.frame.height + self.scrollView.frame.height * 0.5
}
self.scrollView.center = targetPosition.offsetBy(dx: 0.0, dy: offset)
transition.animateView(allowUserInteraction: true, {
self.scrollView.center = targetPosition
})
if let headerContent = self.headerView {
headerContent.view?.layer.animateAlpha(from: 0.1, to: 0.0, duration: 0.15)
}
}
private func animateOut(initialVelocity: CGFloat? = nil, completion: @escaping () -> Void) {
if self.isAnimatingOut {
completion()
return
}
self.isAnimatingOut = true
self.isUserInteractionEnabled = false
self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
if let headerContent = self.headerView {
headerContent.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
guard let contentView = self.contentView.view else {
return
}
if let initialVelocity = initialVelocity {
let transition = ContainedViewLayoutTransition.animated(duration: 0.35, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity))
let contentOffset = (self.scrollView.contentOffset.y + self.scrollView.contentInset.top - self.scrollView.contentSize.height) * -1.0
let dismissalOffset = self.scrollView.contentSize.height + abs(contentView.frame.minY)
let delta = dismissalOffset - contentOffset
var targetPosition = self.scrollView.center.y + delta
if self.isCentered {
targetPosition = self.frame.height + self.scrollView.frame.height * 0.5
}
transition.updatePosition(layer: self.scrollView.layer, position: CGPoint(x: self.scrollView.center.x, y: targetPosition), completion: { _ in
completion()
})
} else {
var targetOffset: CGFloat = self.scrollView.contentSize.height + abs(contentView.frame.minY)
if self.isCentered {
targetOffset = self.frame.height + self.scrollView.frame.height * 0.5
}
self.scrollView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: targetOffset), duration: 0.25, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in
completion()
})
}
}
private var currentHasInputHeight = false
private var isCentered = false
private var currentAvailableSize: CGSize?
func update(component: SheetComponent<ChildEnvironmentType>, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
let previousHasInputHeight = self.currentHasInputHeight
let sheetEnvironment = environment[SheetComponentEnvironment.self].value
component.animateOut.connect { [weak self] completion in
guard let strongSelf = self else {
return
}
strongSelf.animateOut {
completion(Void())
}
}
self.component = component
self.isCentered = sheetEnvironment.isCentered
self.currentHasInputHeight = sheetEnvironment.hasInputHeight
if self.isAnimatingOut {
return availableSize
}
var glassInset: CGFloat = 0.0
var topCornerRadius: CGFloat
var bottomCornerRadius: CGFloat
switch component.style {
case .glass:
topCornerRadius = 38.0
bottomCornerRadius = 56.0
if availableSize.width < availableSize.height {
glassInset = 6.0
}
case .legacy:
topCornerRadius = 12.0
bottomCornerRadius = 12.0
}
var backgroundColor: UIColor = .clear
switch component.backgroundColor {
case let .blur(style):
self.backgroundView.isHidden = true
if self.effectView == nil {
let effectView = UIVisualEffectView(effect: UIBlurEffect(style: style == .dark ? .dark : .light))
effectView.layer.cornerRadius = self.backgroundView.layer.cornerRadius
effectView.layer.masksToBounds = true
self.backgroundView.superview?.insertSubview(effectView, aboveSubview: self.backgroundView)
self.effectView = effectView
}
case let .color(color):
backgroundColor = color
self.backgroundView.isHidden = false
self.effectView?.removeFromSuperview()
self.effectView = nil
}
transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize), completion: nil)
var containerSize: CGSize
if sheetEnvironment.isCentered {
let verticalInset: CGFloat = 44.0
let maxSide = max(availableSize.width, availableSize.height)
let minSide = min(availableSize.width, availableSize.height)
containerSize = CGSize(width: min(availableSize.width - 20.0, floor(maxSide / 2.0)), height: min(availableSize.height, minSide) - verticalInset * 2.0)
if let regularMetricsSize = sheetEnvironment.regularMetricsSize {
containerSize = regularMetricsSize
}
} else {
containerSize = CGSize(width: availableSize.width - glassInset * 2.0, height: .greatestFiniteMagnitude)
}
self.contentView.parentState = state
let contentSize = self.contentView.update(
transition: transition,
component: component.content,
environment: {
environment[ChildEnvironmentType.self]
},
containerSize: containerSize
)
component.externalState?.contentHeight = contentSize.height
self.ignoreScrolling = true
if let contentView = self.contentView.view {
if contentView.superview == nil {
self.scrollView.addSubview(self.clipView)
self.clipView.bottomCornersView.addSubview(contentView)
}
contentView.clipsToBounds = component.clipsContent
contentView.layer.cornerRadius = topCornerRadius
if sheetEnvironment.isCentered {
let y: CGFloat = floorToScreenPixels((availableSize.height - contentSize.height) / 2.0)
let clipFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) / 2.0), y: -y), size: contentSize)
self.clipView.update(size: clipFrame.size, color: .clear, topCornerRadius: topCornerRadius, bottomCornerRadius: topCornerRadius, transition: transition)
transition.setFrame(view: self.clipView, frame: clipFrame, completion: nil)
transition.setFrame(view: contentView, frame: CGRect(origin: .zero, size: clipFrame.size), completion: nil)
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) / 2.0), y: -y), size: contentSize), completion: nil)
if let effectView = self.effectView {
transition.setFrame(view: effectView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) / 2.0), y: -y), size: contentSize), completion: nil)
}
self.backgroundView.update(size: contentSize, color: backgroundColor, topCornerRadius: topCornerRadius, bottomCornerRadius: topCornerRadius, transition: transition)
} else {
switch component.style {
case .glass:
let clipFrame = CGRect(origin: CGPoint(x: glassInset, y: -glassInset), size: CGSize(width: contentSize.width, height: contentSize.height))
self.clipView.update(size: clipFrame.size, color: .clear, topCornerRadius: topCornerRadius, bottomCornerRadius: bottomCornerRadius, transition: transition)
transition.setFrame(view: self.clipView, frame: clipFrame)
transition.setFrame(view: contentView, frame: CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height)), completion: nil)
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: glassInset, y: -glassInset), size: CGSize(width: contentSize.width, height: contentSize.height)), completion: nil)
case .legacy:
let clipFrame = CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height + 100.0))
self.clipView.update(size: clipFrame.size, color: .clear, topCornerRadius: topCornerRadius, bottomCornerRadius: bottomCornerRadius, transition: transition)
transition.setFrame(view: self.clipView, frame: clipFrame)
transition.setFrame(view: contentView, frame: CGRect(origin: .zero, size: clipFrame.size), completion: nil)
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height + 1000.0)), completion: nil)
if let effectView = self.effectView {
transition.setFrame(view: effectView, frame: CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height + 1000.0)), completion: nil)
}
}
self.backgroundView.update(size: contentSize, color: backgroundColor, topCornerRadius: topCornerRadius, bottomCornerRadius: bottomCornerRadius, transition: transition)
}
}
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: availableSize), completion: nil)
if let headerContent = component.headerContent {
let headerView: ComponentView<Empty>
if let current = self.headerView {
headerView = current
} else {
headerView = ComponentView()
self.headerView = headerView
}
let headerSize = headerView.update(
transition: transition,
component: headerContent,
environment: {},
containerSize: CGSize(width: contentSize.width, height: 44.0)
)
let headerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - headerSize.width) / 2.0), y: self.backgroundView.frame.minY - headerSize.height - 10.0), size: headerSize)
if let headerView = headerView.view {
if headerView.superview == nil {
self.scrollView.addSubview(headerView)
}
transition.setFrame(view: headerView, frame: headerFrame)
}
} else if let headerView = self.headerView {
self.headerView = nil
headerView.view?.removeFromSuperview()
}
let previousContentSize = self.scrollView.contentSize
let updateContentSize = {
self.scrollView.contentSize = contentSize
self.scrollView.contentInset = UIEdgeInsets(top: max(0.0, availableSize.height - contentSize.height) + contentSize.height, left: 0.0, bottom: 0.0, right: 0.0)
}
if previousContentSize.height.isZero {
updateContentSize()
}
self.scrollView.isScrollEnabled = component.isScrollEnabled
self.ignoreScrolling = false
if let currentAvailableSize = self.currentAvailableSize, currentAvailableSize.height != availableSize.height {
self.scrollView.contentOffset = CGPoint(x: 0.0, y: -(availableSize.height - contentSize.height))
} else if component.followContentSizeChanges, !previousContentSize.height.isZero, previousContentSize != contentSize {
transition.setBounds(view: self.scrollView, bounds: CGRect(origin: CGPoint(x: 0.0, y: -(availableSize.height - contentSize.height)), size: availableSize), completion: { _ in
updateContentSize()
})
}
if self.currentHasInputHeight != previousHasInputHeight {
transition.setBounds(view: self.scrollView, bounds: CGRect(origin: CGPoint(x: 0.0, y: -(availableSize.height - contentSize.height)), size: self.scrollView.bounds.size))
}
self.currentAvailableSize = availableSize
if !component.hasDimView {
self.dimView.backgroundColor = .clear
}
if environment[SheetComponentEnvironment.self].value.isDisplaying, !self.previousIsDisplaying, let _ = transition.userData(ViewControllerComponentContainer.AnimateInTransition.self) {
self.animateIn()
} else if !environment[SheetComponentEnvironment.self].value.isDisplaying, self.previousIsDisplaying, let _ = transition.userData(ViewControllerComponentContainer.AnimateOutTransition.self) {
if component.autoAnimateOut {
self.animateOut(completion: {})
}
}
if !sheetEnvironment.isDisplaying && !component.autoAnimateOut {
} else {
self.previousIsDisplaying = sheetEnvironment.isDisplaying
}
self.dismiss = sheetEnvironment.dismiss
return availableSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public final class SheetBackgroundView: UIView {
let topCornersView = UIView()
let bottomCornersView = UIView()
public override init(frame: CGRect) {
super.init(frame: frame)
self.topCornersView.clipsToBounds = true
self.topCornersView.layer.cornerCurve = .continuous
self.topCornersView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
self.bottomCornersView.clipsToBounds = true
self.bottomCornersView.layer.cornerCurve = .continuous
self.bottomCornersView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
self.addSubview(self.topCornersView)
self.topCornersView.addSubview(self.bottomCornersView)
}
required init?(coder: NSCoder) {
preconditionFailure()
}
public func update(size: CGSize, color: UIColor, topCornerRadius: CGFloat, bottomCornerRadius: CGFloat, transition: ComponentTransition) {
transition.setCornerRadius(layer: self.topCornersView.layer, cornerRadius: topCornerRadius)
transition.setCornerRadius(layer: self.bottomCornersView.layer, cornerRadius: bottomCornerRadius)
transition.setFrame(view: self.topCornersView, frame: CGRect(origin: .zero, size: size))
transition.setFrame(view: self.bottomCornersView, frame: CGRect(origin: .zero, size: size))
transition.setBackgroundColor(view: self.bottomCornersView, color: color)
}
}
@@ -0,0 +1,20 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "SolidRoundedButtonComponent",
module_name = "SolidRoundedButtonComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,201 @@
import Foundation
import UIKit
import ComponentFlow
import Display
import SolidRoundedButtonNode
import AppBundle
public final class SolidRoundedButtonComponent: Component {
public typealias Theme = SolidRoundedButtonTheme
public let title: String?
public let subtitle: String?
public let label: String?
public let badge: String?
public let icon: UIImage?
public let theme: SolidRoundedButtonTheme
public let font: SolidRoundedButtonFont
public let fontSize: CGFloat
public let height: CGFloat
public let cornerRadius: CGFloat
public let gloss: Bool
public let glass: Bool
public let isEnabled: Bool
public let iconName: String?
public let animationName: String?
public let iconPosition: SolidRoundedButtonIconPosition
public let iconSpacing: CGFloat
public let isLoading: Bool
public let action: () -> Void
public init(
title: String? = nil,
subtitle: String? = nil,
label: String? = nil,
badge: String? = nil,
icon: UIImage? = nil,
theme: SolidRoundedButtonTheme,
font: SolidRoundedButtonFont = .bold,
fontSize: CGFloat = 17.0,
height: CGFloat = 48.0,
cornerRadius: CGFloat = 24.0,
gloss: Bool = false,
glass: Bool = false,
isEnabled: Bool = true,
iconName: String? = nil,
animationName: String? = nil,
iconPosition: SolidRoundedButtonIconPosition = .left,
iconSpacing: CGFloat = 8.0,
isLoading: Bool = false,
action: @escaping () -> Void
) {
self.title = title
self.subtitle = subtitle
self.label = label
self.badge = badge
self.icon = icon
self.theme = theme
self.font = font
self.fontSize = fontSize
self.height = height
self.cornerRadius = cornerRadius
self.gloss = gloss
self.glass = glass
self.isEnabled = isEnabled
self.iconName = iconName
self.animationName = animationName
self.iconPosition = iconPosition
self.iconSpacing = iconSpacing
self.isLoading = isLoading
self.action = action
}
public static func ==(lhs: SolidRoundedButtonComponent, rhs: SolidRoundedButtonComponent) -> Bool {
if lhs.title != rhs.title {
return false
}
if lhs.subtitle != rhs.subtitle {
return false
}
if lhs.label != rhs.label {
return false
}
if lhs.badge != rhs.badge {
return false
}
if lhs.icon !== rhs.icon {
return false
}
if lhs.theme != rhs.theme {
return false
}
if lhs.font != rhs.font {
return false
}
if lhs.fontSize != rhs.fontSize {
return false
}
if lhs.height != rhs.height {
return false
}
if lhs.cornerRadius != rhs.cornerRadius {
return false
}
if lhs.gloss != rhs.gloss {
return false
}
if lhs.glass != rhs.glass {
return false
}
if lhs.isEnabled != rhs.isEnabled {
return false
}
if lhs.iconName != rhs.iconName {
return false
}
if lhs.animationName != rhs.animationName {
return false
}
if lhs.iconPosition != rhs.iconPosition {
return false
}
if lhs.iconSpacing != rhs.iconSpacing {
return false
}
if lhs.isLoading != rhs.isLoading {
return false
}
return true
}
public final class View: UIView {
private var component: SolidRoundedButtonComponent?
private var button: SolidRoundedButtonView?
private var currentIsLoading = false
public func update(component: SolidRoundedButtonComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
if self.button == nil {
let button = SolidRoundedButtonView(
title: component.title,
label: component.label,
badge: component.badge,
icon: component.icon,
theme: component.theme,
font: component.font,
fontSize: component.fontSize,
height: component.height,
cornerRadius: component.cornerRadius,
gloss: component.gloss
)
button.progressType = .embedded
self.button = button
self.addSubview(button)
button.pressed = { [weak self] in
self?.component?.action()
}
}
if let button = self.button {
button.title = component.title
button.subtitle = component.subtitle
button.label = component.label
button.badge = component.badge
button.iconPosition = component.iconPosition
button.iconSpacing = component.iconSpacing
button.icon = component.iconName.flatMap { UIImage(bundleImageName: $0) }
button.animation = component.animationName
button.gloss = component.gloss
button.isEnabled = component.isEnabled
button.isUserInteractionEnabled = component.isEnabled
button.updateTheme(component.theme)
let height = button.updateLayout(width: availableSize.width, transition: .immediate)
transition.setFrame(view: button, frame: CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height)), completion: nil)
if self.currentIsLoading != component.isLoading {
self.currentIsLoading = component.isLoading
if component.isLoading {
button.transitionToProgress()
} else {
button.transitionFromProgress()
}
}
}
self.component = component
return availableSize
}
}
public func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
@@ -0,0 +1,18 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "UndoPanelComponent",
module_name = "UndoPanelComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/ComponentFlow:ComponentFlow",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,67 @@
import Foundation
import UIKit
import ComponentFlow
public final class UndoPanelComponent: Component {
public let icon: AnyComponent<Empty>?
public let content: AnyComponent<Empty>
public let action: AnyComponent<Empty>?
public init(
icon: AnyComponent<Empty>?,
content: AnyComponent<Empty>,
action: AnyComponent<Empty>?
) {
self.icon = icon
self.content = content
self.action = action
}
public static func ==(lhs: UndoPanelComponent, rhs: UndoPanelComponent) -> Bool {
if lhs.icon != rhs.icon {
return false
}
if lhs.content !== rhs.content {
return false
}
if lhs.action != rhs.action {
return false
}
return true
}
public final class View: UIVisualEffectView {
private var iconView: ComponentHostView<Empty>?
private let centralContentView: ComponentHostView<Empty>
private var actionView: ComponentHostView<Empty>?
init() {
self.centralContentView = ComponentHostView()
super.init(effect: nil)
self.addSubview(self.contentView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func update(component: UndoPanelComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
self.effect = UIBlurEffect(style: .dark)
self.layer.cornerRadius = 10.0
return CGSize(width: availableSize.width, height: 50.0)
}
}
public func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
@@ -0,0 +1,83 @@
import Foundation
import UIKit
import ComponentFlow
public final class UndoPanelContainerComponent: Component {
let push: ActionSlot<UndoPanelComponent>
public init(push: ActionSlot<UndoPanelComponent>) {
self.push = push
}
public static func ==(lhs: UndoPanelContainerComponent, rhs: UndoPanelContainerComponent) -> Bool {
if lhs.push != rhs.push {
return false
}
return true
}
public final class View: UIView {
private var topPanel: UndoPanelComponent?
private var topPanelView: ComponentHostView<Empty>?
private var nextPanel: UndoPanelComponent?
public func update(component: UndoPanelContainerComponent, availableSize: CGSize, state: EmptyComponentState, transition: ComponentTransition) -> CGSize {
component.push.connect { [weak self, weak state] panel in
guard let strongSelf = self, let state = state else {
return
}
strongSelf.nextPanel = panel
state.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)))
}
var animateTopPanelIn = false
var topPanelTransition = transition
if let nextPanel = self.nextPanel {
self.nextPanel = nil
self.topPanel = nextPanel
if let topPanelView = self.topPanelView {
self.topPanelView = nil
transition.withAnimationIfAnimated(.curve(duration: 0.3, curve: .easeInOut)).setAlpha(view: topPanelView, alpha: 0.0, completion: { [weak topPanelView] _ in
topPanelView?.removeFromSuperview()
})
}
let topPanelView = ComponentHostView<Empty>()
self.topPanelView = topPanelView
self.addSubview(topPanelView)
topPanelTransition = topPanelTransition.withAnimation(.none)
animateTopPanelIn = true
}
if let topPanel = self.topPanel, let topPanelView = self.topPanelView {
let topPanelSize = topPanelView.update(
transition: topPanelTransition,
component: AnyComponent(topPanel),
environment: {},
containerSize: availableSize
)
if animateTopPanelIn {
let _ = transition.withAnimationIfAnimated(.curve(duration: 0.3, curve: .easeInOut))
}
return CGSize(width: availableSize.width, height: topPanelSize.height)
}
return CGSize(width: availableSize.width, height: 0.0)
}
}
public func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, transition: transition)
}
}
@@ -0,0 +1,23 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ViewControllerComponent",
module_name = "ViewControllerComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AccountContext:AccountContext",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,454 @@
import Foundation
import UIKit
import ComponentFlow
import Display
import SwiftSignalKit
import TelegramPresentationData
import AccountContext
import ComponentDisplayAdapters
private func resolveTheme(baseTheme: PresentationTheme, theme: ViewControllerComponentContainer.Theme) -> PresentationTheme {
switch theme {
case .default:
return baseTheme
case let .custom(value):
return value
case .dark:
return customizeDefaultDarkPresentationTheme(theme: defaultDarkPresentationTheme, editing: false, title: nil, accentColor: baseTheme.list.itemAccentColor, backgroundColors: [], bubbleColors: [], animateBubbleColors: false, wallpaper: nil, baseColor: nil)
}
}
open class ViewControllerComponentContainer: ViewController {
public enum NavigationBarAppearance {
case none
case transparent
case `default`
}
public enum StatusBarStyle {
case none
case ignore
case `default`
}
public enum PresentationMode {
case `default`
case modal
}
public enum Theme {
case `default`
case dark
case custom(PresentationTheme)
}
public enum Style {
case glass
case legacy
}
public final class Environment: Equatable {
public let statusBarHeight: CGFloat
public let navigationHeight: CGFloat
public let safeInsets: UIEdgeInsets
public let additionalInsets: UIEdgeInsets
public let inputHeight: CGFloat
public let metrics: LayoutMetrics
public let deviceMetrics: DeviceMetrics
public let orientation: UIInterfaceOrientation?
public let isVisible: Bool
public let theme: PresentationTheme
public let strings: PresentationStrings
public let dateTimeFormat: PresentationDateTimeFormat
public let controller: () -> ViewController?
public init(
statusBarHeight: CGFloat,
navigationHeight: CGFloat,
safeInsets: UIEdgeInsets,
additionalInsets: UIEdgeInsets,
inputHeight: CGFloat,
metrics: LayoutMetrics,
deviceMetrics: DeviceMetrics,
orientation: UIInterfaceOrientation?,
isVisible: Bool,
theme: PresentationTheme,
strings: PresentationStrings,
dateTimeFormat: PresentationDateTimeFormat,
controller: @escaping () -> ViewController?
) {
self.statusBarHeight = statusBarHeight
self.navigationHeight = navigationHeight
self.safeInsets = safeInsets
self.additionalInsets = additionalInsets
self.inputHeight = inputHeight
self.metrics = metrics
self.deviceMetrics = deviceMetrics
self.orientation = orientation
self.isVisible = isVisible
self.theme = theme
self.strings = strings
self.dateTimeFormat = dateTimeFormat
self.controller = controller
}
public static func ==(lhs: Environment, rhs: Environment) -> Bool {
if lhs === rhs {
return true
}
if lhs.statusBarHeight != rhs.statusBarHeight {
return false
}
if lhs.navigationHeight != rhs.navigationHeight {
return false
}
if lhs.safeInsets != rhs.safeInsets {
return false
}
if lhs.additionalInsets != rhs.additionalInsets {
return false
}
if lhs.inputHeight != rhs.inputHeight {
return false
}
if lhs.metrics != rhs.metrics {
return false
}
if lhs.deviceMetrics != rhs.deviceMetrics {
return false
}
if lhs.orientation != rhs.orientation {
return false
}
if lhs.isVisible != rhs.isVisible {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.dateTimeFormat != rhs.dateTimeFormat {
return false
}
return true
}
}
public final class AnimateInTransition {
public init() {
}
}
public final class AnimateOutTransition {
public init() {
}
}
public final class Node: ViewControllerTracingNode {
fileprivate var presentationData: PresentationData
private weak var controller: ViewControllerComponentContainer?
private var component: AnyComponent<ViewControllerComponentContainer.Environment>
let theme: Theme
var resolvedTheme: PresentationTheme
public let hostView: ComponentHostView<ViewControllerComponentContainer.Environment>
private var currentIsVisible: Bool = false
private var currentLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)?
init(presentationData: PresentationData, controller: ViewControllerComponentContainer, component: AnyComponent<ViewControllerComponentContainer.Environment>, theme: Theme) {
self.presentationData = presentationData
self.controller = controller
self.component = component
self.theme = theme
self.resolvedTheme = resolveTheme(baseTheme: self.presentationData.theme, theme: theme)
self.hostView = ComponentHostView()
super.init()
self.view.addSubview(self.hostView)
}
func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ComponentTransition) {
self.currentLayout = (layout, navigationHeight)
let environment = ViewControllerComponentContainer.Environment(
statusBarHeight: layout.statusBarHeight ?? 0.0,
navigationHeight: navigationHeight,
safeInsets: UIEdgeInsets(top: layout.intrinsicInsets.top + layout.safeInsets.top, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom + layout.safeInsets.bottom, right: layout.safeInsets.right),
additionalInsets: layout.additionalInsets,
inputHeight: layout.inputHeight ?? 0.0,
metrics: layout.metrics,
deviceMetrics: layout.deviceMetrics,
orientation: layout.metrics.orientation,
isVisible: self.currentIsVisible,
theme: self.resolvedTheme,
strings: self.presentationData.strings,
dateTimeFormat: self.presentationData.dateTimeFormat,
controller: { [weak self] in
return self?.controller
}
)
let _ = self.hostView.update(
transition: transition,
component: self.component,
environment: {
environment
},
forceUpdate: self.controller?.forceNextUpdate ?? false,
containerSize: layout.size
)
transition.setFrame(view: self.hostView, frame: CGRect(origin: CGPoint(), size: layout.size), completion: nil)
}
func updateIsVisible(isVisible: Bool, animated: Bool) {
if self.currentIsVisible == isVisible {
return
}
self.currentIsVisible = isVisible
guard let currentLayout = self.currentLayout else {
return
}
self.containerLayoutUpdated(layout: currentLayout.layout, navigationHeight: currentLayout.navigationHeight, transition: animated ? ComponentTransition(animation: .none).withUserData(isVisible ? AnimateInTransition() : AnimateOutTransition()) : .immediate)
}
func updateComponent(component: AnyComponent<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) {
self.component = component
guard let currentLayout = self.currentLayout else {
return
}
self.containerLayoutUpdated(layout: currentLayout.layout, navigationHeight: currentLayout.navigationHeight, transition: transition)
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let result = super.hitTest(point, with: event) {
if result === self.view {
return nil
}
return result
}
return nil
}
}
public var node: Node {
return self.displayNode as! Node
}
private var presentationData: PresentationData
private var theme: Theme
public private(set) var component: AnyComponent<ViewControllerComponentContainer.Environment>
private var presentationDataDisposable: Disposable?
public private(set) var validLayout: ContainerViewLayout?
public var wasDismissed: (() -> Void)?
public var customProceed: (() -> Void)?
public init<C: Component>(
context: AccountContext,
component: C,
navigationBarAppearance: NavigationBarAppearance,
statusBarStyle: StatusBarStyle = .default,
presentationMode: PresentationMode = .default,
theme: Theme = .default,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil
) where C.EnvironmentType == ViewControllerComponentContainer.Environment {
self.component = AnyComponent(component)
self.theme = theme
var effectiveUpdatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)
if let updatedPresentationData {
effectiveUpdatedPresentationData = updatedPresentationData
} else {
effectiveUpdatedPresentationData = (initial: context.sharedContext.currentPresentationData.with { $0 }, signal: context.sharedContext.presentationData)
}
let presentationData = effectiveUpdatedPresentationData.initial
self.presentationData = presentationData
let navigationBarPresentationData: NavigationBarPresentationData?
switch navigationBarAppearance {
case .none:
navigationBarPresentationData = nil
case .transparent:
navigationBarPresentationData = NavigationBarPresentationData(presentationData: presentationData, hideBackground: true, hideBadge: false, hideSeparator: true)
case .default:
navigationBarPresentationData = NavigationBarPresentationData(presentationData: presentationData)
}
super.init(navigationBarPresentationData: navigationBarPresentationData)
self.setupPresentationData(effectiveUpdatedPresentationData, navigationBarAppearance: navigationBarAppearance, statusBarStyle: statusBarStyle, presentationMode: presentationMode)
}
public init<C: Component>(
component: C,
navigationBarAppearance: NavigationBarAppearance,
statusBarStyle: StatusBarStyle = .default,
presentationMode: PresentationMode = .default,
theme: Theme = .default,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)
) where C.EnvironmentType == ViewControllerComponentContainer.Environment {
self.component = AnyComponent(component)
self.theme = theme
let presentationData = updatedPresentationData.initial
self.presentationData = presentationData
let navigationBarPresentationData: NavigationBarPresentationData?
switch navigationBarAppearance {
case .none:
navigationBarPresentationData = nil
case .transparent:
navigationBarPresentationData = NavigationBarPresentationData(presentationData: presentationData, hideBackground: true, hideBadge: false, hideSeparator: true)
case .default:
navigationBarPresentationData = NavigationBarPresentationData(presentationData: presentationData)
}
super.init(navigationBarPresentationData: navigationBarPresentationData)
self.setupPresentationData(updatedPresentationData, navigationBarAppearance: navigationBarAppearance, statusBarStyle: statusBarStyle, presentationMode: presentationMode)
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDataDisposable?.dispose()
}
private func setupPresentationData(_ updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>), navigationBarAppearance: NavigationBarAppearance, statusBarStyle: StatusBarStyle, presentationMode: PresentationMode) {
self.presentationDataDisposable = (updatedPresentationData.signal
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self {
var theme = presentationData.theme
var resolvedTheme = resolveTheme(baseTheme: presentationData.theme, theme: strongSelf.theme)
if case .modal = presentationMode {
theme = theme.withModalBlocksBackground()
resolvedTheme = resolvedTheme.withModalBlocksBackground()
}
let presentationData = presentationData.withUpdated(theme: theme)
strongSelf.node.presentationData = presentationData
strongSelf.node.resolvedTheme = resolvedTheme
switch statusBarStyle {
case .none:
strongSelf.statusBar.statusBarStyle = .Hide
case .ignore:
strongSelf.statusBar.statusBarStyle = .Ignore
case .default:
strongSelf.statusBar.statusBarStyle = resolvedTheme.rootController.statusBarStyle.style
}
let navigationBarPresentationData: NavigationBarPresentationData?
switch navigationBarAppearance {
case .none:
navigationBarPresentationData = nil
case .transparent:
navigationBarPresentationData = NavigationBarPresentationData(presentationData: presentationData, hideBackground: true, hideBadge: false, hideSeparator: true)
case .default:
navigationBarPresentationData = NavigationBarPresentationData(presentationData: presentationData)
}
if let navigationBarPresentationData {
strongSelf.navigationBar?.updatePresentationData(navigationBarPresentationData)
}
if let layout = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, transition: ContainedViewLayoutTransition.immediate)
}
}
}).strict()
let resolvedTheme = resolveTheme(baseTheme: presentationData.theme, theme: self.theme)
switch statusBarStyle {
case .none:
self.statusBar.statusBarStyle = .Hide
case .ignore:
self.statusBar.statusBarStyle = .Ignore
case .default:
self.statusBar.statusBarStyle = resolvedTheme.rootController.statusBarStyle.style
}
}
override open func loadDisplayNode() {
self.displayNode = Node(presentationData: self.presentationData, controller: self, component: self.component, theme: self.theme)
self.displayNodeDidLoad()
}
private var didDismiss = false
open override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if !self.didDismiss {
self.didDismiss = true
self.wasDismissed?()
}
}
override open func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.node.updateIsVisible(isVisible: true, animated: true)
}
override open func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
self.node.updateIsVisible(isVisible: false, animated: animated)
}
open override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
super.dismiss(animated: flag, completion: {
completion?()
})
}
fileprivate var forceNextUpdate = false
public func requestLayout(forceUpdate: Bool, transition: ContainedViewLayoutTransition) {
self.forceNextUpdate = forceUpdate
self.requestLayout(transition: transition)
self.forceNextUpdate = false
}
public func requestLayout(forceUpdate: Bool, transition: ComponentTransition) {
self.forceNextUpdate = forceUpdate
if self.isViewLoaded, let validLayout = self.validLayout {
self.containerLayoutUpdated(validLayout, transition: transition)
}
self.forceNextUpdate = false
}
override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
let navigationHeight = self.navigationLayout(layout: layout).navigationFrame.maxY
self.validLayout = layout
self.node.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(transition))
}
public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ComponentTransition) {
super.containerLayoutUpdated(layout, transition: transition.containedViewLayoutTransition)
let navigationHeight = self.navigationLayout(layout: layout).navigationFrame.maxY
self.validLayout = layout
self.node.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: transition)
}
public func updateComponent(component: AnyComponent<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) {
self.component = component
self.node.updateComponent(component: component, transition: transition)
}
}