import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import ViewControllerComponent
import ComponentDisplayAdapters
import TelegramPresentationData
import AccountContext
import TelegramCore
import Postbox
import MultilineTextComponent
import EmojiStatusComponent
import Markdown
import ContextUI
import AnimatedAvatarSetNode
import AvatarNode
import RadialStatusNode
import UndoUI
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import TelegramStringFormatting
import GalleryData
import AnimatedTextComponent
import BottomButtonPanelComponent

#if DEBUG
import os.signpost

private class SignpostContext {
    enum EventType {
        case begin
        case end
    }
    
    class OpaqueData {
    }
    
    static var shared: SignpostContext? = {
        if #available(iOS 15.0, *) {
            return SignpostContextImpl()
        } else {
            return nil
        }
    }()
    
    func begin(name: StaticString) -> OpaqueData {
        preconditionFailure()
    }
    
    func end(name: StaticString, data: OpaqueData) {
    }
}

@available(iOS 15.0, *)
private final class SignpostContextImpl: SignpostContext {
    final class OpaqueDataImpl: OpaqueData {
        let state: OSSignpostIntervalState
        let timestamp: Double
        
        init(state: OSSignpostIntervalState, timestamp: Double) {
            self.state = state
            self.timestamp = timestamp
        }
    }
    
    private let signpost = OSSignposter(subsystem: "org.telegram.Telegram-iOS", category: "StorageUsageScreen")
    private let id: OSSignpostID
    
    override init() {
        self.id = self.signpost.makeSignpostID()
        
        super.init()
    }
    
    override func begin(name: StaticString) -> OpaqueData {
        let result = self.signpost.beginInterval(name, id: self.id)
        return OpaqueDataImpl(state: result, timestamp: CFAbsoluteTimeGetCurrent())
    }
    
    override func end(name: StaticString, data: OpaqueData) {
        if let data = data as? OpaqueDataImpl {
            self.signpost.endInterval(name, data.state)
            print("Signpost \(name): \((CFAbsoluteTimeGetCurrent() - data.timestamp) * 1000.0) ms")
        }
    }
}

#endif

private extension StorageUsageScreenComponent.Category {
    init(_ category: StorageUsageStats.CategoryKey) {
        switch category {
        case .photos:
            self = .photos
        case .videos:
            self = .videos
        case .files:
            self = .files
        case .music:
            self = .music
        case .stickers:
            self = .stickers
        case .avatars:
            self = .avatars
        case .misc:
            self = .misc
        case .stories:
            self = .stories
        }
    }
}

final class StorageUsageScreenComponent: Component {
    typealias EnvironmentType = ViewControllerComponentContainer.Environment
    
    let context: AccountContext
    let makeStorageUsageExceptionsScreen: (CacheStorageSettings.PeerStorageCategory) -> ViewController?
    let peer: EnginePeer?
    let ready: Promise<Bool>
    
    init(
        context: AccountContext,
        makeStorageUsageExceptionsScreen: @escaping (CacheStorageSettings.PeerStorageCategory) -> ViewController?,
        peer: EnginePeer?,
        ready: Promise<Bool>
    ) {
        self.context = context
        self.makeStorageUsageExceptionsScreen = makeStorageUsageExceptionsScreen
        self.peer = peer
        self.ready = ready
    }
    
    static func ==(lhs: StorageUsageScreenComponent, rhs: StorageUsageScreenComponent) -> Bool {
        if lhs.context !== rhs.context {
            return false
        }
        if lhs.peer != rhs.peer {
            return false
        }
        return true
    }
    
    private final class ScrollViewImpl: UIScrollView {
        override func touchesShouldCancel(in view: UIView) -> Bool {
            return true
        }
        
        override var contentOffset: CGPoint {
            set(value) {
                var value = value
                if value.y > self.contentSize.height - self.bounds.height {
                    value.y = max(0.0, self.contentSize.height - self.bounds.height)
                    self.bounces = false
                } else {
                    self.bounces = true
                }
                super.contentOffset = value
            } get {
                return super.contentOffset
            }
        }
    }
    
    private final class AnimationHint {
        enum Value {
            case firstStatsUpdate
            case clearedItems
        }
        let value: Value
        
        init(value: Value) {
            self.value = value
        }
    }
    
    final class SelectionState: Equatable {
        let selectedPeers: Set<EnginePeer.Id>
        let selectedMessages: Set<EngineMessage.Id>
        
        var isEmpty: Bool {
            if !self.selectedPeers.isEmpty {
                return false
            }
            if !self.selectedMessages.isEmpty {
                return false
            }
            return true
        }
        
        init(
            selectedPeers: Set<EnginePeer.Id>,
            selectedMessages: Set<EngineMessage.Id>
        ) {
            self.selectedPeers = selectedPeers
            self.selectedMessages = selectedMessages
        }
        
        convenience init() {
            self.init(
                selectedPeers: Set(),
                selectedMessages: Set()
            )
        }
        
        static func ==(lhs: SelectionState, rhs: SelectionState) -> Bool {
            if lhs.selectedPeers != rhs.selectedPeers {
                return false
            }
            if lhs.selectedMessages != rhs.selectedMessages {
                return false
            }
            return true
        }
        
        func togglePeer(id: EnginePeer.Id, availableMessages: [EngineMessage.Id: Message]) -> SelectionState {
            var selectedPeers = self.selectedPeers
            var selectedMessages = self.selectedMessages
            
            if selectedPeers.contains(id) {
                selectedPeers.remove(id)
                
                for (messageId, _) in availableMessages {
                    if messageId.peerId == id {
                        selectedMessages.remove(messageId)
                    }
                }
            } else {
                selectedPeers.insert(id)
                
                for (messageId, _) in availableMessages {
                    if messageId.peerId == id {
                        selectedMessages.insert(messageId)
                    }
                }
            }
            
            return SelectionState(
                selectedPeers: selectedPeers,
                selectedMessages: selectedMessages
            )
        }
        
        func toggleMessage(id: EngineMessage.Id) -> SelectionState {
            var selectedMessages = self.selectedMessages
            if selectedMessages.contains(id) {
                selectedMessages.remove(id)
            } else {
                selectedMessages.insert(id)
            }
            
            return SelectionState(
                selectedPeers: self.selectedPeers,
                selectedMessages: selectedMessages
            )
        }
    }
    
    enum Category: Hashable {
        case photos
        case videos
        case files
        case music
        case other
        case stickers
        case avatars
        case misc
        case stories
        
        var color: UIColor {
            switch self {
            case .photos:
                return UIColor(rgb: 0x5AC8FA)
            case .videos:
                return UIColor(rgb: 0x3478F6)
            case .files:
                return UIColor(rgb: 0x34C759)
            case .music:
                return UIColor(rgb: 0xFF2D55)
            case .other:
                return UIColor(rgb: 0xC4C4C6)
            case .stickers:
                return UIColor(rgb: 0x5856D6)
            case .avatars:
                return UIColor(rgb: 0xAF52DE)
            case .misc:
                return UIColor(rgb: 0xFF9500)
            case .stories:
                return UIColor(rgb: 0x3478F6)
            }
        }
        
        func title(strings: PresentationStrings) -> String {
            switch self {
            case .photos:
                return strings.StorageManagement_SectionPhotos
            case .videos:
                return strings.StorageManagement_SectionVideos
            case .files:
                return strings.StorageManagement_SectionFiles
            case .music:
                return strings.StorageManagement_SectionMusic
            case .other:
                return strings.StorageManagement_SectionOther
            case .stickers:
                return strings.StorageManagement_SectionStickers
            case .avatars:
                return strings.StorageManagement_SectionAvatars
            case .misc:
                return strings.StorageManagement_SectionMiscellaneous
            case .stories:
                return strings.StorageManagement_SectionStories
            }
        }
        
        var particle: String? {
            switch self {
            case .photos:
                return "Settings/Storage/ParticlePhotos"
            case .videos:
                return "Settings/Storage/ParticleVideos"
            case .files:
                return "Settings/Storage/ParticleDocuments"
            case .music:
                return "Settings/Storage/ParticleMusic"
            case .other:
                return "Settings/Storage/ParticleOther"
            case .stickers:
                return "Settings/Storage/ParticleStickers"
            case .avatars:
                return "Settings/Storage/ParticleAvatars"
            case .misc:
                return "Settings/Storage/ParticleOther"
            case .stories:
                return "Settings/Storage/ParticleOther"
            }
        }
    }
    
    private final class AggregatedData {
        let peerId: EnginePeer.Id?
        let stats: AllStorageUsageStats
        let contextStats: StorageUsageStats
        let messages: [MessageId: Message]
        
        var isSelectingPeers: Bool = false
        private(set) var selectionState: SelectionState
        
        let existingCategories: Set<Category>
        private(set) var selectedCategories: Set<Category>
        
        let peerItems: StoragePeerListPanelComponent.Items?
        let imageItems: StorageMediaGridPanelComponent.Items?
        let fileItems: StorageFileListPanelComponent.Items?
        let musicItems: StorageFileListPanelComponent.Items?
        
        private let allPhotos: Set<EngineMessage.Id>
        private let allVideos: Set<EngineMessage.Id>
        private let allFiles: Set<EngineMessage.Id>
        private let allMusic: Set<EngineMessage.Id>
        
        private(set) var selectedSize: Int64 = 0
        private(set) var clearIncludeMessages: [Message] = []
        private(set) var clearExcludeMessages: [Message] = []
        
        init(
            peerId: EnginePeer.Id?,
            stats: AllStorageUsageStats,
            messages: [MessageId: Message],
            peerItems: StoragePeerListPanelComponent.Items?,
            imageItems: StorageMediaGridPanelComponent.Items?,
            fileItems: StorageFileListPanelComponent.Items?,
            musicItems: StorageFileListPanelComponent.Items?
        ) {
            self.peerId = peerId
            self.stats = stats
            if let peerId {
                self.contextStats = stats.peers[peerId]?.stats ?? StorageUsageStats(categories: [:])
            } else {
                self.contextStats = stats.totalStats
            }
            
            self.messages = messages
            
            self.selectionState = SelectionState()
            
            self.peerItems = peerItems
            self.imageItems = imageItems
            self.fileItems = fileItems
            self.musicItems = musicItems
            
            var allPhotos = Set<EngineMessage.Id>()
            var allVideos = Set<EngineMessage.Id>()
            if let imageItems = self.imageItems {
                for item in imageItems.items {
                    var isImage = false
                    for media in item.message.media {
                        if media is TelegramMediaImage {
                            isImage = true
                            break
                        }
                    }
                    if isImage {
                        allPhotos.insert(item.message.id)
                    } else {
                        allVideos.insert(item.message.id)
                    }
                }
            }
            self.allPhotos = allPhotos
            self.allVideos = allVideos
            
            var allFiles = Set<EngineMessage.Id>()
            if let fileItems = self.fileItems {
                for item in fileItems.items {
                    allFiles.insert(item.message.id)
                }
            }
            self.allFiles = allFiles
            
            var allMusic = Set<EngineMessage.Id>()
            if let musicItems = self.musicItems {
                for item in musicItems.items {
                    allMusic.insert(item.message.id)
                }
            }
            self.allMusic = allMusic
            
            var existingCategories = Set<Category>()
            for (category, value) in self.contextStats.categories {
                if value.size != 0 {
                    existingCategories.insert(StorageUsageScreenComponent.Category(category))
                }
            }
            self.existingCategories = existingCategories
            self.selectedCategories = existingCategories
            
            if self.peerId != nil {
                var selectedMessages = self.selectionState.selectedMessages
                selectedMessages.formUnion(self.allPhotos)
                selectedMessages.formUnion(self.allVideos)
                selectedMessages.formUnion(self.allFiles)
                selectedMessages.formUnion(self.allMusic)
                
                self.selectionState = SelectionState(selectedPeers: self.selectionState.selectedPeers, selectedMessages: selectedMessages)
            }
            
            self.refreshSelectionStats()
        }
        
        func setIsCategorySelected(category: Category, isSelected: Bool) {
            if isSelected {
                self.selectedCategories.insert(category)
            } else {
                self.selectedCategories.remove(category)
            }
            
            if self.peerId != nil {
                var selectedMessages = self.selectionState.selectedMessages
                switch category {
                case .photos:
                    if isSelected {
                        selectedMessages.formUnion(self.allPhotos)
                    } else {
                        selectedMessages.subtract(self.allPhotos)
                    }
                case .videos:
                    if isSelected {
                        selectedMessages.formUnion(self.allVideos)
                    } else {
                        selectedMessages.subtract(self.allVideos)
                    }
                case .files:
                    if let fileItems = self.fileItems {
                        for item in fileItems.items {
                            if isSelected {
                                selectedMessages.insert(item.message.id)
                            } else {
                                selectedMessages.remove(item.message.id)
                            }
                        }
                    }
                case .music:
                    if let fileItems = self.musicItems {
                        for item in fileItems.items {
                            if isSelected {
                                selectedMessages.insert(item.message.id)
                            } else {
                                selectedMessages.remove(item.message.id)
                            }
                        }
                    }
                default:
                    break
                }
                self.selectionState = SelectionState(selectedPeers: self.selectionState.selectedPeers, selectedMessages: selectedMessages)
            }
            
            self.refreshSelectionStats()
        }
        
        func clearPeerSelection() {
            self.selectionState = SelectionState(selectedPeers: Set(), selectedMessages: Set())
            
            self.refreshSelectionStats()
        }
        
        func togglePeerSelection(id: EnginePeer.Id) {
            self.selectionState = self.selectionState.togglePeer(id: id, availableMessages: self.messages)
            
            self.refreshSelectionStats()
        }
        
        func toggleMessageSelection(id: EngineMessage.Id) {
            self.selectionState = self.selectionState.toggleMessage(id: id)
            
            if self.peerId != nil {
                if self.allPhotos.contains(id) {
                    if !self.selectionState.selectedMessages.contains(id) {
                        if self.allPhotos.intersection(self.selectionState.selectedMessages).isEmpty {
                            self.selectedCategories.remove(.photos)
                        }
                    } else {
                        if self.allPhotos.intersection(self.selectionState.selectedMessages) == self.allPhotos {
                            self.selectedCategories.insert(.photos)
                        }
                    }
                } else if self.allVideos.contains(id) {
                    if !self.selectionState.selectedMessages.contains(id) {
                        if self.allVideos.intersection(self.selectionState.selectedMessages).isEmpty {
                            self.selectedCategories.remove(.videos)
                        }
                    } else {
                        if self.allVideos.intersection(self.selectionState.selectedMessages) == self.allVideos {
                            self.selectedCategories.insert(.videos)
                        }
                    }
                } else if self.allFiles.contains(id) {
                    if !self.selectionState.selectedMessages.contains(id) {
                        if self.allFiles.intersection(self.selectionState.selectedMessages).isEmpty {
                            self.selectedCategories.remove(.files)
                        }
                    } else {
                        if self.allFiles.intersection(self.selectionState.selectedMessages) == self.allFiles {
                            self.selectedCategories.insert(.files)
                        }
                    }
                } else if self.allMusic.contains(id) {
                    if !self.selectionState.selectedMessages.contains(id) {
                        if self.allMusic.intersection(self.selectionState.selectedMessages).isEmpty {
                            self.selectedCategories.remove(.music)
                        }
                    } else {
                        if self.allMusic.intersection(self.selectionState.selectedMessages) == self.allMusic {
                            self.selectedCategories.insert(.music)
                        }
                    }
                }
            }
            
            self.refreshSelectionStats()
        }
        
