diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 97a82aa15a..ca7726692f 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -7299,6 +7299,7 @@ Sorry for the inconvenience."; "Contacts.Sort.ByLastSeen" = "by Last Seen"; "ClearCache.Progress" = "Clearing the Cache • %d%"; +"ClearCache.NoProgress" = "Clearing the Cache"; "ClearCache.KeepOpenedDescription" = "Please keep this window open until the clearing is completed."; "Share.ShareAsLink" = "Share as Link"; diff --git a/submodules/DebugSettingsUI/Sources/DebugController.swift b/submodules/DebugSettingsUI/Sources/DebugController.swift index 6bd98f1dbb..f59c799aad 100644 --- a/submodules/DebugSettingsUI/Sources/DebugController.swift +++ b/submodules/DebugSettingsUI/Sources/DebugController.swift @@ -77,6 +77,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { case resetDatabaseAndCache(PresentationTheme) case resetHoles(PresentationTheme) case reindexUnread(PresentationTheme) + case resetCacheIndex case reindexCache case resetBiometricsData(PresentationTheme) case resetWebViewCache(PresentationTheme) @@ -112,7 +113,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return DebugControllerSection.logging.rawValue case .enableRaiseToSpeak, .keepChatNavigationStack, .skipReadHistory, .crashOnSlowQueries: return DebugControllerSection.experiments.rawValue - case .clearTips, .crash, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .reindexCache, .resetBiometricsData, .resetWebViewCache, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .playerEmbedding, .playlistPlayback, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .experimentalBackground, .inlineForums, .localTranscription, . enableReactionOverrides, .restorePurchases: + case .clearTips, .crash, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .resetWebViewCache, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .playerEmbedding, .playlistPlayback, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .experimentalBackground, .inlineForums, .localTranscription, . enableReactionOverrides, .restorePurchases: return DebugControllerSection.experiments.rawValue case .preferredVideoCodec: return DebugControllerSection.videoExperiments.rawValue @@ -171,42 +172,44 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 21 case .reindexUnread: return 22 - case .reindexCache: + case .resetCacheIndex: return 23 - case .resetBiometricsData: + case .reindexCache: return 24 - case .resetWebViewCache: + case .resetBiometricsData: return 25 - case .optimizeDatabase: + case .resetWebViewCache: return 26 - case .photoPreview: + case .optimizeDatabase: return 27 - case .knockoutWallpaper: + case .photoPreview: return 28 - case .experimentalCompatibility: + case .knockoutWallpaper: return 29 - case .enableDebugDataDisplay: + case .experimentalCompatibility: return 30 - case .acceleratedStickers: + case .enableDebugDataDisplay: return 31 - case .experimentalBackground: + case .acceleratedStickers: return 32 - case .inlineForums: + case .experimentalBackground: return 33 - case .localTranscription: + case .inlineForums: return 34 - case .enableReactionOverrides: + case .localTranscription: return 35 - case .restorePurchases: + case .enableReactionOverrides: return 36 - case .playerEmbedding: + case .restorePurchases: return 37 - case .playlistPlayback: + case .playerEmbedding: return 38 - case .voiceConference: + case .playlistPlayback: return 39 + case .voiceConference: + return 40 case let .preferredVideoCodec(index, _, _, _): - return 40 + index + return 41 + index case .disableVideoAspectScaling: return 100 case .enableVoipTcp: @@ -970,6 +973,14 @@ private enum DebugControllerEntry: ItemListNodeEntry { controller.dismiss() }) }) + case .resetCacheIndex: + return ItemListActionItem(presentationData: presentationData, title: "Reset Cache Index [!]", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { + guard let context = arguments.context else { + return + } + + context.account.postbox.mediaBox.storageBox.reset() + }) case .reindexCache: return ItemListActionItem(presentationData: presentationData, title: "Reindex Cache", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { guard let context = arguments.context else { @@ -1253,6 +1264,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present entries.append(.resetHoles(presentationData.theme)) if isMainApp { entries.append(.reindexUnread(presentationData.theme)) + entries.append(.resetCacheIndex) entries.append(.reindexCache) entries.append(.resetWebViewCache(presentationData.theme)) } diff --git a/submodules/Postbox/Sources/StorageBox/StorageBox.swift b/submodules/Postbox/Sources/StorageBox/StorageBox.swift index 53f1fb33f4..df1a99a85c 100644 --- a/submodules/Postbox/Sources/StorageBox/StorageBox.swift +++ b/submodules/Postbox/Sources/StorageBox/StorageBox.swift @@ -230,6 +230,19 @@ public final class StorageBox { }) } + func reset() { + self.valueBox.begin() + + self.valueBox.removeAllFromTable(self.hashIdToInfoTable) + self.valueBox.removeAllFromTable(self.idToReferenceTable) + self.valueBox.removeAllFromTable(self.peerIdToIdTable) + self.valueBox.removeAllFromTable(self.peerContentTypeStatsTable) + self.valueBox.removeAllFromTable(self.contentTypeStatsTable) + self.valueBox.removeAllFromTable(self.metadataTable) + + self.valueBox.commit() + } + private func internalAddSize(contentType: UInt8, delta: Int64) { let key = ValueBoxKey(length: 1) key.setUInt8(0, value: contentType) @@ -893,4 +906,10 @@ public final class StorageBox { completion(ids) } } + + public func reset() { + self.impl.with { impl in + impl.reset() + } + } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift b/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift index fdf198d986..719679c2a8 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift @@ -91,10 +91,14 @@ public final class AllStorageUsageStats { } } + public var deviceAvailableSpace: Int64 + public var deviceFreeSpace: Int64 public fileprivate(set) var totalStats: StorageUsageStats public fileprivate(set) var peers: [EnginePeer.Id: PeerStats] - public init(totalStats: StorageUsageStats, peers: [EnginePeer.Id: PeerStats]) { + public init(deviceAvailableSpace: Int64, deviceFreeSpace: Int64, totalStats: StorageUsageStats, peers: [EnginePeer.Id: PeerStats]) { + self.deviceAvailableSpace = deviceAvailableSpace + self.deviceFreeSpace = deviceFreeSpace self.totalStats = totalStats self.peers = peers } @@ -242,7 +246,13 @@ func _internal_collectStorageUsageStats(account: Account) -> Signal= $1.value }).prefix(1000) { if result[id] == nil { if let message = existingMessages[id] { result[id] = message diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/BUILD b/submodules/TelegramUI/Components/StorageUsageScreen/BUILD index 538749ea59..3d20c87422 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/BUILD +++ b/submodules/TelegramUI/Components/StorageUsageScreen/BUILD @@ -33,6 +33,10 @@ swift_library( "//submodules/AvatarNode", "//submodules/PhotoResources", "//submodules/SemanticStatusNode", + "//submodules/RadialStatusNode", + "//submodules/UndoUI", + "//submodules/AnimatedStickerNode", + "//submodules/TelegramAnimatedStickerNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift index d8e91a9dac..f588f7ba2c 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift @@ -87,12 +87,12 @@ private func processChartData(data: PieChartComponent.ChartData) -> PieChartComp final class PieChartComponent: Component { struct ChartData: Equatable { struct Item: Equatable { - var id: AnyHashable + var id: StorageUsageScreenComponent.Category var displayValue: Double var value: Double var color: UIColor - init(id: AnyHashable, displayValue: Double, value: Double, color: UIColor) { + init(id: StorageUsageScreenComponent.Category, displayValue: Double, value: Double, color: UIColor) { self.id = id self.displayValue = displayValue self.value = value @@ -131,12 +131,12 @@ final class PieChartComponent: Component { private final class ChartDataView: UIView { private(set) var theme: PresentationTheme? private(set) var data: ChartData? - private(set) var selectedKey: AnyHashable? + private(set) var selectedKey: StorageUsageScreenComponent.Category? private var currentAnimation: (start: ChartData, end: ChartData, current: ChartData, progress: CGFloat)? private var animator: DisplayLinkAnimator? - private var labels: [AnyHashable: ComponentView] = [:] + private var labels: [StorageUsageScreenComponent.Category: ComponentView] = [:] override init(frame: CGRect) { super.init(frame: frame) @@ -153,7 +153,7 @@ final class PieChartComponent: Component { self.animator?.invalidate() } - func setItems(theme: PresentationTheme, data: ChartData, selectedKey: AnyHashable?, animated: Bool) { + func setItems(theme: PresentationTheme, data: ChartData, selectedKey: StorageUsageScreenComponent.Category?, animated: Bool) { let data = processChartData(data: data) if self.theme !== theme || self.data != data || self.selectedKey != selectedKey { @@ -366,7 +366,7 @@ final class PieChartComponent: Component { var minDistance: CGFloat = 1000.0 for distance in distances { - minDistance = min(minDistance, distance + 1.0) + minDistance = min(minDistance, distance) } let diagonalAngle = atan2(labelSize.height, labelSize.width) @@ -467,8 +467,8 @@ final class PieChartComponent: Component { class View: UIView { private let dataView: ChartDataView - private var labels: [AnyHashable: ComponentView] = [:] - var selectedKey: AnyHashable? + private var labels: [StorageUsageScreenComponent.Category: ComponentView] = [:] + var selectedKey: StorageUsageScreenComponent.Category? private weak var state: EmptyComponentState? diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageCategoryItemCompoment.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageCategoryItemCompoment.swift index 78ae0bc938..0e56f8b6a4 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageCategoryItemCompoment.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageCategoryItemCompoment.swift @@ -167,7 +167,7 @@ final class StorageCategoryItemComponent: Component { } func update(component: StorageCategoryItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { - let themeUpdated = self.component?.theme !== component.theme + let themeUpdated = self.component?.theme !== component.theme || self.component?.category.color != component.category.color self.component = component @@ -276,7 +276,21 @@ final class StorageCategoryItemComponent: Component { transition.setFrame(view: labelView, frame: labelFrame) } + var copyCheckLayer: CheckLayer? if themeUpdated { + if !transition.animation.isImmediate { + let copyLayer = CheckLayer(theme: self.checkLayer.theme) + copyLayer.frame = self.checkLayer.frame + copyLayer.setSelected(self.checkLayer.selected, animated: false) + self.layer.addSublayer(copyLayer) + copyCheckLayer = copyLayer + transition.setAlpha(layer: copyLayer, alpha: 0.0, completion: { [weak copyLayer] _ in + copyLayer?.removeFromSuperlayer() + }) + self.checkLayer.opacity = 0.0 + transition.setAlpha(layer: self.checkLayer, alpha: 1.0) + } + self.checkLayer.theme = CheckNodeTheme( backgroundColor: component.category.color, strokeColor: component.theme.list.itemCheckColors.foregroundColor, @@ -289,7 +303,11 @@ final class StorageCategoryItemComponent: Component { let checkDiameter: CGFloat = 22.0 let checkFrame = CGRect(origin: CGPoint(x: titleFrame.minX - 20.0 - checkDiameter, y: floor((height - checkDiameter) / 2.0)), size: CGSize(width: checkDiameter, height: checkDiameter)) - self.checkLayer.frame = checkFrame + transition.setFrame(layer: self.checkLayer, frame: checkFrame) + + if let copyCheckLayer { + transition.setFrame(layer: copyCheckLayer, frame: checkFrame) + } transition.setFrame(view: self.checkButtonArea, frame: CGRect(origin: CGPoint(x: additionalLeftInset, y: 0.0), size: CGSize(width: leftInset - additionalLeftInset, height: height))) diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift index c4dc47be30..4150c96f89 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift @@ -16,6 +16,11 @@ import Markdown import ContextUI import AnimatedAvatarSetNode import AvatarNode +import RadialStatusNode +import UndoUI +import AnimatedStickerNode +import TelegramAnimatedStickerNode +import TelegramStringFormatting private extension StorageUsageScreenComponent.Category { init(_ category: StorageUsageStats.CategoryKey) { @@ -110,6 +115,13 @@ final class StorageUsageScreenComponent: Component { 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 @@ -206,6 +218,8 @@ final class StorageUsageScreenComponent: Component { private let scrollView: ScrollViewImpl private var currentStats: AllStorageUsageStats? + private var existingCategories: Set = Set() + private var currentMessages: [MessageId: Message] = [:] private var cacheSettings: CacheStorageSettings? private var peerItems: StoragePeerListPanelComponent.Items? @@ -215,6 +229,8 @@ final class StorageUsageScreenComponent: Component { private var selectionState: SelectionState? + private var isClearing: Bool = false + private var selectedCategories: Set = Set() private var isOtherCategoryExpanded: Bool = false @@ -232,6 +248,9 @@ final class StorageUsageScreenComponent: Component { private var chartAvatarNode: AvatarNode? + private var doneStatusCircle: SimpleShapeLayer? + private var doneStatusNode: RadialStatusNode? + private let pieChartView = ComponentView() private let chartTotalLabel = ComponentView() private let categoriesView = ComponentView() @@ -246,6 +265,8 @@ final class StorageUsageScreenComponent: Component { private var selectionPanel: ComponentView? + private var clearingNode: StorageUsageClearProgressOverlayNode? + private var component: StorageUsageScreenComponent? private weak var state: EmptyComponentState? private var navigationMetrics: (navigationHeight: CGFloat, statusBarHeight: CGFloat)? @@ -267,6 +288,7 @@ final class StorageUsageScreenComponent: Component { self.navigationBackgroundView = BlurredBackgroundView(color: nil, enableBlur: true) self.navigationSeparatorLayer = SimpleLayer() + self.navigationSeparatorLayer.opacity = 0.0 self.scrollView = ScrollViewImpl() @@ -434,7 +456,7 @@ final class StorageUsageScreenComponent: Component { } }) - self.reloadStats(firstTime: true) + self.reloadStats(firstTime: true, completion: {}) } var wasLockedAtPanels = false @@ -446,13 +468,20 @@ final class StorageUsageScreenComponent: Component { let animationHint = transition.userData(AnimationHint.self) - if let animationHint, case .firstStatsUpdate = animationHint.value { - var alphaTransition = transition + if let animationHint { if case .firstStatsUpdate = animationHint.value { - alphaTransition = .easeInOut(duration: 0.25) + let alphaTransition: Transition = .easeInOut(duration: 0.25) + alphaTransition.setAlpha(view: self.scrollView, alpha: self.currentStats != nil ? 1.0 : 0.0) + alphaTransition.setAlpha(view: self.headerOffsetContainer, alpha: self.currentStats != nil ? 1.0 : 0.0) + } else if case .clearedItems = animationHint.value { + if let snapshotView = self.snapshotView(afterScreenUpdates: false) { + snapshotView.frame = self.bounds + self.addSubview(snapshotView) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } } - alphaTransition.setAlpha(view: self.scrollView, alpha: self.currentStats != nil ? 1.0 : 0.0) - alphaTransition.setAlpha(view: self.headerOffsetContainer, alpha: self.currentStats != nil ? 1.0 : 0.0) } else { transition.setAlpha(view: self.scrollView, alpha: self.currentStats != nil ? 1.0 : 0.0) transition.setAlpha(view: self.headerOffsetContainer, alpha: self.currentStats != nil ? 1.0 : 0.0) @@ -620,19 +649,17 @@ final class StorageUsageScreenComponent: Component { .misc ] - if let animationHint, case .firstStatsUpdate = animationHint.value, let currentStats = self.currentStats { - let contextStats: StorageUsageStats - if let peer = component.peer { - contextStats = currentStats.peers[peer.id]?.stats ?? StorageUsageStats(categories: [:]) - } else { - contextStats = currentStats.totalStats - } - - for (category, value) in contextStats.categories { - if value.size != 0 { - self.selectedCategories.insert(StorageUsageScreenComponent.Category(category)) + if let _ = self.currentStats { + if let animationHint { + switch animationHint.value { + case .firstStatsUpdate, .clearedItems: + self.selectedCategories = self.existingCategories } } + + self.selectedCategories.formIntersection(self.existingCategories) + } else { + self.selectedCategories.removeAll() } var chartItems: [PieChartComponent.ChartData.Item] = [] @@ -697,7 +724,7 @@ final class StorageUsageScreenComponent: Component { var chartCategoryColor = category.color if !self.isOtherCategoryExpanded && otherCategories.contains(category) { - chartCategoryColor = Category.other.color + chartCategoryColor = Category.misc.color } chartItems.append(PieChartComponent.ChartData.Item(id: category, displayValue: categoryFraction, value: categoryChartFraction, color: chartCategoryColor)) @@ -732,8 +759,36 @@ final class StorageUsageScreenComponent: Component { let isSelected = otherListCategories.allSatisfy { item in return self.selectedCategories.contains(item.key) } + + let listColor: UIColor + if self.isOtherCategoryExpanded { + listColor = Category.other.color + } else { + listColor = Category.misc.color + } + listCategories.append(StorageCategoriesComponent.CategoryData( - key: Category.other, color: Category.other.color, title: Category.other.title(strings: environment.strings), size: totalOtherSize, sizeFraction: categoryFraction, isSelected: isSelected, subcategories: otherListCategories)) + key: Category.other, color: listColor, title: Category.other.title(strings: environment.strings), size: totalOtherSize, sizeFraction: categoryFraction, isSelected: isSelected, subcategories: otherListCategories)) + } + + if !self.isOtherCategoryExpanded { + var otherSum: CGFloat = 0.0 + for i in 0 ..< chartItems.count { + if otherCategories.contains(chartItems[i].id) { + var itemValue = chartItems[i].value + if itemValue > 0.00001 { + itemValue = max(itemValue, 0.01) + } + otherSum += itemValue + if case .misc = chartItems[i].id { + } else { + chartItems[i].value = 0.0 + } + } + } + if let index = chartItems.firstIndex(where: { $0.id == .misc }) { + chartItems[index].value = otherSum + } } let chartData = PieChartComponent.ChartData(items: chartItems) @@ -754,13 +809,76 @@ final class StorageUsageScreenComponent: Component { } transition.setFrame(view: pieChartComponentView, frame: pieChartFrame) + transition.setAlpha(view: pieChartComponentView, alpha: listCategories.isEmpty ? 0.0 : 1.0) } - contentHeight += pieChartSize.height + if let _ = self.currentStats, 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.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.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 += 26.0 + let headerText: String + if listCategories.isEmpty { + headerText = "Storage Cleared" + } else if let peer = component.peer { + headerText = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast) + } else { + headerText = "Storage Usage" + } let headerViewSize = self.headerView.update( transition: transition, - component: AnyComponent(Text(text: "Storage Usage", font: Font.semibold(22.0), color: environment.theme.list.itemPrimaryTextColor)), + component: AnyComponent(Text(text: headerText, font: Font.semibold(22.0), color: environment.theme.list.itemPrimaryTextColor)), environment: {}, containerSize: CGSize(width: floor((availableSize.width - navigationRightButtonMaxWidth * 2.0) / 0.8), height: 100.0) ) @@ -780,14 +898,78 @@ final class StorageUsageScreenComponent: Component { let bold = MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.freeTextColor) //TODO:localize + var usageFraction: Double = 0.0 + let totalUsageText: String + if listCategories.isEmpty { + totalUsageText = "All media can be re-downloaded from the Telegram cloud if you need it again." + } else if let currentStats = self.currentStats { + let contextStats: StorageUsageStats + if let peer = component.peer { + contextStats = currentStats.peers[peer.id]?.stats ?? StorageUsageStats(categories: [:]) + } else { + contextStats = currentStats.totalStats + } + + var totalStatsSize: Int64 = 0 + for (_, value) in contextStats.categories { + totalStatsSize += value.size + } + + if let _ = component.peer { + var allStatsSize: Int64 = 0 + for (_, value) in currentStats.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 = "This chat uses \(fractionString)% of your Telegram cache." + } else { + let fraction: Double + if currentStats.deviceFreeSpace != 0 && totalStatsSize != 0 { + fraction = Double(totalStatsSize) / Double(currentStats.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 = "Telegram uses \(fractionString)% of your free disk space." + } + } else { + totalUsageText = " " + } let headerDescriptionSize = self.headerDescriptionView.update( transition: transition, - component: AnyComponent(MultilineTextComponent(text: .markdown(text: "Telegram users 9.7% of your free disk space.", attributes: MarkdownAttributes( + component: AnyComponent(MultilineTextComponent(text: .markdown(text: totalUsageText, attributes: MarkdownAttributes( body: body, bold: bold, link: body, linkAttribute: { _ in nil } - )), maximumNumberOfLines: 0)), + )), horizontalAlignment: .center, maximumNumberOfLines: 0)), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 15.0 * 2.0, height: 10000.0) ) @@ -807,12 +989,15 @@ final class StorageUsageScreenComponent: Component { transition.setCornerRadius(layer: self.headerProgressBackgroundLayer, cornerRadius: headerProgressFrame.height * 0.5) self.headerProgressBackgroundLayer.backgroundColor = environment.theme.list.itemAccentColor.withMultipliedAlpha(0.2).cgColor - let headerProgress: CGFloat = 0.097 - transition.setFrame(layer: self.headerProgressForegroundLayer, frame: CGRect(origin: headerProgressFrame.origin, size: CGSize(width: floorToScreenPixels(headerProgress * headerProgressFrame.width), height: headerProgressFrame.height))) + 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 { @@ -831,6 +1016,7 @@ final class StorageUsageScreenComponent: Component { 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 chartTotalLabelSize = self.chartTotalLabel.update( transition: transition, @@ -841,6 +1027,7 @@ final class StorageUsageScreenComponent: Component { self.scrollView.addSubview(chartTotalLabelView) } transition.setFrame(view: chartTotalLabelView, frame: 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.setAlpha(view: chartTotalLabelView, alpha: listCategories.isEmpty ? 0.0 : 1.0) } } @@ -858,17 +1045,18 @@ final class StorageUsageScreenComponent: Component { return } if key == Category.other { - let otherCategories: [Category] = [.stickers, .avatars, .misc] - if otherCategories.allSatisfy(self.selectedCategories.contains) { - for item in otherCategories { - self.selectedCategories.remove(item) + var otherCategories: [Category] = [.stickers, .avatars, .misc] + otherCategories = otherCategories.filter(self.existingCategories.contains) + if !otherCategories.isEmpty { + if otherCategories.allSatisfy(self.selectedCategories.contains) { + for item in otherCategories { + self.selectedCategories.remove(item) + } + } else { + for item in otherCategories { + let _ = self.selectedCategories.insert(item) + } } - self.selectedCategories.remove(Category.other) - } else { - for item in otherCategories { - let _ = self.selectedCategories.insert(item) - } - let _ = self.selectedCategories.insert(Category.other) } } else { if self.selectedCategories.contains(key) { @@ -1097,7 +1285,7 @@ final class StorageUsageScreenComponent: Component { return } if self.selectionState == nil { - self.selectionState = SelectionState(selectedPeers: Set(), selectedMessages: Set()) + self.selectionState = SelectionState() } self.selectionState = self.selectionState?.toggleMessage(id: messageId) self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) @@ -1118,7 +1306,7 @@ final class StorageUsageScreenComponent: Component { return } if self.selectionState == nil { - self.selectionState = SelectionState(selectedPeers: Set(), selectedMessages: Set()) + self.selectionState = SelectionState() } self.selectionState = self.selectionState?.toggleMessage(id: messageId) self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) @@ -1139,7 +1327,7 @@ final class StorageUsageScreenComponent: Component { return } if self.selectionState == nil { - self.selectionState = SelectionState(selectedPeers: Set(), selectedMessages: Set()) + self.selectionState = SelectionState() } self.selectionState = self.selectionState?.toggleMessage(id: messageId) self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) @@ -1199,24 +1387,81 @@ final class StorageUsageScreenComponent: Component { 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) + } + + 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, transition: .immediate) + + if animateIn { + clearingNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: 0.15) + } + } else { + if let clearingNode = self.clearingNode { + self.clearingNode = nil + + let animationTransition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut)) + animationTransition.setAlpha(view: clearingNode.view, alpha: 0.0, completion: { [weak clearingNode] _ in + clearingNode?.removeFromSupernode() + }) + } + } + return availableSize } - private func reloadStats(firstTime: Bool) { - if let controller = self.controller?() as? StorageUsageScreen { - controller.reloadParent?() + 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: .window(.root)) + } + + 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 } - self.currentStats = stats + + var existingCategories = Set() + let contextStats: StorageUsageStats + if let peer = component.peer { + contextStats = stats.peers[peer.id]?.stats ?? StorageUsageStats(categories: [:]) + } else { + contextStats = stats.totalStats + } + for (category, value) in contextStats.categories { + if value.size != 0 { + existingCategories.insert(StorageUsageScreenComponent.Category(category)) + } + } + + if firstTime { + self.currentStats = stats + self.existingCategories = existingCategories + } var peerItems: [StoragePeerListPanelComponent.Item] = [] @@ -1240,9 +1485,10 @@ final class StorageUsageScreenComponent: Component { } } - self.peerItems = StoragePeerListPanelComponent.Items(items: peerItems) - - self.state?.updated(transition: Transition(animation: .none).withUserData(AnimationHint(value: firstTime ? .firstStatsUpdate : .clearedItems))) + if firstTime { + self.peerItems = StoragePeerListPanelComponent.Items(items: peerItems) + self.state?.updated(transition: Transition(animation: .none).withUserData(AnimationHint(value: .firstStatsUpdate))) + } class RenderResult { var messages: [MessageId: Message] = [:] @@ -1251,13 +1497,6 @@ final class StorageUsageScreenComponent: Component { var musicItems: [StorageFileListPanelComponent.Item] = [] } - let contextStats: StorageUsageStats - if let peer = component.peer { - contextStats = stats.peers[peer.id]?.stats ?? StorageUsageStats(categories: [:]) - } else { - contextStats = stats.totalStats - } - self.messagesDisposable = (component.context.engine.resources.renderStorageUsageStatsMessages(stats: contextStats, categories: [.files, .photos, .videos, .music], existingMessages: self.currentMessages) |> deliverOn(Queue()) |> map { messages -> RenderResult in @@ -1344,17 +1583,59 @@ final class StorageUsageScreenComponent: Component { return result } |> deliverOnMainQueue).start(next: { [weak self] result in - guard let self else { + 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({}) + } + } + } + + if !firstTime { + self.currentStats = stats + self.existingCategories = existingCategories + self.peerItems = StoragePeerListPanelComponent.Items(items: peerItems) + } + self.currentMessages = result.messages self.imageItems = StorageFileListPanelComponent.Items(items: result.imageItems) self.fileItems = StorageFileListPanelComponent.Items(items: result.fileItems) self.musicItems = StorageFileListPanelComponent.Items(items: result.musicItems) - self.state?.updated(transition: Transition(animation: .none)) + if self.selectionState != nil { + if result.imageItems.isEmpty && result.fileItems.isEmpty && result.musicItems.isEmpty && peerItems.isEmpty { + self.selectionState = nil + } else { + self.selectionState = SelectionState() + } + } + + self.isClearing = false + + self.state?.updated(transition: Transition(animation: .none).withUserData(AnimationHint(value: .clearedItems))) + + completion() }) }) } @@ -1368,11 +1649,13 @@ final class StorageUsageScreenComponent: Component { } let childController = StorageUsageScreen(context: component.context, makeStorageUsageExceptionsScreen: component.makeStorageUsageExceptionsScreen, peer: peer) - childController.reloadParent = { [weak self] in + childController.childCompleted = { [weak self] completed in guard let self else { return } - self.reloadStats(firstTime: false) + self.reloadStats(firstTime: false, completion: { + completed() + }) } controller.push(childController) } @@ -1429,6 +1712,10 @@ final class StorageUsageScreenComponent: Component { mappedCategories.append(.misc) } } + + self.isClearing = true + self.state?.updated(transition: .immediate) + let _ = (component.context.engine.resources.clearStorage(peerId: peerId, categories: mappedCategories) |> deliverOnMainQueue).start(completed: { [weak self] in guard let self, let component = self.component, let currentStats = self.currentStats else { @@ -1469,41 +1756,84 @@ final class StorageUsageScreenComponent: Component { } } - for category in categories { - self.selectedCategories.remove(category) - } - self.selectionState = nil - - self.reloadStats(firstTime: false) - - self.state?.updated(transition: Transition(animation: .curve(duration: 0.45, curve: .spring)).withUserData(AnimationHint(value: .clearedItems))) + self.reloadStats(firstTime: false, completion: { [weak self] in + guard let self else { + return + } + if totalSize != 0 { + self.reportClearedStorage(size: totalSize) + } + }) }) } else if !peers.isEmpty { + self.isClearing = true + self.state?.updated(transition: .immediate) + + var totalSize: Int64 = 0 + if let peerItems = self.peerItems { + for item in peerItems.items { + if peers.contains(item.peer.id) { + totalSize += item.size + } + } + } + let _ = (component.context.engine.resources.clearStorage(peerIds: peers) |> deliverOnMainQueue).start(completed: { [weak self] in guard let self else { return } - self.selectionState = nil - self.reloadStats(firstTime: false) + self.reloadStats(firstTime: false, completion: { [weak self] in + guard let self else { + return + } + if totalSize != 0 { + self.reportClearedStorage(size: totalSize) + } + }) }) } else if !messages.isEmpty { var messageItems: [Message] = [] + var totalSize: Int64 = 0 + + let contextStats: StorageUsageStats + if let peer = component.peer { + contextStats = self.currentStats?.peers[peer.id]?.stats ?? StorageUsageStats(categories: [:]) + } else { + contextStats = self.currentStats?.totalStats ?? StorageUsageStats(categories: [:]) + } + for id in messages { if let message = self.currentMessages[id] { messageItems.append(message) + + for (_, value) in contextStats.categories { + if let size = value.messages[id] { + totalSize += size + } + } } } + self.isClearing = true + self.state?.updated(transition: .immediate) + let _ = (component.context.engine.resources.clearStorage(messages: messageItems) |> deliverOnMainQueue).start(completed: { [weak self] in guard let self else { return } - self.selectionState = nil - self.reloadStats(firstTime: false) + self.reloadStats(firstTime: false, completion: { [weak self] in + guard let self else { + return + } + + if totalSize != 0 { + self.reportClearedStorage(size: totalSize) + } + }) }) } } @@ -1716,7 +2046,7 @@ final class StorageUsageScreenComponent: Component { public final class StorageUsageScreen: ViewControllerComponentContainer { private let context: AccountContext - fileprivate var reloadParent: (() -> Void)? + fileprivate var childCompleted: ((@escaping () -> Void) -> Void)? public init(context: AccountContext, makeStorageUsageExceptionsScreen: @escaping (CacheStorageSettings.PeerStorageCategory) -> ViewController?, peer: EnginePeer? = nil) { self.context = context @@ -1948,3 +2278,122 @@ private final class MultiplePeerAvatarsContextItemNode: ASDisplayNode, ContextMe 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? + + 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) { + self.progressDisposable.set((signal + |> deliverOnMainQueue).start(next: { [weak self] progress in + if let strongSelf = self { + strongSelf.setProgress(progress) + } + })) + } + + private var progress: Float = 0.0 + private func setProgress(_ progress: Float) { + self.progress = progress + + if let size = self.validLayout { + self.updateLayout(size: size, transition: .animated(duration: 0.5, curve: .linear)) + } + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayout = size + + 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) + + let progressFrame = CGRect(x: inset, y: size.height - inset - progressHeight, width: size.width - inset * 2.0, height: progressHeight) + self.progressBackgroundNode.frame = progressFrame + let progressForegroundFrame = CGRect(x: inset, y: size.height - inset - 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) + + self.progressTextNode.attributedText = NSAttributedString(string: self.presentationData.strings.ClearCache_NoProgress, 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 + } +}