diff --git a/submodules/Postbox/Sources/StorageBox/StorageBox.swift b/submodules/Postbox/Sources/StorageBox/StorageBox.swift index e26e15fe3e..a8a879e075 100644 --- a/submodules/Postbox/Sources/StorageBox/StorageBox.swift +++ b/submodules/Postbox/Sources/StorageBox/StorageBox.swift @@ -19,14 +19,24 @@ private func md5Hash(_ data: Data) -> HashId { } public final class StorageBox { - public struct Stats { - public var contentTypes: [UInt8: Int64] + public final class Stats { + public fileprivate(set) var contentTypes: [UInt8: Int64] public init(contentTypes: [UInt8: Int64]) { self.contentTypes = contentTypes } } + public final class AllStats { + public fileprivate(set) var total: Stats + public fileprivate(set) var peers: [PeerId: Stats] + + public init(total: Stats, peers: [PeerId: Stats]) { + self.total = total + self.peers = peers + } + } + public struct Reference { public var peerId: Int64 public var messageNamespace: UInt8 @@ -122,6 +132,10 @@ public final class StorageBox { } } + private struct Metadata: Codable { + var version: Int32 + } + private final class Impl { let queue: Queue let logger: StorageBox.Logger @@ -133,6 +147,7 @@ public final class StorageBox { let peerIdTable: ValueBoxTable let peerContentTypeStatsTable: ValueBoxTable let contentTypeStatsTable: ValueBoxTable + let metadataTable: ValueBoxTable init(queue: Queue, logger: StorageBox.Logger, basePath: String) { self.queue = queue @@ -157,6 +172,54 @@ public final class StorageBox { self.peerIdTable = ValueBoxTable(id: 18, keyType: .binary, compactValuesOnCreation: true) self.peerContentTypeStatsTable = ValueBoxTable(id: 19, keyType: .binary, compactValuesOnCreation: true) self.contentTypeStatsTable = ValueBoxTable(id: 20, keyType: .binary, compactValuesOnCreation: true) + self.metadataTable = ValueBoxTable(id: 21, keyType: .binary, compactValuesOnCreation: true) + + self.performUpdatesIfNeeded() + } + + private func performUpdatesIfNeeded() { + self.valueBox.begin() + + let mainMetadataKey = ValueBoxKey(length: 2) + mainMetadataKey.setUInt8(0, value: 0) + mainMetadataKey.setUInt8(1, value: 0) + + var metadata: Metadata + if let value = self.valueBox.get(self.metadataTable, key: mainMetadataKey), let parsedValue = try? JSONDecoder().decode(Metadata.self, from: value.makeData()) { + metadata = parsedValue + } else { + metadata = Metadata(version: 0) + } + + if metadata.version != 2 { + self.reindexPeerStats() + + metadata.version = 2 + if let data = try? JSONEncoder().encode(metadata) { + self.valueBox.set(self.metadataTable, key: mainMetadataKey, value: MemoryBuffer(data: data)) + } + } + + self.valueBox.commit() + } + + private func reindexPeerStats() { + self.valueBox.removeAllFromTable(self.peerContentTypeStatsTable) + + let mainKey = ValueBoxKey(length: 16) + self.valueBox.scan(self.peerIdToIdTable, keys: { key in + let peerId = key.getInt64(0) + let hashId = key.getData(8, length: 16) + + mainKey.setData(0, value: hashId) + + if let currentInfoValue = self.valueBox.get(self.hashIdToInfoTable, key: mainKey) { + let info = ItemInfo(buffer: currentInfoValue) + self.internalAddSize(peerId: peerId, contentType: info.contentType, delta: info.size) + } + + return true + }) } private func internalAddSize(contentType: UInt8, delta: Int64) { @@ -171,7 +234,7 @@ public final class StorageBox { currentSize += delta if currentSize < 0 { - assertionFailure() + //assertionFailure() currentSize = 0 } @@ -184,18 +247,18 @@ public final class StorageBox { key.setUInt8(8, value: contentType) var currentSize: Int64 = 0 - if let value = self.valueBox.get(self.contentTypeStatsTable, key: key) { + if let value = self.valueBox.get(self.peerContentTypeStatsTable, key: key) { value.read(¤tSize, offset: 0, length: 8) } currentSize += delta if currentSize < 0 { - assertionFailure() + //assertionFailure() currentSize = 0 } - self.valueBox.set(self.contentTypeStatsTable, key: key, value: MemoryBuffer(memory: ¤tSize, capacity: 8, length: 8, freeWhenDone: false)) + self.valueBox.set(self.peerContentTypeStatsTable, key: key, value: MemoryBuffer(memory: ¤tSize, capacity: 8, length: 8, freeWhenDone: false)) } func add(reference: Reference, to id: Data, contentType: UInt8) { @@ -224,6 +287,10 @@ public final class StorageBox { if size != 0 { self.internalAddSize(contentType: previousContentType, delta: -size) self.internalAddSize(contentType: contentType, delta: size) + + for peerId in self.peerIdsReferencing(hashId: hashId) { + self.internalAddSize(peerId: peerId, contentType: previousContentType, delta: -size) + } } } @@ -274,9 +341,31 @@ public final class StorageBox { } } + if let previousContentType = previousContentType, previousContentType != contentType { + if size != 0 { + for peerId in self.peerIdsReferencing(hashId: hashId) { + self.internalAddSize(peerId: peerId, contentType: contentType, delta: size) + } + } + } + self.valueBox.commit() } + private func peerIdsReferencing(hashId: HashId) -> Set { + let mainKey = ValueBoxKey(length: 16) + mainKey.setData(0, value: hashId.data) + + var peerIds = Set() + self.valueBox.range(self.idToReferenceTable, start: mainKey, end: mainKey.successor, keys: { key in + let peerId = key.getInt64(16) + peerIds.insert(peerId) + return true + }, limit: 0) + + return peerIds + } + func update(id: Data, size: Int64) { self.valueBox.begin() @@ -300,14 +389,8 @@ public final class StorageBox { self.internalAddSize(contentType: info.contentType, delta: sizeDelta) } - var peerIds: [Int64] = [] - self.valueBox.range(self.idToReferenceTable, start: mainKey, end: mainKey.successor, keys: { key in - peerIds.append(key.getInt64(0)) - return true - }, limit: 0) - - for peerId in peerIds { - let _ = peerId + for peerId in self.peerIdsReferencing(hashId: hashId) { + self.internalAddSize(peerId: peerId, contentType: info.contentType, delta: sizeDelta) } } @@ -334,6 +417,8 @@ public final class StorageBox { self.valueBox.set(self.hashIdToInfoTable, key: mainKey, value: ItemInfo(id: id, contentType: contentType, size: size).serialize()) + self.internalAddSize(contentType: contentType, delta: size) + let idKey = ValueBoxKey(length: hashId.data.count + 8 + 1 + 4) idKey.setData(0, value: hashId.data) idKey.setInt64(hashId.data.count, value: reference.peerId) @@ -378,6 +463,8 @@ public final class StorageBox { } peerIdCount += 1 self.valueBox.set(self.peerIdTable, key: peerIdKey, value: MemoryBuffer(memory: &peerIdCount, capacity: 4, length: 4, freeWhenDone: false)) + + self.internalAddSize(peerId: 0, contentType: contentType, delta: size) } } } @@ -564,18 +651,39 @@ public final class StorageBox { return result } - func getStats() -> Stats { - var contentTypes: [UInt8: Int64] = [:] + func getAllStats() -> AllStats { + self.valueBox.begin() + + let allStats = AllStats(total: StorageBox.Stats(contentTypes: [:]), peers: [:]) self.valueBox.scan(self.contentTypeStatsTable, values: { key, value in var size: Int64 = 0 value.read(&size, offset: 0, length: 8) - contentTypes[key.getUInt8(0)] = size + allStats.total.contentTypes[key.getUInt8(0)] = size return true }) - return Stats(contentTypes: contentTypes) + self.valueBox.scan(self.peerContentTypeStatsTable, values: { key, value in + var size: Int64 = 0 + value.read(&size, offset: 0, length: 8) + + let peerId = key.getInt64(0) + if peerId != 0 { + assert(true) + } + let contentType = key.getUInt8(0) + if allStats.peers[PeerId(peerId)] == nil { + allStats.peers[PeerId(peerId)] = StorageBox.Stats(contentTypes: [:]) + } + allStats.peers[PeerId(peerId)]?.contentTypes[contentType] = size + + return true + }) + + self.valueBox.commit() + + return allStats } } @@ -651,9 +759,9 @@ public final class StorageBox { } } - public func getStats() -> Signal { + public func getAllStats() -> Signal { return self.impl.signalWith { impl, subscriber in - subscriber.putNext(impl.getStats()) + subscriber.putNext(impl.getAllStats()) subscriber.putCompletion() return EmptyDisposable diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift b/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift index cac05196d9..5a7ed3b93e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Resources/CollectCacheUsageStats.swift @@ -52,7 +52,7 @@ private final class CacheUsageStatsState { var upperBound: MessageIndex? } -public struct StorageUsageStats: Equatable { +public final class StorageUsageStats { public enum CategoryKey: Hashable { case photos case videos @@ -71,17 +71,17 @@ public struct StorageUsageStats: Equatable { } } - public var categories: [CategoryKey: CategoryData] + public fileprivate(set) var categories: [CategoryKey: CategoryData] public init(categories: [CategoryKey: CategoryData]) { self.categories = categories } } -public struct AllStorageUsageStats: Equatable { - public struct PeerStats: Equatable { - public var peer: EnginePeer - public var stats: StorageUsageStats +public final class AllStorageUsageStats { + public final class PeerStats { + public let peer: EnginePeer + public let stats: StorageUsageStats public init(peer: EnginePeer, stats: StorageUsageStats) { self.peer = peer @@ -89,8 +89,8 @@ public struct AllStorageUsageStats: Equatable { } } - public var totalStats: StorageUsageStats - public var peers: [EnginePeer.Id: PeerStats] + public fileprivate(set) var totalStats: StorageUsageStats + public fileprivate(set) var peers: [EnginePeer.Id: PeerStats] public init(totalStats: StorageUsageStats, peers: [EnginePeer.Id: PeerStats]) { self.totalStats = totalStats @@ -98,53 +98,10 @@ public struct AllStorageUsageStats: Equatable { } } -func _internal_collectStorageUsageStats(account: Account) -> Signal { - let additionalStats = Signal { subscriber in - DispatchQueue.global().async { - var totalSize: Int64 = 0 - - let additionalPaths: [String] = [ - "cache", - "animation-cache", - "short-cache", - ] - - for path in additionalPaths { - let fullPath: String - if path.isEmpty { - fullPath = account.postbox.mediaBox.basePath - } else { - fullPath = account.postbox.mediaBox.basePath + "/\(path)" - } - - var s = darwin_dirstat() - var result = dirstat_np(fullPath, 1, &s, MemoryLayout.size) - if result != -1 { - totalSize += Int64(s.total_size) - } else { - result = dirstat_np(fullPath, 0, &s, MemoryLayout.size) - if result != -1 { - totalSize += Int64(s.total_size) - print(s.descendants) - } - } - } - - subscriber.putNext(totalSize) - subscriber.putCompletion() - } - - return EmptyDisposable - } - - return combineLatest( - additionalStats, - account.postbox.mediaBox.storageBox.getStats() - ) - |> deliverOnMainQueue - |> mapToSignal { additionalStats, allStats -> Signal in +private extension StorageUsageStats { + convenience init(_ stats: StorageBox.Stats) { var mappedCategories: [StorageUsageStats.CategoryKey: StorageUsageStats.CategoryData] = [:] - for (key, value) in allStats.contentTypes { + for (key, value) in stats.contentTypes { let mappedCategory: StorageUsageStats.CategoryKey switch key { case MediaResourceUserContentType.image.rawValue: @@ -165,14 +122,126 @@ func _internal_collectStorageUsageStats(account: Account) -> Signal Signal { + let additionalStats = Signal { subscriber in + DispatchQueue.global().async { + var totalSize: Int64 = 0 + + let additionalPaths: [String] = [ + "cache", + "animation-cache", + "short-cache", + ] + + func statForDirectory(path: String) -> Int64 { + var s = darwin_dirstat() + var result = dirstat_np(path, 1, &s, MemoryLayout.size) + if result != -1 { + return Int64(s.total_size) + } else { + result = dirstat_np(path, 0, &s, MemoryLayout.size) + if result != -1 { + return Int64(s.total_size) + } else { + return 0 + } + } + } + + var delayedDirs: [String] = [] + + for path in additionalPaths { + let fullPath: String + if path.isEmpty { + fullPath = account.postbox.mediaBox.basePath + } else { + fullPath = account.postbox.mediaBox.basePath + "/\(path)" + } + + if path == "animation-cache" { + if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: fullPath), includingPropertiesForKeys: [.isDirectoryKey], options: .skipsSubdirectoryDescendants) { + for url in enumerator { + guard let url = url as? URL else { + continue + } + delayedDirs.append(fullPath + "/" + url.lastPathComponent) + } + } + } else { + totalSize += statForDirectory(path: fullPath) + } + } + + if !delayedDirs.isEmpty { + let concurrentSize = Atomic<[Int64]>(value: []) + + DispatchQueue.concurrentPerform(iterations: delayedDirs.count, execute: { index in + let directorySize = statForDirectory(path: delayedDirs[index]) + let result = concurrentSize.modify { current in + return current + [directorySize] + } + if result.count == delayedDirs.count { + var aggregatedCount: Int64 = 0 + for item in result { + aggregatedCount += item + } + subscriber.putNext(totalSize + aggregatedCount) + subscriber.putCompletion() + } + }) + } else { + subscriber.putNext(totalSize) + subscriber.putCompletion() + } } - return .single(AllStorageUsageStats( - totalStats: StorageUsageStats(categories: mappedCategories), - peers: [:] - )) + return EmptyDisposable + } + + return combineLatest( + additionalStats, + account.postbox.mediaBox.storageBox.getAllStats() + ) + |> deliverOnMainQueue + |> mapToSignal { additionalStats, allStats -> Signal in + return account.postbox.transaction { transaction -> AllStorageUsageStats in + let total = StorageUsageStats(allStats.total) + if additionalStats != 0 { + total.categories[.misc, default: StorageUsageStats.CategoryData(size: 0)].size += additionalStats + } + + var peers: [EnginePeer.Id: AllStorageUsageStats.PeerStats] = [:] + + for (peerId, peerStats) in allStats.peers { + if peerId.id._internalGetInt64Value() == 0 { + continue + } + + var peerSize: Int64 = 0 + for (_, size) in peerStats.contentTypes { + peerSize += size + } + if peerSize == 0 { + continue + } + + if let peer = transaction.getPeer(peerId), transaction.getPeerChatListIndex(peerId) != nil { + peers[peerId] = AllStorageUsageStats.PeerStats( + peer: EnginePeer(peer), + stats: StorageUsageStats(peerStats) + ) + } + } + + return AllStorageUsageStats( + totalStats: total, + peers: peers + ) + } } } diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/BUILD b/submodules/TelegramUI/Components/StorageUsageScreen/BUILD index c6b2aa3ce2..5e3883e0c2 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/BUILD +++ b/submodules/TelegramUI/Components/StorageUsageScreen/BUILD @@ -30,6 +30,7 @@ swift_library( "//submodules/Markdown", "//submodules/ContextUI", "//submodules/AnimatedAvatarSetNode", + "//submodules/AvatarNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift index e3cd0b51bc..09fbc4729a 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift @@ -110,10 +110,12 @@ final class PieChartComponent: Component { for i in 0 ..< component.chartData.items.count { let item = component.chartData.items[i] var angle = item.value / valueSum * CGFloat.pi * 2.0 - if angle < minAngle { - angle = minAngle + if angle > .ulpOfOne { + if angle < minAngle { + angle = minAngle + } + totalAngle += angle } - totalAngle += angle angles.append(angle) } if totalAngle > CGFloat.pi * 2.0 { @@ -207,41 +209,160 @@ final class PieChartComponent: Component { } let labelSize = label.update(transition: .immediate, component: AnyComponent(Text(text: "\(fractionString)%", font: Font.with(size: 16.0, design: .round, weight: .semibold), color: component.theme.list.itemCheckColors.foregroundColor)), environment: {}, containerSize: CGSize(width: 100.0, height: 100.0)) - var centerOffset: CGFloat = 0.5 + var labelFrame: CGRect? - var labelScale: CGFloat = 1.0 - if angleValue < 0.38 { - labelScale = angleValue / 0.38 - centerOffset = labelScale * 0.6 + (1.0 - labelScale) * 0.5 - } + for step in 0 ... 6 { + let stepFraction: CGFloat = CGFloat(step) / 6.0 + let centerOffset: CGFloat = 0.5 * (1.0 - stepFraction) + 0.65 * stepFraction - let midAngle: CGFloat = (innerStartAngle + innerEndAngle) * 0.5 - let centerDistance: CGFloat = (innerDiameter * 0.5 + (diameter * 0.5 - innerDiameter * 0.5) * centerOffset) - let labelCenter = CGPoint( - x: shapeLayerFrame.midX + cos(midAngle) * centerDistance, - y: shapeLayerFrame.midY + sin(midAngle) * centerDistance - ) - let labelFrame = CGRect(origin: CGPoint(x: labelCenter.x - labelSize.width * 0.5, y: labelCenter.y - labelSize.height * 0.5), size: labelSize) + let midAngle: CGFloat = (innerStartAngle + innerEndAngle) * 0.5 + let centerDistance: CGFloat = (innerDiameter * 0.5 + (diameter * 0.5 - innerDiameter * 0.5) * centerOffset) + + let relLabelCenter = CGPoint( + x: cos(midAngle) * centerDistance, + y: sin(midAngle) * centerDistance + ) + + let labelCenter = CGPoint( + x: shapeLayerFrame.midX + relLabelCenter.x, + y: shapeLayerFrame.midY + relLabelCenter.y + ) + + func lineCircleIntersection(_ center: CGPoint, _ p1: CGPoint, _ p2: CGPoint, _ r: CGFloat) -> CGFloat { + let dx: CGFloat = p2.x - p1.x + let dy: CGFloat = p2.y - p1.y + let dr: CGFloat = sqrt(dx * dx + dy * dy) + let D: CGFloat = p1.x * p2.y - p2.x * p1.y + + var minDistance: CGFloat = 10000.0 + + for i in 0 ..< 2 { + let signFactor: CGFloat = i == 0 ? 1.0 : (-1.0) + let dysign: CGFloat = dy < 0.0 ? -1.0 : 1.0 + let ix: CGFloat = (D * dy + signFactor * dysign * dx * sqrt(r * r * dr * dr - D * D)) / (dr * dr) + let iy: CGFloat = (-D * dx + signFactor * abs(dy) * sqrt(r * r * dr * dr - D * D)) / (dr * dr) + let distance: CGFloat = sqrt(pow(ix - center.x, 2.0) + pow(iy - center.y, 2.0)) + minDistance = min(minDistance, distance) + } + + return minDistance + } + + func lineLineIntersection(_ p1: CGPoint, _ p2: CGPoint, _ p3: CGPoint, _ p4: CGPoint) -> CGFloat { + let x1 = p1.x + let y1 = p1.y + let x2 = p2.x + let y2 = p2.y + let x3 = p3.x + let y3 = p3.y + let x4 = p4.x + let y4 = p4.y + + let d: CGFloat = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4) + if abs(d) <= 0.00001 { + return 10000.0 + } + + let px: CGFloat = ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / d + let py: CGFloat = ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / d + + let distance: CGFloat = sqrt(pow(px - p1.x, 2.0) + pow(py - p1.y, 2.0)) + return distance + } + + let intersectionOuterTopRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), diameter * 0.5) + let intersectionInnerTopRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), innerDiameter * 0.5) + let intersectionOuterBottomRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), diameter * 0.5) + let intersectionInnerBottomRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), innerDiameter * 0.5) + + let intersectionLine1TopRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerStartAngle), y: sin(innerStartAngle))) + let intersectionLine1BottomRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerStartAngle), y: sin(innerStartAngle))) + let intersectionLine2TopRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerEndAngle), y: sin(innerEndAngle))) + let intersectionLine2BottomRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerEndAngle), y: sin(innerEndAngle))) + + var distances: [CGFloat] = [ + intersectionOuterTopRight, + intersectionInnerTopRight, + intersectionOuterBottomRight, + intersectionInnerBottomRight + ] + + if angleValue < CGFloat.pi / 2.0 { + distances.append(contentsOf: [ + intersectionLine1TopRight, + intersectionLine1BottomRight, + intersectionLine2TopRight, + intersectionLine2BottomRight + ] as [CGFloat]) + } + + var minDistance: CGFloat = 1000.0 + for distance in distances { + minDistance = min(minDistance, distance + 1.0) + } + + let diagonalAngle = atan2(labelSize.height, labelSize.width) + + let maxHalfWidth = cos(diagonalAngle) * minDistance + let maxHalfHeight = sin(diagonalAngle) * minDistance + + let maxSize = CGSize(width: maxHalfWidth * 2.0, height: maxHalfHeight * 2.0) + let finalSize = CGSize(width: min(labelSize.width, maxSize.width), height: min(labelSize.height, maxSize.height)) + + let currentFrame = CGRect(origin: CGPoint(x: labelCenter.x - finalSize.width * 0.5, y: labelCenter.y - finalSize.height * 0.5), size: finalSize) + + if finalSize.width >= labelSize.width { + labelFrame = currentFrame + break + } + if let labelFrame { + if labelFrame.width > finalSize.width { + continue + } + } + labelFrame = currentFrame + } - if let labelView = label.view { + if let labelView = label.view, let labelFrame { if labelView.superview == nil { self.addSubview(labelView) } - labelView.bounds = CGRect(origin: CGPoint(), size: labelFrame.size) - transition.setPosition(view: labelView, position: labelFrame.center) - transition.setScale(view: labelView, scale: labelScale) - let normalAlpha: CGFloat = labelScale < 0.5 ? 0.0 : 1.0 + labelView.bounds = CGRect(origin: CGPoint(), size: labelSize) + var labelScale = labelFrame.width / labelSize.width + + let normalAlpha: CGFloat = labelScale < 0.4 ? 0.0 : 1.0 + + var relLabelCenter = CGPoint( + x: labelFrame.midX - shapeLayerFrame.midX, + y: labelFrame.midY - shapeLayerFrame.midY + ) if let selectedKey = self.selectedKey { if selectedKey == item.id { transition.setAlpha(view: labelView, alpha: normalAlpha) } else { transition.setAlpha(view: labelView, alpha: 0.0) + + let reducedFactor: CGFloat = (reducedDiameter - innerDiameter) / (diameter - innerDiameter) + let reducedDiameterFactor: CGFloat = reducedDiameter / diameter + + labelScale *= reducedFactor + + relLabelCenter.x *= reducedDiameterFactor + relLabelCenter.y *= reducedDiameterFactor } } else { transition.setAlpha(view: labelView, alpha: normalAlpha) } + + let labelCenter = CGPoint( + x: shapeLayerFrame.midX + relLabelCenter.x, + y: shapeLayerFrame.midY + relLabelCenter.y + ) + + transition.setPosition(view: labelView, position: labelCenter) + transition.setScale(view: labelView, scale: labelScale) } } diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerListPanelComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerListPanelComponent.swift new file mode 100644 index 0000000000..46fca7acc3 --- /dev/null +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerListPanelComponent.swift @@ -0,0 +1,435 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import SwiftSignalKit +import ViewControllerComponent +import ComponentDisplayAdapters +import TelegramPresentationData +import AccountContext +import TelegramCore +import MultilineTextComponent +import EmojiStatusComponent +import Postbox +import TelegramStringFormatting +import CheckNode +import AvatarNode + +private let avatarFont = avatarPlaceholderFont(size: 15.0) + +private final class PeerListItemComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let sideInset: CGFloat + let title: String + let peer: EnginePeer? + let label: String + let hasNext: Bool + + init( + context: AccountContext, + theme: PresentationTheme, + sideInset: CGFloat, + title: String, + peer: EnginePeer?, + label: String, + hasNext: Bool + ) { + self.context = context + self.theme = theme + self.sideInset = sideInset + self.title = title + self.peer = peer + self.label = label + self.hasNext = hasNext + } + + static func ==(lhs: PeerListItemComponent, rhs: PeerListItemComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.sideInset != rhs.sideInset { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.peer != rhs.peer { + return false + } + if lhs.label != rhs.label { + return false + } + if lhs.hasNext != rhs.hasNext { + return false + } + return true + } + + final class View: HighlightTrackingButton { + private let title = ComponentView() + private let label = ComponentView() + private let separatorLayer: SimpleLayer + private let avatarNode: AvatarNode + + private var component: PeerListItemComponent? + + override init(frame: CGRect) { + self.separatorLayer = SimpleLayer() + self.avatarNode = AvatarNode(font: avatarFont) + self.avatarNode.isLayerBacked = true + + super.init(frame: frame) + + self.layer.addSublayer(self.separatorLayer) + self.layer.addSublayer(self.avatarNode.layer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: PeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let themeUpdated = self.component?.theme !== component.theme + self.component = component + + let height: CGFloat = 52.0 + let leftInset: CGFloat = 62.0 + component.sideInset + let rightInset: CGFloat = 16.0 + component.sideInset + + let avatarSize: CGFloat = 40.0 + + self.avatarNode.frame = CGRect(origin: CGPoint(x: component.sideInset + 10.0, y: floor((height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize)) + if let peer = component.peer { + self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, displayDimensions: CGSize(width: avatarSize, height: avatarSize)) + } + + let labelSize = self.label.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.label, font: Font.regular(17.0), textColor: component.theme.list.itemSecondaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) + ) + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset - labelSize.width - 4.0, height: 100.0) + ) + + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: leftInset, y: floor((height - titleSize.height) / 2.0)), size: titleSize)) + } + if let labelView = self.label.view { + if labelView.superview == nil { + self.addSubview(labelView) + } + transition.setFrame(view: labelView, frame: CGRect(origin: CGPoint(x: availableSize.width - rightInset - labelSize.width, y: floor((height - labelSize.height) / 2.0)), size: labelSize)) + } + + if themeUpdated { + self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor + } + transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel))) + self.separatorLayer.isHidden = !component.hasNext + + return CGSize(width: availableSize.width, height: height) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class StoragePeerListPanelComponent: Component { + typealias EnvironmentType = StorageUsagePanelEnvironment + + final class Item: Equatable { + let peer: EnginePeer + let size: Int64 + + init( + peer: EnginePeer, + size: Int64 + ) { + self.peer = peer + self.size = size + } + + static func ==(lhs: Item, rhs: Item) -> Bool { + if lhs.peer != rhs.peer { + return false + } + if lhs.size != rhs.size { + return false + } + return true + } + } + + final class Items: Equatable { + let items: [Item] + + init(items: [Item]) { + self.items = items + } + + static func ==(lhs: Items, rhs: Items) -> Bool { + if lhs === rhs { + return true + } + return lhs.items == rhs.items + } + } + + let context: AccountContext + let items: Items? + + init( + context: AccountContext, + items: Items? + ) { + self.context = context + self.items = items + } + + static func ==(lhs: StoragePeerListPanelComponent, rhs: StoragePeerListPanelComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.items != rhs.items { + return false + } + return true + } + + private struct ItemLayout: Equatable { + let containerInsets: UIEdgeInsets + let containerWidth: CGFloat + let itemHeight: CGFloat + let itemCount: Int + + let contentHeight: CGFloat + + init( + containerInsets: UIEdgeInsets, + containerWidth: CGFloat, + itemHeight: CGFloat, + itemCount: Int + ) { + self.containerInsets = containerInsets + self.containerWidth = containerWidth + self.itemHeight = itemHeight + self.itemCount = itemCount + + self.contentHeight = containerInsets.top + containerInsets.bottom + CGFloat(itemCount) * itemHeight + } + + func visibleItems(for rect: CGRect) -> Range? { + let offsetRect = rect.offsetBy(dx: -self.containerInsets.left, dy: -self.containerInsets.top) + var minVisibleRow = Int(floor((offsetRect.minY) / (self.itemHeight))) + minVisibleRow = max(0, minVisibleRow) + let maxVisibleRow = Int(ceil((offsetRect.maxY) / (self.itemHeight))) + + let minVisibleIndex = minVisibleRow + let maxVisibleIndex = maxVisibleRow + + if maxVisibleIndex >= minVisibleIndex { + return minVisibleIndex ..< (maxVisibleIndex + 1) + } else { + return nil + } + } + + func itemFrame(for index: Int) -> CGRect { + return CGRect(origin: CGPoint(x: 0.0, y: self.containerInsets.top + CGFloat(index) * self.itemHeight), size: CGSize(width: self.containerWidth, height: self.itemHeight)) + } + } + + class View: UIView, UIScrollViewDelegate { + private let scrollView: UIScrollView + + private let measureItem = ComponentView() + private var visibleItems: [EnginePeer.Id: ComponentView] = [:] + + private var ignoreScrolling: Bool = false + + private var component: StoragePeerListPanelComponent? + private var environment: StorageUsagePanelEnvironment? + private var itemLayout: ItemLayout? + + override init(frame: CGRect) { + self.scrollView = UIScrollView() + + super.init(frame: frame) + + self.scrollView.delaysContentTouches = true + self.scrollView.canCancelContentTouches = true + self.scrollView.clipsToBounds = false + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + self.scrollView.contentInsetAdjustmentBehavior = .never + } + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.showsVerticalScrollIndicator = true + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.alwaysBounceHorizontal = false + self.scrollView.scrollsToTop = false + self.scrollView.delegate = self + self.scrollView.clipsToBounds = true + self.addSubview(self.scrollView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if !self.ignoreScrolling { + self.updateScrolling(transition: .immediate) + } + } + + private func updateScrolling(transition: Transition) { + guard let component = self.component, let environment = self.environment, let items = component.items, let itemLayout = self.itemLayout else { + return + } + + let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -100.0) + + let dataSizeFormatting = DataSizeStringFormatting(strings: environment.strings, decimalSeparator: ".") + + var validIds = Set() + if let visibleItems = itemLayout.visibleItems(for: visibleBounds) { + for index in visibleItems.lowerBound ..< visibleItems.upperBound { + if index >= items.items.count { + continue + } + let item = items.items[index] + let id = item.peer.id + validIds.insert(id) + + var itemTransition = transition + let itemView: ComponentView + if let current = self.visibleItems[id] { + itemView = current + } else { + itemTransition = .immediate + itemView = ComponentView() + self.visibleItems[id] = itemView + } + + let _ = itemView.update( + transition: itemTransition, + component: AnyComponent(PeerListItemComponent( + context: component.context, + theme: environment.theme, + sideInset: environment.containerInsets.left, + title: item.peer.displayTitle(strings: environment.strings, displayOrder: .firstLast), + peer: item.peer, + label: dataSizeString(item.size, formatting: dataSizeFormatting), + hasNext: index != items.items.count - 1 + )), + environment: {}, + containerSize: CGSize(width: itemLayout.containerWidth, height: itemLayout.itemHeight) + ) + let itemFrame = itemLayout.itemFrame(for: index) + if let itemComponentView = itemView.view { + if itemComponentView.superview == nil { + self.scrollView.addSubview(itemComponentView) + } + itemTransition.setFrame(view: itemComponentView, frame: itemFrame) + } + } + } + + var removeIds: [EnginePeer.Id] = [] + for (id, itemView) in self.visibleItems { + if !validIds.contains(id) { + removeIds.append(id) + if let itemComponentView = itemView.view { + transition.setAlpha(view: itemComponentView, alpha: 0.0, completion: { [weak itemComponentView] _ in + itemComponentView?.removeFromSuperview() + }) + } + } + } + for id in removeIds { + self.visibleItems.removeValue(forKey: id) + } + } + + func update(component: StoragePeerListPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + + let environment = environment[StorageUsagePanelEnvironment.self].value + self.environment = environment + + let measureItemSize = self.measureItem.update( + transition: .immediate, + component: AnyComponent(PeerListItemComponent( + context: component.context, + theme: environment.theme, + sideInset: environment.containerInsets.left, + title: "ABCDEF", + peer: nil, + label: "1000", + hasNext: false + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 1000.0) + ) + + let itemLayout = ItemLayout( + containerInsets: environment.containerInsets, + containerWidth: availableSize.width, + itemHeight: measureItemSize.height, + itemCount: component.items?.items.count ?? 0 + ) + self.itemLayout = itemLayout + + self.ignoreScrolling = true + let contentOffset = self.scrollView.bounds.minY + transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: availableSize)) + let contentSize = CGSize(width: availableSize.width, height: itemLayout.contentHeight) + if self.scrollView.contentSize != contentSize { + self.scrollView.contentSize = contentSize + } + self.scrollView.scrollIndicatorInsets = environment.containerInsets + if !transition.animation.isImmediate && self.scrollView.bounds.minY != contentOffset { + let deltaOffset = self.scrollView.bounds.minY - contentOffset + transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: -deltaOffset), to: CGPoint(), additive: true) + } + self.ignoreScrolling = false + self.updateScrolling(transition: .immediate) + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerTypeItemComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerTypeItemComponent.swift index dad53704ba..9f6bbcdcd7 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerTypeItemComponent.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerTypeItemComponent.swift @@ -205,7 +205,6 @@ final class StoragePeerTypeItemComponent: Component { } transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel))) - transition.setAlpha(layer: self.separatorLayer, alpha: component.hasNext ? 1.0 : 0.0) self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height + (component.hasNext ? UIScreenPixel : 0.0))) diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsagePanelContainerComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsagePanelContainerComponent.swift new file mode 100644 index 0000000000..2ee62f3215 --- /dev/null +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsagePanelContainerComponent.swift @@ -0,0 +1,377 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import ComponentDisplayAdapters +import TelegramPresentationData + +final class StorageUsagePanelEnvironment: Equatable { + let theme: PresentationTheme + let strings: PresentationStrings + let containerInsets: UIEdgeInsets + + init( + theme: PresentationTheme, + strings: PresentationStrings, + containerInsets: UIEdgeInsets + ) { + self.theme = theme + self.strings = strings + self.containerInsets = containerInsets + } + + static func ==(lhs: StorageUsagePanelEnvironment, rhs: StorageUsagePanelEnvironment) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.containerInsets != rhs.containerInsets { + return false + } + return true + } +} + +private final class StorageUsageHeaderItemComponent: CombinedComponent { + let theme: PresentationTheme + let title: String + + init( + theme: PresentationTheme, + title: String + ) { + self.theme = theme + self.title = title + } + + static func ==(lhs: StorageUsageHeaderItemComponent, rhs: StorageUsageHeaderItemComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.title != rhs.title { + return false + } + return true + } + + static var body: Body { + let text = Child(Text.self) + + return { context in + let text = text.update( + component: Text(text: context.component.title, font: Font.semibold(15.0), color: context.component.theme.list.itemAccentColor), + availableSize: context.availableSize, + transition: .immediate + ) + + context.add(text.position(CGPoint(x: text.size.width * 0.5, y: text.size.height * 0.5))) + + return text.size + } + } +} + +private final class StorageUsageHeaderComponent: Component { + struct Item: Equatable { + let id: AnyHashable + let title: String + + init( + id: AnyHashable, + title: String + ) { + self.id = id + self.title = title + } + } + + let theme: PresentationTheme + let items: [Item] + + init( + theme: PresentationTheme, + items: [Item] + ) { + self.theme = theme + self.items = items + } + + static func ==(lhs: StorageUsageHeaderComponent, rhs: StorageUsageHeaderComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.items != rhs.items { + return false + } + return true + } + + class View: UIView { + private var component: StorageUsageHeaderComponent? + + private var visibleItems: [AnyHashable: ComponentView] = [:] + private let activeItemLayer: SimpleLayer + + override init(frame: CGRect) { + self.activeItemLayer = SimpleLayer() + self.activeItemLayer.cornerRadius = 2.0 + self.activeItemLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + + super.init(frame: frame) + + self.layer.addSublayer(self.activeItemLayer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: StorageUsageHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let themeUpdated = self.component?.theme !== component.theme + + self.component = component + + var validIds = Set() + for item in component.items { + validIds.insert(item.id) + + let itemView: ComponentView + var itemTransition = transition + if let current = self.visibleItems[item.id] { + itemView = current + } else { + itemTransition = .immediate + itemView = ComponentView() + self.visibleItems[item.id] = itemView + } + let itemSize = itemView.update( + transition: itemTransition, + component: AnyComponent(StorageUsageHeaderItemComponent( + theme: component.theme, + title: item.title + )), + environment: {}, + containerSize: availableSize + ) + let itemFrame = CGRect(origin: CGPoint(x: 34.0, y: floor((availableSize.height - itemSize.height) / 2.0)), size: itemSize) + if let itemComponentView = itemView.view { + if itemComponentView.superview == nil { + self.addSubview(itemComponentView) + } + itemTransition.setFrame(view: itemComponentView, frame: itemFrame) + } + + transition.setFrame(layer: self.activeItemLayer, frame: CGRect(origin: CGPoint(x: itemFrame.minX, y: availableSize.height - 3.0), size: CGSize(width: itemFrame.width, height: 3.0))) + } + + if themeUpdated { + self.activeItemLayer.backgroundColor = component.theme.list.itemAccentColor.cgColor + } + + var removeIds: [AnyHashable] = [] + for (id, itemView) in self.visibleItems { + if !validIds.contains(id) { + removeIds.append(id) + if let itemComponentView = itemView.view { + itemComponentView.removeFromSuperview() + } + } + } + for id in removeIds { + self.visibleItems.removeValue(forKey: id) + } + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class StorageUsagePanelContainerComponent: Component { + struct Item: Equatable { + let id: AnyHashable + let title: String + let panel: AnyComponent + + init( + id: AnyHashable, + title: String, + panel: AnyComponent + ) { + self.id = id + self.title = title + self.panel = panel + } + } + + let theme: PresentationTheme + let strings: PresentationStrings + let insets: UIEdgeInsets + let items: [Item] + + init( + theme: PresentationTheme, + strings: PresentationStrings, + insets: UIEdgeInsets, + items: [Item] + ) { + self.theme = theme + self.strings = strings + self.insets = insets + self.items = items + } + + static func ==(lhs: StorageUsagePanelContainerComponent, rhs: StorageUsagePanelContainerComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.insets != rhs.insets { + return false + } + if lhs.items != rhs.items { + return false + } + return true + } + + class View: UIView { + private let topPanelBackgroundView: BlurredBackgroundView + private let topPanelSeparatorLayer: SimpleLayer + private let header = ComponentView() + + private var component: StorageUsagePanelContainerComponent? + + private var visiblePanels: [AnyHashable: ComponentView] = [:] + private var currentId: AnyHashable? + + override init(frame: CGRect) { + self.topPanelBackgroundView = BlurredBackgroundView(color: nil, enableBlur: true) + self.topPanelSeparatorLayer = SimpleLayer() + + super.init(frame: frame) + + self.addSubview(self.topPanelBackgroundView) + self.layer.addSublayer(self.topPanelSeparatorLayer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: StorageUsagePanelContainerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let themeUpdated = self.component?.theme !== component.theme + + self.component = component + + if themeUpdated { + self.backgroundColor = component.theme.list.itemBlocksBackgroundColor + self.topPanelBackgroundView.updateColor(color: component.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.8), transition: .immediate) + self.topPanelSeparatorLayer.backgroundColor = component.theme.list.itemBlocksSeparatorColor.cgColor + } + + let topPanelFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: 44.0)) + transition.setFrame(view: self.topPanelBackgroundView, frame: topPanelFrame) + self.topPanelBackgroundView.update(size: topPanelFrame.size, transition: transition.containedViewLayoutTransition) + + transition.setFrame(layer: self.topPanelSeparatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: availableSize.width, height: UIScreenPixel))) + + let _ = self.header.update( + transition: transition, + component: AnyComponent(StorageUsageHeaderComponent( + theme: component.theme, + items: component.items.map { item -> StorageUsageHeaderComponent.Item in + return StorageUsageHeaderComponent.Item( + id: item.id, + title: item.title + ) + } + )), + environment: {}, + containerSize: topPanelFrame.size + ) + if let headerView = self.header.view { + if headerView.superview == nil { + self.addSubview(headerView) + } + transition.setFrame(view: headerView, frame: topPanelFrame) + } + + if let currentIdValue = self.currentId, !component.items.contains(where: { $0.id == currentIdValue }) { + self.currentId = nil + } + if self.currentId == nil { + self.currentId = component.items.first?.id + } + + let childEnvironment = StorageUsagePanelEnvironment( + theme: component.theme, + strings: component.strings, + containerInsets: UIEdgeInsets(top: topPanelFrame.height, left: component.insets.left, bottom: component.insets.bottom, right: component.insets.right) + ) + + var validIds = Set() + if let currentId = self.currentId, let panelItem = component.items.first(where: { $0.id == currentId }) { + validIds.insert(panelItem.id) + + let panel: ComponentView + var panelTransition = transition + if let current = self.visiblePanels[panelItem.id] { + panel = current + } else { + panelTransition = .immediate + panel = ComponentView() + self.visiblePanels[panelItem.id] = panel + } + let _ = panel.update( + transition: panelTransition, + component: panelItem.panel, + environment: { + childEnvironment + }, + containerSize: availableSize + ) + if let panelView = panel.view { + if panelView.superview == nil { + self.insertSubview(panelView, belowSubview: self.topPanelBackgroundView) + } + panelTransition.setFrame(view: panelView, frame: CGRect(origin: CGPoint(), size: availableSize)) + } + } + + var removeIds: [AnyHashable] = [] + for (id, panel) in self.visiblePanels { + if !validIds.contains(id) { + removeIds.append(id) + if let panelView = panel.view { + panelView.removeFromSuperview() + } + } + } + for id in removeIds { + self.visiblePanels.removeValue(forKey: id) + } + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift index 56f6806935..230f0db7a0 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift @@ -41,6 +41,21 @@ private final class StorageUsageScreenComponent: Component { 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 = self.contentSize.height - self.bounds.height + self.bounces = false + } else { + self.bounces = true + } + super.contentOffset = value + } get { + return super.contentOffset + } + } } private final class AnimationHint { @@ -56,6 +71,7 @@ private final class StorageUsageScreenComponent: Component { private var currentStats: AllStorageUsageStats? private var cacheSettings: CacheStorageSettings? + private var peerItems: StoragePeerListPanelComponent.Items? private var selectedCategories: Set = Set() @@ -79,11 +95,17 @@ private final class StorageUsageScreenComponent: Component { private var keepDurationSectionContainerView: UIView private var keepDurationItems: [AnyHashable: ComponentView] = [:] + private let panelContainer = ComponentView() + 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? @@ -107,7 +129,6 @@ private final class StorageUsageScreenComponent: Component { super.init(frame: frame) - self.scrollView.layer.anchorPoint = CGPoint() self.scrollView.delaysContentTouches = true self.scrollView.canCancelContentTouches = true self.scrollView.clipsToBounds = false @@ -144,12 +165,49 @@ private final class StorageUsageScreenComponent: Component { self.statsDisposable?.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) { + guard let navigationMetrics = self.navigationMetrics else { + return + } + let _ = navigationMetrics + + 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 @@ -183,6 +241,8 @@ private final class StorageUsageScreenComponent: Component { self.component = component self.state = state + let environment = environment[ViewControllerComponentContainer.Environment.self].value + if self.statsDisposable == nil { self.cacheSettingsDisposable = (component.context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.cacheStorageSettings]) |> map { sharedData -> CacheStorageSettings in @@ -210,10 +270,40 @@ private final class StorageUsageScreenComponent: Component { return } self.currentStats = stats + + var peerItems: [StoragePeerListPanelComponent.Item] = [] + + 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 + )) + } + + self.peerItems = StoragePeerListPanelComponent.Items(items: peerItems) + self.state?.updated(transition: Transition(animation: .none).withUserData(AnimationHint(isFirstStatsUpdate: true))) }) } + var wasLockedAtPanels = false + if let panelContainerView = self.panelContainer.view, let navigationMetrics = self.navigationMetrics { + if abs(self.scrollView.bounds.minY - (panelContainerView.frame.minY - navigationMetrics.navigationHeight)) <= UIScreenPixel { + wasLockedAtPanels = true + } + } + let animationHint = transition.userData(AnimationHint.self) if let animationHint, animationHint.isFirstStatsUpdate { @@ -228,8 +318,6 @@ private final class StorageUsageScreenComponent: Component { transition.setAlpha(view: self.headerOffsetContainer, alpha: self.currentStats != nil ? 1.0 : 0.0) } - let environment = environment[ViewControllerComponentContainer.Environment.self].value - self.controller = environment.controller self.navigationMetrics = (environment.navigationHeight, environment.statusBarHeight) @@ -300,7 +388,7 @@ private final class StorageUsageScreenComponent: Component { var contentHeight: CGFloat = 0.0 let topInset: CGFloat = 19.0 - let sideInset: CGFloat = 16.0 + let sideInset: CGFloat = 16.0 + environment.safeInsets.left contentHeight += environment.statusBarHeight + topInset @@ -682,17 +770,52 @@ private final class StorageUsageScreenComponent: Component { contentHeight += keepDurationDescriptionSize.height contentHeight += 40.0 - contentHeight += availableSize.height + //TODO:localize + let panelContainerSize = self.panelContainer.update( + transition: transition, + component: AnyComponent(StorageUsagePanelContainerComponent( + theme: environment.theme, + strings: environment.strings, + insets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: environment.safeInsets.bottom, right: environment.safeInsets.right), + items: [ + StorageUsagePanelContainerComponent.Item( + id: "peers", + title: "Chats", + panel: AnyComponent(StoragePeerListPanelComponent( + context: component.context, + items: self.peerItems + )) + ) + ]) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: availableSize.height - environment.navigationHeight) + ) + if let panelContainerView = self.panelContainer.view { + if panelContainerView.superview == nil { + self.scrollView.addSubview(panelContainerView) + } + transition.setFrame(view: panelContainerView, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: panelContainerSize)) + } + contentHeight += panelContainerSize.height self.ignoreScrolling = true let contentOffset = self.scrollView.bounds.minY - transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: availableSize)) + 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 } - if !transition.animation.isImmediate && self.scrollView.bounds.minY != contentOffset { + + 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) } diff --git a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m index 28bbb9cbd8..9230689fca 100644 --- a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m +++ b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m @@ -116,6 +116,9 @@ static bool notyfyingShiftState = false; @implementation UIScrollView (FrameRateRangeOverride) - (void)fixScrollDisplayLink { + if (@available(iOS 16.0, *)) { + return; + } static NSString *scrollHeartbeatKey = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{