        private func refreshSelectionStats() {
            if let _ = self.peerId {
                var selectedSize: Int64 = 0
                for (category, value) in self.contextStats.categories {
                    let mappedCategory = StorageUsageScreenComponent.Category(category)
                    if self.selectedCategories.contains(mappedCategory) {
                        selectedSize += value.size
                    }
                }
                
                var clearIncludeMessages: [Message] = []
                var clearExcludeMessages: [Message] = []
                
                if self.selectedCategories.contains(.photos) {
                    let deselectedPhotos = self.allPhotos.subtracting(self.selectionState.selectedMessages)
                    if !deselectedPhotos.isEmpty, let imageItems = self.imageItems {
                        for item in imageItems.items {
                            if deselectedPhotos.contains(item.message.id) {
                                selectedSize -= item.size
                                clearExcludeMessages.append(item.message._asMessage())
                            }
                        }
                    }
                } else {
                    let selectedPhotos = self.allPhotos.intersection(self.selectionState.selectedMessages)
                    if !selectedPhotos.isEmpty, let imageItems = self.imageItems {
                        for item in imageItems.items {
                            if selectedPhotos.contains(item.message.id) {
                                selectedSize += item.size
                                clearIncludeMessages.append(item.message._asMessage())
                            }
                        }
                    }
                }
                
                if self.selectedCategories.contains(.videos) {
                    let deselectedVideos = self.allVideos.subtracting(self.selectionState.selectedMessages)
                    if !deselectedVideos.isEmpty, let imageItems = self.imageItems {
                        for item in imageItems.items {
                            if deselectedVideos.contains(item.message.id) {
                                selectedSize -= item.size
                                clearExcludeMessages.append(item.message._asMessage())
                            }
                        }
                    }
                } else {
                    let selectedVideos = self.allVideos.intersection(self.selectionState.selectedMessages)
                    if !selectedVideos.isEmpty, let imageItems = self.imageItems {
                        for item in imageItems.items {
                            if selectedVideos.contains(item.message.id) {
                                selectedSize += item.size
                                clearIncludeMessages.append(item.message._asMessage())
                            }
                        }
                    }
                }
                
                if self.selectedCategories.contains(.files) {
                    let deselectedFiles = self.allFiles.subtracting(self.selectionState.selectedMessages)
                    if !deselectedFiles.isEmpty, let fileItems = self.fileItems {
                        for item in fileItems.items {
                            if deselectedFiles.contains(item.message.id) {
                                selectedSize -= item.size
                                clearExcludeMessages.append(item.message)
                            }
                        }
                    }
                } else {
                    let selectedFiles = self.allFiles.intersection(self.selectionState.selectedMessages)
                    if !selectedFiles.isEmpty, let fileItems = self.fileItems {
                        for item in fileItems.items {
                            if selectedFiles.contains(item.message.id) {
                                selectedSize += item.size
                                clearIncludeMessages.append(item.message)
                            }
                        }
                    }
                }
                
                if self.selectedCategories.contains(.music) {
                    let deselectedMusic = self.allMusic.subtracting(self.selectionState.selectedMessages)
                    if !deselectedMusic.isEmpty, let musicItems = self.musicItems {
                        for item in musicItems.items {
                            if deselectedMusic.contains(item.message.id) {
                                selectedSize -= item.size
                                clearExcludeMessages.append(item.message)
                            }
                        }
                    }
                } else {
                    let selectedMusic = self.allMusic.intersection(self.selectionState.selectedMessages)
                    if !selectedMusic.isEmpty, let musicItems = self.musicItems {
                        for item in musicItems.items {
                            if selectedMusic.contains(item.message.id) {
                                selectedSize += item.size
                                clearIncludeMessages.append(item.message)
                            }
                        }
                    }
                }
                
                self.selectedSize = selectedSize
                self.clearExcludeMessages = clearExcludeMessages
                self.clearIncludeMessages = clearIncludeMessages
            } else {
                var selectedSize: Int64 = 0
                    
                for peerId in self.selectionState.selectedPeers {
                    if let stats = self.stats.peers[peerId] {
                        let peerSize = stats.stats.categories.values.reduce(0, {
                            $0 + $1.size
                        })
                        selectedSize += peerSize
                        
                        for (messageId, _) in self.messages {
                            if messageId.peerId == peerId {
                                if !self.selectionState.selectedMessages.contains(messageId) {
                                    inner: for (_, category) in stats.stats.categories {
                                        if let messageSize = category.messages[messageId] {
                                            selectedSize -= messageSize
                                            break inner
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
                
                for messageId in self.selectionState.selectedMessages {
                    for (_, category) in self.contextStats.categories {
                        if let messageSize = category.messages[messageId] {
                            if !self.selectionState.selectedPeers.contains(messageId.peerId) {
                                selectedSize += messageSize
                            }
                            break
                        }
                    }
                }
                
                self.selectedSize = selectedSize
                self.clearIncludeMessages = []
                self.clearExcludeMessages = []
            }
        }
    }
    
    class View: UIView, UIScrollViewDelegate {
        private let scrollView: ScrollViewImpl
        
        private var aggregatedData: AggregatedData?
        private var otherCategories: Set<Category> = Set()
        
        private var cacheSettings: CacheStorageSettings?
        private var cacheSettingsExceptionCount: [CacheStorageSettings.PeerStorageCategory: Int32]?
        
        private var currentSelectedPanelId: AnyHashable?
        
        private var clearingDisplayTimestamp: Double?
        private var isClearing: Bool = false {
            didSet {
                if self.isClearing != oldValue {
                    if self.isClearing {
                        if self.keepScreenActiveDisposable == nil {
                            self.keepScreenActiveDisposable = self.component?.context.sharedContext.applicationBindings.pushIdleTimerExtension()
                        }
                    } else {
                        if let keepScreenActiveDisposable = self.keepScreenActiveDisposable {
                            self.keepScreenActiveDisposable = nil
                            keepScreenActiveDisposable.dispose()
                        }
                    }
                }
            }
        }
        
        private var isOtherCategoryExpanded: Bool = false
        
        private let navigationBackgroundView: BlurredBackgroundView
        private let navigationSeparatorLayer: SimpleLayer
        private let navigationSeparatorLayerContainer: SimpleLayer
        private let navigationEditButton = ComponentView<Empty>()
        private let navigationDoneButton = ComponentView<Empty>()
        
        private let headerView = ComponentView<Empty>()
        private let headerOffsetContainer: UIView
        private let headerDescriptionView = ComponentView<Empty>()
        
        private let headerProgressBackgroundLayer: SimpleLayer
        private let headerProgressForegroundLayer: SimpleLayer
        
        private var chartAvatarNode: AvatarNode?
        
        private var doneStatusCircle: SimpleShapeLayer?
        private var doneStatusNode: RadialStatusNode?
        
        private let scrollContainerView: UIView
        
        private let pieChartView = ComponentView<Empty>()
        private let chartTotalLabel = ComponentView<Empty>()
        private let categoriesView = ComponentView<Empty>()
        private let categoriesDescriptionView = ComponentView<Empty>()
        
        private let keepDurationTitleView = ComponentView<Empty>()
        private let keepDurationDescriptionView = ComponentView<Empty>()
        private var keepDurationSectionContainerView: UIView
        private var keepDurationItems: [AnyHashable: ComponentView<Empty>] = [:]
        
        private let keepSizeTitleView = ComponentView<Empty>()
        private let keepSizeView = ComponentView<Empty>()
        private let keepSizeDescriptionView = ComponentView<Empty>()
        
        private let panelContainer = ComponentView<StorageUsagePanelContainerEnvironment>()
        
        private var selectionPanel: ComponentView<Empty>?
        
        private var clearingNode: StorageUsageClearProgressOverlayNode?
        
        private var loadingView: UIActivityIndicatorView?
        
        private var component: StorageUsageScreenComponent?
        private weak var state: EmptyComponentState?
        private var navigationMetrics: (navigationHeight: CGFloat, statusBarHeight: CGFloat)?
        private var controller: (() -> ViewController?)?
        
        private var enableVelocityTracking: Bool = false
        private var previousVelocityM1: CGFloat = 0.0
        private var previousVelocity: CGFloat = 0.0
        
        private var ignoreScrolling: Bool = false
        
        private var statsDisposable: Disposable?
        private var messagesDisposable: Disposable?
        private var cacheSettingsDisposable: Disposable?
        private var keepScreenActiveDisposable: Disposable?
        
        override init(frame: CGRect) {
            self.headerOffsetContainer = UIView()
            self.headerOffsetContainer.isUserInteractionEnabled = false
            
            self.navigationBackgroundView = BlurredBackgroundView(color: nil, enableBlur: true)
            self.navigationBackgroundView.alpha = 0.0
            
            self.navigationSeparatorLayer = SimpleLayer()
            self.navigationSeparatorLayer.opacity = 0.0
            self.navigationSeparatorLayerContainer = SimpleLayer()
            self.navigationSeparatorLayerContainer.opacity = 0.0
            
            self.scrollContainerView = UIView()
            
            self.scrollView = ScrollViewImpl()
            
            self.keepDurationSectionContainerView = UIView()
            self.keepDurationSectionContainerView.clipsToBounds = true
            self.keepDurationSectionContainerView.layer.cornerRadius = 10.0
            
            self.headerProgressBackgroundLayer = SimpleLayer()
            self.headerProgressForegroundLayer = SimpleLayer()
            
            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 = false
            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.scrollView.addSubview(self.scrollContainerView)
            
            self.scrollContainerView.addSubview(self.keepDurationSectionContainerView)
            
            self.scrollView.layer.addSublayer(self.headerProgressBackgroundLayer)
            self.scrollView.layer.addSublayer(self.headerProgressForegroundLayer)
            
            self.addSubview(self.navigationBackgroundView)
            
            self.navigationSeparatorLayerContainer.addSublayer(self.navigationSeparatorLayer)
            self.layer.addSublayer(self.navigationSeparatorLayerContainer)
            
            self.addSubview(self.headerOffsetContainer)
        }
        
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
        
        deinit {
            self.statsDisposable?.dispose()
            self.messagesDisposable?.dispose()
            self.keepScreenActiveDisposable?.dispose()
        }
        
        func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
            self.enableVelocityTracking = true
        }
        
        func scrollViewDidScroll(_ scrollView: UIScrollView) {
            if !self.ignoreScrolling {
                if self.enableVelocityTracking {
                    self.previousVelocityM1 = self.previousVelocity
                    if let value = (scrollView.value(forKey: (["_", "verticalVelocity"] as [String]).joined()) as? NSNumber)?.doubleValue {
                        self.previousVelocity = CGFloat(value)
                    }
                }
                
                self.updateScrolling(transition: .immediate)
            }
        }
        
        func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
            guard let _ = self.navigationMetrics else {
                return
            }
            
            let paneAreaExpansionDistance: CGFloat = 32.0
            let paneAreaExpansionFinalPoint: CGFloat = scrollView.contentSize.height - scrollView.bounds.height
            if targetContentOffset.pointee.y > paneAreaExpansionFinalPoint - paneAreaExpansionDistance && targetContentOffset.pointee.y < paneAreaExpansionFinalPoint {
                targetContentOffset.pointee.y = paneAreaExpansionFinalPoint
                self.enableVelocityTracking = false
                self.previousVelocity = 0.0
                self.previousVelocityM1 = 0.0
            }
        }
        
        func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
            if let panelContainerView = self.panelContainer.view as? StorageUsagePanelContainerComponent.View {
                let _ = panelContainerView
                let paneAreaExpansionFinalPoint: CGFloat = scrollView.contentSize.height - scrollView.bounds.height
                if abs(scrollView.contentOffset.y - paneAreaExpansionFinalPoint) < .ulpOfOne {
                    //panelContainerView.transferVelocity(self.previousVelocityM1)
                }
            }
        }
        
        private func updateScrolling(transition: Transition) {
            let scrollBounds = self.scrollView.bounds
            
            let isLockedAtPanels = scrollBounds.maxY == self.scrollView.contentSize.height
            
            if let headerView = self.headerView.view, let navigationMetrics = self.navigationMetrics {
                var headerOffset: CGFloat = scrollBounds.minY
                
                let minY = navigationMetrics.statusBarHeight + floor((navigationMetrics.navigationHeight - navigationMetrics.statusBarHeight) / 2.0)
                
                let minOffset = headerView.center.y - minY
                
                headerOffset = min(headerOffset, minOffset)
                
                let animatedTransition = Transition(animation: .curve(duration: 0.18, curve: .easeInOut))
                let navigationBackgroundAlpha: CGFloat = abs(headerOffset - minOffset) < 4.0 ? 1.0 : 0.0
                
                animatedTransition.setAlpha(view: self.navigationBackgroundView, alpha: navigationBackgroundAlpha)
                animatedTransition.setAlpha(layer: self.navigationSeparatorLayerContainer, alpha: navigationBackgroundAlpha)
                
                var buttonsMasterAlpha: CGFloat = 1.0
                if let component = self.component, component.peer != nil {
                    buttonsMasterAlpha = 0.0
                } else {
                    if self.currentSelectedPanelId == nil || self.currentSelectedPanelId == AnyHashable("peers") {
                        buttonsMasterAlpha = 1.0
                    } else {
                        buttonsMasterAlpha = 0.0
                    }
                }
                
                let isSelectingPeers = self.aggregatedData?.isSelectingPeers ?? false
                
                if let navigationEditButtonView = self.navigationEditButton.view {
                    animatedTransition.setAlpha(view: navigationEditButtonView, alpha: (isSelectingPeers ? 0.0 : 1.0) * buttonsMasterAlpha * navigationBackgroundAlpha)
                }
                if let navigationDoneButtonView = self.navigationDoneButton.view {
                    animatedTransition.setAlpha(view: navigationDoneButtonView, alpha: (isSelectingPeers ? 1.0 : 0.0) * buttonsMasterAlpha * navigationBackgroundAlpha)
                }
                
                let expansionDistance: CGFloat = 32.0
                var expansionDistanceFactor: CGFloat = abs(scrollBounds.maxY - self.scrollView.contentSize.height) / expansionDistance
                expansionDistanceFactor = max(0.0, min(1.0, expansionDistanceFactor))
                
                transition.setAlpha(layer: self.navigationSeparatorLayer, alpha: expansionDistanceFactor)
                if let panelContainerView = self.panelContainer.view as? StorageUsagePanelContainerComponent.View {
                    panelContainerView.updateNavigationMergeFactor(value: 1.0 - expansionDistanceFactor, transition: transition)
                }
                
                var offsetFraction: CGFloat = abs(headerOffset - minOffset) / 60.0
                offsetFraction = min(1.0, max(0.0, offsetFraction))
                transition.setScale(view: headerView, scale: 1.0 * offsetFraction + 0.8 * (1.0 - offsetFraction))
                
                transition.setBounds(view: self.headerOffsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: headerOffset), size: self.headerOffsetContainer.bounds.size))
            }
            
            let _ = self.panelContainer.updateEnvironment(
                transition: transition,
                environment: {
                    StorageUsagePanelContainerEnvironment(isScrollable: isLockedAtPanels)
                }
            )
        }
        
        func update(component: StorageUsageScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: Transition) -> CGSize {
            self.component = component
            self.state = state
            
            let environment = environment[ViewControllerComponentContainer.Environment.self].value
            
            if self.aggregatedData == nil {
                let loadingView: UIActivityIndicatorView
                if let current = self.loadingView {
                    loadingView = current
                } else {
                    let style: UIActivityIndicatorView.Style
                    if environment.theme.overallDarkAppearance {
                        style = .whiteLarge
                    } else {
                        if #available(iOS 13.0, *) {
                            style = .large
                        } else {
                            style = .gray
                        }
                    }
                    loadingView = UIActivityIndicatorView(style: style)
                    self.loadingView = loadingView
                    loadingView.sizeToFit()
                    self.insertSubview(loadingView, belowSubview: self.scrollView)
                }
                let loadingViewSize = loadingView.bounds.size
                transition.setFrame(view: loadingView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - loadingViewSize.width) / 2.0), y: floor((availableSize.height - loadingViewSize.height) / 2.0)), size: loadingViewSize))
                if !loadingView.isAnimating {
                    loadingView.startAnimating()
                }
            } else {
                if let loadingView = self.loadingView {
                    self.loadingView = nil
                    if environment.isVisible {
                        loadingView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak loadingView] _ in
                            loadingView?.removeFromSuperview()
                        })
                    } else {
                        loadingView.removeFromSuperview()
                    }
                }
            }
            
            if self.statsDisposable == nil {
                let context = component.context
                let viewKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.accountSpecificCacheStorageSettings]))
                let cacheSettingsExceptionCount: Signal<[CacheStorageSettings.PeerStorageCategory: Int32], NoError> = component.context.account.postbox.combinedView(keys: [viewKey])
                |> map { views -> AccountSpecificCacheStorageSettings in
                    let cacheSettings: AccountSpecificCacheStorageSettings
                    if let view = views.views[viewKey] as? PreferencesView, let value = view.values[PreferencesKeys.accountSpecificCacheStorageSettings]?.get(AccountSpecificCacheStorageSettings.self) {
                        cacheSettings = value
                    } else {
                        cacheSettings = AccountSpecificCacheStorageSettings.defaultSettings
                    }
                    
                    return cacheSettings
                }
                |> distinctUntilChanged
                |> mapToSignal { accountSpecificSettings -> Signal<[CacheStorageSettings.PeerStorageCategory: Int32], NoError> in
                    return context.engine.data.get(
                        EngineDataMap(accountSpecificSettings.peerStorageTimeoutExceptions.map(\.key).map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:)))
                    )
                    |> map { peers -> [CacheStorageSettings.PeerStorageCategory: Int32] in
                        var result: [CacheStorageSettings.PeerStorageCategory: Int32] = [:]
                        
                        for (_, peer) in peers {
                            guard let peer else {
                                continue
                            }
                            switch peer {
                            case .user, .secretChat:
                                result[.privateChats, default: 0] += 1
                            case .legacyGroup:
                                result[.groups, default: 0] += 1
                            case let .channel(channel):
                                if case .group = channel.info {
                                    result[.groups, default: 0] += 1
                                } else {
                                    result[.channels, default: 0] += 1
                                }
                            }
                        }
                        
                        return result
                    }
                }
                
                self.cacheSettingsDisposable = (combineLatest(queue: .mainQueue(),
                    component.context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.cacheStorageSettings])
                    |> map { sharedData -> CacheStorageSettings in
                        let cacheSettings: CacheStorageSettings
                        if let value = sharedData.entries[SharedDataKeys.cacheStorageSettings]?.get(CacheStorageSettings.self) {
                            cacheSettings = value
                        } else {
                            cacheSettings = CacheStorageSettings.defaultSettings
                        }
                        
                        return cacheSettings
                    },
                    cacheSettingsExceptionCount
                )
                |> deliverOnMainQueue).start(next: { [weak self] cacheSettings, cacheSettingsExceptionCount in
                    guard let self else {
                        return
                    }
                    self.cacheSettings = cacheSettings
                    self.cacheSettingsExceptionCount = cacheSettingsExceptionCount
                    if self.aggregatedData != nil {
                        self.state?.updated(transition: .immediate)
                    }
                })
                
                self.reloadStats(firstTime: true, completion: {})
            }
            
            var wasLockedAtPanels = false
            if let panelContainerView = self.panelContainer.view, let navigationMetrics = self.navigationMetrics {
                if self.scrollView.bounds.minY > 0.0 && abs(self.scrollView.bounds.minY - (panelContainerView.frame.minY - navigationMetrics.navigationHeight)) <= UIScreenPixel {
                    wasLockedAtPanels = true
                }
            }
            
            let animationHint = transition.userData(AnimationHint.self)
            
            if let animationHint {
                if case .firstStatsUpdate = animationHint.value {
                    let alphaTransition: Transition
                    if environment.isVisible {
                        alphaTransition = .easeInOut(duration: 0.25)
                    } else {
                        alphaTransition = .immediate
                    }
                    alphaTransition.setAlpha(view: self.scrollView, alpha: self.aggregatedData != nil ? 1.0 : 0.0)
                    alphaTransition.setAlpha(view: self.headerOffsetContainer, alpha: self.aggregatedData != nil ? 1.0 : 0.0)
                } else if case .clearedItems = animationHint.value {
                    if let snapshotView = self.scrollContainerView.snapshotView(afterScreenUpdates: false) {
                        snapshotView.frame = self.scrollContainerView.frame
                        self.scrollView.insertSubview(snapshotView, aboveSubview: self.scrollContainerView)
                        self.scrollContainerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
                        snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak snapshotView] _ in
                            snapshotView?.removeFromSuperview()
                        })
                    }
                }
            } else {
                transition.setAlpha(view: self.scrollView, alpha: self.aggregatedData != nil ? 1.0 : 0.0)
                transition.setAlpha(view: self.headerOffsetContainer, alpha: self.aggregatedData != nil ? 1.0 : 0.0)
            }
            
            self.controller = environment.controller
            
            self.navigationMetrics = (environment.navigationHeight, environment.statusBarHeight)
            
            self.navigationSeparatorLayer.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor
            
            let navigationFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: environment.navigationHeight))
            self.navigationBackgroundView.updateColor(color: environment.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate)
            self.navigationBackgroundView.update(size: navigationFrame.size, transition: transition.containedViewLayoutTransition)
            transition.setFrame(view: self.navigationBackgroundView, frame: navigationFrame)
            
            let navigationSeparatorFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationFrame.maxY), size: CGSize(width: availableSize.width, height: UIScreenPixel))
            
            transition.setFrame(layer: self.navigationSeparatorLayerContainer, frame: navigationSeparatorFrame)
            transition.setFrame(layer: self.navigationSeparatorLayer, frame: CGRect(origin: CGPoint(), size: navigationSeparatorFrame.size))
            
            let navigationEditButtonSize = self.navigationEditButton.update(
                transition: transition,
                component: AnyComponent(Button(
                    content: AnyComponent(Text(text: environment.strings.Common_Edit, font: Font.regular(17.0), color: environment.theme.rootController.navigationBar.accentTextColor)),
                    action: { [weak self] in
                        guard let self else {
                            return
                        }
                        if let aggregatedData = self.aggregatedData, !aggregatedData.isSelectingPeers {
                            aggregatedData.isSelectingPeers = true
                            self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
                        }
                    }
                ).minSize(CGSize(width: 16.0, height: environment.navigationHeight - environment.statusBarHeight))),
                environment: {},
                containerSize: CGSize(width: 150.0, height: environment.navigationHeight - environment.statusBarHeight)
            )
            if let navigationEditButtonView = self.navigationEditButton.view {
                if navigationEditButtonView.superview == nil {
                    self.addSubview(navigationEditButtonView)
                }
                transition.setFrame(view: navigationEditButtonView, frame: CGRect(origin: CGPoint(x: availableSize.width - 12.0 - environment.safeInsets.right - navigationEditButtonSize.width, y: environment.statusBarHeight), size: navigationEditButtonSize))
            }
            
            let navigationDoneButtonSize = self.navigationDoneButton.update(
                transition: transition,
                component: AnyComponent(Button(
                    content: AnyComponent(Text(text: environment.strings.Common_Done, font: Font.semibold(17.0), color: environment.theme.rootController.navigationBar.accentTextColor)),
                    action: { [weak self] in
                        guard let self, let aggregatedData = self.aggregatedData else {
                            return
                        }
                        aggregatedData.isSelectingPeers = false
                        aggregatedData.clearPeerSelection()
                        self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
                    }
                ).minSize(CGSize(width: 16.0, height: environment.navigationHeight - environment.statusBarHeight))),
                environment: {},
                containerSize: CGSize(width: 150.0, height: environment.navigationHeight - environment.statusBarHeight)
            )
            if let navigationDoneButtonView = self.navigationDoneButton.view {
                if navigationDoneButtonView.superview == nil {
                    self.addSubview(navigationDoneButtonView)
                }
                transition.setFrame(view: navigationDoneButtonView, frame: CGRect(origin: CGPoint(x: availableSize.width - 12.0 - environment.safeInsets.right - navigationDoneButtonSize.width, y: environment.statusBarHeight), size: navigationDoneButtonSize))
            }
            
            let navigationRightButtonMaxWidth: CGFloat = max(navigationEditButtonSize.width, navigationDoneButtonSize.width)
            
            self.backgroundColor = environment.theme.list.blocksBackgroundColor
            
            var contentHeight: CGFloat = 0.0
            
            let topInset: CGFloat = 19.0
            let sideInset: CGFloat = 16.0 + environment.safeInsets.left
            
            var bottomInset: CGFloat = environment.safeInsets.bottom
            
            var bottomPanelSelectionData: (size: Int64, isComplete: Bool)?
            if let aggregatedData = self.aggregatedData {
                if let _ = component.peer {
                    bottomPanelSelectionData = (aggregatedData.selectedSize, true)
                } else if !aggregatedData.selectionState.isEmpty {
                    bottomPanelSelectionData = (aggregatedData.selectedSize, false)
                }
            }
            
            if let bottomPanelSelectionData {
                let selectionPanel: ComponentView<Empty>
                var selectionPanelTransition = transition
                if let current = self.selectionPanel {
                    selectionPanel = current
                } else {
                    selectionPanelTransition = .immediate
                    selectionPanel = ComponentView()
                    self.selectionPanel = selectionPanel
                }
                
                let selectionPanelSize = selectionPanel.update(
                    transition: selectionPanelTransition,
                    component: AnyComponent(BottomButtonPanelComponent(
                        theme: environment.theme,
                        title: bottomPanelSelectionData.isComplete ? environment.strings.StorageManagement_ClearCache : environment.strings.StorageManagement_ClearSelected,
                        label: bottomPanelSelectionData.size == 0 ? nil : dataSizeString(Int(bottomPanelSelectionData.size), formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: ".")),
                        isEnabled: bottomPanelSelectionData.size != 0,
                        insets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: environment.safeInsets.bottom, right: sideInset),
                        action: { [weak self] in
                            guard let self else {
                                return
                            }
                            self.requestClear(fromCategories: false)
                        }
                    )),
                    environment: {},
                    containerSize: availableSize
                )
                if let selectionPanelView = selectionPanel.view {
                    var animateIn = false
                    if selectionPanelView.superview == nil {
                        self.addSubview(selectionPanelView)
                        animateIn = true
                    }
                    selectionPanelTransition.setFrame(view: selectionPanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - selectionPanelSize.height), size: selectionPanelSize))
                    if animateIn {
                        transition.animatePosition(view: selectionPanelView, from: CGPoint(x: 0.0, y: selectionPanelSize.height), to: CGPoint(), additive: true)
                    }
                }
                bottomInset = selectionPanelSize.height
            } else if let selectionPanel = self.selectionPanel {
                self.selectionPanel = nil
                if let selectionPanelView = selectionPanel.view {
                    transition.setPosition(view: selectionPanelView, position: CGPoint(x: selectionPanelView.center.x, y: availableSize.height + selectionPanelView.bounds.height * 0.5), completion: { [weak selectionPanelView] _ in
                        selectionPanelView?.removeFromSuperview()
                    })
                }
            }
            
            contentHeight += environment.statusBarHeight + topInset
            
            let allCategories: [Category] = [
                .photos,
                .videos,
                .files,
                .music,
                .stickers,
                .avatars,
                .misc,
                .stories
            ]
            
            var listCategories: [StorageCategoriesComponent.CategoryData] = []
            
            var totalSize: Int64 = 0
            var totalSelectedCategorySize: Int64 = 0
            if let aggregatedData = self.aggregatedData {
                for (key, value) in aggregatedData.contextStats.categories {
                    totalSize += value.size
                    if aggregatedData.selectedCategories.isEmpty || aggregatedData.selectedCategories.contains(Category(key)) {
                        totalSelectedCategorySize += value.size
                    }
                }
                
                for category in allCategories {
                    let mappedCategory: StorageUsageStats.CategoryKey
                    switch category {
                    case .photos:
                        mappedCategory = .photos
                    case .videos:
                        mappedCategory = .videos
                    case .files:
                        mappedCategory = .files
                    case .music:
                        mappedCategory = .music
                    case .stickers:
                        mappedCategory = .stickers
                    case .avatars:
                        mappedCategory = .avatars
                    case .misc:
                        mappedCategory = .misc
                    case .stories:
                        mappedCategory = .stories
                    case .other:
                        continue
                    }
                    
                    var categorySize: Int64 = 0
                    if let categoryData = aggregatedData.contextStats.categories[mappedCategory] {
                        categorySize = categoryData.size
                    }
                    
                    let categoryFraction: Double
                    if !aggregatedData.selectedCategories.isEmpty && !aggregatedData.selectedCategories.contains(category) {
                        categoryFraction = 0.0
                    } else if categorySize == 0 || totalSelectedCategorySize == 0 {
                        categoryFraction = 0.0
                    } else {
                        categoryFraction = Double(categorySize) / Double(totalSelectedCategorySize)
                    }
                    
                    if categorySize != 0 {
                        listCategories.append(StorageCategoriesComponent.CategoryData(
                            key: category, color: category.color, title: category.title(strings: environment.strings), size: categorySize, sizeFraction: categoryFraction, isSelected: aggregatedData.selectedCategories.contains(category), subcategories: []))
                    }
                }
            }
            
            listCategories.sort(by: { $0.size > $1.size })
            
            var otherListCategories: [StorageCategoriesComponent.CategoryData] = []
            if listCategories.count > 5 {
                for i in (4 ..< listCategories.count).reversed() {
                    otherListCategories.insert(listCategories[i], at: 0)
                    listCategories.remove(at: i)
                }
            }
            self.otherCategories = Set(otherListCategories.map(\.key))
            
            var chartItems: [PieChartComponent.ChartData.Item] = []
            for listCategory in listCategories {
                var categoryChartFraction: CGFloat = listCategory.sizeFraction
                if let aggregatedData = self.aggregatedData, !aggregatedData.selectedCategories.isEmpty && !aggregatedData.selectedCategories.contains(listCategory.key) {
                    categoryChartFraction = 0.0
                }
                chartItems.append(PieChartComponent.ChartData.Item(id: listCategory.key, displayValue: listCategory.sizeFraction, displaySize: listCategory.size, value: categoryChartFraction, color: listCategory.color, particle: listCategory.key.particle, title: listCategory.key.title(strings: environment.strings), mergeable: false, mergeFactor: 1.0))
            }
            
            var totalOtherSize: Int64 = 0
            for listCategory in otherListCategories {
                totalOtherSize += listCategory.size
            }
            
            if !otherListCategories.isEmpty {
                let categoryFraction: Double = otherListCategories.reduce(0.0, { $0 + $1.sizeFraction })
                let isSelected = otherListCategories.allSatisfy { item in
                    return self.aggregatedData?.selectedCategories.contains(item.key) ?? false
                }
                
                let listColor: UIColor
                if self.isOtherCategoryExpanded {
                    listColor = Category.other.color
                } else {
                    listColor = Category.misc.color
                }
                
                listCategories.append(StorageCategoriesComponent.CategoryData(
                    key: Category.other, color: listColor, title: Category.other.title(strings: environment.strings), size: totalOtherSize, sizeFraction: categoryFraction, isSelected: isSelected, subcategories: otherListCategories))
            }
            
            var otherSum: CGFloat = 0.0
            var otherRealSum: CGFloat = 0.0
            for listCategory in otherListCategories {
                var categoryChartFraction: CGFloat = listCategory.sizeFraction
                if let aggregatedData = self.aggregatedData, !aggregatedData.selectedCategories.isEmpty, !aggregatedData.selectedCategories.contains(listCategory.key) {
                    categoryChartFraction = 0.0
                }
                
                var chartItem = PieChartComponent.ChartData.Item(id: listCategory.key, displayValue: listCategory.sizeFraction, displaySize: listCategory.size, value: categoryChartFraction, color: listCategory.color, particle: listCategory.key.particle, title: listCategory.key.title(strings: environment.strings), mergeable: false, mergeFactor: 1.0)
                
                if chartItem.value > 0.00001 {
                    chartItem.value = max(chartItem.value, 0.01)
                }
                otherSum += chartItem.value
                otherRealSum += chartItem.displayValue
                
                if !self.isOtherCategoryExpanded {
                    chartItem.value = 0.0
                }
                
                chartItems.append(chartItem)
            }
            
            if !listCategories.isEmpty {
                chartItems.append(PieChartComponent.ChartData.Item(id: AnyHashable(Category.other), displayValue: otherRealSum, displaySize: totalOtherSize, value: self.isOtherCategoryExpanded ? 0.0 : otherSum, color: Category.misc.color, particle: Category.misc.particle, title: Category.misc.title(strings: environment.strings), mergeable: false, mergeFactor: 1.0))
            }
            
            let chartData = PieChartComponent.ChartData(items: chartItems)
            self.pieChartView.parentState = state
            
            var pieChartTransition = transition
            if transition.animation.isImmediate, let animationHint, case .clearedItems = animationHint.value {
                pieChartTransition = Transition(animation: .curve(duration: 0.4, curve: .spring))
            }
            
            let pieChartSize = self.pieChartView.update(
                transition: pieChartTransition,
                component: AnyComponent(PieChartComponent(
                    theme: environment.theme,
                    strings: environment.strings,
                    emptyColor: UIColor(rgb: 0x34C759),
                    chartData: chartData
                )),
                environment: {},
                containerSize: CGSize(width: availableSize.width, height: 60.0)
            )
            let pieChartFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: pieChartSize)
            if let pieChartComponentView = self.pieChartView.view {
                if pieChartComponentView.superview == nil {
                    self.scrollView.addSubview(pieChartComponentView)
                }
                
                pieChartTransition.setFrame(view: pieChartComponentView, frame: pieChartFrame)
            }
            if let _ = self.aggregatedData, listCategories.isEmpty {
                let checkColor = UIColor(rgb: 0x34C759)
                
                let doneStatusNode: RadialStatusNode
                var animateIn = false
                if let current = self.doneStatusNode {
                    doneStatusNode = current
                } else {
                    doneStatusNode = RadialStatusNode(backgroundNodeColor: .clear)
                    self.doneStatusNode = doneStatusNode
                    self.scrollView.addSubnode(doneStatusNode)
                    animateIn = true
                }
                let doneSize = CGSize(width: 100.0, height: 100.0)
                doneStatusNode.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - doneSize.width) / 2.0), y: contentHeight), size: doneSize)
                
                let doneStatusCircle: SimpleShapeLayer
                if let current = self.doneStatusCircle {
                    doneStatusCircle = current
                } else {
                    doneStatusCircle = SimpleShapeLayer()
                    self.doneStatusCircle = doneStatusCircle
                    //self.scrollView.layer.addSublayer(doneStatusCircle)
                    doneStatusCircle.opacity = 0.0
                }
                
                if animateIn {
                    Queue.mainQueue().after(0.18, {
                        doneStatusNode.transitionToState(.check(checkColor), animated: true)
                        doneStatusCircle.opacity = 1.0
                        doneStatusCircle.animateAlpha(from: 0.0, to: 1.0, duration: 0.12)
                    })
                }
                
                doneStatusCircle.lineWidth = 6.0
                doneStatusCircle.strokeColor = checkColor.cgColor
                doneStatusCircle.fillColor = nil
                doneStatusCircle.path = UIBezierPath(ovalIn: CGRect(origin: CGPoint(x: doneStatusCircle.lineWidth * 0.5, y: doneStatusCircle.lineWidth * 0.5), size: CGSize(width: doneSize.width - doneStatusCircle.lineWidth * 0.5, height: doneSize.height - doneStatusCircle.lineWidth * 0.5))).cgPath
                
                doneStatusCircle.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - doneSize.width) / 2.0), y: contentHeight), size: doneSize).insetBy(dx: -doneStatusCircle.lineWidth * 0.5, dy: -doneStatusCircle.lineWidth * 0.5)
                
                contentHeight += doneSize.height
            } else {
                contentHeight += pieChartSize.height
                
                if let doneStatusNode = self.doneStatusNode {
                    self.doneStatusNode = nil
                    doneStatusNode.removeFromSupernode()
                }
                if let doneStatusCircle = self.doneStatusCircle {
                    self.doneStatusCircle = nil
                    doneStatusCircle.removeFromSuperlayer()
                }
            }
            
            contentHeight += 23.0
            
            let headerText: String
            if listCategories.isEmpty {
                headerText = environment.strings.StorageManagement_TitleCleared
            } else if let peer = component.peer {
                if peer.id == component.context.account.peerId {
                    headerText = environment.strings.DialogList_SavedMessages
                } else {
                    headerText = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast)
                }
            } else {
                headerText = environment.strings.StorageManagement_Title
            }
            let headerViewSize = self.headerView.update(
                transition: transition,
                component: AnyComponent(Text(text: headerText, font: Font.semibold(20.0), color: environment.theme.list.itemPrimaryTextColor)),
                environment: {},
                containerSize: CGSize(width: floor((availableSize.width - navigationRightButtonMaxWidth * 2.0) / 0.8), height: 100.0)
            )
            let headerViewFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - headerViewSize.width) / 2.0), y: contentHeight), size: headerViewSize)
            if let headerComponentView = self.headerView.view {
                if headerComponentView.superview == nil {
                    self.headerOffsetContainer.addSubview(headerComponentView)
                }
                transition.setPosition(view: headerComponentView, position: headerViewFrame.center)
                transition.setBounds(view: headerComponentView, bounds: CGRect(origin: CGPoint(), size: headerViewFrame.size))
            }
            contentHeight += headerViewSize.height
            
            contentHeight += 6.0
            
            let body = MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.freeTextColor)
            let bold = MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.freeTextColor)
            
            var usageFraction: Double = 0.0
            let totalUsageText: String
            if listCategories.isEmpty {
                totalUsageText = environment.strings.StorageManagement_DescriptionCleared
            } else if let aggregatedData = self.aggregatedData {
                var totalStatsSize: Int64 = 0
                for (_, value) in aggregatedData.contextStats.categories {
                    totalStatsSize += value.size
                }
                
                if let _ = component.peer {
                    var allStatsSize: Int64 = 0
                    for (_, value) in aggregatedData.stats.totalStats.categories {
                        allStatsSize += value.size
                    }
                    
                    let fraction: Double
                    if allStatsSize != 0 {
                        fraction = Double(totalStatsSize) / Double(allStatsSize)
                    } else {
                        fraction = 0.0
                    }
                    usageFraction = fraction
                    let fractionValue: Double = floor(fraction * 100.0 * 10.0) / 10.0
                    let fractionString: String
                    if fractionValue < 0.1 {
                        fractionString = "<0.1"
                    } else if abs(Double(Int(fractionValue)) - fractionValue) < 0.001 {
                        fractionString = "\(Int(fractionValue))"
                    } else {
                        fractionString = "\(fractionValue)"
                    }
                        
                    totalUsageText = environment.strings.StorageManagement_DescriptionChatUsage(fractionString).string
                } else {
                    let fraction: Double
                    if aggregatedData.stats.deviceFreeSpace != 0 && totalStatsSize != 0 {
                        fraction = Double(totalStatsSize) / Double(aggregatedData.stats.deviceFreeSpace + totalStatsSize)
                    } else {
                        fraction = 0.0
                    }
                    usageFraction = fraction
                    let fractionValue: Double = floor(fraction * 100.0 * 10.0) / 10.0
                    let fractionString: String
                    if fractionValue < 0.1 {
                        fractionString = "<0.1"
                    } else if abs(Double(Int(fractionValue)) - fractionValue) < 0.001 {
                        fractionString = "\(Int(fractionValue))"
                    } else {
                        fractionString = "\(fractionValue)"
                    }
                        
                    totalUsageText = environment.strings.StorageManagement_DescriptionAppUsage(fractionString).string
                }
            } else {
                totalUsageText = " "
            }
            let headerDescriptionSize = self.headerDescriptionView.update(
                transition: transition,
                component: AnyComponent(MultilineTextComponent(text: .markdown(text: totalUsageText, attributes: MarkdownAttributes(
                    body: body,
                    bold: bold,
                    link: body,
                    linkAttribute: { _ in nil }
                )), horizontalAlignment: .center, maximumNumberOfLines: 0)),
                environment: {},
                containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 15.0 * 2.0, height: 10000.0)
            )
            let headerDescriptionFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - headerDescriptionSize.width) / 2.0), y: contentHeight), size: headerDescriptionSize)
            if let headerDescriptionComponentView = self.headerDescriptionView.view {
                if headerDescriptionComponentView.superview == nil {
                    self.scrollContainerView.addSubview(headerDescriptionComponentView)
                }
                transition.setFrame(view: headerDescriptionComponentView, frame: headerDescriptionFrame)
            }
            contentHeight += headerDescriptionSize.height
            contentHeight += 8.0
            
            let headerProgressWidth: CGFloat = min(200.0, availableSize.width - sideInset * 2.0)
            let headerProgressFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - headerProgressWidth) / 2.0), y: contentHeight), size: CGSize(width: headerProgressWidth, height: 4.0))
            transition.setFrame(layer: self.headerProgressBackgroundLayer, frame: headerProgressFrame)
            transition.setCornerRadius(layer: self.headerProgressBackgroundLayer, cornerRadius: headerProgressFrame.height * 0.5)
            self.headerProgressBackgroundLayer.backgroundColor = environment.theme.list.itemAccentColor.withMultipliedAlpha(0.2).cgColor
            
            let headerProgress: CGFloat = usageFraction
            transition.setFrame(layer: self.headerProgressForegroundLayer, frame: CGRect(origin: headerProgressFrame.origin, size: CGSize(width: max(headerProgressFrame.height, floorToScreenPixels(headerProgress * headerProgressFrame.width)), height: headerProgressFrame.height)))
            transition.setCornerRadius(layer: self.headerProgressForegroundLayer, cornerRadius: headerProgressFrame.height * 0.5)
            self.headerProgressForegroundLayer.backgroundColor = environment.theme.list.itemAccentColor.cgColor
            contentHeight += 4.0
            
            transition.setAlpha(layer: self.headerProgressBackgroundLayer, alpha: listCategories.isEmpty ? 0.0 : 1.0)
            transition.setAlpha(layer: self.headerProgressForegroundLayer, alpha: listCategories.isEmpty ? 0.0 : 1.0)
            
            contentHeight += 24.0
            
            if let peer = component.peer {
                let avatarSize = CGSize(width: 72.0, height: 72.0)
                let avatarFrame: CGRect = CGRect(origin: CGPoint(x: pieChartFrame.minX + floor((pieChartFrame.width - avatarSize.width) / 2.0), y: pieChartFrame.minY + floor((pieChartFrame.height - avatarSize.height) / 2.0)), size: avatarSize)
                
                let chartAvatarNode: AvatarNode
                if let current = self.chartAvatarNode {
                    chartAvatarNode = current
                    transition.setFrame(view: chartAvatarNode.view, frame: avatarFrame)
                } else {
                    chartAvatarNode = AvatarNode(font: avatarPlaceholderFont(size: 17.0))
                    self.chartAvatarNode = chartAvatarNode
                    self.scrollContainerView.addSubview(chartAvatarNode.view)
                    chartAvatarNode.frame = avatarFrame
                    
                    if peer.id == component.context.account.peerId {
                        chartAvatarNode.setPeer(context: component.context, theme: environment.theme, peer: peer, overrideImage: .savedMessagesIcon, displayDimensions: avatarSize)
                    } else {
                        chartAvatarNode.setPeer(context: component.context, theme: environment.theme, peer: peer, displayDimensions: avatarSize)
                    }
                }
                transition.setAlpha(view: chartAvatarNode.view, alpha: listCategories.isEmpty ? 0.0 : 1.0)
            } else {
                let sizeText = dataSizeString(Int(totalSelectedCategorySize), forceDecimal: true, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: "."))
                
                var animatedTextItems: [AnimatedTextComponent.Item] = []
                var remainingSizeText = sizeText
                if let index = remainingSizeText.firstIndex(of: ".") {
                    animatedTextItems.append(AnimatedTextComponent.Item(id: "n-full", content: .text(String(remainingSizeText[remainingSizeText.startIndex ..< index]))))
                    animatedTextItems.append(AnimatedTextComponent.Item(id: "dot", content: .text(".")))
                    remainingSizeText = String(remainingSizeText[remainingSizeText.index(after: index)...])
                }
                if let index = remainingSizeText.firstIndex(of: " ") {
                    animatedTextItems.append(AnimatedTextComponent.Item(id: "n-fract", content: .text(String(remainingSizeText[remainingSizeText.startIndex ..< index]))))
                    remainingSizeText = String(remainingSizeText[index...])
                }
                if !remainingSizeText.isEmpty {
                    animatedTextItems.append(AnimatedTextComponent.Item(id: "rest", isUnbreakable: true, content: .text(remainingSizeText)))
                }
                
                let chartTotalLabelSize = self.chartTotalLabel.update(
                    transition: transition,
                    /*component: AnyComponent(Text(
                        text: dataSizeString(Int(totalSelectedCategorySize), formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: ".")),
                        font: Font.with(size: 20.0, design: .round, weight: .bold), color: environment.theme.list.itemPrimaryTextColor
                    )),*/
                    component: AnyComponent(AnimatedTextComponent(
                        font: Font.with(size: 20.0, design: .round, weight: .bold),
                        color: environment.theme.list.itemPrimaryTextColor,
                        items: animatedTextItems
                    )),
                    environment: {},
                    containerSize: CGSize(width: 200.0, height: 200.0)
                )
                if let chartTotalLabelView = self.chartTotalLabel.view {
                    if chartTotalLabelView.superview == nil {
                        self.scrollContainerView.addSubview(chartTotalLabelView)
                    }
                    let totalLabelFrame = CGRect(origin: CGPoint(x: pieChartFrame.minX + floor((pieChartFrame.width - chartTotalLabelSize.width) / 2.0), y: pieChartFrame.minY + floor((pieChartFrame.height - chartTotalLabelSize.height) / 2.0)), size: chartTotalLabelSize)
                    transition.setFrame(view: chartTotalLabelView, frame: totalLabelFrame)
                    transition.setAlpha(view: chartTotalLabelView, alpha: listCategories.isEmpty ? 0.0 : 1.0)
                }
            }
            
            if !listCategories.isEmpty {
                self.categoriesView.parentState = state
                let categoriesSize = self.categoriesView.update(
                    transition: transition,
                    component: AnyComponent(StorageCategoriesComponent(
                        theme: environment.theme,
                        strings: environment.strings,
                        categories: listCategories,
                        isOtherExpanded: self.isOtherCategoryExpanded,
                        displayAction: component.peer == nil,
                        toggleCategorySelection: { [weak self] key in
                            guard let self, let aggregatedData = self.aggregatedData else {
                                return
                            }
                            if key == Category.other {
                                let otherCategories = self.otherCategories.filter(aggregatedData.existingCategories.contains)
                                if !otherCategories.isEmpty {
                                    if otherCategories.allSatisfy(aggregatedData.selectedCategories.contains) {
                                        for item in otherCategories {
                                            aggregatedData.setIsCategorySelected(category: item, isSelected: false)
                                        }
                                    } else {
                                        for item in otherCategories {
                                            aggregatedData.setIsCategorySelected(category: item, isSelected: true)
                                        }
                                    }
                                }
                            } else {
                                if aggregatedData.selectedCategories.contains(key) {
                                    aggregatedData.setIsCategorySelected(category: key, isSelected: false)
                                } else {
                                    aggregatedData.setIsCategorySelected(category: key, isSelected: true)
                                }
                            }
                            self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
                        },
                        toggleOtherExpanded: { [weak self] in
                            guard let self else {
                                return
                            }
                            
                            self.isOtherCategoryExpanded = !self.isOtherCategoryExpanded
                            self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
                        },
                        clearAction: { [weak self] in
                            guard let self else {
                                return
                            }
                            self.requestClear(fromCategories: true)
                        }
                    )),
                    environment: {},
                    containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude)
                )
                if let categoriesComponentView = self.categoriesView.view {
                    if categoriesComponentView.superview == nil {
                        self.scrollContainerView.addSubview(categoriesComponentView)
                    }
                    
                    transition.setFrame(view: categoriesComponentView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: categoriesSize))
                }
                contentHeight += categoriesSize.height
                contentHeight += 8.0
                
                
                let categoriesDescriptionSize = self.categoriesDescriptionView.update(
                    transition: transition,
                    component: AnyComponent(MultilineTextComponent(text: .markdown(text: environment.strings.StorageManagement_SectionsDescription, attributes: MarkdownAttributes(
                        body: body,
                        bold: bold,
                        link: body,
                        linkAttribute: { _ in nil }
                    )), maximumNumberOfLines: 0)),
                    environment: {},
                    containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 15.0 * 2.0, height: 10000.0)
                )
                let categoriesDescriptionFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: categoriesDescriptionSize)
                if let categoriesDescriptionComponentView = self.categoriesDescriptionView.view {
                    if categoriesDescriptionComponentView.superview == nil {
                        self.scrollContainerView.addSubview(categoriesDescriptionComponentView)
                    }
                    transition.setFrame(view: categoriesDescriptionComponentView, frame: categoriesDescriptionFrame)
                }
                contentHeight += categoriesDescriptionSize.height
                contentHeight += 40.0
            } else {
                self.categoriesView.view?.removeFromSuperview()
                self.categoriesDescriptionView.view?.removeFromSuperview()
            }
            
            if component.peer == nil {
                let keepDurationTitleSize = self.keepDurationTitleView.update(
                    transition: transition,
                    component: AnyComponent(MultilineTextComponent(
                        text: .markdown(
                            text: environment.strings.StorageManagement_AutoremoveHeader, attributes: MarkdownAttributes(
                                body: body,
                                bold: bold,
                                link: body,
                                linkAttribute: { _ in nil }
                            )
                        ),
                        maximumNumberOfLines: 0
                    )),
                    environment: {},
                    containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 15.0 * 2.0, height: 10000.0)
                )
                let keepDurationTitleFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: keepDurationTitleSize)
                if let keepDurationTitleComponentView = self.keepDurationTitleView.view {
                    if keepDurationTitleComponentView.superview == nil {
                        self.scrollContainerView.addSubview(keepDurationTitleComponentView)
                    }
                    transition.setFrame(view: keepDurationTitleComponentView, frame: keepDurationTitleFrame)
                }
                contentHeight += keepDurationTitleSize.height
                contentHeight += 8.0
                
                var keepContentHeight: CGFloat = 0.0
                for i in 0 ..< 4 {
                    let item: ComponentView<Empty>
                    if let current = self.keepDurationItems[i] {
                        item = current
                    } else {
                        item = ComponentView<Empty>()
                        self.keepDurationItems[i] = item
                    }
                    
                    let mappedCategory: CacheStorageSettings.PeerStorageCategory
                    
                    let iconName: String
                    let title: String
                    switch i {
                    case 0:
                        iconName = "Settings/Menu/EditProfile"
                        title = environment.strings.Notifications_PrivateChats
                        mappedCategory = .privateChats
                    case 1:
                        iconName = "Settings/Menu/GroupChats"
                        title = environment.strings.Notifications_GroupChats
                        mappedCategory = .groups
                    case 3:
                        iconName = "Settings/Menu/Stories"
                        title = environment.strings.Notifications_Stories
                        mappedCategory = .stories
                    default:
                        iconName = "Settings/Menu/Channels"
                        title = environment.strings.Notifications_Channels
                        mappedCategory = .channels
                    }
                    
                    let value = self.cacheSettings?.categoryStorageTimeout[mappedCategory] ?? Int32.max
                    let optionText: String
                    if value == Int32.max {
                        optionText = environment.strings.ClearCache_Never
                    } else {
                        optionText = timeIntervalString(strings: environment.strings, value: value)
                    }
                    
                    var subtitle: String?
                    if mappedCategory != .stories {
                        if let cacheSettingsExceptionCount = self.cacheSettingsExceptionCount, let categoryCount = cacheSettingsExceptionCount[mappedCategory] {
                            subtitle = environment.strings.CacheEvictionMenu_CategoryExceptions(Int32(categoryCount))
                        }
                    }
                    
                    let itemSize = item.update(
                        transition: transition,
                        component: AnyComponent(StoragePeerTypeItemComponent(
                            theme: environment.theme,
                            iconName: iconName,
                            title: title,
                            subtitle: subtitle,
                            value: optionText,
                            hasNext: i != 4 - 1,
                            action: { [weak self] sourceView in
                                guard let self else {
                                    return
                                }
                                self.openKeepMediaCategory(mappedCategory: mappedCategory, sourceView: sourceView)
                            }
                        )),
                        environment: {},
                        containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
                    )
                    let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: keepContentHeight), size: itemSize)
                    if let itemView = item.view {
                        if itemView.superview == nil {
                            self.keepDurationSectionContainerView.addSubview(itemView)
                        }
                        transition.setFrame(view: itemView, frame: itemFrame)
                    }
                    keepContentHeight += itemSize.height
                }
                self.keepDurationSectionContainerView.backgroundColor = environment.theme.list.itemBlocksBackgroundColor
                transition.setFrame(view: self.keepDurationSectionContainerView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: keepContentHeight)))
                contentHeight += keepContentHeight
                contentHeight += 8.0
                
                let keepDurationDescriptionSize = self.keepDurationDescriptionView.update(
                    transition: transition,
                    component: AnyComponent(MultilineTextComponent(
                        text: .markdown(
                            text: environment.strings.StorageManagement_AutoremoveDescription, attributes: MarkdownAttributes(
                                body: body,
                                bold: bold,
                                link: body,
                                linkAttribute: { _ in nil }
                            )
                        ),
                        maximumNumberOfLines: 0
                    )),
                    environment: {},
                    containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 15.0 * 2.0, height: 10000.0)
                )
                let keepDurationDescriptionFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: keepDurationDescriptionSize)
                if let keepDurationDescriptionComponentView = self.keepDurationDescriptionView.view {
                    if keepDurationDescriptionComponentView.superview == nil {
                        self.scrollContainerView.addSubview(keepDurationDescriptionComponentView)
                    }
                    transition.setFrame(view: keepDurationDescriptionComponentView, frame: keepDurationDescriptionFrame)
                }
                contentHeight += keepDurationDescriptionSize.height
                contentHeight += 40.0
                
                let keepSizeTitleSize = self.keepSizeTitleView.update(
                    transition: transition,
                    component: AnyComponent(MultilineTextComponent(
                        text: .markdown(
                            text: environment.strings.Cache_MaximumCacheSize.uppercased(), attributes: MarkdownAttributes(
                                body: body,
                                bold: bold,
                                link: body,
                                linkAttribute: { _ in nil }
                            )
                        ),
                        maximumNumberOfLines: 0
                    )),
                    environment: {},
                    containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 15.0 * 2.0, height: 10000.0)
                )
                let keepSizeTitleFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: keepSizeTitleSize)
                if let keepSizeTitleComponentView = self.keepSizeTitleView.view {
                    if keepSizeTitleComponentView.superview == nil {
                        self.scrollContainerView.addSubview(keepSizeTitleComponentView)
                    }
                    transition.setFrame(view: keepSizeTitleComponentView, frame: keepSizeTitleFrame)
                }
                contentHeight += keepSizeTitleSize.height
                contentHeight += 8.0
                
                let keepSizeSize = self.keepSizeView.update(
                    transition: transition,
                    component: AnyComponent(StorageKeepSizeComponent(
                        theme: environment.theme,
                        strings: environment.strings,
                        value: cacheSettings?.defaultCacheStorageLimitGigabytes ?? 16,
                        updateValue: { [weak self] value in
                            guard let self, let component = self.component else {
                                return
                            }
                            let value = max(5, value)
                            let _ = updateCacheStorageSettingsInteractively(accountManager: component.context.sharedContext.accountManager, { current in
                                var current = current
                                current.defaultCacheStorageLimitGigabytes = value
                                return current
                            }).start()
                        }
                    )),
                    environment: {},
                    containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
                )
                let keepSizeFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: keepSizeSize)
                if let keepSizeComponentView = self.keepSizeView.view {
                    if keepSizeComponentView.superview == nil {
                        self.scrollContainerView.addSubview(keepSizeComponentView)
                    }
                    transition.setFrame(view: keepSizeComponentView, frame: keepSizeFrame)
                }
                contentHeight += keepSizeSize.height
                contentHeight += 8.0
                
                let keepSizeDescriptionSize = self.keepSizeDescriptionView.update(
                    transition: transition,
                    component: AnyComponent(MultilineTextComponent(
                        text: .markdown(
                            text: environment.strings.StorageManagement_AutoremoveSpaceDescription, attributes: MarkdownAttributes(
                                body: body,
                                bold: bold,
                                link: body,
                                linkAttribute: { _ in nil }
                            )
                        ),
                        maximumNumberOfLines: 0
                    )),
                    environment: {},
                    containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 15.0 * 2.0, height: 10000.0)
                )
                let keepSizeDescriptionFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: keepSizeDescriptionSize)
                if let keepSizeDescriptionComponentView = self.keepSizeDescriptionView.view {
                    if keepSizeDescriptionComponentView.superview == nil {
                        self.scrollContainerView.addSubview(keepSizeDescriptionComponentView)
                    }
                    transition.setFrame(view: keepSizeDescriptionComponentView, frame: keepSizeDescriptionFrame)
                }
                contentHeight += keepSizeDescriptionSize.height
                contentHeight += 40.0
            }
            
            var panelItems: [StorageUsagePanelContainerComponent.Item] = []
            if let aggregatedData = self.aggregatedData, let peerItems = aggregatedData.peerItems, !peerItems.items.isEmpty, !listCategories.isEmpty {
                panelItems.append(StorageUsagePanelContainerComponent.Item(
                    id: "peers",
                    title: environment.strings.StorageManagement_TabChats,
                    panel: AnyComponent(StoragePeerListPanelComponent(
                        context: component.context,
                        items: peerItems,
                        selectionState: aggregatedData.isSelectingPeers ? aggregatedData.selectionState : nil,
                        peerAction: { [weak self] peer in
                            guard let self, let aggregatedData = self.aggregatedData else {
                                return
                            }
                            if aggregatedData.isSelectingPeers {
                                aggregatedData.togglePeerSelection(id: peer.id)
                                
                                self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
                            } else {
                                self.openPeer(peer: peer)
                            }
                        },
                        contextAction: { [weak self] peer, sourceView, gesture in
                            guard let self, let component = self.component else {
                                return
                            }
                            
                            let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
                            
                            var itemList: [ContextMenuItem] = []
                            itemList.append(.action(ContextMenuActionItem(
                                text: presentationData.strings.StorageManagement_PeerShowDetails,
                                icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.contextMenu.primaryColor) },
                                action: { [weak self] c, _ in
                                    c.dismiss(completion: { [weak self] in
                                        guard let self else {
                                            return
                                        }
                                        self.openPeer(peer: peer)
                                    })
                                })
                            ))
                            itemList.append(.action(ContextMenuActionItem(
                                text: presentationData.strings.StorageManagement_PeerOpenProfile,
                                icon: { theme in
                                    if case .user = peer {
                                        return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/User"), color: theme.contextMenu.primaryColor)
                                    } else {
                                        return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Groups"), color: theme.contextMenu.primaryColor)
                                    }
                                },
                                action: { [weak self] c, _ in
                                    c.dismiss(completion: { [weak self] in
                                        guard let self, let component = self.component, let controller = self.controller?() else {
                                            return
                                        }
                                        let peerInfoController = component.context.sharedContext.makePeerInfoController(
                                            context: component.context,
                                            updatedPresentationData: nil,
                                            peer: peer._asPeer(),
                                            mode: .generic,
                                            avatarInitiallyExpanded: false,
                                            fromChat: false,
                                            requestsContext: nil
                                        )
                                        if let peerInfoController {
                                            controller.push(peerInfoController)
                                        }
                                    })
                                })
                            ))
                            itemList.append(.action(ContextMenuActionItem(
                                text: presentationData.strings.StorageManagement_ContextSelect,
                                icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) },
                                action: { [weak self] c, _ in
                                    c.dismiss(completion: {
                                    })
                                    
                                    guard let self, let aggregatedData = self.aggregatedData else {
                                        return
                                    }
                                    aggregatedData.togglePeerSelection(id: peer.id)
                                    self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
                                })
                            ))
                            let items = ContextController.Items(content: .list(itemList))
                            
                            let controller = ContextController(
                                account: component.context.account,
                                presentationData: presentationData,
                                source: .extracted(StorageUsageListContextExtractedContentSource(contentView: sourceView)), items: .single(items), recognizer: nil, gesture: gesture)
                            
                            self.controller?()?.forEachController({ controller in
                                if let controller = controller as? UndoOverlayController {
                                    controller.dismiss()
                                }
                                return true
                            })
                            self.controller?()?.presentInGlobalOverlay(controller)
                        }
                    ))
                ))
            }
            if let aggregatedData = self.aggregatedData, let imageItems = aggregatedData.imageItems, !imageItems.items.isEmpty, !listCategories.isEmpty {
                panelItems.append(StorageUsagePanelContainerComponent.Item(
                    id: "images",
                    title: environment.strings.StorageManagement_TabMedia,
                    panel: AnyComponent(StorageMediaGridPanelComponent(
                        context: component.context,
                        items: aggregatedData.imageItems,
                        selectionState: aggregatedData.selectionState,
                        action: { [weak self] messageId in
                            guard let self, let aggregatedData = self.aggregatedData else {
                                return
                            }
                            guard let _ = aggregatedData.messages[messageId] else {
                                return
                            }
                            aggregatedData.toggleMessageSelection(id: messageId)
                            self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
                        },
                        contextAction: { [weak self] messageId, containerView, sourceRect, gesture in
                            guard let self else {
                                return
                            }
                            self.messageGaleryContextAction(messageId: messageId, sourceView: containerView, sourceRect: sourceRect, gesture: gesture)
                        }
                    ))
                ))
            }
            if let aggregatedData = self.aggregatedData, let fileItems = aggregatedData.fileItems, !fileItems.items.isEmpty, !listCategories.isEmpty {
                panelItems.append(StorageUsagePanelContainerComponent.Item(
                    id: "files",
                    title: environment.strings.StorageManagement_TabFiles,
                    panel: AnyComponent(StorageFileListPanelComponent(
                        context: component.context,
                        items: fileItems,
                        selectionState: aggregatedData.selectionState,
                        action: { [weak self] messageId in
                            guard let self, let aggregatedData = self.aggregatedData else {
                                return
                            }
                            guard let _ = aggregatedData.messages[messageId] else {
                                return
                            }
                            aggregatedData.toggleMessageSelection(id: messageId)
                            self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
                        },
                        contextAction: { [weak self] messageId, containerView, gesture in
                            guard let self else {
                                return
                            }
                            self.messageContextAction(messageId: messageId, sourceView: containerView, gesture: gesture)
                        }
                    ))
                ))
            }
            if let aggregatedData = self.aggregatedData, let musicItems = aggregatedData.musicItems, !musicItems.items.isEmpty, !listCategories.isEmpty {
                panelItems.append(StorageUsagePanelContainerComponent.Item(
                    id: "music",
                    title: environment.strings.StorageManagement_TabMusic,
                    panel: AnyComponent(StorageFileListPanelComponent(
                        context: component.context,
                        items: musicItems,
                        selectionState: aggregatedData.selectionState,
                        action: { [weak self] messageId in
                            guard let self, let aggregatedData = self.aggregatedData else {
                                return
                            }
                            guard let _ = aggregatedData.messages[messageId] else {
                                return
                            }
                            aggregatedData.toggleMessageSelection(id: messageId)
                            self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
                        },
                        contextAction: { [weak self] messageId, containerView, gesture in
                            guard let self else {
                                return
                            }
                            self.messageContextAction(messageId: messageId, sourceView: containerView, gesture: gesture)
                        }
                    ))
                ))
            }
            
            if !panelItems.isEmpty {
                let panelContainerSize = self.panelContainer.update(
                    transition: transition,
                    component: AnyComponent(StorageUsagePanelContainerComponent(
                        theme: environment.theme,
                        strings: environment.strings,
                        dateTimeFormat: environment.dateTimeFormat,
                        insets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: bottomInset, right: environment.safeInsets.right),
                        items: panelItems,
                        currentPanelUpdated: { [weak self] id, transition in
                            guard let self else {
                                return
                            }
                            self.currentSelectedPanelId = id
                            self.state?.updated(transition: transition)
                        }
                    )),
                    environment: {
                        StorageUsagePanelContainerEnvironment(isScrollable: wasLockedAtPanels)
                    },
                    containerSize: CGSize(width: availableSize.width, height: availableSize.height - environment.navigationHeight)
                )
                if let panelContainerView = self.panelContainer.view {
                    if panelContainerView.superview == nil {
                        self.scrollContainerView.addSubview(panelContainerView)
                    }
                    transition.setFrame(view: panelContainerView, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: panelContainerSize))
                }
                contentHeight += panelContainerSize.height
            } else {
                self.panelContainer.view?.removeFromSuperview()
            }
            
            self.ignoreScrolling = true
            
            let contentOffset = self.scrollView.bounds.minY
            transition.setPosition(view: self.scrollView, position: CGRect(origin: CGPoint(), size: availableSize).center)
            let contentSize = CGSize(width: availableSize.width, height: contentHeight)
            if self.scrollView.contentSize != contentSize {
                self.scrollView.contentSize = contentSize
            }
            transition.setFrame(view: self.scrollContainerView, frame: CGRect(origin: CGPoint(), size: contentSize))
            
            var scrollViewBounds = self.scrollView.bounds
            scrollViewBounds.size = availableSize
            if wasLockedAtPanels, let panelContainerView = self.panelContainer.view {
                scrollViewBounds.origin.y = panelContainerView.frame.minY - environment.navigationHeight
            }
            transition.setBounds(view: self.scrollView, bounds: scrollViewBounds)
            
            if !wasLockedAtPanels && !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)
            
            if self.isClearing {
                let clearingNode: StorageUsageClearProgressOverlayNode
                var animateIn = false
                if let current = self.clearingNode {
                    clearingNode = current
                } else {
                    animateIn = true
                    clearingNode = StorageUsageClearProgressOverlayNode(presentationData: component.context.sharedContext.currentPresentationData.with { $0 })
                    self.clearingNode = clearingNode
                    self.addSubnode(clearingNode)
                    self.clearingDisplayTimestamp = CFAbsoluteTimeGetCurrent()
                }
                
                let clearingSize = CGSize(width: availableSize.width, height: availableSize.height)
                clearingNode.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - clearingSize.width) / 2.0), y: floor((availableSize.height - clearingSize.height) / 2.0)), size: clearingSize)
                clearingNode.updateLayout(size: clearingSize, bottomInset: environment.safeInsets.bottom, transition: .immediate)
                
                if animateIn {
                    clearingNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: 0.4)
                }
            } else {
                if let clearingNode = self.clearingNode {
                    self.clearingNode = nil
                    
                    var delay: Double = 0.0
                    if let clearingDisplayTimestamp = self.clearingDisplayTimestamp {
                        let timeDelta = CFAbsoluteTimeGetCurrent() - clearingDisplayTimestamp
                        if timeDelta < 0.4 {
                            delay = 0.0
                        } else if timeDelta < 1.0 {
                            delay = 1.0
                        }
                    }
                    
                    if delay == 0.0 {
                        let animationTransition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut))
                        animationTransition.setAlpha(view: clearingNode.view, alpha: 0.0, completion: { [weak clearingNode] _ in
                            clearingNode?.removeFromSupernode()
                        })
                    } else {
                        clearingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: delay, removeOnCompletion: false, completion: { [weak clearingNode] _ in
                            clearingNode?.removeFromSupernode()
                        })
                    }
                }
            }
            
            return availableSize
        }
        
        private func reportClearedStorage(size: Int64) {
            guard let component = self.component else {
                return
            }
            guard let controller = self.controller?() else {
                return
            }
            
            let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
            controller.present(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(size, formatting: DataSizeStringFormatting(presentationData: presentationData)))", stringForDeviceType()).string), elevatedLayout: false, action: { _ in return false }), in: .current)
        }
        
        private func reloadStats(firstTime: Bool, completion: @escaping () -> Void) {
            guard let component = self.component else {
                completion()
                return
            }
            
            self.statsDisposable = (component.context.engine.resources.collectStorageUsageStats()
            |> deliverOnMainQueue).start(next: { [weak self] stats in
                guard let self, let component = self.component else {
                    completion()
                    return
                }
                
                var peerItems: [StoragePeerListPanelComponent.Item] = []
                
                if component.peer == nil {
                    for item in stats.peers.values.sorted(by: { lhs, rhs in
                        let lhsSize: Int64 = lhs.stats.categories.values.reduce(0, {
                            $0 + $1.size
                        })
                        let rhsSize: Int64 = rhs.stats.categories.values.reduce(0, {
                            $0 + $1.size
                        })
                        return lhsSize > rhsSize
                    }) {
                        let itemSize: Int64 = item.stats.categories.values.reduce(0, {
                            $0 + $1.size
                        })
                        peerItems.append(StoragePeerListPanelComponent.Item(
                            peer: item.peer,
                            size: itemSize
                        ))
                    }
                }
                
                let initialAggregatedData = AggregatedData(
                    peerId: component.peer?.id,
                    stats: stats,
                    messages: [:],
                    peerItems: StoragePeerListPanelComponent.Items(items: peerItems),
                    imageItems: nil,
                    fileItems: nil,
                    musicItems: nil
                )
                let contextStats = initialAggregatedData.contextStats
                
                if firstTime {
                    self.aggregatedData = initialAggregatedData
                    
                    self.state?.updated(transition: Transition(animation: .none).withUserData(AnimationHint(value: .firstStatsUpdate)))
                    self.component?.ready.set(.single(true))
                }
                
                class RenderResult {
                    var messages: [MessageId: Message] = [:]
                    var imageItems: [StorageMediaGridPanelComponent.Item] = []
                    var fileItems: [StorageFileListPanelComponent.Item] = []
                    var musicItems: [StorageFileListPanelComponent.Item] = []
                }
                
                self.messagesDisposable = (component.context.engine.resources.renderStorageUsageStatsMessages(stats: contextStats, categories: [.files, .photos, .videos, .music], existingMessages: self.aggregatedData?.messages ?? [:])
                |> deliverOn(Queue())
                |> map { messages -> RenderResult in
                    let result = RenderResult()
                    
                    result.messages = messages
                    
                    var mergedMedia: [MessageId: Int64] = [:]
                    if let categoryStats = contextStats.categories[.photos] {
                        mergedMedia = categoryStats.messages
                    }
                    if let categoryStats = contextStats.categories[.videos] {
                        for (id, value) in categoryStats.messages {
                            mergedMedia[id] = value
                        }
                    }
                    
                    if !mergedMedia.isEmpty {
                        for (id, messageSize) in mergedMedia.sorted(by: { $0.value > $1.value }) {
                            if let message = messages[id] {
                                var matches = false
                                for media in message.media {
                                    if media is TelegramMediaImage {
                                        matches = true
                                        break
                                    } else if let file = media as? TelegramMediaFile {
                                        if file.isVideo {
                                            matches = true
                                            break
                                        }
                                    }
                                }
                                
                                if matches {
                                    result.imageItems.append(StorageMediaGridPanelComponent.Item(
                                        message: EngineMessage(message),
                                        size: messageSize
                                    ))
                                }
                            }
                        }
                    }
                    
                    if let categoryStats = contextStats.categories[.files] {
                        for (id, messageSize) in categoryStats.messages.sorted(by: { $0.value > $1.value }) {
                            if let message = messages[id] {
                                var matches = false
                                for media in message.media {
                                    if let file = media as? TelegramMediaFile {
                                        if file.isSticker || file.isCustomEmoji {
                                        } else {
                                            matches = true
                                        }
                                    }
                                }
                                
                                if matches {
                                    result.fileItems.append(StorageFileListPanelComponent.Item(
                                        message: message,
                                        size: messageSize
                                    ))
                                }
                            }
                        }
                    }
                    
                    if let categoryStats = contextStats.categories[.music] {
                        for (id, messageSize) in categoryStats.messages.sorted(by: { $0.value > $1.value }) {
                            if let message = messages[id] {
                                var matches = false
                                for media in message.media {
                                    if media is TelegramMediaFile {
                                        matches = true
                                    }
                                }
                                
                                if matches {
                                    result.musicItems.append(StorageFileListPanelComponent.Item(
                                        message: message,
                                        size: messageSize
                                    ))
                                }
                            }
                        }
                    }
                    
                    return result
                }
                |> deliverOnMainQueue).start(next: { [weak self] result in
                    guard let self, let component = self.component else {
                        completion()
                        return
                    }
                    
                    if !firstTime {
                        if let peer = component.peer, let controller = self.controller?() as? StorageUsageScreen, let childCompleted = controller.childCompleted {
                            let contextStats: StorageUsageStats = stats.peers[peer.id]?.stats ?? StorageUsageStats(categories: [:])
                            var totalSize: Int64 = 0
                            for (_, value) in contextStats.categories {
                                totalSize += value.size
                            }
                            
                            if totalSize == 0 {
                                childCompleted({ [weak self] in
                                    completion()
                                    
                                    if let self {
                                        self.controller?()?.dismiss(animated: true)
                                    }
                                })
                                return
                            } else {
                                childCompleted({})
                            }
                        }
                    }
                    
                    self.aggregatedData = AggregatedData(
                        peerId: component.peer?.id,
                        stats: stats,
                        messages: result.messages,
                        peerItems: initialAggregatedData.peerItems,
                        imageItems: StorageMediaGridPanelComponent.Items(items: result.imageItems),
                        fileItems: StorageFileListPanelComponent.Items(items: result.fileItems),
                        musicItems: StorageFileListPanelComponent.Items(items: result.musicItems)
                    )
                    
                    self.isClearing = false
                    
                    if !firstTime {
                        self.state?.updated(transition: Transition(animation: .none).withUserData(AnimationHint(value: .clearedItems)))
                    } else {
                        self.state?.updated(transition: Transition(animation: .none))
                    }
                    
                    completion()
                })
            })
        }
        
        private func openPeer(peer: EnginePeer) {
            guard let component = self.component else {
                return
            }
            guard let controller = self.controller?() else {
                return
            }
            
            let childController = StorageUsageScreen(context: component.context, makeStorageUsageExceptionsScreen: component.makeStorageUsageExceptionsScreen, peer: peer)
            childController.childCompleted = { [weak self] completed in
                guard let self else {
                    return
                }
                self.reloadStats(firstTime: false, completion: {
                    completed()
                })
            }
            controller.push(childController)
        }
        
        private func messageGaleryContextAction(messageId: EngineMessage.Id, sourceView: UIView, sourceRect: CGRect, gesture: ContextGesture) {
            guard let component = self.component, let aggregatedData = self.aggregatedData, let message = aggregatedData.messages[messageId] else {
                gesture.cancel()
                return
            }
            
            let _ = (chatMediaListPreviewControllerData(
                context: component.context,
                chatLocation: .peer(id: message.id.peerId),
                chatLocationContextHolder: nil,
                message: message,
                standalone: true,
                reverseMessageGalleryOrder: false,
                navigationController: self.controller?()?.navigationController as? NavigationController
            )
            |> deliverOnMainQueue).start(next: { [weak self] previewData in
                guard let self, let component = self.component, let previewData else {
                    gesture.cancel()
                    return
                }
                
                let context = component.context
                let presentationData = context.sharedContext.currentPresentationData.with { $0 }
                let strings = presentationData.strings
                
                var items: [ContextMenuItem] = []
                
                var openTitle: String = presentationData.strings.StorageManagement_OpenPhoto
                for media in message.media {
                    if let _ = media as? TelegramMediaImage {
                        openTitle = presentationData.strings.StorageManagement_OpenPhoto
                    } else if let file = media as? TelegramMediaFile {
                        if file.isVideo {
                            openTitle = presentationData.strings.StorageManagement_OpenVideo
                        } else {
                            openTitle = presentationData.strings.StorageManagement_OpenFile
                        }
                    }
                }
                
                items.append(.action(ContextMenuActionItem(
                    text: openTitle,
                    icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Expand"), color: theme.contextMenu.primaryColor) },
                    action: { [weak self] c, _ in
                        c.dismiss(completion: { [weak self] in
                            guard let self else {
                                return
                            }
                            self.openMessage(message: message)
                        })
                    })
                ))
                
                items.append(.action(ContextMenuActionItem(text: strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in
                    c.dismiss(completion: { [weak self] in
                        guard let self, let component = self.component, let controller = self.controller?(), let navigationController = controller.navigationController as? NavigationController else {
                            return
                        }
                        guard let peer = message.peers[message.id.peerId].flatMap(EnginePeer.init) else {
                            return
                        }
                        
                        var chatLocation: NavigateToChatControllerParams.Location = .peer(peer)
                        if case let .channel(channel) = peer, channel.flags.contains(.isForum), let threadId = message.threadId {
                            chatLocation = .replyThread(ChatReplyThreadMessage(messageId: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false))
                        }
                        
                        component.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(
                            navigationController: navigationController,
                            context: component.context,
                            chatLocation: chatLocation,
                            subject: .message(id: .id(message.id), highlight: true, timecode: nil),
                            keepStack: .always
                        ))
                    })
                })))
                    
                items.append(.action(ContextMenuActionItem(text: strings.Conversation_ContextMenuSelect, icon: { theme in
                    return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.actionSheet.primaryTextColor)
                }, action: { [weak self] c, _ in
                    c.dismiss(completion: {
                    })
                    
                    guard let self, let aggregatedData = self.aggregatedData else {
                        return
                    }
                    aggregatedData.toggleMessageSelection(id: message.id)
                    self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
                })))
                
                switch previewData {
                case let .gallery(gallery):
                    gallery.setHintWillBePresentedInPreviewingContext(true)
                    let contextController = ContextController(
                        account: component.context.account,
                        presentationData: presentationData,
                        source: .controller(StorageUsageListContextGalleryContentSourceImpl(
                            controller: gallery,
                            sourceView: sourceView,
                            sourceRect: sourceRect
                        )),
                        items: .single(ContextController.Items(content: .list(items))),
                        gesture: gesture
                    )
                    self.controller?()?.presentInGlobalOverlay(contextController)
                case .instantPage:
                    break
                }
            })
        }
        
        private func messageContextAction(messageId: EngineMessage.Id, sourceView: ContextExtractedContentContainingView, gesture: ContextGesture) {
            guard let component = self.component, let aggregatedData = self.aggregatedData else {
                return
            }
            guard let message = aggregatedData.messages[messageId] else {
                return
            }
            
            let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
            
            var openTitle: String = presentationData.strings.Conversation_LinkDialogOpen
            var isAudio: Bool = false
            for media in message.media {
                if let _ = media as? TelegramMediaImage {
                    openTitle = presentationData.strings.StorageManagement_OpenPhoto
                } else if let file = media as? TelegramMediaFile {
                    if file.isVideo {
                        openTitle = presentationData.strings.StorageManagement_OpenVideo
                    } else {
                        openTitle = presentationData.strings.StorageManagement_OpenFile
                    }
                    isAudio = file.isMusic || file.isVoice
                }
            }
            
            var itemList: [ContextMenuItem] = []
            if !isAudio {
                itemList.append(.action(ContextMenuActionItem(
                    text: openTitle,
                    icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Expand"), color: theme.contextMenu.primaryColor) },
                    action: { [weak self] c, _ in
                        c.dismiss(completion: { [weak self] in
                            guard let self else {
                                return
                            }
                            self.openMessage(message: message)
                        })
                    })
                ))
            }
            
            itemList.append(.action(ContextMenuActionItem(
                text: presentationData.strings.SharedMedia_ViewInChat,
                icon: { theme in
                    return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor)
                },
                action: { [weak self] c, _ in
                    c.dismiss(completion: { [weak self] in
                        guard let self, let component = self.component, let controller = self.controller?(), let navigationController = controller.navigationController as? NavigationController else {
                            return
                        }
                        guard let peer = message.peers[message.id.peerId].flatMap(EnginePeer.init) else {
                            return
                        }
                        
                        var chatLocation: NavigateToChatControllerParams.Location = .peer(peer)
                        if case let .channel(channel) = peer, channel.flags.contains(.isForum), let threadId = message.threadId {
                            chatLocation = .replyThread(ChatReplyThreadMessage(messageId: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false))
                        }
                        
                        component.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(
                            navigationController: navigationController,
                            context: component.context,
                            chatLocation: chatLocation,
                            subject: .message(id: .id(message.id), highlight: true, timecode: nil),
                            keepStack: .always
                        ))
                    })
                })
            ))
            itemList.append(.action(ContextMenuActionItem(
                text: aggregatedData.selectionState.selectedMessages.contains(messageId) ? presentationData.strings.StorageManagement_ContextDeselect : presentationData.strings.StorageManagement_ContextSelect,
                icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) },
                action: { [weak self] c, _ in
                    c.dismiss(completion: {
                    })
                    
                    guard let self, let aggregatedData = self.aggregatedData else {
                        return
                    }
                    aggregatedData.toggleMessageSelection(id: message.id)
                    self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
                })
            ))
            let items = ContextController.Items(content: .list(itemList))
            
            let controller = ContextController(
                account: component.context.account,
                presentationData: presentationData,
                source: .extracted(StorageUsageListContextExtractedContentSource(contentView: sourceView)), items: .single(items), recognizer: nil, gesture: gesture)
            
            self.controller?()?.forEachController({ controller in
                if let controller = controller as? UndoOverlayController {
                    controller.dismiss()
                }
                return true
            })
            self.controller?()?.presentInGlobalOverlay(controller)
        }
        
        private func openMessage(message: Message) {
            guard let component = self.component else {
                return
            }
            guard let controller = self.controller?(), let navigationController = controller.navigationController as? NavigationController else {
                return
            }
            let foundGalleryMessage: Message? = message
            guard let galleryMessage = foundGalleryMessage else {
                return
            }
            self.endEditing(true)
            
            let _ = component.context.sharedContext.openChatMessage(OpenChatMessageParams(
                context: component.context,
                chatLocation: .peer(id: message.id.peerId),
                chatLocationContextHolder: nil,
                message: galleryMessage,
                standalone: true,
                reverseMessageGalleryOrder: true,
                navigationController: navigationController,
                dismissInput: { [weak self] in
                    self?.endEditing(true)
                }, present: { [weak self] c, a in
                    guard let self else {
                        return
                    }
                    self.controller?()?.present(c, in: .window(.root), with: a, blockInteraction: true)
                },
                transitionNode: { [weak self] messageId, media, _ in
                    guard let self else {
                        return nil
                    }
                    
                    if let panelContainerView = self.panelContainer.view as? StorageUsagePanelContainerComponent.View {
                        if let currentPanelView = panelContainerView.currentPanelView as? StorageMediaGridPanelComponent.View {
                            return currentPanelView.transitionNodeForGallery(messageId: messageId, media: EngineMedia(media))
                        }
                    }
                    
                    return nil
                }, addToTransitionSurface: { [weak self] view in
                    guard let self else {
                        return
                    }
                    if let panelContainerView = self.panelContainer.view as? StorageUsagePanelContainerComponent.View {
                        panelContainerView.currentPanelView?.addSubview(view)
                    }
                }, openUrl: { [weak self] url in
                    guard let self else {
                        return
                    }
                    let _ = self
                }, openPeer: { [weak self] peer, navigation in
                    guard let self else {
                        return
                    }
                    let _ = self
                },
                callPeer: { _, _ in
                    //self?.controllerInteraction?.callPeer(peerId)
                },
                enqueueMessage: { _ in
                },
                sendSticker: nil,
                sendEmoji: nil,
                setupTemporaryHiddenMedia: { _, _, _ in },
                chatAvatarHiddenMedia: { _, _ in },
                actionInteraction: GalleryControllerActionInteraction(openUrl: { [weak self] url, concealed in
                    guard let self else {
                        return
                    }
                    let _ = self
                    //strongSelf.openUrl(url: url, concealed: false, external: false)
                }, openUrlIn: { [weak self] url in
                    guard let self else {
                        return
                    }
                    let _ = self
                }, openPeerMention: { [weak self] mention in
                    guard let self else {
                        return
                    }
                    let _ = self
                }, openPeer: { [weak self] peer in
                    guard let self else {
                        return
                    }
                    let _ = self
                }, openHashtag: { [weak self] peerName, hashtag in
                    guard let self else {
                        return
                    }
                    let _ = self
                }, openBotCommand: { _ in
                }, addContact: { _ in
                }, storeMediaPlaybackState: { [weak self] messageId, timestamp, playbackRate in
                    guard let self else {
                        return
                    }
                    let _ = self
                }, editMedia: { _, _, _ in
                }, updateCanReadHistory: { _ in
                }),
                centralItemUpdated: { [weak self] messageId in
                    //let _ = self?.paneContainerNode.requestExpandTabs?()
                    //self?.paneContainerNode.currentPane?.node.ensureMessageIsVisible(id: messageId)
                    
                    guard let self else {
                        return
                    }
                    let _ = self
                }
            ))
        }
        
        private func requestClear(fromCategories: Bool) {
            guard let component = self.component, let aggregatedData = self.aggregatedData else {
                return
            }
            let context = component.context
            
            let presentationData = context.sharedContext.currentPresentationData.with { $0 }
            let actionSheet = ActionSheetController(presentationData: presentationData)
            
            let clearTitle: String
            if let _ = aggregatedData.peerId {
                clearTitle = presentationData.strings.StorageManagement_ClearSelected
            } else {
                if aggregatedData.selectedCategories == aggregatedData.existingCategories, fromCategories {
                    clearTitle = presentationData.strings.StorageManagement_ClearAll
                } else {
                    clearTitle = presentationData.strings.StorageManagement_ClearSelected
                }
            }
            
            actionSheet.setItemGroups([ActionSheetItemGroup(items: [
                ActionSheetTextItem(title: presentationData.strings.StorageManagement_ClearConfirmationText, parseMarkdown: true),
                ActionSheetButtonItem(title: clearTitle, color: .destructive, action: { [weak self, weak actionSheet] in
                    actionSheet?.dismissAnimated()
                    
                    self?.commitClear(fromCategories: fromCategories)
                })
            ]), ActionSheetItemGroup(items: [
                ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
                    actionSheet?.dismissAnimated()
                })
            ])])
            self.controller?()?.present(actionSheet, in: .window(.root))
        }
        
        private func commitClear(fromCategories: Bool) {
            guard let component = self.component, let aggregatedData = self.aggregatedData else {
                return
            }
            
            if let _ = aggregatedData.peerId {
                var mappedCategories: [StorageUsageStats.CategoryKey] = []
                for category in aggregatedData.selectedCategories {
                    switch category {
                    case .photos:
                        mappedCategories.append(.photos)
                    case .videos:
                        mappedCategories.append(.videos)
                    case .files:
                        mappedCategories.append(.files)
                    case .music:
                        mappedCategories.append(.music)
                    case .other:
                        break
                    case .stickers:
                        mappedCategories.append(.stickers)
                    case .avatars:
                        mappedCategories.append(.avatars)
                    case .misc:
                        mappedCategories.append(.misc)
                    case .stories:
                        mappedCategories.append(.stories)
                    }
                }
                
                self.isClearing = true
                self.state?.updated(transition: .immediate)
                
                let totalSize = aggregatedData.selectedSize
                
                let _ = (component.context.engine.resources.clearStorage(peerId: component.peer?.id, categories: mappedCategories, includeMessages: aggregatedData.clearIncludeMessages, excludeMessages: aggregatedData.clearExcludeMessages)
                |> deliverOnMainQueue).start(next: { [weak self] progress in
                    guard let self else {
                        return
                    }
                    self.updateClearProgress(progress: progress)
                }, completed: { [weak self] in
                    guard let self, let _ = self.component else {
                        return
                    }
                    
                    self.reloadStats(firstTime: false, completion: { [weak self] in
                        guard let self else {
                            return
                        }
                        if totalSize != 0 {
                            self.reportClearedStorage(size: totalSize)
                        }
                    })
                })
            } else {
                if fromCategories {
                    var mappedCategories: [StorageUsageStats.CategoryKey] = []
                    for category in aggregatedData.selectedCategories {
                        switch category {
                        case .photos:
                            mappedCategories.append(.photos)
                        case .videos:
                            mappedCategories.append(.videos)
                        case .files:
                            mappedCategories.append(.files)
                        case .music:
                            mappedCategories.append(.music)
                        case .other:
                            break
                        case .stickers:
                            mappedCategories.append(.stickers)
                        case .avatars:
                            mappedCategories.append(.avatars)
                        case .misc:
                            mappedCategories.append(.misc)
                        case .stories:
                            mappedCategories.append(.stories)
                        }
                    }
                    
                    self.isClearing = true
                    self.state?.updated(transition: .immediate)
                    
                    var totalSize: Int64 = 0
                    
                    let contextStats = aggregatedData.contextStats
                    
                    for category in aggregatedData.selectedCategories {
                        let mappedCategory: StorageUsageStats.CategoryKey
                        switch category {
                        case .photos:
                            mappedCategory = .photos
                        case .videos:
                            mappedCategory = .videos
                        case .files:
                            mappedCategory = .files
                        case .music:
                            mappedCategory = .music
                        case .other:
                            continue
                        case .stickers:
                            mappedCategory = .stickers
                        case .avatars:
                            mappedCategory = .avatars
                        case .misc:
                            mappedCategory = .misc
                        case .stories:
                            mappedCategory = .stories
                        }
                        
                        if let value = contextStats.categories[mappedCategory] {
                            totalSize += value.size
                        }
                    }
                    
                    let _ = (component.context.engine.resources.clearStorage(peerId: component.peer?.id, categories: mappedCategories, includeMessages: [], excludeMessages: [])
                    |> deliverOnMainQueue).start(next: { [weak self] progress in
                        guard let self else {
                            return
                        }
                        self.updateClearProgress(progress: progress)
                    }, completed: { [weak self] in
                        guard let self else {
                            return
                        }
                        
                        self.reloadStats(firstTime: false, completion: { [weak self] in
                            guard let self else {
                                return
                            }
                            if totalSize != 0 {
                                self.reportClearedStorage(size: totalSize)
                            }
                        })
                    })
                } else {
                    self.isClearing = true
                    self.state?.updated(transition: .immediate)
                    
                    var totalSize: Int64 = 0
                    if let peerItems = aggregatedData.peerItems {
                        for item in peerItems.items {
                            if aggregatedData.selectionState.selectedPeers.contains(item.peer.id) {
                                totalSize += item.size
                            }
                        }
                    }
                    
                    var includeMessages: [Message] = []
                    var excludeMessages: [Message] = []
                    
                    for (id, message) in aggregatedData.messages {
                        if aggregatedData.selectionState.selectedPeers.contains(id.peerId) {
                            if !aggregatedData.selectionState.selectedMessages.contains(id) {
                                excludeMessages.append(message)
                            }
                        } else {
                            if aggregatedData.selectionState.selectedMessages.contains(id) {
                                includeMessages.append(message)
                            }
                        }
                    }
                    
                    let _ = (component.context.engine.resources.clearStorage(peerIds: aggregatedData.selectionState.selectedPeers, includeMessages: includeMessages, excludeMessages: excludeMessages)
                    |> deliverOnMainQueue).start(next: { [weak self] progress in
                        guard let self else {
                            return
                        }
                        self.updateClearProgress(progress: progress)
                    }, completed: { [weak self] in
                        guard let self else {
                            return
                        }
                        
                        self.reloadStats(firstTime: false, completion: { [weak self] in
                            guard let self else {
                                return
                            }
                            if totalSize != 0 {
                                self.reportClearedStorage(size: totalSize)
                            }
                        })
                    })
                }
            }
        }
        
        private func updateClearProgress(progress: Float) {
            if let clearingNode = self.clearingNode {
                clearingNode.setProgress(progress)
            }
        }
        
        private func openKeepMediaCategory(mappedCategory: CacheStorageSettings.PeerStorageCategory, sourceView: StoragePeerTypeItemComponent.View) {
            guard let component = self.component else {
                return
            }
            let context = component.context
            let makeStorageUsageExceptionsScreen = component.makeStorageUsageExceptionsScreen
            
            let pushControllerImpl: ((ViewController) -> Void)? = { [weak self] c in
                guard let self else {
                    return
                }
                self.controller?()?.push(c)
            }
            let presentInGlobalOverlay: ((ViewController) -> Void)? = { [weak self] c in
                guard let self else {
                    return
                }
                self.controller?()?.presentInGlobalOverlay(c, with: nil)
            }
            
            let viewKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.accountSpecificCacheStorageSettings]))
            let accountSpecificSettings: Signal<AccountSpecificCacheStorageSettings, NoError> = context.account.postbox.combinedView(keys: [viewKey])
            |> map { views -> AccountSpecificCacheStorageSettings in
                let cacheSettings: AccountSpecificCacheStorageSettings
                if let view = views.views[viewKey] as? PreferencesView, let value = view.values[PreferencesKeys.accountSpecificCacheStorageSettings]?.get(AccountSpecificCacheStorageSettings.self) {
                    cacheSettings = value
                } else {
                    cacheSettings = AccountSpecificCacheStorageSettings.defaultSettings
                }

                return cacheSettings
            }
            |> distinctUntilChanged
            
            let peerExceptions: Signal<[(peer: FoundPeer, value: Int32)], NoError> = accountSpecificSettings
            |> mapToSignal { accountSpecificSettings -> Signal<[(peer: FoundPeer, value: Int32)], NoError> in
                return context.account.postbox.transaction { transaction -> [(peer: FoundPeer, value: Int32)] in
                    var result: [(peer: FoundPeer, value: Int32)] = []
                    
                    for item in accountSpecificSettings.peerStorageTimeoutExceptions {
                        let peerId = item.key
                        let value = item.value
                        
                        guard let peer = transaction.getPeer(peerId) else {
                            continue
                        }
                        let peerCategory: CacheStorageSettings.PeerStorageCategory
                        var subscriberCount: Int32?
                        if peer is TelegramUser {
                            peerCategory = .privateChats
                        } else if peer is TelegramGroup {
                            peerCategory = .groups
                            
                            if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedGroupData {
                                subscriberCount = (cachedData.participants?.participants.count).flatMap(Int32.init)
                            }
                        } else if let channel = peer as? TelegramChannel {
                            if case .group = channel.info {
                                peerCategory = .groups
                            } else {
                                peerCategory = .channels
                            }
                            if peerCategory == mappedCategory {
                                if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData {
                                    subscriberCount = cachedData.participantsSummary.memberCount
                                }
                            }
                        } else {
                            continue
                        }
                            
                        if peerCategory != mappedCategory {
                            continue
                        }
                        
                        result.append((peer: FoundPeer(peer: peer, subscribers: subscriberCount), value: value))
                    }
                    
                    return result.sorted(by: { lhs, rhs in
                        if lhs.value != rhs.value {
                            return lhs.value < rhs.value
                        }
                        return lhs.peer.peer.debugDisplayTitle < rhs.peer.peer.debugDisplayTitle
                    })
                }
            }
            
            let cacheSettings = context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.cacheStorageSettings])
            |> map { sharedData -> CacheStorageSettings in
                let cacheSettings: CacheStorageSettings
                if let value = sharedData.entries[SharedDataKeys.cacheStorageSettings]?.get(CacheStorageSettings.self) {
                    cacheSettings = value
                } else {
                    cacheSettings = CacheStorageSettings.defaultSettings
                }
                
                return cacheSettings
            }
            
            let _ = (combineLatest(
                cacheSettings |> take(1),
                peerExceptions |> take(1)
            )
            |> deliverOnMainQueue).start(next: { cacheSettings, peerExceptions in
                let currentValue: Int32 = cacheSettings.categoryStorageTimeout[mappedCategory] ?? Int32.max
                
                let applyValue: (Int32) -> Void = { value in
                    let _ = updateCacheStorageSettingsInteractively(accountManager: context.sharedContext.accountManager, { cacheSettings in
                        var cacheSettings = cacheSettings
                        cacheSettings.categoryStorageTimeout[mappedCategory] = value
                        return cacheSettings
                    }).start()
                }
                
                var subItems: [ContextMenuItem] = []
                let presentationData = context.sharedContext.currentPresentationData.with { $0 }
                
                var presetValues: [Int32]
                
                if case .stories = mappedCategory {
                    presetValues = [
                        7 * 24 * 60 * 60,
                        2 * 24 * 60 * 60,
                        1 * 24 * 60 * 60
                    ]
                } else {
                    presetValues = [
                        Int32.max,
                        31 * 24 * 60 * 60,
                        7 * 24 * 60 * 60,
                        1 * 24 * 60 * 60
                    ]
                }
                
                if currentValue != 0 && !presetValues.contains(currentValue) {
                    presetValues.append(currentValue)
                    presetValues.sort(by: >)
                }
                
                for value in presetValues {
                    let optionText: String
                    if value == Int32.max {
                        optionText = presentationData.strings.ClearCache_Never
                    } else {
                        optionText = timeIntervalString(strings: presentationData.strings, value: value)
                    }
                    subItems.append(.action(ContextMenuActionItem(text: optionText, icon: { theme in
                        if currentValue == value {
                            return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
                        } else {
                            return nil
                        }
                    }, action: { _, f in
                        applyValue(value)
                        f(.default)
                    })))
                }
                
                subItems.append(.separator)
                
                if mappedCategory != .stories {
                    if peerExceptions.isEmpty {
                        let exceptionsText = presentationData.strings.GroupInfo_Permissions_AddException
                        subItems.append(.action(ContextMenuActionItem(text: exceptionsText, icon: { theme in
                            if case .privateChats = mappedCategory {
                                return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddUser"), color: theme.contextMenu.primaryColor)
                            } else {
                                return generateTintedImage(image: UIImage(bundleImageName: "Location/CreateGroupIcon"), color: theme.contextMenu.primaryColor)
                            }
                        }, action: { _, f in
                            f(.default)
                            
                            if let exceptionsController = makeStorageUsageExceptionsScreen(mappedCategory) {
                                pushControllerImpl?(exceptionsController)
                            }
                        })))
                    } else {
                        subItems.append(.custom(MultiplePeerAvatarsContextItem(context: context, peers: peerExceptions.prefix(3).map { EnginePeer($0.peer.peer) }, totalCount: peerExceptions.count, action: { c, _ in
                            c.dismiss(completion: {
                                
                            })
                            if let exceptionsController = makeStorageUsageExceptionsScreen(mappedCategory) {
                                pushControllerImpl?(exceptionsController)
                            }
                        }), false))
                    }
                }
                
                if let sourceLabelView = sourceView.labelView {
                    let items: Signal<ContextController.Items, NoError> = .single(ContextController.Items(content: .list(subItems)))
                    let source: ContextContentSource = .reference(StorageUsageContextReferenceContentSource(sourceView: sourceLabelView))
                    
                    let contextController = ContextController(
                        account: context.account,
                        presentationData: presentationData,
                        source: source,
                        items: items,
                        gesture: nil
                    )
                    sourceView.setHasAssociatedMenu(true)
                    contextController.dismissed = { [weak sourceView] in
                        sourceView?.setHasAssociatedMenu(false)
                    }
                    presentInGlobalOverlay?(contextController)
                }
            })
        }
    }
    
    func makeView() -> View {
        return View(frame: CGRect())
    }
    
    func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: Transition) -> CGSize {
        return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
    }
}

