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,255 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramPresentationData
import ComponentFlow
import MultilineTextComponent
import BundleIconComponent
import PlainButtonComponent
private final class WeakController {
private weak var _value: MinimizableController?
public var value: MinimizableController? {
return self._value
}
public init(_ value: MinimizableController) {
self._value = value
}
}
final class MinimizedHeaderNode: ASDisplayNode {
var theme: NavigationControllerTheme {
didSet {
self.backgroundView.backgroundColor = self.theme.navigationBar.opaqueBackgroundColor
self.progressView.backgroundColor = self.theme.navigationBar.primaryTextColor.withAlphaComponent(0.06)
self.iconView.tintColor = self.theme.navigationBar.primaryTextColor
}
}
let strings: PresentationStrings
private let backgroundView = UIView()
private let progressView = UIView()
private var iconView = UIImageView()
private let titleLabel = ComponentView<Empty>()
private let closeButton = ComponentView<Empty>()
private var titleDisposable: Disposable?
private var _controllers: [WeakController] = []
var controllers: [MinimizableController] {
get {
return self._controllers.compactMap { $0.value }
}
set {
if !newValue.isEmpty {
if self.controllers.count == 1, let icon = self.controllers.first?.minimizedIcon {
self.icon = icon
} else {
self.icon = nil
}
if self.controllers.count == 1, let progress = self.controllers.first?.minimizedProgress {
self.progress = progress
} else {
self.progress = nil
}
if newValue.count != self.controllers.count {
self._controllers = newValue.map { WeakController($0) }
self.titleDisposable?.dispose()
self.titleDisposable = nil
var signals: [Signal<String?, NoError>] = []
for controller in newValue {
signals.append(controller.titleSignal)
}
self.titleDisposable = (combineLatest(signals)
|> deliverOnMainQueue).start(next: { [weak self] titles in
guard let self else {
return
}
let titles = titles.compactMap { $0 }.filter { !$0.isEmpty }
if titles.count == 1, let title = titles.first {
self.title = title
} else if let title = titles.last {
var trimmedTitle = title
if trimmedTitle.count > 20 {
trimmedTitle = "\(trimmedTitle.prefix(20).trimmingCharacters(in: .whitespacesAndNewlines))\u{2026}"
}
let othersString = self.strings.WebApp_MinimizedTitle_Others(Int32(titles.count - 1))
self.title = self.strings.WebApp_MinimizedTitleFormat(trimmedTitle, othersString).string
} else {
self.title = nil
}
})
}
} else {
self.icon = nil
self.titleDisposable?.dispose()
self.titleDisposable = nil
}
}
}
var icon: UIImage? {
didSet {
self.iconView.image = self.icon
if let (size, insets, isExpanded) = self.validLayout {
self.update(size: size, insets: insets, isExpanded: isExpanded, transition: .immediate)
}
}
}
var progress: Float? {
didSet {
if let (size, insets, isExpanded) = self.validLayout {
self.update(size: size, insets: insets, isExpanded: isExpanded, transition: .immediate)
}
}
}
var title: String? {
didSet {
if let (size, insets, isExpanded) = self.validLayout {
self.update(size: size, insets: insets, isExpanded: isExpanded, transition: .immediate)
}
}
}
var requestClose: () -> Void = {}
var requestMaximize: () -> Void = {}
private var validLayout: (CGSize, UIEdgeInsets, Bool)?
init(theme: NavigationControllerTheme, strings: PresentationStrings) {
self.theme = theme
self.strings = strings
self.backgroundView.clipsToBounds = true
self.backgroundView.backgroundColor = self.theme.navigationBar.opaqueBackgroundColor
self.backgroundView.layer.cornerRadius = 10.0
if #available(iOS 11.0, *) {
self.backgroundView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
}
self.progressView.backgroundColor = self.theme.navigationBar.primaryTextColor.withAlphaComponent(0.06)
self.iconView.contentMode = .scaleAspectFit
self.iconView.clipsToBounds = true
self.iconView.layer.cornerRadius = 2.5
self.iconView.tintColor = self.theme.navigationBar.primaryTextColor
super.init()
self.clipsToBounds = true
self.view.addSubview(self.backgroundView)
self.backgroundView.addSubview(self.progressView)
self.backgroundView.addSubview(self.iconView)
applySmoothRoundedCorners(self.backgroundView.layer)
}
override func didLoad() {
super.didLoad()
self.backgroundView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.maximizeTapGesture(_:))))
}
@objc private func maximizeTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
let location = recognizer.location(in: self.view)
if location.x < 48.0 {
self.requestClose()
} else {
self.requestMaximize()
}
}
}
func update(size: CGSize, insets: UIEdgeInsets, isExpanded: Bool, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, insets, isExpanded)
let headerHeight: CGFloat = 44.0
let titleSpacing: CGFloat = 6.0
var titleSideInset: CGFloat = 56.0
if !isExpanded {
titleSideInset += insets.left
}
let iconSize = CGSize(width: 20.0, height: 20.0)
let titleSize = self.titleLabel.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: self.title ?? "", font: Font.bold(17.0), textColor: self.theme.navigationBar.primaryTextColor)), horizontalAlignment: .center, maximumNumberOfLines: 1)
),
environment: {},
containerSize: CGSize(width: size.width - titleSideInset * 2.0, height: headerHeight)
)
var totalWidth = titleSize.width
if isExpanded, let icon = self.icon {
self.iconView.image = icon
totalWidth += iconSize.width + titleSpacing
} else {
self.iconView.image = nil
}
let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - totalWidth) / 2.0), y: floorToScreenPixels((headerHeight - iconSize.height) / 2.0)), size: iconSize)
self.iconView.frame = iconFrame
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - totalWidth) / 2.0) + totalWidth - titleSize.width, y: floorToScreenPixels((headerHeight - titleSize.height) / 2.0)), size: titleSize)
if let view = self.titleLabel.view {
if view.superview == nil {
self.backgroundView.addSubview(view)
}
view.bounds = CGRect(origin: .zero, size: titleFrame.size)
transition.updatePosition(layer: view.layer, position: titleFrame.center)
}
let _ = self.closeButton.update(
transition: .immediate,
component: AnyComponent(
PlainButtonComponent(
content: AnyComponent(
BundleIconComponent(
name: "Instant View/Close",
tintColor: self.theme.navigationBar.primaryTextColor
)
),
effectAlignment: .center,
minSize: CGSize(width: 44.0, height: 44.0),
action: { [weak self] in
self?.requestClose()
},
animateScale: false
)
),
environment: {},
containerSize: CGSize(width: 44.0, height: 44.0)
)
let closeButtonFrame = CGRect(origin: CGPoint(x: isExpanded ? 0.0 : insets.left, y: 0.0), size: CGSize(width: 44.0, height: 44.0))
if let view = self.closeButton.view {
if view.superview == nil {
self.backgroundView.addSubview(view)
}
transition.updateFrame(view: view, frame: closeButtonFrame)
}
transition.updateFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: CGSize(width: size.width, height: 243.0)))
transition.updateAlpha(layer: self.progressView.layer, alpha: isExpanded && self.progress != nil ? 1.0 : 0.0)
if let progress = self.progress {
self.progressView.frame = CGRect(origin: .zero, size: CGSize(width: size.width * CGFloat(progress), height: 243.0))
}
}
}
@@ -0,0 +1,186 @@
import Foundation
import UIKit
import Display
extension CATransform3D {
func interpolate(with other: CATransform3D, fraction: CGFloat) -> CATransform3D {
var vectors = Array<CGFloat>(repeating: 0.0, count: 16)
vectors[0] = self.m11 + (other.m11 - self.m11) * fraction
vectors[1] = self.m12 + (other.m12 - self.m12) * fraction
vectors[2] = self.m13 + (other.m13 - self.m13) * fraction
vectors[3] = self.m14 + (other.m14 - self.m14) * fraction
vectors[4] = self.m21 + (other.m21 - self.m21) * fraction
vectors[5] = self.m22 + (other.m22 - self.m22) * fraction
vectors[6] = self.m23 + (other.m23 - self.m23) * fraction
vectors[7] = self.m24 + (other.m24 - self.m24) * fraction
vectors[8] = self.m31 + (other.m31 - self.m31) * fraction
vectors[9] = self.m32 + (other.m32 - self.m32) * fraction
vectors[10] = self.m33 + (other.m33 - self.m33) * fraction
vectors[11] = self.m34 + (other.m34 - self.m34) * fraction
vectors[12] = self.m41 + (other.m41 - self.m41) * fraction
vectors[13] = self.m42 + (other.m42 - self.m42) * fraction
vectors[14] = self.m43 + (other.m43 - self.m43) * fraction
vectors[15] = self.m44 + (other.m44 - self.m44) * fraction
return CATransform3D(m11: vectors[0], m12: vectors[1], m13: vectors[2], m14: vectors[3], m21: vectors[4], m22: vectors[5], m23: vectors[6], m24: vectors[7], m31: vectors[8], m32: vectors[9], m33: vectors[10], m34: vectors[11], m41: vectors[12], m42: vectors[13], m43: vectors[14], m44: vectors[15])
}
}
private extension CGFloat {
func interpolate(with other: CGFloat, fraction: CGFloat) -> CGFloat {
let invT = 1.0 - fraction
let result = other * fraction + self * invT
return result
}
}
private extension CGPoint {
func interpolate(with other: CGPoint, fraction: CGFloat) -> CGPoint {
return CGPoint(x: self.x.interpolate(with: other.x, fraction: fraction), y: self.y.interpolate(with: other.y, fraction: fraction))
}
}
private extension CGSize {
func interpolate(with other: CGSize, fraction: CGFloat) -> CGSize {
return CGSize(width: self.width.interpolate(with: other.width, fraction: fraction), height: self.height.interpolate(with: other.height, fraction: fraction))
}
}
extension CGRect {
func interpolate(with other: CGRect, fraction: CGFloat) -> CGRect {
return CGRect(origin: self.origin.interpolate(with: other.origin, fraction: fraction), size: self.size.interpolate(with: other.size, fraction: fraction))
}
}
private let maxInteritemSpacing: CGFloat = 240.0
let additionalInsetTop: CGFloat = 16.0
private let additionalInsetBottom: CGFloat = 0.0
private let zOffset: CGFloat = -60.0
private let perspectiveCorrection: CGFloat = -1.0 / 1000.0
private let maxRotationAngle: CGFloat = -CGFloat.pi / 2.2
func angle(for origin: CGFloat, itemCount: Int, scrollBounds: CGRect, contentHeight: CGFloat?, insets: UIEdgeInsets) -> CGFloat {
var rotationAngle = rotationAngleAt0(itemCount: itemCount)
var contentOffset = scrollBounds.origin.y
if contentOffset < 0.0 {
contentOffset *= 2.0
}
var yOnScreen = origin - contentOffset - additionalInsetTop - insets.top
if yOnScreen < 0 {
yOnScreen = 0
} else if yOnScreen > scrollBounds.height {
yOnScreen = scrollBounds.height
}
let maxRotationVariance = maxRotationAngle - rotationAngleAt0(itemCount: itemCount)
rotationAngle += (maxRotationVariance / scrollBounds.height) * yOnScreen
return rotationAngle
}
func final3dTransform(for origin: CGFloat, size: CGSize, contentHeight: CGFloat?, itemCount: Int, forcedAngle: CGFloat? = nil, additionalAngle: CGFloat? = nil, scrollBounds: CGRect, insets: UIEdgeInsets) -> CATransform3D {
var transform = CATransform3DIdentity
transform.m34 = perspectiveCorrection
let rotationAngle = forcedAngle ?? angle(for: origin, itemCount: itemCount, scrollBounds: scrollBounds, contentHeight: contentHeight, insets: insets)
var effectiveRotationAngle = rotationAngle
if let additionalAngle = additionalAngle {
effectiveRotationAngle += additionalAngle
}
let r = size.height / 2.0 + abs(zOffset / sin(rotationAngle))
let zTranslation = r * sin(rotationAngle)
let yTranslation: CGFloat = r * (1 - cos(rotationAngle))
let zTranslateTransform = CATransform3DTranslate(transform, 0.0, -yTranslation, zTranslation)
let rotateTransform = CATransform3DRotate(zTranslateTransform, effectiveRotationAngle, 1.0, 0.0, 0.0)
return rotateTransform
}
func interitemSpacing(itemCount: Int, boundingSize: CGSize, insets: UIEdgeInsets) -> CGFloat {
var interitemSpacing = maxInteritemSpacing
if itemCount > 0 {
interitemSpacing = (boundingSize.height - additionalInsetTop - additionalInsetBottom - insets.top) / CGFloat(min(itemCount, 5))
}
return interitemSpacing
}
func frameForIndex(index: Int, size: CGSize, insets: UIEdgeInsets, itemCount: Int, boundingSize: CGSize) -> CGRect {
let spacing = interitemSpacing(itemCount: itemCount, boundingSize: boundingSize, insets: insets)
var y = additionalInsetTop + insets.top + spacing * CGFloat(index)
if itemCount == 1 {
y += 72.0
}
let origin = CGPoint(x: insets.left, y: y)
return CGRect(origin: origin, size: CGSize(width: size.width - insets.left - insets.right, height: size.height))
}
func rotationAngleAt0(itemCount: Int) -> CGFloat {
let multiplier: CGFloat = min(CGFloat(itemCount), 5.0) - 1.0
return -CGFloat.pi / 7.0 - CGFloat.pi / 7.0 * multiplier / 4.0
}
final class BlurView: UIVisualEffectView {
private func setup() {
for subview in self.subviews {
if subview.description.contains("VisualEffectSubview") {
subview.isHidden = true
}
}
if let sublayer = self.layer.sublayers?[0], let filters = sublayer.filters {
sublayer.backgroundColor = nil
sublayer.isOpaque = false
let allowedKeys: [String] = [
"gaussianBlur",
"colorSaturate"
]
sublayer.filters = filters.filter { filter in
guard let filter = filter as? NSObject else {
return true
}
let filterName = String(describing: filter)
if !allowedKeys.contains(filterName) {
return false
}
return true
}
}
}
override var effect: UIVisualEffect? {
get {
return super.effect
}
set {
super.effect = newValue
self.setup()
}
}
override func didAddSubview(_ subview: UIView) {
super.didAddSubview(subview)
self.setup()
}
}
let shadowImage: UIImage? = {
return generateImage(CGSize(width: 1.0, height: 480.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
let gradientColors = [UIColor.black.withAlphaComponent(0.0).cgColor, UIColor.black.withAlphaComponent(0.55).cgColor, UIColor.black.withAlphaComponent(0.55).cgColor] as CFArray
var locations: [CGFloat] = [0.0, 0.65, 1.0]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: bounds.height), options: [])
})
}()