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,52 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "StorageUsageScreen",
module_name = "StorageUsageScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/Components/ViewControllerComponent:ViewControllerComponent",
"//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters",
"//submodules/Components/MultilineTextComponent:MultilineTextComponent",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/AccountContext:AccountContext",
"//submodules/AppBundle:AppBundle",
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/TelegramUI/Components/EmojiStatusComponent",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/TelegramUI/Components/BottomButtonPanelComponent",
"//submodules/TelegramUI/Components/SliderComponent",
"//submodules/TelegramUI/Components/SegmentControlComponent",
"//submodules/CheckNode",
"//submodules/Markdown",
"//submodules/ContextUI",
"//submodules/AnimatedAvatarSetNode",
"//submodules/AvatarNode",
"//submodules/PhotoResources",
"//submodules/SemanticStatusNode",
"//submodules/RadialStatusNode",
"//submodules/UndoUI",
"//submodules/AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode",
"//submodules/LegacyComponents",
"//submodules/GalleryData",
"//submodules/SegmentedControlNode",
"//submodules/TelegramUIPreferences",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,139 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import ViewControllerComponent
import ComponentDisplayAdapters
import TelegramPresentationData
import AccountContext
import TelegramCore
import MultilineTextComponent
import EmojiStatusComponent
import TelegramStringFormatting
import CheckNode
final class DataButtonComponent: Component {
let theme: PresentationTheme
let title: String
let action: () -> Void
init(
theme: PresentationTheme,
title: String,
action: @escaping () -> Void
) {
self.theme = theme
self.title = title
self.action = action
}
static func ==(lhs: DataButtonComponent, rhs: DataButtonComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.title != rhs.title {
return false
}
return true
}
class View: HighlightTrackingButton {
private let title = ComponentView<Empty>()
private var component: DataButtonComponent?
private var highlightBackgroundFrame: CGRect?
private var highlightBackgroundLayer: SimpleLayer?
override init(frame: CGRect) {
super.init(frame: frame)
self.clipsToBounds = true
self.layer.cornerRadius = 26.0
self.highligthedChanged = { [weak self] isHighlighted in
guard let self, let component = self.component, let highlightBackgroundFrame = self.highlightBackgroundFrame else {
return
}
if isHighlighted {
self.superview?.bringSubviewToFront(self)
let highlightBackgroundLayer: SimpleLayer
if let current = self.highlightBackgroundLayer {
highlightBackgroundLayer = current
} else {
highlightBackgroundLayer = SimpleLayer()
self.highlightBackgroundLayer = highlightBackgroundLayer
self.layer.insertSublayer(highlightBackgroundLayer, at: 0)
highlightBackgroundLayer.backgroundColor = component.theme.list.itemHighlightedBackgroundColor.cgColor
}
highlightBackgroundLayer.frame = highlightBackgroundFrame
highlightBackgroundLayer.opacity = 1.0
} else {
if let highlightBackgroundLayer = self.highlightBackgroundLayer {
self.highlightBackgroundLayer = nil
highlightBackgroundLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak highlightBackgroundLayer] _ in
highlightBackgroundLayer?.removeFromSuperlayer()
})
}
}
}
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func pressed() {
guard let component = self.component else {
return
}
component.action()
}
func update(component: DataButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let themeUpdated = self.component?.theme !== component.theme
self.component = component
if themeUpdated {
self.backgroundColor = component.theme.list.itemBlocksBackgroundColor
}
let titleSize = self.title.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: component.title, font: Font.regular(17.0), textColor: component.theme.list.itemDestructiveColor)))),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 100.0)
)
let height: CGFloat = 52.0
let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: floor((height - titleSize.height) / 2.0)), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.isUserInteractionEnabled = false
self.addSubview(titleView)
}
transition.setFrame(view: titleView, frame: titleFrame)
}
self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height))
return CGSize(width: availableSize.width, height: height)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
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,192 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import ViewControllerComponent
import ComponentDisplayAdapters
import TelegramPresentationData
import AccountContext
import TelegramCore
import MultilineTextComponent
import EmojiStatusComponent
import CheckNode
import SolidRoundedButtonComponent
final class DataCategoriesComponent: Component {
struct CategoryData: Equatable {
var key: DataUsageScreenComponent.Category
var color: UIColor
var title: String
var size: Int64
var sizeFraction: Double
var incoming: Int64
var outgoing: Int64
var isSeparable: Bool
var isExpanded: Bool
init(key: DataUsageScreenComponent.Category, color: UIColor, title: String, size: Int64, sizeFraction: Double, incoming: Int64, outgoing: Int64, isSeparable: Bool, isExpanded: Bool) {
self.key = key
self.title = title
self.color = color
self.size = size
self.sizeFraction = sizeFraction
self.incoming = incoming
self.outgoing = outgoing
self.isSeparable = isSeparable
self.isExpanded = isExpanded
}
}
let theme: PresentationTheme
let strings: PresentationStrings
let categories: [CategoryData]
let toggleCategoryExpanded: ((DataUsageScreenComponent.Category) -> Void)?
init(
theme: PresentationTheme,
strings: PresentationStrings,
categories: [CategoryData],
toggleCategoryExpanded: ((DataUsageScreenComponent.Category) -> Void)?
) {
self.theme = theme
self.strings = strings
self.categories = categories
self.toggleCategoryExpanded = toggleCategoryExpanded
}
static func ==(lhs: DataCategoriesComponent, rhs: DataCategoriesComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.categories != rhs.categories {
return false
}
return true
}
class View: UIView {
private let containerView: UIView
private var itemViews: [DataUsageScreenComponent.Category: ComponentView<Empty>] = [:]
private var component: DataCategoriesComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.containerView = UIView()
super.init(frame: frame)
self.clipsToBounds = true
self.layer.cornerRadius = 26.0
self.addSubview(self.containerView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: DataCategoriesComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
var itemsTransition = transition
if let animationHint = transition.userData(DataUsageScreenComponent.AnimationHint.self) {
switch animationHint.value {
case .clearedItems, .modeChanged:
if let copyView = self.containerView.snapshotView(afterScreenUpdates: false) {
itemsTransition = .immediate
self.addSubview(copyView)
self.containerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.16)
copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak copyView] _ in
copyView?.removeFromSuperview()
})
}
}
}
var contentHeight: CGFloat = 0.0
var validKeys = Set<DataUsageScreenComponent.Category>()
for i in 0 ..< component.categories.count {
let category = component.categories[i]
validKeys.insert(category.key)
var itemTransition = itemsTransition
let itemView: ComponentView<Empty>
if let current = self.itemViews[category.key] {
itemView = current
} else {
itemTransition = .immediate
itemView = ComponentView()
itemView.parentState = state
self.itemViews[category.key] = itemView
}
let itemSize = itemView.update(
transition: itemTransition,
component: AnyComponent(DataCategoryItemComponent(
theme: component.theme,
strings: component.strings,
category: category,
isExpanded: category.isExpanded,
hasNext: i != component.categories.count - 1,
action: component.toggleCategoryExpanded == nil ? nil : { [weak self] key in
guard let self, let component = self.component else {
return
}
component.toggleCategoryExpanded?(key)
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 1000.0)
)
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: itemSize)
if let itemComponentView = itemView.view {
if itemComponentView.superview == nil {
self.containerView.addSubview(itemComponentView)
}
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
}
contentHeight += itemSize.height
}
var removeKeys: [DataUsageScreenComponent.Category] = []
for (key, itemView) in self.itemViews {
if !validKeys.contains(key) {
if let itemComponentView = itemView.view {
transition.setAlpha(view: itemComponentView, alpha: 0.0, completion: { [weak itemComponentView] _ in
itemComponentView?.removeFromSuperview()
})
}
removeKeys.append(key)
}
}
for key in removeKeys {
self.itemViews.removeValue(forKey: key)
}
self.backgroundColor = component.theme.list.itemBlocksBackgroundColor
self.containerView.backgroundColor = component.theme.list.itemBlocksBackgroundColor
self.containerView.frame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: contentHeight))
return CGSize(width: availableSize.width, height: contentHeight)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
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,611 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import ViewControllerComponent
import ComponentDisplayAdapters
import TelegramPresentationData
import AccountContext
import TelegramCore
import MultilineTextComponent
import EmojiStatusComponent
import TelegramStringFormatting
import CheckNode
private final class SubItemComponent: Component {
let theme: PresentationTheme
let strings: PresentationStrings
let isIncoming: Bool
let value: Int64
let hasNext: Bool
init(
theme: PresentationTheme,
strings: PresentationStrings,
isIncoming: Bool,
value: Int64,
hasNext: Bool
) {
self.theme = theme
self.strings = strings
self.isIncoming = isIncoming
self.value = value
self.hasNext = hasNext
}
static func ==(lhs: SubItemComponent, rhs: SubItemComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.isIncoming != rhs.isIncoming {
return false
}
if lhs.value != rhs.value {
return false
}
return true
}
class View: UIView {
private let iconView: UIImageView
private let title = ComponentView<Empty>()
private let titleValue = ComponentView<Empty>()
private let label = ComponentView<Empty>()
private let separatorLayer: SimpleLayer
private var component: SubItemComponent?
private var highlightBackgroundFrame: CGRect?
private var highlightBackgroundLayer: SimpleLayer?
override init(frame: CGRect) {
self.iconView = UIImageView()
self.separatorLayer = SimpleLayer()
super.init(frame: frame)
self.layer.addSublayer(self.separatorLayer)
self.addSubview(self.iconView)
/*self.highligthedChanged = { [weak self] isHighlighted in
guard let self, let component = self.component, let highlightBackgroundFrame = self.highlightBackgroundFrame else {
return
}
if isHighlighted {
self.superview?.bringSubviewToFront(self)
let highlightBackgroundLayer: SimpleLayer
if let current = self.highlightBackgroundLayer {
highlightBackgroundLayer = current
} else {
highlightBackgroundLayer = SimpleLayer()
self.highlightBackgroundLayer = highlightBackgroundLayer
self.layer.insertSublayer(highlightBackgroundLayer, above: self.separatorLayer)
highlightBackgroundLayer.backgroundColor = component.theme.list.itemHighlightedBackgroundColor.cgColor
}
highlightBackgroundLayer.frame = highlightBackgroundFrame
highlightBackgroundLayer.opacity = 1.0
} else {
if let highlightBackgroundLayer = self.highlightBackgroundLayer {
self.highlightBackgroundLayer = nil
highlightBackgroundLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak highlightBackgroundLayer] _ in
highlightBackgroundLayer?.removeFromSuperlayer()
})
}
}
}
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
self.isEnabled = false*/
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func pressed() {
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let result = super.hitTest(point, with: event) else {
return nil
}
return result
}
func update(component: SubItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let themeUpdated = self.component?.theme !== component.theme || self.component?.isIncoming != component.isIncoming
self.component = component
if themeUpdated {
self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: component.isIncoming ? "Settings/Menu/DataExpandedIn" : "Settings/Menu/DataExpandedOut"), color: component.theme.list.itemPrimaryTextColor)
}
var leftInset: CGFloat = 62.0
var additionalLeftInset: CGFloat = 0.0
additionalLeftInset += 45.0
leftInset += additionalLeftInset
let rightInset: CGFloat = 16.0
var availableWidth: CGFloat = availableSize.width - leftInset - rightInset
let fractionString: String = ""
/*if component.category.sizeFraction != 0.0 {
let fractionValue: Double = floor(component.category.sizeFraction * 100.0 * 10.0) / 10.0
if fractionValue < 0.1 {
fractionString = "<0.1%"
} else if abs(Double(Int(fractionValue)) - fractionValue) < 0.001 {
fractionString = "\(Int(fractionValue))%"
} else {
fractionString = "\(fractionValue)%"
}
} else {
fractionString = ""
}*/
let labelSize = self.label.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: dataSizeString(Int(component.value), formatting: DataSizeStringFormatting(strings: component.strings, decimalSeparator: ".")), font: Font.regular(17.0), textColor: component.theme.list.itemSecondaryTextColor)))),
environment: {},
containerSize: CGSize(width: availableWidth, height: 100.0)
)
availableWidth = max(1.0, availableWidth - labelSize.width - 1.0)
let titleValueSize = self.titleValue.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: fractionString, font: Font.regular(17.0), textColor: component.theme.list.itemSecondaryTextColor)))),
environment: {},
containerSize: CGSize(width: availableWidth, height: 100.0)
)
availableWidth = max(1.0, availableWidth - titleValueSize.width - 4.0)
let titleSize = self.title.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: component.isIncoming ? component.strings.DataUsage_MediaDirectionIncoming : component.strings.DataUsage_MediaDirectionOutgoing, font: Font.regular(17.0), textColor: component.theme.list.itemPrimaryTextColor)))),
environment: {},
containerSize: CGSize(width: availableWidth, height: 100.0)
)
let height: CGFloat = 52.0
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - titleSize.height) / 2.0)), size: titleSize)
let titleValueFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: floor((height - titleValueSize.height) / 2.0)), size: titleValueSize)
let labelFrame = CGRect(origin: CGPoint(x: availableSize.width - rightInset - labelSize.width, y: floor((height - labelSize.height) / 2.0)), size: labelSize)
if let image = self.iconView.image {
transition.setFrame(view: self.iconView, frame: CGRect(origin: CGPoint(x: leftInset - additionalLeftInset + floor((additionalLeftInset - image.size.width) * 0.5), y: floor((height - image.size.height) * 0.5)), size: image.size))
}
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.isUserInteractionEnabled = false
self.addSubview(titleView)
}
transition.setFrame(view: titleView, frame: titleFrame)
}
if let titleValueView = self.titleValue.view {
if titleValueView.superview == nil {
titleValueView.isUserInteractionEnabled = false
self.addSubview(titleValueView)
}
if titleValueView.bounds.size != titleValueFrame.size {
titleValueView.frame = titleValueFrame
} else {
transition.setFrame(view: titleValueView, frame: titleValueFrame)
}
}
if let labelView = self.label.view {
if labelView.superview == nil {
labelView.isUserInteractionEnabled = false
self.addSubview(labelView)
}
transition.setFrame(view: labelView, frame: labelFrame)
}
if themeUpdated {
self.separatorLayer.backgroundColor = component.theme.list.itemBlocksSeparatorColor.cgColor
}
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel)))
transition.setAlpha(layer: self.separatorLayer, alpha: (component.hasNext) ? 1.0 : 0.0)
self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height + ((component.hasNext) ? UIScreenPixel : 0.0)))
return CGSize(width: availableSize.width, height: height)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
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)
}
}
final class DataCategoryItemComponent: Component {
let theme: PresentationTheme
let strings: PresentationStrings
let category: DataCategoriesComponent.CategoryData
let isExpanded: Bool
let hasNext: Bool
let action: ((DataUsageScreenComponent.Category) -> Void)?
init(
theme: PresentationTheme,
strings: PresentationStrings,
category: DataCategoriesComponent.CategoryData,
isExpanded: Bool,
hasNext: Bool,
action: ((DataUsageScreenComponent.Category) -> Void)?
) {
self.theme = theme
self.strings = strings
self.category = category
self.isExpanded = isExpanded
self.hasNext = hasNext
self.action = action
}
static func ==(lhs: DataCategoryItemComponent, rhs: DataCategoryItemComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.category != rhs.category {
return false
}
if lhs.isExpanded != rhs.isExpanded {
return false
}
if lhs.hasNext != rhs.hasNext {
return false
}
return true
}
class View: HighlightTrackingButton {
private let iconView: UIImageView
private var expandIconView: UIImageView?
private let title = ComponentView<Empty>()
private let titleValue = ComponentView<Empty>()
private let label = ComponentView<Empty>()
private let separatorLayer: SimpleLayer
private let subcategoryClippingContainer: UIView
private var itemViews: [AnyHashable: ComponentView<Empty>] = [:]
private var component: DataCategoryItemComponent?
private var highlightBackgroundFrame: CGRect?
private var highlightBackgroundLayer: SimpleLayer?
override init(frame: CGRect) {
self.iconView = UIImageView()
self.separatorLayer = SimpleLayer()
self.subcategoryClippingContainer = UIView()
self.subcategoryClippingContainer.clipsToBounds = true
super.init(frame: frame)
self.addSubview(self.subcategoryClippingContainer)
self.layer.addSublayer(self.separatorLayer)
self.addSubview(self.iconView)
self.highligthedChanged = { [weak self] isHighlighted in
guard let self, let component = self.component, let highlightBackgroundFrame = self.highlightBackgroundFrame else {
return
}
if isHighlighted {
self.superview?.bringSubviewToFront(self)
let highlightBackgroundLayer: SimpleLayer
if let current = self.highlightBackgroundLayer {
highlightBackgroundLayer = current
} else {
highlightBackgroundLayer = SimpleLayer()
self.highlightBackgroundLayer = highlightBackgroundLayer
self.layer.insertSublayer(highlightBackgroundLayer, above: self.separatorLayer)
highlightBackgroundLayer.backgroundColor = component.theme.list.itemHighlightedBackgroundColor.cgColor
}
highlightBackgroundLayer.frame = highlightBackgroundFrame
highlightBackgroundLayer.opacity = 1.0
} else {
if let highlightBackgroundLayer = self.highlightBackgroundLayer {
self.highlightBackgroundLayer = nil
highlightBackgroundLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak highlightBackgroundLayer] _ in
highlightBackgroundLayer?.removeFromSuperlayer()
})
}
}
}
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func pressed() {
guard let component = self.component else {
return
}
component.action?(component.category.key)
}
@objc private func checkPressed() {
guard let component = self.component else {
return
}
component.action?(component.category.key)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let result = super.hitTest(point, with: event) else {
return nil
}
if result === self.subcategoryClippingContainer {
return self
}
return result
}
func update(component: DataCategoryItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let themeUpdated = self.component?.theme !== component.theme || self.component?.category.color != component.category.color
self.component = component
if themeUpdated {
let imageName: String
switch component.category.key {
case .photos:
imageName = "Settings/Menu/DataPhotos"
case .videos:
imageName = "Settings/Menu/DataVideo"
case .files:
imageName = "Settings/Menu/DataFiles"
case .music:
imageName = "Settings/Menu/DataMusic"
case .messages:
imageName = "Settings/Menu/DataMessages"
case .stickers:
imageName = "Settings/Menu/DataStickers"
case .voiceMessages:
imageName = "Settings/Menu/DataVoice"
case .calls:
imageName = "Settings/Menu/DataCalls"
case .totalIn:
imageName = "Settings/Menu/DataIn"
case .totalOut:
imageName = "Settings/Menu/DataOut"
}
self.iconView.image = UIImage(bundleImageName: imageName)
}
var leftInset: CGFloat = 62.0
let additionalLeftInset: CGFloat = 0.0
leftInset += additionalLeftInset
let rightInset: CGFloat = 16.0
var availableWidth: CGFloat = availableSize.width - leftInset - rightInset
if component.category.isSeparable {
let expandIconView: UIImageView
if let current = self.expandIconView {
expandIconView = current
if themeUpdated {
expandIconView.image = PresentationResourcesItemList.disclosureArrowImage(component.theme)
}
} else {
expandIconView = UIImageView()
expandIconView.image = PresentationResourcesItemList.disclosureArrowImage(component.theme)
self.expandIconView = expandIconView
self.addSubview(expandIconView)
}
if let image = expandIconView.image {
availableWidth -= image.size.width + 6.0
transition.setBounds(view: expandIconView, bounds: CGRect(origin: CGPoint(), size: image.size))
}
} else if let expandIconView = self.expandIconView {
self.expandIconView = nil
expandIconView.removeFromSuperview()
}
let fractionString: String
if component.category.sizeFraction != 0.0 {
let fractionValue: Double = floor(component.category.sizeFraction * 100.0 * 10.0) / 10.0
if fractionValue < 0.1 {
fractionString = "<0.1%"
} else if abs(Double(Int(fractionValue)) - fractionValue) < 0.001 {
fractionString = "\(Int(fractionValue))%"
} else {
fractionString = "\(fractionValue)%"
}
} else {
fractionString = ""
}
let labelSize = self.label.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: dataSizeString(Int(component.category.size), formatting: DataSizeStringFormatting(strings: component.strings, decimalSeparator: ".")), font: Font.regular(17.0), textColor: component.theme.list.itemSecondaryTextColor)))),
environment: {},
containerSize: CGSize(width: availableWidth, height: 100.0)
)
availableWidth = max(1.0, availableWidth - labelSize.width - 1.0)
let titleValueSize = self.titleValue.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: fractionString, font: Font.regular(17.0), textColor: component.theme.list.itemSecondaryTextColor)))),
environment: {},
containerSize: CGSize(width: availableWidth, height: 100.0)
)
availableWidth = max(1.0, availableWidth - titleValueSize.width - 4.0)
let titleSize = self.title.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: component.category.title, font: Font.regular(17.0), textColor: component.theme.list.itemPrimaryTextColor)))),
environment: {},
containerSize: CGSize(width: availableWidth, height: 100.0)
)
var height: CGFloat = 52.0
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - titleSize.height) / 2.0)), size: titleSize)
let titleValueFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: floor((height - titleValueSize.height) / 2.0)), size: titleValueSize)
var labelFrame = CGRect(origin: CGPoint(x: availableSize.width - rightInset - labelSize.width, y: floor((height - labelSize.height) / 2.0)), size: labelSize)
if let expandIconView = self.expandIconView, let image = expandIconView.image {
labelFrame.origin.x -= image.size.width - 6.0
transition.setPosition(view: expandIconView, position: CGPoint(x: availableSize.width - rightInset + 6.0 - floor(image.size.width * 0.5), y: floor(height * 0.5)))
let angle: CGFloat = component.isExpanded ? CGFloat.pi : 0.0
transition.setTransform(view: expandIconView, transform: CATransform3DMakeRotation(CGFloat.pi * 0.5 - angle, 0.0, 0.0, 1.0))
}
if let image = self.iconView.image {
transition.setFrame(view: self.iconView, frame: CGRect(origin: CGPoint(x: floor((leftInset - image.size.width) * 0.5), y: floor((height - image.size.height) * 0.5)), size: image.size))
}
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.isUserInteractionEnabled = false
self.addSubview(titleView)
}
transition.setFrame(view: titleView, frame: titleFrame)
}
if let titleValueView = self.titleValue.view {
if titleValueView.superview == nil {
titleValueView.isUserInteractionEnabled = false
self.addSubview(titleValueView)
}
if titleValueView.bounds.size != titleValueFrame.size {
titleValueView.frame = titleValueFrame
} else {
transition.setFrame(view: titleValueView, frame: titleValueFrame)
}
}
if let labelView = self.label.view {
if labelView.superview == nil {
labelView.isUserInteractionEnabled = false
self.addSubview(labelView)
}
transition.setFrame(view: labelView, frame: labelFrame)
}
if themeUpdated {
self.separatorLayer.backgroundColor = component.theme.list.itemBlocksSeparatorColor.cgColor
}
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel)))
transition.setAlpha(layer: self.separatorLayer, alpha: (component.isExpanded || component.hasNext) ? 1.0 : 0.0)
self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height + ((component.isExpanded || component.hasNext) ? UIScreenPixel : 0.0)))
var validKeys = Set<AnyHashable>()
if component.isExpanded, component.category.isSeparable {
struct SubItem {
var id: AnyHashable
var isIncoming: Bool
var value: Int64
}
let items: [SubItem] = [
SubItem(id: "in", isIncoming: true, value: component.category.incoming),
SubItem(id: "out", isIncoming: false, value: component.category.outgoing)
]
for i in 0 ..< items.count {
let item = items[i]
validKeys.insert(item.id)
var itemTransition = transition
let itemView: ComponentView<Empty>
if let current = self.itemViews[item.id] {
itemView = current
} else {
itemTransition = .immediate
itemView = ComponentView()
self.itemViews[item.id] = itemView
}
itemView.parentState = state
let itemSize = itemView.update(
transition: itemTransition,
component: AnyComponent(SubItemComponent(
theme: component.theme,
strings: component.strings,
isIncoming: item.isIncoming,
value: item.value,
hasNext: i != items.count - 1
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 1000.0)
)
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: height), size: itemSize)
if let itemComponentView = itemView.view {
if itemComponentView.superview == nil {
self.subcategoryClippingContainer.addSubview(itemComponentView)
if !transition.animation.isImmediate {
itemComponentView.alpha = 0.0
transition.setAlpha(view: itemComponentView, alpha: 1.0)
}
}
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
}
height += itemSize.height
}
}
var removeKeys: [AnyHashable] = []
for (key, itemView) in self.itemViews {
if !validKeys.contains(key) {
if let itemComponentView = itemView.view {
transition.setAlpha(view: itemComponentView, alpha: 0.0, completion: { [weak itemComponentView] _ in
itemComponentView?.removeFromSuperview()
})
}
removeKeys.append(key)
}
}
for key in removeKeys {
self.itemViews.removeValue(forKey: key)
}
transition.setFrame(view: self.subcategoryClippingContainer, frame: CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height)))
self.isEnabled = component.action != nil
return CGSize(width: availableSize.width, height: height)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
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,289 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import ViewControllerComponent
import ComponentDisplayAdapters
import TelegramPresentationData
import AccountContext
import TelegramCore
import MultilineTextComponent
import EmojiStatusComponent
import CheckNode
import ButtonComponent
final class StorageCategoriesComponent: Component {
struct CategoryData: Equatable {
var key: StorageUsageScreenComponent.Category
var color: UIColor
var title: String
var size: Int64
var sizeFraction: Double
var isSelected: Bool
var subcategories: [CategoryData]
init(key: StorageUsageScreenComponent.Category, color: UIColor, title: String, size: Int64, sizeFraction: Double, isSelected: Bool, subcategories: [CategoryData]) {
self.key = key
self.title = title
self.color = color
self.size = size
self.sizeFraction = sizeFraction
self.isSelected = isSelected
self.subcategories = subcategories
}
}
let theme: PresentationTheme
let strings: PresentationStrings
let categories: [CategoryData]
let isOtherExpanded: Bool
let displayAction: Bool
let toggleCategorySelection: (StorageUsageScreenComponent.Category) -> Void
let toggleOtherExpanded: () -> Void
let clearAction: () -> Void
init(
theme: PresentationTheme,
strings: PresentationStrings,
categories: [CategoryData],
isOtherExpanded: Bool,
displayAction: Bool,
toggleCategorySelection: @escaping (StorageUsageScreenComponent.Category) -> Void,
toggleOtherExpanded: @escaping () -> Void,
clearAction: @escaping () -> Void
) {
self.theme = theme
self.strings = strings
self.categories = categories
self.isOtherExpanded = isOtherExpanded
self.displayAction = displayAction
self.toggleCategorySelection = toggleCategorySelection
self.toggleOtherExpanded = toggleOtherExpanded
self.clearAction = clearAction
}
static func ==(lhs: StorageCategoriesComponent, rhs: StorageCategoriesComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.categories != rhs.categories {
return false
}
if lhs.isOtherExpanded != rhs.isOtherExpanded {
return false
}
if lhs.displayAction != rhs.displayAction {
return false
}
return true
}
class View: UIView {
private var itemViews: [StorageUsageScreenComponent.Category: ComponentView<Empty>] = [:]
private let button = ComponentView<Empty>()
private var component: StorageCategoriesComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
self.clipsToBounds = true
self.layer.cornerRadius = 26.0
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: StorageCategoriesComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let expandedCategory: StorageUsageScreenComponent.Category? = component.isOtherExpanded ? .other : nil
var totalSelectedSize: Int64 = 0
var hasDeselected = false
for category in component.categories {
if !category.subcategories.isEmpty {
for subcategory in category.subcategories {
if subcategory.isSelected {
totalSelectedSize += subcategory.size
} else {
hasDeselected = true
}
}
} else {
if category.isSelected {
totalSelectedSize += category.size
} else {
hasDeselected = true
}
}
}
var contentHeight: CGFloat = 0.0
var validKeys = Set<StorageUsageScreenComponent.Category>()
for i in 0 ..< component.categories.count {
let category = component.categories[i]
validKeys.insert(category.key)
var itemTransition = transition
let itemView: ComponentView<Empty>
if let current = self.itemViews[category.key] {
itemView = current
} else {
itemTransition = .immediate
itemView = ComponentView()
itemView.parentState = state
self.itemViews[category.key] = itemView
}
let itemSize = itemView.update(
transition: itemTransition,
component: AnyComponent(StorageCategoryItemComponent(
theme: component.theme,
strings: component.strings,
category: category,
isExpandedLevel: false,
isExpanded: expandedCategory == category.key,
hasNext: i != component.categories.count - 1,
action: { [weak self] key, actionType in
guard let self, let component = self.component else {
return
}
switch actionType {
case .generic:
if let category = component.categories.first(where: { $0.key == key }), !category.subcategories.isEmpty {
component.toggleOtherExpanded()
} else {
component.toggleCategorySelection(key)
}
case .toggle:
component.toggleCategorySelection(key)
}
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 1000.0)
)
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: itemSize)
if let itemComponentView = itemView.view {
if itemComponentView.superview == nil {
self.addSubview(itemComponentView)
}
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
}
contentHeight += itemSize.height
}
var removeKeys: [StorageUsageScreenComponent.Category] = []
for (key, itemView) in self.itemViews {
if !validKeys.contains(key) {
if let itemComponentView = itemView.view {
transition.setAlpha(view: itemComponentView, alpha: 0.0, completion: { [weak itemComponentView] _ in
itemComponentView?.removeFromSuperview()
})
}
removeKeys.append(key)
}
}
for key in removeKeys {
self.itemViews.removeValue(forKey: key)
}
if component.displayAction {
let clearTitle: String
let label: String?
if totalSelectedSize == 0 {
clearTitle = component.strings.StorageManagement_ClearSelected
label = nil
} else if hasDeselected {
clearTitle = component.strings.StorageManagement_ClearSelected
label = dataSizeString(totalSelectedSize, formatting: DataSizeStringFormatting(strings: component.strings, decimalSeparator: "."))
} else {
clearTitle = component.strings.StorageManagement_ClearAll
label = dataSizeString(totalSelectedSize, formatting: DataSizeStringFormatting(strings: component.strings, decimalSeparator: "."))
}
var buttonContents: [AnyComponentWithIdentity<Empty>] = [
AnyComponentWithIdentity(id: "title", component: AnyComponent(
MultilineTextComponent(text: .plain(NSMutableAttributedString(string: clearTitle, font: Font.semibold(17.0), textColor: component.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)))
))
]
if let label {
buttonContents.append(
AnyComponentWithIdentity(id: "label", component: AnyComponent(
MultilineTextComponent(text: .plain(NSMutableAttributedString(string: label, font: Font.semibold(17.0), textColor: component.theme.list.itemCheckColors.foregroundColor.withMultipliedAlpha(0.6), paragraphAlignment: .center)))
))
)
}
contentHeight += 8.0
let buttonSize = self.button.update(
transition: transition,
component: AnyComponent(
ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
color: component.theme.list.itemCheckColors.fillColor,
foreground: component.theme.list.itemCheckColors.foregroundColor,
pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
),
content: AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(
HStack(buttonContents, spacing: 4.0)
)
),
isEnabled: totalSelectedSize != 0,
displaysProgress: false,
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.clearAction()
}
)
),
environment: {},
containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: 52.0)
)
let buttonFrame = CGRect(origin: CGPoint(x: 16.0, y: contentHeight), size: buttonSize)
if let buttonView = self.button.view {
if buttonView.superview == nil {
self.addSubview(buttonView)
}
transition.setFrame(view: buttonView, frame: buttonFrame)
}
contentHeight += buttonSize.height
contentHeight += 16.0
} else {
if let buttonView = self.button.view {
buttonView.removeFromSuperview()
}
}
self.backgroundColor = component.theme.list.itemBlocksBackgroundColor
return CGSize(width: availableSize.width, height: contentHeight)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
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,415 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import ViewControllerComponent
import ComponentDisplayAdapters
import TelegramPresentationData
import AccountContext
import TelegramCore
import MultilineTextComponent
import EmojiStatusComponent
import TelegramStringFormatting
import CheckNode
final class StorageCategoryItemComponent: Component {
enum ActionType {
case toggle
case generic
}
let theme: PresentationTheme
let strings: PresentationStrings
let category: StorageCategoriesComponent.CategoryData
let isExpandedLevel: Bool
let isExpanded: Bool
let hasNext: Bool
let action: (StorageUsageScreenComponent.Category, ActionType) -> Void
init(
theme: PresentationTheme,
strings: PresentationStrings,
category: StorageCategoriesComponent.CategoryData,
isExpandedLevel: Bool,
isExpanded: Bool,
hasNext: Bool,
action: @escaping (StorageUsageScreenComponent.Category, ActionType) -> Void
) {
self.theme = theme
self.strings = strings
self.category = category
self.isExpandedLevel = isExpandedLevel
self.isExpanded = isExpanded
self.hasNext = hasNext
self.action = action
}
static func ==(lhs: StorageCategoryItemComponent, rhs: StorageCategoryItemComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.category != rhs.category {
return false
}
if lhs.isExpandedLevel != rhs.isExpandedLevel {
return false
}
if lhs.isExpanded != rhs.isExpanded {
return false
}
if lhs.hasNext != rhs.hasNext {
return false
}
return true
}
class View: HighlightTrackingButton {
private let checkLayer: CheckLayer
private let title = ComponentView<Empty>()
private let titleValue = ComponentView<Empty>()
private let label = ComponentView<Empty>()
private var iconView: UIImageView?
private let separatorLayer: SimpleLayer
private let checkButtonArea: HighlightTrackingButton
private let subcategoryClippingContainer: UIView
private var itemViews: [StorageUsageScreenComponent.Category: ComponentView<Empty>] = [:]
private var component: StorageCategoryItemComponent?
private var highlightBackgroundFrame: CGRect?
private var highlightBackgroundLayer: SimpleLayer?
override init(frame: CGRect) {
self.checkLayer = CheckLayer()
self.separatorLayer = SimpleLayer()
self.checkButtonArea = HighlightTrackingButton()
self.subcategoryClippingContainer = UIView()
self.subcategoryClippingContainer.clipsToBounds = true
super.init(frame: frame)
self.addSubview(self.subcategoryClippingContainer)
self.layer.addSublayer(self.separatorLayer)
self.layer.addSublayer(self.checkLayer)
self.addSubview(self.checkButtonArea)
self.highligthedChanged = { [weak self] isHighlighted in
guard let self, let component = self.component, let highlightBackgroundFrame = self.highlightBackgroundFrame else {
return
}
if isHighlighted {
self.superview?.bringSubviewToFront(self)
let highlightBackgroundLayer: SimpleLayer
if let current = self.highlightBackgroundLayer {
highlightBackgroundLayer = current
} else {
highlightBackgroundLayer = SimpleLayer()
self.highlightBackgroundLayer = highlightBackgroundLayer
self.layer.insertSublayer(highlightBackgroundLayer, above: self.separatorLayer)
highlightBackgroundLayer.backgroundColor = component.theme.list.itemHighlightedBackgroundColor.cgColor
}
highlightBackgroundLayer.frame = highlightBackgroundFrame
highlightBackgroundLayer.opacity = 1.0
} else {
if let highlightBackgroundLayer = self.highlightBackgroundLayer {
self.highlightBackgroundLayer = nil
highlightBackgroundLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak highlightBackgroundLayer] _ in
highlightBackgroundLayer?.removeFromSuperlayer()
})
}
}
}
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
self.checkButtonArea.addTarget(self, action: #selector(self.checkPressed), for: .touchUpInside)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func pressed() {
guard let component = self.component else {
return
}
component.action(component.category.key, .generic)
}
@objc private func checkPressed() {
guard let component = self.component else {
return
}
component.action(component.category.key, .toggle)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let result = super.hitTest(point, with: event) else {
return nil
}
if result === self.subcategoryClippingContainer {
return self
}
return result
}
func update(component: StorageCategoryItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let themeUpdated = self.component?.theme !== component.theme || self.component?.category.color != component.category.color
self.component = component
var leftInset: CGFloat = 62.0
var additionalLeftInset: CGFloat = 0.0
if component.isExpandedLevel {
additionalLeftInset += 45.0
}
leftInset += additionalLeftInset
let rightInset: CGFloat = 16.0
var availableWidth: CGFloat = availableSize.width - leftInset - rightInset
if !component.category.subcategories.isEmpty {
let iconView: UIImageView
if let current = self.iconView {
iconView = current
if themeUpdated {
iconView.image = PresentationResourcesItemList.disclosureArrowImage(component.theme)
}
} else {
iconView = UIImageView()
iconView.image = PresentationResourcesItemList.disclosureArrowImage(component.theme)
self.iconView = iconView
self.addSubview(iconView)
}
if let image = iconView.image {
availableWidth -= image.size.width + 6.0
transition.setBounds(view: iconView, bounds: CGRect(origin: CGPoint(), size: image.size))
}
} else if let iconView = self.iconView {
self.iconView = nil
iconView.removeFromSuperview()
}
let fractionString: String
if component.category.sizeFraction != 0.0 {
let fractionValue: Double = floor(component.category.sizeFraction * 100.0 * 10.0) / 10.0
if fractionValue < 0.1 {
fractionString = "<0.1%"
} else if abs(Double(Int(fractionValue)) - fractionValue) < 0.001 {
fractionString = "\(Int(fractionValue))%"
} else {
fractionString = "\(fractionValue)%"
}
} else {
fractionString = ""
}
let labelSize = self.label.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: dataSizeString(Int(component.category.size), formatting: DataSizeStringFormatting(strings: component.strings, decimalSeparator: ".")), font: Font.regular(17.0), textColor: component.theme.list.itemSecondaryTextColor)))),
environment: {},
containerSize: CGSize(width: availableWidth, height: 100.0)
)
availableWidth = max(1.0, availableWidth - labelSize.width - 1.0)
let titleValueSize = self.titleValue.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: fractionString, font: Font.regular(17.0), textColor: component.theme.list.itemSecondaryTextColor)))),
environment: {},
containerSize: CGSize(width: availableWidth, height: 100.0)
)
availableWidth = max(1.0, availableWidth - titleValueSize.width - 4.0)
let titleSize = self.title.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: component.category.title, font: Font.regular(17.0), textColor: component.theme.list.itemPrimaryTextColor)))),
environment: {},
containerSize: CGSize(width: availableWidth, height: 100.0)
)
var height: CGFloat = 52.0
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - titleSize.height) / 2.0)), size: titleSize)
let titleValueFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: floor((height - titleValueSize.height) / 2.0)), size: titleValueSize)
var labelFrame = CGRect(origin: CGPoint(x: availableSize.width - rightInset - labelSize.width, y: floor((height - labelSize.height) / 2.0)), size: labelSize)
if let iconView = self.iconView, let image = iconView.image {
labelFrame.origin.x -= image.size.width - 6.0
transition.setPosition(view: iconView, position: CGPoint(x: availableSize.width - rightInset + 6.0 - floor(image.size.width * 0.5), y: floor(height * 0.5)))
let angle: CGFloat = component.isExpanded ? CGFloat.pi : 0.0
transition.setTransform(view: iconView, transform: CATransform3DMakeRotation(CGFloat.pi * 0.5 - angle, 0.0, 0.0, 1.0))
}
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.isUserInteractionEnabled = false
self.addSubview(titleView)
}
transition.setFrame(view: titleView, frame: titleFrame)
}
if let titleValueView = self.titleValue.view {
if titleValueView.superview == nil {
titleValueView.isUserInteractionEnabled = false
self.addSubview(titleValueView)
}
if titleValueView.bounds.size != titleValueFrame.size {
titleValueView.frame = titleValueFrame
} else {
transition.setFrame(view: titleValueView, frame: titleValueFrame)
}
}
if let labelView = self.label.view {
if labelView.superview == nil {
labelView.isUserInteractionEnabled = false
self.addSubview(labelView)
}
transition.setFrame(view: labelView, frame: labelFrame)
}
var copyCheckLayer: CheckLayer?
if themeUpdated {
if !transition.animation.isImmediate {
let copyLayer = CheckLayer(theme: self.checkLayer.theme)
copyLayer.frame = self.checkLayer.frame
copyLayer.setSelected(self.checkLayer.selected, animated: false)
self.layer.addSublayer(copyLayer)
copyCheckLayer = copyLayer
transition.setAlpha(layer: copyLayer, alpha: 0.0, completion: { [weak copyLayer] _ in
copyLayer?.removeFromSuperlayer()
})
self.checkLayer.opacity = 0.0
transition.setAlpha(layer: self.checkLayer, alpha: 1.0)
}
self.checkLayer.theme = CheckNodeTheme(
backgroundColor: component.category.color,
strokeColor: component.theme.list.itemCheckColors.foregroundColor,
borderColor: component.theme.list.itemCheckColors.strokeColor,
overlayBorder: false,
hasInset: false,
hasShadow: false
)
}
let checkDiameter: CGFloat = 22.0
let checkFrame = CGRect(origin: CGPoint(x: titleFrame.minX - 20.0 - checkDiameter, y: floor((height - checkDiameter) / 2.0)), size: CGSize(width: checkDiameter, height: checkDiameter))
transition.setFrame(layer: self.checkLayer, frame: checkFrame)
if let copyCheckLayer {
transition.setFrame(layer: copyCheckLayer, frame: checkFrame)
}
transition.setFrame(view: self.checkButtonArea, frame: CGRect(origin: CGPoint(x: additionalLeftInset, y: 0.0), size: CGSize(width: leftInset - additionalLeftInset, height: height)))
if self.checkLayer.selected != component.category.isSelected {
self.checkLayer.setSelected(component.category.isSelected, animated: !transition.animation.isImmediate)
}
if themeUpdated {
self.separatorLayer.backgroundColor = component.theme.list.itemBlocksSeparatorColor.cgColor
}
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel)))
transition.setAlpha(layer: self.separatorLayer, alpha: (component.isExpanded || component.hasNext) ? 1.0 : 0.0)
self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height + ((component.isExpanded || component.hasNext) ? UIScreenPixel : 0.0)))
var validKeys = Set<StorageUsageScreenComponent.Category>()
if component.isExpanded {
for i in 0 ..< component.category.subcategories.count {
let category = component.category.subcategories[i]
validKeys.insert(category.key)
var itemTransition = transition
let itemView: ComponentView<Empty>
if let current = self.itemViews[category.key] {
itemView = current
} else {
itemTransition = .immediate
itemView = ComponentView()
self.itemViews[category.key] = itemView
}
itemView.parentState = state
let itemSize = itemView.update(
transition: itemTransition,
component: AnyComponent(StorageCategoryItemComponent(
theme: component.theme,
strings: component.strings,
category: category,
isExpandedLevel: true,
isExpanded: false,
hasNext: i != component.category.subcategories.count - 1,
action: { [weak self] key, _ in
guard let self else {
return
}
self.component?.action(key, .toggle)
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 1000.0)
)
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: height), size: itemSize)
if let itemComponentView = itemView.view {
if itemComponentView.superview == nil {
self.subcategoryClippingContainer.addSubview(itemComponentView)
if !transition.animation.isImmediate {
itemComponentView.alpha = 0.0
transition.setAlpha(view: itemComponentView, alpha: 1.0)
}
}
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
}
height += itemSize.height
}
}
var removeKeys: [StorageUsageScreenComponent.Category] = []
for (key, itemView) in self.itemViews {
if !validKeys.contains(key) {
if let itemComponentView = itemView.view {
transition.setAlpha(view: itemComponentView, alpha: 0.0, completion: { [weak itemComponentView] _ in
itemComponentView?.removeFromSuperview()
})
}
removeKeys.append(key)
}
}
for key in removeKeys {
self.itemViews.removeValue(forKey: key)
}
transition.setFrame(view: self.subcategoryClippingContainer, frame: CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height)))
return CGSize(width: availableSize.width, height: height)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
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,179 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import ViewControllerComponent
import ComponentDisplayAdapters
import TelegramPresentationData
import AccountContext
import TelegramCore
import MultilineTextComponent
import LegacyComponents
import SliderComponent
private func stringForCacheSize(strings: PresentationStrings, size: Int32) -> String {
if size > 100 {
return strings.Cache_NoLimit
} else {
return dataSizeString(Int64(size) * 1024 * 1024 * 1024, formatting: DataSizeStringFormatting(strings: strings, decimalSeparator: "."))
}
}
private func totalDiskSpace() -> Int64 {
do {
let systemAttributes = try FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory() as String)
return (systemAttributes[FileAttributeKey.systemSize] as? NSNumber)?.int64Value ?? 0
} catch {
return 0
}
}
private let maximumCacheSizeValues: [Int32] = {
let diskSpace = totalDiskSpace()
if diskSpace > 100 * 1024 * 1024 * 1024 {
return [5, 20, 50, Int32.max]
} else if diskSpace > 50 * 1024 * 1024 * 1024 {
return [5, 16, 32, Int32.max]
} else if diskSpace > 24 * 1024 * 1024 * 1024 {
return [2, 8, 16, Int32.max]
} else {
return [1, 4, 8, Int32.max]
}
}()
final class StorageKeepSizeComponent: Component {
let theme: PresentationTheme
let strings: PresentationStrings
let value: Int32
let updateValue: (Int32) -> Void
init(
theme: PresentationTheme,
strings: PresentationStrings,
value: Int32,
updateValue: @escaping (Int32) -> Void
) {
self.theme = theme
self.strings = strings
self.value = value
self.updateValue = updateValue
}
static func ==(lhs: StorageKeepSizeComponent, rhs: StorageKeepSizeComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.value != rhs.value {
return false
}
return true
}
class View: UIView {
private let titles: [ComponentView<Empty>]
private let slider: ComponentView<Empty>
//private var sliderView: TGPhotoEditorSliderView?
private var component: StorageKeepSizeComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.titles = (0 ..< 4).map { _ in ComponentView<Empty>() }
self.slider = ComponentView<Empty>()
super.init(frame: frame)
self.clipsToBounds = true
self.layer.cornerRadius = 26.0
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: StorageKeepSizeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let themeUpdated = self.component?.theme !== component.theme
self.component = component
self.state = state
if themeUpdated {
self.backgroundColor = component.theme.list.itemBlocksBackgroundColor
}
let height: CGFloat = 96.0
var titleSizes: [CGSize] = []
for i in 0 ..< self.titles.count {
let titleSize = self.titles[i].update(
transition: .immediate,
component: AnyComponent(Text(text: stringForCacheSize(strings: component.strings, size: maximumCacheSizeValues[i]), font: Font.regular(13.0), color: component.theme.list.itemSecondaryTextColor)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
titleSizes.append(titleSize)
}
let delta = (availableSize.width - 18.0 * 2.0) / CGFloat(titleSizes.count - 1)
for i in 0 ..< titleSizes.count {
let titleSize = titleSizes[i]
if let titleView = self.titles[i].view {
if titleView.superview == nil {
self.addSubview(titleView)
}
var position: CGFloat = 18.0 + delta * CGFloat(i)
if i == titleSizes.count - 1 {
position -= titleSize.width
} else if i > 0 {
position -= titleSize.width / 2.0
}
transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: position, y: 19.0), size: titleSize))
}
}
let sliderSize = self.slider.update(
transition: transition,
component: AnyComponent(
SliderComponent(
content: .discrete(.init(
valueCount: 4,
value: maximumCacheSizeValues.firstIndex(where: { $0 == component.value }) ?? 0,
markPositions: true,
valueUpdated: { value in
let sizeValue = maximumCacheSizeValues[value]
component.updateValue(sizeValue)
}
)),
useNative: true,
trackBackgroundColor: component.theme.list.itemSwitchColors.frameColor,
trackForegroundColor: component.theme.list.itemAccentColor
)
),
environment: {},
containerSize: CGSize(width: availableSize.width - 15.0 * 2.0, height: 44.0)
)
if let sliderView = self.slider.view {
if sliderView.superview == nil {
self.addSubview(sliderView)
}
transition.setFrame(view: sliderView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - sliderSize.width) / 2.0), y: 41.0), size: sliderSize))
}
return CGSize(width: availableSize.width, height: height)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
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,663 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import ViewControllerComponent
import ComponentDisplayAdapters
import TelegramPresentationData
import AccountContext
import TelegramCore
import MultilineTextComponent
import EmojiStatusComponent
import TelegramStringFormatting
import CheckNode
import AvatarNode
import PhotoResources
import SemanticStatusNode
private let badgeFont = Font.regular(12.0)
private let videoIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat List/MiniThumbnailPlay"), color: .white)
private final class MediaGridLayer: SimpleLayer {
enum SelectionState: Equatable {
case none
case editing(isSelected: Bool)
}
private(set) var message: EngineMessage?
private var disposable: Disposable?
private var size: CGSize?
private var selectionState: SelectionState = .none
private var theme: PresentationTheme?
private var checkLayer: CheckLayer?
private let badgeOverlay: SimpleLayer
override init() {
self.badgeOverlay = SimpleLayer()
self.badgeOverlay.contentsScale = UIScreenScale
self.badgeOverlay.contentsGravity = .topRight
super.init()
self.isOpaque = true
self.masksToBounds = true
self.contentsGravity = .resizeAspectFill
self.addSublayer(self.badgeOverlay)
}
override init(layer: Any) {
self.badgeOverlay = SimpleLayer()
guard let other = layer as? MediaGridLayer else {
preconditionFailure()
}
super.init(layer: other)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.disposable?.dispose()
}
func prepareForReuse() {
self.message = nil
if let disposable = self.disposable {
self.disposable = nil
disposable.dispose()
}
}
func setup(context: AccountContext, strings: PresentationStrings, message: EngineMessage, size: Int64) {
self.message = message
var isVideo = false
var dimensions: CGSize?
var signal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
for media in message.media {
if let file = media as? TelegramMediaFile, let representation = file.previewRepresentations.last {
isVideo = file.isVideo
signal = chatWebpageSnippetFile(
account: context.account,
userLocation: .peer(message.id.peerId),
mediaReference: FileMediaReference.standalone(media: file).abstract,
representation: representation,
automaticFetch: false
)
dimensions = representation.dimensions.cgSize
} else if let image = media as? TelegramMediaImage, let representation = image.representations.last {
signal = mediaGridMessagePhoto(
account: context.account,
userLocation: .peer(message.id.peerId),
photoReference: ImageMediaReference.standalone(media: image),
automaticFetch: false
)
dimensions = representation.dimensions.cgSize
}
}
if let signal, let dimensions {
self.disposable = (signal
|> map { generator -> UIImage? in
return generator(TransformImageArguments(corners: ImageCorners(radius: 0.0), imageSize: dimensions, boundingSize: CGSize(width: 100.0, height: 100.0), intrinsicInsets: UIEdgeInsets()))?.generateImage()
}
|> deliverOnMainQueue).start(next: { [weak self] image in
guard let self, let image else {
return
}
self.contents = image.cgImage
})
}
let text: String = dataSizeString(Int(size), formatting: DataSizeStringFormatting(strings: strings, decimalSeparator: "."))
let attributedText = NSAttributedString(string: text, font: badgeFont, textColor: .white)
let textBounds = attributedText.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
let textSize = CGSize(width: ceil(textBounds.width), height: ceil(textBounds.height))
let textLeftInset: CGFloat
let textRightInset: CGFloat = 6.0
if isVideo {
textLeftInset = 18.0
} else {
textLeftInset = textRightInset
}
let badgeSize = CGSize(width: textLeftInset + textRightInset + textSize.width, height: 18.0)
self.badgeOverlay.contents = generateImage(badgeSize, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor(white: 0.0, alpha: 0.5).cgColor)
context.setBlendMode(.copy)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.height, height: size.height)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - size.height, y: 0.0), size: CGSize(width: size.height, height: size.height)))
context.fill(CGRect(origin: CGPoint(x: size.height * 0.5, y: 0.0), size: CGSize(width: size.width - size.height, height: size.height)))
context.setBlendMode(.normal)
UIGraphicsPushContext(context)
if isVideo, let videoIcon {
videoIcon.draw(at: CGPoint(x: 2.0, y: floor((size.height - videoIcon.size.height) / 2.0)))
}
attributedText.draw(in: textBounds.offsetBy(dx: textLeftInset, dy: UIScreenPixel + floor((size.height - textSize.height) * 0.5)))
UIGraphicsPopContext()
})?.cgImage
}
func updateSelection(size: CGSize, selectionState: SelectionState, theme: PresentationTheme, transition: ComponentTransition) {
if self.size == size && self.selectionState == selectionState && self.theme === theme {
return
}
self.selectionState = selectionState
self.size = size
let themeUpdated = self.theme !== theme
self.theme = theme
switch selectionState {
case .none:
if let checkLayer = self.checkLayer {
self.checkLayer = nil
if !transition.animation.isImmediate {
checkLayer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false)
checkLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak checkLayer] _ in
checkLayer?.removeFromSuperlayer()
})
} else {
checkLayer.removeFromSuperlayer()
}
}
case let .editing(isSelected):
let checkWidth: CGFloat
if size.width <= 60.0 {
checkWidth = 22.0
} else {
checkWidth = 28.0
}
let checkSize = CGSize(width: checkWidth, height: checkWidth)
let checkFrame = CGRect(origin: CGPoint(x: self.bounds.size.width - checkSize.width - 2.0, y: 2.0), size: checkSize)
if let checkLayer = self.checkLayer {
if checkLayer.bounds.size != checkFrame.size {
checkLayer.setNeedsDisplay()
}
transition.setFrame(layer: checkLayer, frame: checkFrame)
if themeUpdated {
checkLayer.theme = CheckNodeTheme(theme: theme, style: .overlay)
}
checkLayer.setSelected(isSelected, animated: !transition.animation.isImmediate)
} else {
let checkLayer = CheckLayer(theme: CheckNodeTheme(theme: theme, style: .overlay))
self.checkLayer = checkLayer
self.addSublayer(checkLayer)
checkLayer.frame = checkFrame
checkLayer.setSelected(isSelected, animated: false)
checkLayer.setNeedsDisplay()
if !transition.animation.isImmediate {
checkLayer.animateScale(from: 0.001, to: 1.0, duration: 0.2)
checkLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
}
self.badgeOverlay.frame = CGRect(origin: CGPoint(x: size.width - 3.0, y: size.height - 3.0), size: CGSize(width: 0.0, height: 0.0))
}
}
private final class MediaGridLayerDataContext {
}
final class StorageMediaGridPanelComponent: Component {
typealias EnvironmentType = StorageUsagePanelEnvironment
final class Item: Equatable {
let message: EngineMessage
let size: Int64
init(
message: EngineMessage,
size: Int64
) {
self.message = message
self.size = size
}
static func ==(lhs: Item, rhs: Item) -> Bool {
if lhs.message.id != rhs.message.id {
return false
}
if lhs.size != rhs.size {
return false
}
return true
}
}
final class Items: Equatable {
let items: [Item]
init(items: [Item]) {
self.items = items
}
static func ==(lhs: Items, rhs: Items) -> Bool {
if lhs === rhs {
return true
}
return lhs.items == rhs.items
}
}
let context: AccountContext
let items: Items?
let selectionState: StorageUsageScreenComponent.SelectionState?
let action: (EngineMessage.Id) -> Void
let contextAction: (EngineMessage.Id, UIView, CGRect, ContextGesture) -> Void
init(
context: AccountContext,
items: Items?,
selectionState: StorageUsageScreenComponent.SelectionState?,
action: @escaping (EngineMessage.Id) -> Void,
contextAction: @escaping (EngineMessage.Id, UIView, CGRect, ContextGesture) -> Void
) {
self.context = context
self.items = items
self.selectionState = selectionState
self.action = action
self.contextAction = contextAction
}
static func ==(lhs: StorageMediaGridPanelComponent, rhs: StorageMediaGridPanelComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.items != rhs.items {
return false
}
if lhs.selectionState != rhs.selectionState {
return false
}
return true
}
private struct ItemLayout: Equatable {
var width: CGFloat
var itemCount: Int
var nativeItemSize: CGFloat
let visibleItemSize: CGFloat
var itemInsets: UIEdgeInsets
var itemSpacing: CGFloat
var itemsPerRow: Int
var contentSize: CGSize
init(
width: CGFloat,
containerInsets: UIEdgeInsets,
itemCount: Int
) {
self.width = width
self.itemCount = itemCount
let minItemsPerRow: Int = 3
let itemSpacing: CGFloat = UIScreenPixel
self.itemSpacing = itemSpacing
let itemInsets: UIEdgeInsets = UIEdgeInsets(top: containerInsets.top, left: containerInsets.left, bottom: containerInsets.bottom, right: containerInsets.right)
self.nativeItemSize = 120.0
self.itemInsets = itemInsets
let itemHorizontalSpace = width - self.itemInsets.left - self.itemInsets.right
self.itemsPerRow = max(minItemsPerRow, Int((itemHorizontalSpace + itemSpacing) / (self.nativeItemSize + itemSpacing)))
let proposedItemSize = floor((itemHorizontalSpace - itemSpacing * (CGFloat(self.itemsPerRow) - 1.0)) / CGFloat(self.itemsPerRow))
self.visibleItemSize = proposedItemSize
let numRows = (itemCount + (self.itemsPerRow - 1)) / self.itemsPerRow
self.contentSize = CGSize(
width: width,
height: self.itemInsets.top + self.itemInsets.bottom + CGFloat(numRows) * self.visibleItemSize + CGFloat(max(0, numRows - 1)) * self.itemSpacing
)
}
func frame(itemIndex: Int) -> CGRect {
let row = itemIndex / self.itemsPerRow
let column = itemIndex % self.itemsPerRow
var result = CGRect(
origin: CGPoint(
x: self.itemInsets.left + CGFloat(column) * (self.visibleItemSize + self.itemSpacing),
y: self.itemInsets.top + CGFloat(row) * (self.visibleItemSize + self.itemSpacing)
),
size: CGSize(
width: self.visibleItemSize,
height: self.visibleItemSize
)
)
if column == self.itemsPerRow - 1 {
result.size.width = max(result.size.width, self.width - self.itemInsets.right - result.minX)
}
return result
}
func visibleItems(for rect: CGRect) -> Range<Int>? {
let offsetRect = rect.offsetBy(dx: -self.itemInsets.left, dy: -self.itemInsets.top)
var minVisibleRow = Int(floor((offsetRect.minY - self.itemSpacing) / (self.visibleItemSize + self.itemSpacing)))
minVisibleRow = max(0, minVisibleRow)
let maxVisibleRow = Int(ceil((offsetRect.maxY - self.itemSpacing) / (self.visibleItemSize + self.itemSpacing)))
let minVisibleIndex = minVisibleRow * self.itemsPerRow
let maxVisibleIndex = min(self.itemCount - 1, (maxVisibleRow + 1) * self.itemsPerRow - 1)
return maxVisibleIndex >= minVisibleIndex ? (minVisibleIndex ..< (maxVisibleIndex + 1)) : nil
}
}
class View: ContextControllerSourceView, UIScrollViewDelegate {
private let scrollView: UIScrollView
private var visibleLayers: [EngineMessage.Id: MediaGridLayer] = [:]
private var layersAvailableForReuse: [MediaGridLayer] = []
private var ignoreScrolling: Bool = false
private var component: StorageMediaGridPanelComponent?
private var environment: StorageUsagePanelEnvironment?
private var itemLayout: ItemLayout?
private weak var currentGestureItemLayer: MediaGridLayer?
override init(frame: CGRect) {
self.scrollView = UIScrollView()
super.init(frame: frame)
self.scrollView.delaysContentTouches = true
self.scrollView.canCancelContentTouches = true
self.scrollView.clipsToBounds = false
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.scrollView.contentInsetAdjustmentBehavior = .never
}
if #available(iOS 13.0, *) {
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
}
self.scrollView.showsVerticalScrollIndicator = true
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.alwaysBounceHorizontal = false
self.scrollView.scrollsToTop = false
self.scrollView.delegate = self
self.scrollView.clipsToBounds = true
self.addSubview(self.scrollView)
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
self.shouldBegin = { [weak self] point in
guard let self else {
return false
}
var itemLayer: MediaGridLayer?
let scrollPoint = self.convert(point, to: self.scrollView)
for (_, itemLayerValue) in self.visibleLayers {
if itemLayerValue.frame.contains(scrollPoint) {
itemLayer = itemLayerValue
break
}
}
guard let itemLayer else {
return false
}
self.currentGestureItemLayer = itemLayer
return true
}
self.customActivationProgress = { [weak self] progress, update in
guard let self, let itemLayer = self.currentGestureItemLayer else {
return
}
let targetContentRect = CGRect(origin: CGPoint(), size: itemLayer.bounds.size)
let scaleSide = itemLayer.bounds.width
let minScale: CGFloat = max(0.7, (scaleSide - 15.0) / scaleSide)
let currentScale = 1.0 * (1.0 - progress) + minScale * progress
let originalCenterOffsetX: CGFloat = itemLayer.bounds.width / 2.0 - targetContentRect.midX
let scaledCenterOffsetX: CGFloat = originalCenterOffsetX * currentScale
let originalCenterOffsetY: CGFloat = itemLayer.bounds.height / 2.0 - targetContentRect.midY
let scaledCenterOffsetY: CGFloat = originalCenterOffsetY * currentScale
let scaleMidX: CGFloat = scaledCenterOffsetX - originalCenterOffsetX
let scaleMidY: CGFloat = scaledCenterOffsetY - originalCenterOffsetY
switch update {
case .update:
let sublayerTransform = CATransform3DTranslate(CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0), scaleMidX, scaleMidY, 0.0)
itemLayer.transform = sublayerTransform
case .begin:
let sublayerTransform = CATransform3DTranslate(CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0), scaleMidX, scaleMidY, 0.0)
itemLayer.transform = sublayerTransform
case .ended:
let sublayerTransform = CATransform3DTranslate(CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0), scaleMidX, scaleMidY, 0.0)
let previousTransform = itemLayer.transform
itemLayer.transform = sublayerTransform
itemLayer.animate(from: NSValue(caTransform3D: previousTransform), to: NSValue(caTransform3D: sublayerTransform), keyPath: "transform", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.2)
}
}
self.activated = { [weak self] gesture, _ in
guard let self, let component = self.component, let itemLayer = self.currentGestureItemLayer else {
return
}
self.currentGestureItemLayer = nil
guard let message = itemLayer.message else {
return
}
let rect = self.convert(itemLayer.frame, from: self.scrollView)
component.contextAction(message.id, self, rect, gesture)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func transitionNodeForGallery(messageId: EngineMessage.Id, media: EngineMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
var foundItemLayer: MediaGridLayer?
for (_, itemLayer) in self.visibleLayers {
if let message = itemLayer.message, message.id == messageId {
foundItemLayer = itemLayer
}
}
guard let itemLayer = foundItemLayer else {
return nil
}
let itemFrame = self.convert(itemLayer.frame, from: self.scrollView)
let proxyNode = ASDisplayNode()
proxyNode.frame = itemFrame
if let contents = itemLayer.contents {
if let image = contents as? UIImage {
proxyNode.contents = image.cgImage
} else {
proxyNode.contents = contents
}
}
proxyNode.isHidden = true
self.addSubnode(proxyNode)
let escapeNotification = EscapeNotification {
proxyNode.removeFromSupernode()
}
return (proxyNode, proxyNode.bounds, {
let view = UIView()
view.frame = proxyNode.frame
view.layer.contents = proxyNode.layer.contents
escapeNotification.keep()
return (view, nil)
})
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
guard let component = self.component else {
return
}
let point = recognizer.location(in: self.scrollView)
for (id, itemLayer) in self.visibleLayers {
if itemLayer.frame.contains(point) {
component.action(id)
break
}
}
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.ignoreScrolling {
self.updateScrolling(transition: .immediate)
}
}
private func updateScrolling(transition: ComponentTransition) {
guard let component = self.component, let environment = self.environment, let items = component.items, let itemLayout = self.itemLayout else {
return
}
let _ = environment
var validIds = Set<EngineMessage.Id>()
let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -100.0)
if let visibleItems = itemLayout.visibleItems(for: visibleBounds) {
for index in visibleItems.lowerBound ..< visibleItems.upperBound {
if index >= items.items.count {
continue
}
let item = items.items[index]
let id = item.message.id
validIds.insert(id)
}
var removeIds: [EngineMessage.Id] = []
for (id, itemLayer) in self.visibleLayers {
if !validIds.contains(id) {
removeIds.append(id)
itemLayer.isHidden = true
self.layersAvailableForReuse.append(itemLayer)
itemLayer.prepareForReuse()
}
}
for id in removeIds {
self.visibleLayers.removeValue(forKey: id)
}
for index in visibleItems.lowerBound ..< visibleItems.upperBound {
if index >= items.items.count {
continue
}
let item = items.items[index]
let id = item.message.id
var setupItemLayer = false
let itemLayer: MediaGridLayer
if let current = self.visibleLayers[id] {
itemLayer = current
} else if !self.layersAvailableForReuse.isEmpty {
setupItemLayer = true
itemLayer = self.layersAvailableForReuse.removeLast()
itemLayer.isHidden = false
self.visibleLayers[id] = itemLayer
} else {
setupItemLayer = true
itemLayer = MediaGridLayer()
self.visibleLayers[id] = itemLayer
self.scrollView.layer.addSublayer(itemLayer)
}
let itemFrame = itemLayout.frame(itemIndex: index)
itemLayer.frame = itemFrame
if setupItemLayer {
itemLayer.setup(context: component.context, strings: environment.strings, message: item.message, size: item.size)
}
let itemSelectionState: MediaGridLayer.SelectionState
if let selectionState = component.selectionState {
itemSelectionState = .editing(isSelected: selectionState.selectedMessages.contains(id))
} else {
itemSelectionState = .none
}
itemLayer.updateSelection(size: itemFrame.size, selectionState: itemSelectionState, theme: environment.theme, transition: transition)
}
}
}
func update(component: StorageMediaGridPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<StorageUsagePanelEnvironment>, transition: ComponentTransition) -> CGSize {
self.component = component
let environment = environment[StorageUsagePanelEnvironment.self].value
self.environment = environment
let itemLayout = ItemLayout(
width: availableSize.width,
containerInsets: environment.containerInsets,
itemCount: component.items?.items.count ?? 0
)
self.itemLayout = itemLayout
self.ignoreScrolling = true
let contentOffset = self.scrollView.bounds.minY
transition.setPosition(view: self.scrollView, position: CGRect(origin: CGPoint(), size: availableSize).center)
var scrollBounds = self.scrollView.bounds
scrollBounds.size = availableSize
if !environment.isScrollable {
scrollBounds.origin = CGPoint()
}
transition.setBounds(view: self.scrollView, bounds: scrollBounds)
self.scrollView.isScrollEnabled = environment.isScrollable
let contentSize = CGSize(width: availableSize.width, height: itemLayout.contentSize.height)
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize
}
self.scrollView.verticalScrollIndicatorInsets = environment.containerInsets
if !transition.animation.isImmediate && self.scrollView.bounds.minY != contentOffset {
let deltaOffset = self.scrollView.bounds.minY - contentOffset
transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: -deltaOffset), to: CGPoint(), additive: true)
}
self.ignoreScrolling = false
self.updateScrolling(transition: transition)
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<StorageUsagePanelEnvironment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,705 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import ViewControllerComponent
import ComponentDisplayAdapters
import TelegramPresentationData
import AccountContext
import TelegramCore
import MultilineTextComponent
import EmojiStatusComponent
import TelegramStringFormatting
import CheckNode
import AvatarNode
private let avatarFont = avatarPlaceholderFont(size: 15.0)
func cancelContextGestures(view: UIView) {
if let gestureRecognizers = view.gestureRecognizers {
for gesture in gestureRecognizers {
if let gesture = gesture as? ContextGesture {
gesture.cancel()
}
}
}
for subview in view.subviews {
cancelContextGestures(view: subview)
}
}
private final class PeerListItemComponent: Component {
enum SelectionState: Equatable {
case none
case editing(isSelected: Bool)
}
let context: AccountContext
let theme: PresentationTheme
let sideInset: CGFloat
let title: String
let peer: EnginePeer?
let label: String
let selectionState: SelectionState
let hasNext: Bool
let action: (EnginePeer) -> Void
let contextAction: (EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void
init(
context: AccountContext,
theme: PresentationTheme,
sideInset: CGFloat,
title: String,
peer: EnginePeer?,
label: String,
selectionState: SelectionState,
hasNext: Bool,
action: @escaping (EnginePeer) -> Void,
contextAction: @escaping (EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void
) {
self.context = context
self.theme = theme
self.sideInset = sideInset
self.title = title
self.peer = peer
self.label = label
self.selectionState = selectionState
self.hasNext = hasNext
self.action = action
self.contextAction = contextAction
}
static func ==(lhs: PeerListItemComponent, rhs: PeerListItemComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.sideInset != rhs.sideInset {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.peer != rhs.peer {
return false
}
if lhs.label != rhs.label {
return false
}
if lhs.selectionState != rhs.selectionState {
return false
}
if lhs.hasNext != rhs.hasNext {
return false
}
return true
}
final class View: ContextControllerSourceView {
private let extractedContainerView: ContextExtractedContentContainingView
private let containerButton: HighlightTrackingButton
private let title = ComponentView<Empty>()
private let label = ComponentView<Empty>()
private let separatorLayer: SimpleLayer
private let avatarNode: AvatarNode
private var checkLayer: CheckLayer?
private var isExtractedToContextMenu: Bool = false
private var highlightBackgroundFrame: CGRect?
private var highlightBackgroundLayer: SimpleLayer?
private var component: PeerListItemComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.separatorLayer = SimpleLayer()
self.extractedContainerView = ContextExtractedContentContainingView()
self.containerButton = HighlightTrackingButton()
self.avatarNode = AvatarNode(font: avatarFont)
self.avatarNode.isLayerBacked = true
super.init(frame: frame)
self.layer.addSublayer(self.separatorLayer)
self.addSubview(self.extractedContainerView)
self.targetViewForActivationProgress = self.extractedContainerView.contentView
self.extractedContainerView.contentView.addSubview(self.containerButton)
self.containerButton.layer.addSublayer(self.avatarNode.layer)
self.extractedContainerView.isExtractedToContextPreviewUpdated = { [weak self] value in
guard let self, let component = self.component else {
return
}
self.containerButton.clipsToBounds = value
self.containerButton.backgroundColor = value ? component.theme.list.plainBackgroundColor : nil
self.containerButton.layer.cornerRadius = value ? 10.0 : 0.0
}
self.extractedContainerView.willUpdateIsExtractedToContextPreview = { [weak self] value, transition in
guard let self else {
return
}
self.isExtractedToContextMenu = value
let mappedTransition: ComponentTransition
if value {
mappedTransition = ComponentTransition(transition)
} else {
mappedTransition = ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut))
}
self.state?.updated(transition: mappedTransition)
}
self.containerButton.highligthedChanged = { [weak self] isHighlighted in
guard let self, let component = self.component, let highlightBackgroundFrame = self.highlightBackgroundFrame else {
return
}
if isHighlighted, case .none = component.selectionState {
self.superview?.bringSubviewToFront(self)
let highlightBackgroundLayer: SimpleLayer
if let current = self.highlightBackgroundLayer {
highlightBackgroundLayer = current
} else {
highlightBackgroundLayer = SimpleLayer()
self.highlightBackgroundLayer = highlightBackgroundLayer
self.layer.insertSublayer(highlightBackgroundLayer, above: self.separatorLayer)
highlightBackgroundLayer.backgroundColor = component.theme.list.itemHighlightedBackgroundColor.cgColor
}
highlightBackgroundLayer.frame = highlightBackgroundFrame
highlightBackgroundLayer.opacity = 1.0
} else {
if let highlightBackgroundLayer = self.highlightBackgroundLayer {
self.highlightBackgroundLayer = nil
highlightBackgroundLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak highlightBackgroundLayer] _ in
highlightBackgroundLayer?.removeFromSuperlayer()
})
}
}
}
self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
self.activated = { [weak self] gesture, _ in
guard let self, let component = self.component, let peer = component.peer else {
gesture.cancel()
return
}
component.contextAction(peer, self.extractedContainerView, gesture)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func pressed() {
guard let component = self.component, let peer = component.peer else {
return
}
component.action(peer)
}
func update(component: PeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let themeUpdated = self.component?.theme !== component.theme
var hasSelectionUpdated = false
if let previousComponent = self.component {
switch previousComponent.selectionState {
case .none:
if case .none = component.selectionState {
} else {
hasSelectionUpdated = true
}
case .editing:
if case .editing = component.selectionState {
} else {
hasSelectionUpdated = true
}
}
}
self.component = component
self.state = state
let contextInset: CGFloat = self.isExtractedToContextMenu ? 12.0 : 0.0
let height: CGFloat = 52.0
let verticalInset: CGFloat = 1.0
var leftInset: CGFloat = 62.0 + component.sideInset
var avatarLeftInset: CGFloat = component.sideInset + 10.0
if case let .editing(isSelected) = component.selectionState {
leftInset += 48.0
avatarLeftInset += 48.0
let checkSize: CGFloat = 22.0
let checkLayer: CheckLayer
if let current = self.checkLayer {
checkLayer = current
if themeUpdated {
checkLayer.theme = CheckNodeTheme(theme: component.theme, style: .plain)
}
checkLayer.setSelected(isSelected, animated: !transition.animation.isImmediate)
} else {
checkLayer = CheckLayer(theme: CheckNodeTheme(theme: component.theme, style: .plain))
self.checkLayer = checkLayer
self.containerButton.layer.addSublayer(checkLayer)
checkLayer.frame = CGRect(origin: CGPoint(x: -checkSize, y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize))
checkLayer.setSelected(isSelected, animated: false)
checkLayer.setNeedsDisplay()
}
transition.setFrame(layer: checkLayer, frame: CGRect(origin: CGPoint(x: component.sideInset + 20.0, y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize)))
} else {
if let checkLayer = self.checkLayer {
self.checkLayer = nil
transition.setPosition(layer: checkLayer, position: CGPoint(x: -checkLayer.bounds.width * 0.5, y: checkLayer.position.y), completion: { [weak checkLayer] _ in
checkLayer?.removeFromSuperlayer()
})
}
}
let rightInset: CGFloat = contextInset * 2.0 + 16.0 + component.sideInset
let avatarSize: CGFloat = 40.0
let avatarFrame = CGRect(origin: CGPoint(x: avatarLeftInset, y: floor((height - verticalInset * 2.0 - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))
if self.avatarNode.bounds.isEmpty {
self.avatarNode.frame = avatarFrame
} else {
transition.setFrame(layer: self.avatarNode.layer, frame: avatarFrame)
}
if let peer = component.peer {
let clipStyle: AvatarNodeClipStyle
if case let .channel(channel) = peer, channel.isForumOrMonoForum {
clipStyle = .roundedRect
} else {
clipStyle = .round
}
if peer.id == component.context.account.peerId {
self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, overrideImage: .savedMessagesIcon, clipStyle: clipStyle, displayDimensions: CGSize(width: avatarSize, height: avatarSize))
} else {
self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, clipStyle: clipStyle, displayDimensions: CGSize(width: avatarSize, height: avatarSize))
}
}
let labelSize = self.label.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.label, font: Font.regular(17.0), textColor: component.theme.list.itemSecondaryTextColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
)
let previousTitleFrame = self.title.view?.frame
var previousTitleContents: UIView?
if hasSelectionUpdated && !"".isEmpty {
previousTitleContents = self.title.view?.snapshotView(afterScreenUpdates: false)
}
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.title, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - leftInset - rightInset - labelSize.width - 4.0, height: 100.0)
)
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - verticalInset * 2.0 - titleSize.height) / 2.0)), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.isUserInteractionEnabled = false
self.containerButton.addSubview(titleView)
}
titleView.frame = titleFrame
if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x {
transition.animatePosition(view: titleView, from: CGPoint(x: previousTitleFrame.origin.x - titleFrame.origin.x, y: 0.0), to: CGPoint(), additive: true)
}
if let previousTitleFrame, let previousTitleContents, previousTitleFrame.size != titleSize {
previousTitleContents.frame = CGRect(origin: previousTitleFrame.origin, size: previousTitleFrame.size)
self.addSubview(previousTitleContents)
transition.setFrame(view: previousTitleContents, frame: CGRect(origin: titleFrame.origin, size: previousTitleFrame.size))
transition.setAlpha(view: previousTitleContents, alpha: 0.0, completion: { [weak previousTitleContents] _ in
previousTitleContents?.removeFromSuperview()
})
transition.animateAlpha(view: titleView, from: 0.0, to: 1.0)
}
}
if let labelView = self.label.view {
if labelView.superview == nil {
labelView.isUserInteractionEnabled = false
self.containerButton.addSubview(labelView)
}
transition.setFrame(view: labelView, frame: CGRect(origin: CGPoint(x: availableSize.width - rightInset - labelSize.width, y: floor((height - verticalInset * 2.0 - labelSize.height) / 2.0)), size: labelSize))
}
if themeUpdated {
self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor
}
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel)))
self.separatorLayer.isHidden = !component.hasNext
self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height + ((component.hasNext) ? UIScreenPixel : 0.0)))
let resultBounds = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height))
transition.setFrame(view: self.extractedContainerView, frame: resultBounds)
transition.setFrame(view: self.extractedContainerView.contentView, frame: resultBounds)
self.extractedContainerView.contentRect = resultBounds
let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0))
transition.setFrame(view: self.containerButton, frame: containerFrame)
return CGSize(width: availableSize.width, height: height)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
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)
}
}
final class StoragePeerListPanelComponent: Component {
typealias EnvironmentType = StorageUsagePanelEnvironment
final class Item: Equatable {
let peer: EnginePeer
let size: Int64
init(
peer: EnginePeer,
size: Int64
) {
self.peer = peer
self.size = size
}
static func ==(lhs: Item, rhs: Item) -> Bool {
if lhs.peer != rhs.peer {
return false
}
if lhs.size != rhs.size {
return false
}
return true
}
}
final class Items: Equatable {
let items: [Item]
init(items: [Item]) {
self.items = items
}
static func ==(lhs: Items, rhs: Items) -> Bool {
if lhs === rhs {
return true
}
return lhs.items == rhs.items
}
}
let context: AccountContext
let items: Items?
let selectionState: StorageUsageScreenComponent.SelectionState?
let peerAction: (EnginePeer) -> Void
let contextAction: (EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void
init(
context: AccountContext,
items: Items?,
selectionState: StorageUsageScreenComponent.SelectionState?,
peerAction: @escaping (EnginePeer) -> Void,
contextAction: @escaping (EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void
) {
self.context = context
self.items = items
self.selectionState = selectionState
self.peerAction = peerAction
self.contextAction = contextAction
}
static func ==(lhs: StoragePeerListPanelComponent, rhs: StoragePeerListPanelComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.items != rhs.items {
return false
}
if lhs.selectionState != rhs.selectionState {
return false
}
return true
}
private struct ItemLayout: Equatable {
let containerInsets: UIEdgeInsets
let containerWidth: CGFloat
let itemHeight: CGFloat
let itemCount: Int
let contentHeight: CGFloat
init(
containerInsets: UIEdgeInsets,
containerWidth: CGFloat,
itemHeight: CGFloat,
itemCount: Int
) {
self.containerInsets = containerInsets
self.containerWidth = containerWidth
self.itemHeight = itemHeight
self.itemCount = itemCount
self.contentHeight = containerInsets.top + containerInsets.bottom + CGFloat(itemCount) * itemHeight
}
func visibleItems(for rect: CGRect) -> Range<Int>? {
let offsetRect = rect.offsetBy(dx: -self.containerInsets.left, dy: -self.containerInsets.top)
var minVisibleRow = Int(floor((offsetRect.minY) / (self.itemHeight)))
minVisibleRow = max(0, minVisibleRow)
let maxVisibleRow = Int(ceil((offsetRect.maxY) / (self.itemHeight)))
let minVisibleIndex = minVisibleRow
let maxVisibleIndex = maxVisibleRow
if maxVisibleIndex >= minVisibleIndex {
return minVisibleIndex ..< (maxVisibleIndex + 1)
} else {
return nil
}
}
func itemFrame(for index: Int) -> CGRect {
return CGRect(origin: CGPoint(x: 0.0, y: self.containerInsets.top + CGFloat(index) * self.itemHeight), size: CGSize(width: self.containerWidth, height: self.itemHeight))
}
}
private final class ScrollViewImpl: UIScrollView {
override func touchesShouldCancel(in view: UIView) -> Bool {
return true
}
}
class View: UIView, UIScrollViewDelegate {
private let scrollView: ScrollViewImpl
private let measureItem = ComponentView<Empty>()
private var visibleItems: [EnginePeer.Id: ComponentView<Empty>] = [:]
private var ignoreScrolling: Bool = false
private var component: StoragePeerListPanelComponent?
private var environment: StorageUsagePanelEnvironment?
private var itemLayout: ItemLayout?
override init(frame: CGRect) {
self.scrollView = ScrollViewImpl()
super.init(frame: frame)
self.scrollView.delaysContentTouches = true
self.scrollView.canCancelContentTouches = true
self.scrollView.clipsToBounds = false
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.scrollView.contentInsetAdjustmentBehavior = .never
}
if #available(iOS 13.0, *) {
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
}
self.scrollView.showsVerticalScrollIndicator = true
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.alwaysBounceHorizontal = false
self.scrollView.scrollsToTop = false
self.scrollView.delegate = self
self.scrollView.clipsToBounds = true
self.addSubview(self.scrollView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.ignoreScrolling {
self.updateScrolling(transition: .immediate)
}
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
cancelContextGestures(view: scrollView)
}
private func updateScrolling(transition: ComponentTransition) {
guard let component = self.component, let environment = self.environment, let items = component.items, let itemLayout = self.itemLayout else {
return
}
let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -100.0)
let dataSizeFormatting = DataSizeStringFormatting(strings: environment.strings, decimalSeparator: ".")
var validIds = Set<EnginePeer.Id>()
if let visibleItems = itemLayout.visibleItems(for: visibleBounds) {
for index in visibleItems.lowerBound ..< visibleItems.upperBound {
if index >= items.items.count {
continue
}
let item = items.items[index]
let id = item.peer.id
validIds.insert(id)
var itemTransition = transition
let itemView: ComponentView<Empty>
if let current = self.visibleItems[id] {
itemView = current
} else {
itemTransition = .immediate
itemView = ComponentView()
self.visibleItems[id] = itemView
}
let itemSelectionState: PeerListItemComponent.SelectionState
if let selectionState = component.selectionState {
itemSelectionState = .editing(isSelected: selectionState.selectedPeers.contains(id))
} else {
itemSelectionState = .none
}
let itemTitle: String
if item.peer.id == component.context.account.peerId {
itemTitle = environment.strings.DialogList_SavedMessages
} else {
itemTitle = item.peer.displayTitle(strings: environment.strings, displayOrder: .firstLast)
}
let _ = itemView.update(
transition: itemTransition,
component: AnyComponent(PeerListItemComponent(
context: component.context,
theme: environment.theme,
sideInset: environment.containerInsets.left,
title: itemTitle,
peer: item.peer,
label: dataSizeString(item.size, formatting: dataSizeFormatting),
selectionState: itemSelectionState,
hasNext: index != items.items.count - 1,
action: component.peerAction,
contextAction: component.contextAction
)),
environment: {},
containerSize: CGSize(width: itemLayout.containerWidth, height: itemLayout.itemHeight)
)
let itemFrame = itemLayout.itemFrame(for: index)
if let itemComponentView = itemView.view {
if itemComponentView.superview == nil {
self.scrollView.addSubview(itemComponentView)
}
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
}
}
}
var removeIds: [EnginePeer.Id] = []
for (id, itemView) in self.visibleItems {
if !validIds.contains(id) {
removeIds.append(id)
if let itemComponentView = itemView.view {
transition.setAlpha(view: itemComponentView, alpha: 0.0, completion: { [weak itemComponentView] _ in
itemComponentView?.removeFromSuperview()
})
}
}
}
for id in removeIds {
self.visibleItems.removeValue(forKey: id)
}
}
func update(component: StoragePeerListPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<StorageUsagePanelEnvironment>, transition: ComponentTransition) -> CGSize {
self.component = component
let environment = environment[StorageUsagePanelEnvironment.self].value
self.environment = environment
let measureItemSize = self.measureItem.update(
transition: .immediate,
component: AnyComponent(PeerListItemComponent(
context: component.context,
theme: environment.theme,
sideInset: environment.containerInsets.left,
title: "ABCDEF",
peer: nil,
label: "1000",
selectionState: .none,
hasNext: false,
action: { _ in
},
contextAction: { _, _, _ in
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 1000.0)
)
let itemLayout = ItemLayout(
containerInsets: environment.containerInsets,
containerWidth: availableSize.width,
itemHeight: measureItemSize.height,
itemCount: component.items?.items.count ?? 0
)
self.itemLayout = itemLayout
self.ignoreScrolling = true
let contentOffset = self.scrollView.bounds.minY
transition.setPosition(view: self.scrollView, position: CGRect(origin: CGPoint(), size: availableSize).center)
var scrollBounds = self.scrollView.bounds
scrollBounds.size = availableSize
if !environment.isScrollable {
scrollBounds.origin = CGPoint()
}
transition.setBounds(view: self.scrollView, bounds: scrollBounds)
self.scrollView.isScrollEnabled = environment.isScrollable
let contentSize = CGSize(width: availableSize.width, height: itemLayout.contentHeight)
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize
}
self.scrollView.verticalScrollIndicatorInsets = environment.containerInsets
if !transition.animation.isImmediate && self.scrollView.bounds.minY != contentOffset {
let deltaOffset = self.scrollView.bounds.minY - contentOffset
transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: -deltaOffset), to: CGPoint(), additive: true)
}
self.ignoreScrolling = false
self.updateScrolling(transition: transition)
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<StorageUsagePanelEnvironment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,289 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import ViewControllerComponent
import ComponentDisplayAdapters
import TelegramPresentationData
import AccountContext
import TelegramCore
import MultilineTextComponent
import EmojiStatusComponent
import TelegramStringFormatting
import CheckNode
final class StoragePeerTypeItemComponent: Component {
enum ActionType {
case toggle
case generic
}
let theme: PresentationTheme
let iconName: String
let title: String
let subtitle: String?
let value: String
let hasNext: Bool
let action: (View) -> Void
init(
theme: PresentationTheme,
iconName: String,
title: String,
subtitle: String?,
value: String,
hasNext: Bool,
action: @escaping (View) -> Void
) {
self.theme = theme
self.iconName = iconName
self.title = title
self.subtitle = subtitle
self.value = value
self.hasNext = hasNext
self.action = action
}
static func ==(lhs: StoragePeerTypeItemComponent, rhs: StoragePeerTypeItemComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.iconName != rhs.iconName {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.subtitle != rhs.subtitle {
return false
}
if lhs.value != rhs.value {
return false
}
if lhs.hasNext != rhs.hasNext {
return false
}
return true
}
class View: HighlightTrackingButton {
private let iconView: UIImageView
private let title = ComponentView<Empty>()
private var subtitle: ComponentView<Empty>?
private let label = ComponentView<Empty>()
private let separatorLayer: SimpleLayer
private let arrowIconView: UIImageView
private var component: StoragePeerTypeItemComponent?
private var highlightBackgroundFrame: CGRect?
private var highlightBackgroundLayer: SimpleLayer?
var labelView: UIView? {
return self.label.view
}
override init(frame: CGRect) {
self.separatorLayer = SimpleLayer()
self.iconView = UIImageView()
self.arrowIconView = UIImageView()
super.init(frame: frame)
self.layer.addSublayer(self.separatorLayer)
self.addSubview(self.iconView)
self.addSubview(self.arrowIconView)
self.highligthedChanged = { [weak self] isHighlighted in
guard let self, let component = self.component, let highlightBackgroundFrame = self.highlightBackgroundFrame else {
return
}
if isHighlighted {
self.superview?.bringSubviewToFront(self)
let highlightBackgroundLayer: SimpleLayer
if let current = self.highlightBackgroundLayer {
highlightBackgroundLayer = current
} else {
highlightBackgroundLayer = SimpleLayer()
self.highlightBackgroundLayer = highlightBackgroundLayer
self.layer.insertSublayer(highlightBackgroundLayer, above: self.separatorLayer)
highlightBackgroundLayer.backgroundColor = component.theme.list.itemHighlightedBackgroundColor.cgColor
}
highlightBackgroundLayer.frame = highlightBackgroundFrame
highlightBackgroundLayer.opacity = 1.0
} else {
if let highlightBackgroundLayer = self.highlightBackgroundLayer {
self.highlightBackgroundLayer = nil
highlightBackgroundLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak highlightBackgroundLayer] _ in
highlightBackgroundLayer?.removeFromSuperlayer()
})
}
}
}
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func pressed() {
guard let component = self.component else {
return
}
component.action(self)
}
func setHasAssociatedMenu(_ hasAssociatedMenu: Bool) {
let transition: ComponentTransition
if hasAssociatedMenu {
transition = .immediate
} else {
transition = .easeInOut(duration: 0.25)
}
if let view = self.label.view {
transition.setAlpha(view: view, alpha: hasAssociatedMenu ? 0.5 : 1.0)
}
transition.setAlpha(view: self.arrowIconView, alpha: hasAssociatedMenu ? 0.5 : 1.0)
}
func update(component: StoragePeerTypeItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let themeUpdated = self.component?.theme !== component.theme
let previousComponent = self.component
self.component = component
let leftInset: CGFloat = 62.0
let rightInset: CGFloat = 32.0
var availableWidth: CGFloat = availableSize.width - leftInset - rightInset
let labelSize = self.label.update(
transition: transition,
component: AnyComponent(Text(text: component.value, font: Font.regular(17.0), color: component.theme.list.itemSecondaryTextColor)),
environment: {},
containerSize: CGSize(width: availableWidth, height: 100.0)
)
availableWidth = max(1.0, availableWidth - labelSize.width - 4.0)
let titleSize = self.title.update(
transition: transition,
component: AnyComponent(Text(text: component.title, font: Font.regular(17.0), color: component.theme.list.itemPrimaryTextColor)),
environment: {},
containerSize: CGSize(width: availableWidth, height: 100.0)
)
var subtitleSize: CGSize?
if let subtitleValue = component.subtitle {
let subtitle: ComponentView<Empty>
if let current = self.subtitle {
subtitle = current
} else {
subtitle = ComponentView()
self.subtitle = subtitle
}
let subtitleSizeValue = subtitle.update(
transition: transition,
component: AnyComponent(Text(text: subtitleValue, font: Font.regular(15.0), color: component.theme.list.itemSecondaryTextColor)),
environment: {},
containerSize: CGSize(width: availableWidth, height: 100.0)
)
subtitleSize = subtitleSizeValue
} else {
if let subtitle = self.subtitle {
self.subtitle = nil
subtitle.view?.removeFromSuperview()
}
}
var height: CGFloat = 52.0
if subtitleSize != nil {
height = 64.0
}
let titleFrame: CGRect
var subtitleFrame: CGRect?
if let subtitleSize = subtitleSize {
let spacing: CGFloat = 1.0
let verticalSize: CGFloat = titleSize.height + subtitleSize.height + spacing
titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - verticalSize) / 2.0)), size: titleSize)
subtitleFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + spacing), size: subtitleSize)
} else {
titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - titleSize.height) / 2.0)), size: titleSize)
}
let labelFrame = CGRect(origin: CGPoint(x: availableSize.width - rightInset - labelSize.width, y: floor((height - labelSize.height) / 2.0)), size: labelSize)
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.isUserInteractionEnabled = false
titleView.layer.anchorPoint = CGPoint()
self.addSubview(titleView)
}
transition.setPosition(view: titleView, position: titleFrame.topLeft)
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
}
if let subtitleView = self.subtitle?.view, let subtitleFrame {
if subtitleView.superview == nil {
subtitleView.isUserInteractionEnabled = false
subtitleView.layer.anchorPoint = CGPoint()
self.addSubview(subtitleView)
}
transition.setPosition(view: subtitleView, position: subtitleFrame.topLeft)
subtitleView.bounds = CGRect(origin: CGPoint(), size: subtitleFrame.size)
}
if let labelView = self.label.view {
if labelView.superview == nil {
labelView.isUserInteractionEnabled = false
labelView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.0)
self.addSubview(labelView)
}
transition.setPosition(view: labelView, position: labelFrame.topRight)
labelView.bounds = CGRect(origin: CGPoint(), size: labelFrame.size)
}
if themeUpdated || previousComponent?.iconName != component.iconName {
self.separatorLayer.backgroundColor = component.theme.list.itemBlocksSeparatorColor.cgColor
self.iconView.image = UIImage(bundleImageName: component.iconName)
if component.value.isEmpty {
self.arrowIconView.image = PresentationResourcesItemList.disclosureArrowImage(component.theme)
} else {
self.arrowIconView.image = PresentationResourcesItemList.disclosureOptionArrowsImage(component.theme)
}
}
if let image = self.iconView.image {
transition.setFrame(view: self.iconView, frame: CGRect(origin: CGPoint(x: floor((leftInset - image.size.width) / 2.0), y: floor((height - image.size.height) / 2.0)), size: image.size))
}
if let image = self.arrowIconView.image {
transition.setFrame(view: self.arrowIconView, frame: CGRect(origin: CGPoint(x: availableSize.width - rightInset + 5.0, y: floor((height - image.size.height) / 2.0)), size: image.size))
}
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel)))
transition.setAlpha(layer: self.separatorLayer, alpha: component.hasNext ? 1.0 : 0.0)
self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height + (component.hasNext ? UIScreenPixel : 0.0)))
return CGSize(width: availableSize.width, height: height)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
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,769 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import ComponentDisplayAdapters
import TelegramPresentationData
final class StorageUsagePanelContainerEnvironment: Equatable {
let isScrollable: Bool
init(
isScrollable: Bool
) {
self.isScrollable = isScrollable
}
static func ==(lhs: StorageUsagePanelContainerEnvironment, rhs: StorageUsagePanelContainerEnvironment) -> Bool {
if lhs.isScrollable != rhs.isScrollable {
return false
}
return true
}
}
final class StorageUsagePanelEnvironment: Equatable {
let theme: PresentationTheme
let strings: PresentationStrings
let dateTimeFormat: PresentationDateTimeFormat
let containerInsets: UIEdgeInsets
let isScrollable: Bool
init(
theme: PresentationTheme,
strings: PresentationStrings,
dateTimeFormat: PresentationDateTimeFormat,
containerInsets: UIEdgeInsets,
isScrollable: Bool
) {
self.theme = theme
self.strings = strings
self.dateTimeFormat = dateTimeFormat
self.containerInsets = containerInsets
self.isScrollable = isScrollable
}
static func ==(lhs: StorageUsagePanelEnvironment, rhs: StorageUsagePanelEnvironment) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.dateTimeFormat != rhs.dateTimeFormat {
return false
}
if lhs.containerInsets != rhs.containerInsets {
return false
}
if lhs.isScrollable != rhs.isScrollable {
return false
}
return true
}
}
private final class StorageUsageHeaderItemComponent: CombinedComponent {
let theme: PresentationTheme
let title: String
let activityFraction: CGFloat
init(
theme: PresentationTheme,
title: String,
activityFraction: CGFloat
) {
self.theme = theme
self.title = title
self.activityFraction = activityFraction
}
static func ==(lhs: StorageUsageHeaderItemComponent, rhs: StorageUsageHeaderItemComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.activityFraction != rhs.activityFraction {
return false
}
return true
}
static var body: Body {
let activeText = Child(Text.self)
let inactiveText = Child(Text.self)
return { context in
let activeText = activeText.update(
component: Text(text: context.component.title, font: Font.medium(14.0), color: context.component.theme.list.itemAccentColor),
availableSize: context.availableSize,
transition: .immediate
)
let inactiveText = inactiveText.update(
component: Text(text: context.component.title, font: Font.medium(14.0), color: context.component.theme.list.itemSecondaryTextColor),
availableSize: context.availableSize,
transition: .immediate
)
context.add(activeText
.position(CGPoint(x: activeText.size.width * 0.5, y: activeText.size.height * 0.5))
.opacity(context.component.activityFraction)
)
context.add(inactiveText
.position(CGPoint(x: inactiveText.size.width * 0.5, y: inactiveText.size.height * 0.5))
.opacity(1.0 - context.component.activityFraction)
)
return activeText.size
}
}
}
private final class StorageUsageHeaderComponent: Component {
struct Item: Equatable {
let id: AnyHashable
let title: String
init(
id: AnyHashable,
title: String
) {
self.id = id
self.title = title
}
}
let theme: PresentationTheme
let items: [Item]
let activeIndex: Int
let transitionFraction: CGFloat
let switchToPanel: (AnyHashable) -> Void
init(
theme: PresentationTheme,
items: [Item],
activeIndex: Int,
transitionFraction: CGFloat,
switchToPanel: @escaping (AnyHashable) -> Void
) {
self.theme = theme
self.items = items
self.activeIndex = activeIndex
self.transitionFraction = transitionFraction
self.switchToPanel = switchToPanel
}
static func ==(lhs: StorageUsageHeaderComponent, rhs: StorageUsageHeaderComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.items != rhs.items {
return false
}
if lhs.activeIndex != rhs.activeIndex {
return false
}
if lhs.transitionFraction != rhs.transitionFraction {
return false
}
return true
}
class View: UIView {
private var component: StorageUsageHeaderComponent?
private var visibleItems: [AnyHashable: ComponentView<Empty>] = [:]
private let activeItemLayer: SimpleLayer
override init(frame: CGRect) {
self.activeItemLayer = SimpleLayer()
self.activeItemLayer.cornerRadius = 2.0
self.activeItemLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
super.init(frame: frame)
self.layer.addSublayer(self.activeItemLayer)
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
let point = recognizer.location(in: self)
var closestId: (CGFloat, AnyHashable)?
if self.bounds.contains(point) {
for (id, item) in self.visibleItems {
if let itemView = item.view {
let distance: CGFloat = min(abs(point.x - itemView.frame.minX), abs(point.x - itemView.frame.maxX))
if let closestIdValue = closestId {
if distance < closestIdValue.0 {
closestId = (distance, id)
}
} else {
closestId = (distance, id)
}
}
}
}
if let closestId = closestId, let component = self.component {
component.switchToPanel(closestId.1)
}
}
}
func update(component: StorageUsageHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let themeUpdated = self.component?.theme !== component.theme
self.component = component
var validIds = Set<AnyHashable>()
for i in 0 ..< component.items.count {
let item = component.items[i]
validIds.insert(item.id)
let itemView: ComponentView<Empty>
var itemTransition = transition
if let current = self.visibleItems[item.id] {
itemView = current
} else {
itemTransition = .immediate
itemView = ComponentView()
self.visibleItems[item.id] = itemView
}
let activeIndex: CGFloat = CGFloat(component.activeIndex) - component.transitionFraction
let activityDistance: CGFloat = abs(activeIndex - CGFloat(i))
let activityFraction: CGFloat
if activityDistance < 1.0 {
activityFraction = 1.0 - activityDistance
} else {
activityFraction = 0.0
}
let itemSize = itemView.update(
transition: itemTransition,
component: AnyComponent(StorageUsageHeaderItemComponent(
theme: component.theme,
title: item.title,
activityFraction: activityFraction
)),
environment: {},
containerSize: availableSize
)
let itemHorizontalSpace = availableSize.width / CGFloat(component.items.count)
let itemX: CGFloat
if component.items.count == 1 {
itemX = 37.0
} else {
itemX = itemHorizontalSpace * CGFloat(i) + floor((itemHorizontalSpace - itemSize.width) / 2.0)
}
let itemFrame = CGRect(origin: CGPoint(x: itemX, y: floor((availableSize.height - itemSize.height) / 2.0)), size: itemSize)
if let itemComponentView = itemView.view {
if itemComponentView.superview == nil {
self.addSubview(itemComponentView)
itemComponentView.isUserInteractionEnabled = false
}
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
}
}
if component.activeIndex < component.items.count {
let activeView = self.visibleItems[component.items[component.activeIndex].id]?.view
let nextIndex: Int
if component.transitionFraction > 0.0 {
nextIndex = max(0, component.activeIndex - 1)
} else {
nextIndex = min(component.items.count - 1, component.activeIndex + 1)
}
let nextView = self.visibleItems[component.items[nextIndex].id]?.view
if let activeView = activeView, let nextView = nextView {
let mergedFrame = activeView.frame.interpolate(to: nextView.frame, amount: abs(component.transitionFraction))
transition.setFrame(layer: self.activeItemLayer, frame: CGRect(origin: CGPoint(x: mergedFrame.minX, y: availableSize.height - 3.0), size: CGSize(width: mergedFrame.width, height: 3.0)))
}
}
if themeUpdated {
self.activeItemLayer.backgroundColor = component.theme.list.itemAccentColor.cgColor
}
var removeIds: [AnyHashable] = []
for (id, itemView) in self.visibleItems {
if !validIds.contains(id) {
removeIds.append(id)
if let itemComponentView = itemView.view {
itemComponentView.removeFromSuperview()
}
}
}
for id in removeIds {
self.visibleItems.removeValue(forKey: id)
}
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
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)
}
}
final class StorageUsagePanelContainerComponent: Component {
typealias EnvironmentType = StorageUsagePanelContainerEnvironment
struct Item: Equatable {
let id: AnyHashable
let title: String
let panel: AnyComponent<StorageUsagePanelEnvironment>
init(
id: AnyHashable,
title: String,
panel: AnyComponent<StorageUsagePanelEnvironment>
) {
self.id = id
self.title = title
self.panel = panel
}
}
let theme: PresentationTheme
let strings: PresentationStrings
let dateTimeFormat: PresentationDateTimeFormat
let insets: UIEdgeInsets
let items: [Item]
let currentPanelUpdated: (AnyHashable, ComponentTransition) -> Void
init(
theme: PresentationTheme,
strings: PresentationStrings,
dateTimeFormat: PresentationDateTimeFormat,
insets: UIEdgeInsets,
items: [Item],
currentPanelUpdated: @escaping (AnyHashable, ComponentTransition) -> Void
) {
self.theme = theme
self.strings = strings
self.dateTimeFormat = dateTimeFormat
self.insets = insets
self.items = items
self.currentPanelUpdated = currentPanelUpdated
}
static func ==(lhs: StorageUsagePanelContainerComponent, rhs: StorageUsagePanelContainerComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.dateTimeFormat != rhs.dateTimeFormat {
return false
}
if lhs.insets != rhs.insets {
return false
}
if lhs.items != rhs.items {
return false
}
return true
}
class View: UIView, UIGestureRecognizerDelegate {
private let topPanelBackgroundView: UIView
private let topPanelMergedBackgroundView: UIView
private let topPanelSeparatorLayer: SimpleLayer
private let header = ComponentView<Empty>()
private var component: StorageUsagePanelContainerComponent?
private weak var state: EmptyComponentState?
private let panelsBackgroundLayer: SimpleLayer
private var visiblePanels: [AnyHashable: ComponentView<StorageUsagePanelEnvironment>] = [:]
private var actualVisibleIds = Set<AnyHashable>()
private var currentId: AnyHashable?
private var transitionFraction: CGFloat = 0.0
private var animatingTransition: Bool = false
override init(frame: CGRect) {
self.topPanelBackgroundView = UIView()
self.topPanelMergedBackgroundView = UIView()
self.topPanelMergedBackgroundView.alpha = 0.0
self.topPanelSeparatorLayer = SimpleLayer()
self.panelsBackgroundLayer = SimpleLayer()
super.init(frame: frame)
self.layer.addSublayer(self.panelsBackgroundLayer)
self.addSubview(self.topPanelBackgroundView)
self.addSubview(self.topPanelMergedBackgroundView)
self.layer.addSublayer(self.topPanelSeparatorLayer)
let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] point in
guard let self, let component = self.component, let currentId = self.currentId else {
return []
}
guard let index = component.items.firstIndex(where: { $0.id == currentId }) else {
return []
}
/*if strongSelf.tabsContainerNode.bounds.contains(strongSelf.view.convert(point, to: strongSelf.tabsContainerNode.view)) {
return []
}*/
if index == 0 {
return .left
}
return [.left, .right]
})
panRecognizer.delegate = self
panRecognizer.delaysTouchesBegan = false
panRecognizer.cancelsTouchesInView = true
self.addGestureRecognizer(panRecognizer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var currentPanelView: UIView? {
guard let currentId = self.currentId, let panel = self.visiblePanels[currentId] else {
return nil
}
return panel.view
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if let _ = otherGestureRecognizer as? InteractiveTransitionGestureRecognizer {
return false
}
if let _ = otherGestureRecognizer as? UIPanGestureRecognizer {
return true
}
return false
}
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .began:
func cancelContextGestures(view: UIView) {
if let gestureRecognizers = view.gestureRecognizers {
for gesture in gestureRecognizers {
if let gesture = gesture as? ContextGesture {
gesture.cancel()
}
}
}
for subview in view.subviews {
cancelContextGestures(view: subview)
}
}
cancelContextGestures(view: self)
//self.animatingTransition = true
case .changed:
guard let component = self.component, let currentId = self.currentId else {
return
}
guard let index = component.items.firstIndex(where: { $0.id == currentId }) else {
return
}
let translation = recognizer.translation(in: self)
var transitionFraction = translation.x / self.bounds.width
if index <= 0 {
transitionFraction = min(0.0, transitionFraction)
}
if index >= component.items.count - 1 {
transitionFraction = max(0.0, transitionFraction)
}
self.transitionFraction = transitionFraction
self.state?.updated(transition: .immediate)
case .cancelled, .ended:
guard let component = self.component, let currentId = self.currentId else {
return
}
guard let index = component.items.firstIndex(where: { $0.id == currentId }) else {
return
}
let translation = recognizer.translation(in: self)
let velocity = recognizer.velocity(in: self)
var directionIsToRight: Bool?
if abs(velocity.x) > 10.0 {
directionIsToRight = velocity.x < 0.0
} else {
if abs(translation.x) > self.bounds.width / 2.0 {
directionIsToRight = translation.x > self.bounds.width / 2.0
}
}
if let directionIsToRight = directionIsToRight {
var updatedIndex = index
if directionIsToRight {
updatedIndex = min(updatedIndex + 1, component.items.count - 1)
} else {
updatedIndex = max(updatedIndex - 1, 0)
}
self.currentId = component.items[updatedIndex].id
}
self.transitionFraction = 0.0
let transition = ComponentTransition(animation: .curve(duration: 0.35, curve: .spring))
if let currentId = self.currentId {
self.state?.updated(transition: transition)
component.currentPanelUpdated(currentId, transition)
}
self.animatingTransition = false
//self.currentPaneUpdated?(false)
//self.currentPaneStatusPromise.set(self.currentPane?.node.status ?? .single(nil))
default:
break
}
}
func updateNavigationMergeFactor(value: CGFloat, transition: ComponentTransition) {
transition.setAlpha(view: self.topPanelMergedBackgroundView, alpha: value)
transition.setAlpha(view: self.topPanelBackgroundView, alpha: 1.0 - value)
}
func update(component: StorageUsagePanelContainerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<StorageUsagePanelContainerEnvironment>, transition: ComponentTransition) -> CGSize {
let environment = environment[StorageUsagePanelContainerEnvironment.self].value
let themeUpdated = self.component?.theme !== component.theme
self.component = component
self.state = state
if themeUpdated {
self.panelsBackgroundLayer.backgroundColor = component.theme.list.itemBlocksBackgroundColor.cgColor
self.topPanelSeparatorLayer.backgroundColor = component.theme.list.itemBlocksSeparatorColor.cgColor
self.topPanelBackgroundView.backgroundColor = component.theme.list.itemBlocksBackgroundColor
self.topPanelMergedBackgroundView.backgroundColor = component.theme.rootController.navigationBar.blurredBackgroundColor
}
let topPanelCoverHeight: CGFloat = 10.0
let topPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: -topPanelCoverHeight), size: CGSize(width: availableSize.width, height: 44.0))
transition.setFrame(view: self.topPanelBackgroundView, frame: topPanelFrame)
transition.setFrame(view: self.topPanelMergedBackgroundView, frame: topPanelFrame)
transition.setFrame(layer: self.panelsBackgroundLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: availableSize.width, height: availableSize.height - topPanelFrame.maxY)))
transition.setFrame(layer: self.topPanelSeparatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: availableSize.width, height: UIScreenPixel)))
if let currentIdValue = self.currentId, !component.items.contains(where: { $0.id == currentIdValue }) {
self.currentId = nil
}
if self.currentId == nil {
self.currentId = component.items.first?.id
}
var visibleIds = Set<AnyHashable>()
var currentIndex: Int?
if let currentId = self.currentId {
visibleIds.insert(currentId)
if let index = component.items.firstIndex(where: { $0.id == currentId }) {
currentIndex = index
if index != 0 {
visibleIds.insert(component.items[index - 1].id)
}
if index != component.items.count - 1 {
visibleIds.insert(component.items[index + 1].id)
}
}
}
let _ = self.header.update(
transition: transition,
component: AnyComponent(StorageUsageHeaderComponent(
theme: component.theme,
items: component.items.map { item -> StorageUsageHeaderComponent.Item in
return StorageUsageHeaderComponent.Item(
id: item.id,
title: item.title
)
},
activeIndex: currentIndex ?? 0,
transitionFraction: self.transitionFraction,
switchToPanel: { [weak self] id in
guard let self, let component = self.component else {
return
}
if component.items.contains(where: { $0.id == id }) {
self.currentId = id
let transition = ComponentTransition(animation: .curve(duration: 0.35, curve: .spring))
self.state?.updated(transition: transition)
component.currentPanelUpdated(id, transition)
}
}
)),
environment: {},
containerSize: topPanelFrame.size
)
if let headerView = self.header.view {
if headerView.superview == nil {
self.addSubview(headerView)
}
transition.setFrame(view: headerView, frame: topPanelFrame)
}
let childEnvironment = StorageUsagePanelEnvironment(
theme: component.theme,
strings: component.strings,
dateTimeFormat: component.dateTimeFormat,
containerInsets: UIEdgeInsets(top: 0.0, left: component.insets.left, bottom: component.insets.bottom, right: component.insets.right),
isScrollable: environment.isScrollable
)
let centralPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: availableSize.width, height: availableSize.height - topPanelFrame.maxY))
if self.animatingTransition {
visibleIds = visibleIds.filter({ self.visiblePanels[$0] != nil })
}
self.actualVisibleIds = visibleIds
for (id, _) in self.visiblePanels {
visibleIds.insert(id)
}
var validIds = Set<AnyHashable>()
if let currentIndex {
var anyAnchorOffset: CGFloat = 0.0
for (id, panel) in self.visiblePanels {
guard let itemIndex = component.items.firstIndex(where: { $0.id == id }), let panelView = panel.view else {
continue
}
var itemFrame = centralPanelFrame.offsetBy(dx: self.transitionFraction * availableSize.width, dy: 0.0)
if itemIndex < currentIndex {
itemFrame.origin.x -= itemFrame.width
} else if itemIndex > currentIndex {
itemFrame.origin.x += itemFrame.width
}
anyAnchorOffset = itemFrame.minX - panelView.frame.minX
break
}
for id in visibleIds {
guard let itemIndex = component.items.firstIndex(where: { $0.id == id }) else {
continue
}
let panelItem = component.items[itemIndex]
var itemFrame = centralPanelFrame.offsetBy(dx: self.transitionFraction * availableSize.width, dy: 0.0)
if itemIndex < currentIndex {
itemFrame.origin.x -= itemFrame.width
} else if itemIndex > currentIndex {
itemFrame.origin.x += itemFrame.width
}
validIds.insert(panelItem.id)
let panel: ComponentView<StorageUsagePanelEnvironment>
var panelTransition = transition
var animateInIfNeeded = false
if let current = self.visiblePanels[panelItem.id] {
panel = current
if let panelView = panel.view, !panelView.bounds.isEmpty {
var wasHidden = false
if abs(panelView.frame.minX - availableSize.width) < .ulpOfOne || abs(panelView.frame.maxX - 0.0) < .ulpOfOne {
wasHidden = true
}
var isHidden = false
if abs(itemFrame.minX - availableSize.width) < .ulpOfOne || abs(itemFrame.maxX - 0.0) < .ulpOfOne {
isHidden = true
}
if wasHidden && isHidden {
panelTransition = .immediate
}
}
} else {
panelTransition = .immediate
animateInIfNeeded = true
panel = ComponentView()
self.visiblePanels[panelItem.id] = panel
}
let _ = panel.update(
transition: panelTransition,
component: panelItem.panel,
environment: {
childEnvironment
},
containerSize: centralPanelFrame.size
)
if let panelView = panel.view {
if panelView.superview == nil {
self.insertSubview(panelView, belowSubview: self.topPanelBackgroundView)
}
panelTransition.setFrame(view: panelView, frame: itemFrame, completion: { [weak self] _ in
guard let self else {
return
}
if !self.actualVisibleIds.contains(id) {
if let panel = self.visiblePanels[id] {
self.visiblePanels.removeValue(forKey: id)
panel.view?.removeFromSuperview()
}
}
})
if animateInIfNeeded && anyAnchorOffset != 0.0 {
transition.animatePosition(view: panelView, from: CGPoint(x: -anyAnchorOffset, y: 0.0), to: CGPoint(), additive: true)
}
}
}
}
var removeIds: [AnyHashable] = []
for (id, panel) in self.visiblePanels {
if !validIds.contains(id) {
removeIds.append(id)
if let panelView = panel.view {
panelView.removeFromSuperview()
}
}
}
for id in removeIds {
self.visiblePanels.removeValue(forKey: id)
}
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<StorageUsagePanelContainerEnvironment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}