public final class StorageUsageScreen: ViewControllerComponentContainer {
    private let context: AccountContext
    
    private let readyValue = Promise<Bool>()
    override public var ready: Promise<Bool> {
        return self.readyValue
    }
    
    fileprivate var childCompleted: ((@escaping () -> Void) -> Void)?
    
    public init(context: AccountContext, makeStorageUsageExceptionsScreen: @escaping (CacheStorageSettings.PeerStorageCategory) -> ViewController?, peer: EnginePeer? = nil) {
        self.context = context
        
        let componentReady = Promise<Bool>()
        super.init(context: context, component: StorageUsageScreenComponent(context: context, makeStorageUsageExceptionsScreen: makeStorageUsageExceptionsScreen, peer: peer, ready: componentReady), navigationBarAppearance: .transparent)
        
        if peer != nil {
            self.navigationPresentation = .modal
        }
        
        self.readyValue.set(componentReady.get() |> timeout(0.3, queue: .mainQueue(), alternate: .single(true)))
    }
    
    required public init(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override public func viewDidLoad() {
        super.viewDidLoad()
    }
}

private final class StorageUsageContextReferenceContentSource: ContextReferenceContentSource {
    private let sourceView: UIView
    
    init(sourceView: UIView) {
        self.sourceView = sourceView
    }
    
    func transitionInfo() -> ContextControllerReferenceViewInfo? {
        return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, insets: UIEdgeInsets(top: -4.0, left: 0.0, bottom: -4.0, right: 0.0))
    }
}

final class MultiplePeerAvatarsContextItem: ContextMenuCustomItem {
    fileprivate let context: AccountContext
    fileprivate let peers: [EnginePeer]
    fileprivate let totalCount: Int
    fileprivate let action: (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void

    init(context: AccountContext, peers: [EnginePeer], totalCount: Int, action: @escaping (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void) {
        self.context = context
        self.peers = peers
        self.totalCount = totalCount
        self.action = action
    }

    func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode {
        return MultiplePeerAvatarsContextItemNode(presentationData: presentationData, item: self, getController: getController, actionSelected: actionSelected)
    }
}

private final class MultiplePeerAvatarsContextItemNode: ASDisplayNode, ContextMenuCustomNode, ContextActionNodeProtocol {
    private let item: MultiplePeerAvatarsContextItem
    private var presentationData: PresentationData
    private let getController: () -> ContextControllerProtocol?
    private let actionSelected: (ContextMenuActionResult) -> Void

    private let backgroundNode: ASDisplayNode
    private let highlightedBackgroundNode: ASDisplayNode
    private let textNode: ImmediateTextNode

    private let avatarsNode: AnimatedAvatarSetNode
    private let avatarsContext: AnimatedAvatarSetContext

    private let buttonNode: HighlightTrackingButtonNode

    private var pointerInteraction: PointerInteraction?

    init(presentationData: PresentationData, item: MultiplePeerAvatarsContextItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) {
        self.item = item
        self.presentationData = presentationData
        self.getController = getController
        self.actionSelected = actionSelected

        let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize)

        self.backgroundNode = ASDisplayNode()
        self.backgroundNode.isAccessibilityElement = false
        self.backgroundNode.backgroundColor = presentationData.theme.contextMenu.itemBackgroundColor
        self.highlightedBackgroundNode = ASDisplayNode()
        self.highlightedBackgroundNode.isAccessibilityElement = false
        self.highlightedBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor
        self.highlightedBackgroundNode.alpha = 0.0

        self.textNode = ImmediateTextNode()
        self.textNode.isAccessibilityElement = false
        self.textNode.isUserInteractionEnabled = false
        self.textNode.displaysAsynchronously = false
        self.textNode.attributedText = NSAttributedString(string: " ", font: textFont, textColor: presentationData.theme.contextMenu.primaryColor)
        self.textNode.maximumNumberOfLines = 1

        self.buttonNode = HighlightTrackingButtonNode()
        self.buttonNode.isAccessibilityElement = true
        self.buttonNode.accessibilityLabel = presentationData.strings.VoiceChat_StopRecording

        self.avatarsNode = AnimatedAvatarSetNode()
        self.avatarsContext = AnimatedAvatarSetContext()

        super.init()

        self.addSubnode(self.backgroundNode)
        self.addSubnode(self.highlightedBackgroundNode)
        self.addSubnode(self.textNode)
        self.addSubnode(self.avatarsNode)
        self.addSubnode(self.buttonNode)

        self.buttonNode.highligthedChanged = { [weak self] highligted in
            guard let strongSelf = self else {
                return
            }
            if highligted {
                strongSelf.highlightedBackgroundNode.alpha = 1.0
            } else {
                strongSelf.highlightedBackgroundNode.alpha = 0.0
                strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
            }
        }
        self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
        self.buttonNode.isUserInteractionEnabled = true
    }

    deinit {
    }

    override func didLoad() {
        super.didLoad()

        self.pointerInteraction = PointerInteraction(node: self.buttonNode, style: .hover, willEnter: { [weak self] in
            if let strongSelf = self {
                strongSelf.highlightedBackgroundNode.alpha = 0.75
            }
        }, willExit: { [weak self] in
            if let strongSelf = self {
                strongSelf.highlightedBackgroundNode.alpha = 0.0
            }
        })
    }

    private var validLayout: (calculatedWidth: CGFloat, size: CGSize)?

    func updateLayout(constrainedWidth: CGFloat, constrainedHeight: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) {
        let sideInset: CGFloat = 14.0
        let verticalInset: CGFloat = 12.0

        let rightTextInset: CGFloat = sideInset + 36.0

        let calculatedWidth = min(constrainedWidth, 250.0)

        let textFont = Font.regular(self.presentationData.listsFontSize.baseDisplaySize)
        let text: String = self.presentationData.strings.CacheEvictionMenu_CategoryExceptions(Int32(self.item.totalCount))
        self.textNode.attributedText = NSAttributedString(string: text, font: textFont, textColor: self.presentationData.theme.contextMenu.primaryColor)

        let textSize = self.textNode.updateLayout(CGSize(width: calculatedWidth - sideInset - rightTextInset, height: .greatestFiniteMagnitude))

        let combinedTextHeight = textSize.height
        return (CGSize(width: calculatedWidth, height: verticalInset * 2.0 + combinedTextHeight), { size, transition in
            self.validLayout = (calculatedWidth: calculatedWidth, size: size)
            let verticalOrigin = floor((size.height - combinedTextHeight) / 2.0)
            let textFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalOrigin), size: textSize)
            transition.updateFrameAdditive(node: self.textNode, frame: textFrame)

            let avatarsContent: AnimatedAvatarSetContext.Content

            let avatarsPeers: [EnginePeer] = self.item.peers
            
            avatarsContent = self.avatarsContext.update(peers: avatarsPeers, animated: false)

            let avatarsSize = self.avatarsNode.update(context: self.item.context, content: avatarsContent, itemSize: CGSize(width: 24.0, height: 24.0), customSpacing: 10.0, animated: false, synchronousLoad: true)
            self.avatarsNode.frame = CGRect(origin: CGPoint(x: size.width - sideInset - 12.0 - avatarsSize.width, y: floor((size.height - avatarsSize.height) / 2.0)), size: avatarsSize)

            transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
            transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
            transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
        })
    }

    func updateTheme(presentationData: PresentationData) {
        self.presentationData = presentationData

        self.backgroundNode.backgroundColor = presentationData.theme.contextMenu.itemBackgroundColor
        self.highlightedBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor

        let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize)

        self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: textFont, textColor: presentationData.theme.contextMenu.primaryColor)
    }

    @objc private func buttonPressed() {
        self.performAction()
    }

    private var actionTemporarilyDisabled: Bool = false
    
    func canBeHighlighted() -> Bool {
        return self.isActionEnabled
    }
    
    func updateIsHighlighted(isHighlighted: Bool) {
        self.setIsHighlighted(isHighlighted)
    }

    func performAction() {
        if self.actionTemporarilyDisabled {
            return
        }
        self.actionTemporarilyDisabled = true
        Queue.mainQueue().async { [weak self] in
            self?.actionTemporarilyDisabled = false
        }

        guard let controller = self.getController() else {
            return
        }
        self.item.action(controller, { [weak self] result in
            self?.actionSelected(result)
        })
    }

    var isActionEnabled: Bool {
        return true
    }

    func setIsHighlighted(_ value: Bool) {
        if value {
            self.highlightedBackgroundNode.alpha = 1.0
        } else {
            self.highlightedBackgroundNode.alpha = 0.0
        }
    }
    
    func actionNode(at point: CGPoint) -> ContextActionNodeProtocol {
        return self
    }
}

private class StorageUsageClearProgressOverlayNode: ASDisplayNode {
    private let presentationData: PresentationData
    
    private let blurredView: BlurredBackgroundView
    private let animationNode: AnimatedStickerNode
    private let progressTextNode: ImmediateTextNode
    private let descriptionTextNode: ImmediateTextNode
    private let progressBackgroundNode: ASDisplayNode
    private let progressForegroundNode: ASDisplayNode
    
    private let progressDisposable = MetaDisposable()
    
    private var validLayout: (CGSize, CGFloat)?
    
    init(presentationData: PresentationData) {
        self.presentationData = presentationData
        
        self.blurredView = BlurredBackgroundView(color: presentationData.theme.list.plainBackgroundColor.withMultipliedAlpha(0.7), enableBlur: true)
        
        self.animationNode = DefaultAnimatedStickerNodeImpl()
        self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "ClearCache"), width: 256, height: 256, playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
        self.animationNode.visibility = true
        
        self.progressTextNode = ImmediateTextNode()
        self.progressTextNode.textAlignment = .center
        
        self.descriptionTextNode = ImmediateTextNode()
        self.descriptionTextNode.textAlignment = .center
        self.descriptionTextNode.maximumNumberOfLines = 0
        
        self.progressBackgroundNode = ASDisplayNode()
        self.progressBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.controlAccentColor.withMultipliedAlpha(0.2)
        self.progressBackgroundNode.cornerRadius = 3.0
        
        self.progressForegroundNode = ASDisplayNode()
        self.progressForegroundNode.backgroundColor = self.presentationData.theme.actionSheet.controlAccentColor
        self.progressForegroundNode.cornerRadius = 3.0
        
        super.init()
        
        self.view.addSubview(self.blurredView)
        self.addSubnode(self.animationNode)
        self.addSubnode(self.progressTextNode)
        self.addSubnode(self.descriptionTextNode)
        self.addSubnode(self.progressBackgroundNode)
        self.addSubnode(self.progressForegroundNode)
    }
    
    deinit {
        self.progressDisposable.dispose()
    }
    
    func setProgressSignal(_ signal: Signal<Float, NoError>) {
        self.progressDisposable.set((signal
        |> deliverOnMainQueue).start(next: { [weak self] progress in
            if let strongSelf = self {
                strongSelf.setProgress(progress)
            }
        }))
    }
    
    private var progress: Float = 0.0
    func setProgress(_ progress: Float) {
        self.progress = progress
        
        if let (size, bottomInset) = self.validLayout {
            self.updateLayout(size: size, bottomInset: bottomInset, transition: .animated(duration: 0.5, curve: .linear))
        }
    }
    
    func updateLayout(size: CGSize, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) {
        self.validLayout = (size, bottomInset)
        
        transition.updateFrame(view: self.blurredView, frame: CGRect(origin: CGPoint(), size: size))
        self.blurredView.update(size: size, transition: transition)
        
        let inset: CGFloat = 24.0
        let progressHeight: CGFloat = 6.0
        let spacing: CGFloat = 16.0
        
        let imageSide = min(160.0, size.height - 30.0)
        let imageSize = CGSize(width: imageSide, height: imageSide)
        
        let animationFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floorToScreenPixels((size.height - imageSize.height) / 2.0) - 50.0), size: imageSize)
        self.animationNode.frame = animationFrame
        self.animationNode.updateLayout(size: imageSize)
        
        var bottomInset = bottomInset
        if bottomInset.isZero {
            bottomInset = inset
        }
        
        let progressFrame = CGRect(x: inset, y: size.height - bottomInset - progressHeight, width: size.width - inset * 2.0, height: progressHeight)
        self.progressBackgroundNode.frame = progressFrame
        let progressForegroundFrame = CGRect(x: inset, y: size.height - bottomInset - progressHeight, width: floorToScreenPixels(progressFrame.width * CGFloat(self.progress)), height: progressHeight)
        if !self.progressForegroundNode.frame.origin.x.isZero {
            transition.updateFrame(node: self.progressForegroundNode, frame: progressForegroundFrame, beginWithCurrentState: true)
        } else {
            self.progressForegroundNode.frame = progressForegroundFrame
        }
        
        self.descriptionTextNode.attributedText = NSAttributedString(string: self.presentationData.strings.ClearCache_KeepOpenedDescription, font: Font.regular(15.0), textColor: self.presentationData.theme.actionSheet.secondaryTextColor)
        let descriptionTextSize = self.descriptionTextNode.updateLayout(CGSize(width: size.width - inset * 3.0, height: size.height))
        var descriptionTextFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - descriptionTextSize.width) / 2.0), y: animationFrame.maxY + 52.0), size: descriptionTextSize)
        
        let progressText: String = "\(Int(self.progress * 100.0))%"
       
        self.progressTextNode.attributedText = NSAttributedString(string: progressText, font: Font.with(size: 17.0, design: .regular, weight: .semibold, traits: [.monospacedNumbers]), textColor: self.presentationData.theme.actionSheet.primaryTextColor)
        let progressTextSize = self.progressTextNode.updateLayout(size)
        var progressTextFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - progressTextSize.width) / 2.0), y: descriptionTextFrame.minY - spacing - progressTextSize.height), size: progressTextSize)
        
        let availableHeight = progressTextFrame.minY
        if availableHeight < 100.0 {
            let offset = availableHeight / 2.0 - spacing
            descriptionTextFrame = descriptionTextFrame.offsetBy(dx: 0.0, dy: -offset)
            progressTextFrame = progressTextFrame.offsetBy(dx: 0.0, dy: -offset)
            self.animationNode.alpha = 0.0
        } else {
            self.animationNode.alpha = 1.0
        }
        
        self.progressTextNode.frame = progressTextFrame
        self.descriptionTextNode.frame = descriptionTextFrame
    }
}

private final class StorageUsageListContextGalleryContentSourceImpl: ContextControllerContentSource {
    let controller: ViewController
    weak var sourceView: UIView?
    let sourceRect: CGRect
    
    let navigationController: NavigationController? = nil
    
    let passthroughTouches: Bool
    
    init(controller: ViewController, sourceView: UIView?, sourceRect: CGRect = CGRect(origin: CGPoint(), size: CGSize()), passthroughTouches: Bool = false) {
        self.controller = controller
        self.sourceView = sourceView
        self.sourceRect = sourceRect
        self.passthroughTouches = passthroughTouches
    }
    
    func transitionInfo() -> ContextControllerTakeControllerInfo? {
        let sourceView = self.sourceView
        let sourceRect = self.sourceRect
        return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceView] in
            if let sourceView = sourceView {
                let rect = sourceRect.isEmpty ? sourceView.bounds : sourceRect
                return (sourceView, rect)
            } else {
                return nil
            }
        })
    }
    
    func animatedIn() {
        self.controller.didAppearInContextPreview()
    }
}

private final class StorageUsageListContextExtractedContentSource: ContextExtractedContentSource {
    let keepInPlace: Bool = false
    let ignoreContentTouches: Bool = false
    let blurBackground: Bool = true
    
    //let actionsHorizontalAlignment: ContextActionsHorizontalAlignment = .center
    
    private let contentView: ContextExtractedContentContainingView
    
    init(contentView: ContextExtractedContentContainingView) {
        self.contentView = contentView
    }
    
    func takeView() -> ContextControllerTakeViewInfo? {
        return ContextControllerTakeViewInfo(containingItem: .view(self.contentView), contentAreaInScreenSpace: UIScreen.main.bounds)
    }
    
    func putBack() -> ContextControllerPutBackViewInfo? {
        return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds)
    }
}