From 808f5b80ffc19bf45db16881d0903d6c245cdca4 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Sat, 24 Dec 2022 00:04:48 +0400 Subject: [PATCH] Storage improvements --- .../Sources/CallListController.swift | 2 +- .../ChatListFilterTabContainerNode.swift | 4 +- submodules/CheckNode/Sources/CheckNode.swift | 13 + .../Source/Base/Transition.swift | 32 + .../Source/Host/ComponentHostView.swift | 9 + .../Sources/DebugController.swift | 80 +- .../Display/Source/CAAnimationUtils.swift | 45 +- .../Display/Source/DisplayLinkAnimator.swift | 12 +- .../Sources/PhotoResources.swift | 80 +- .../Sources/GifPagerContentComponent.swift | 2 +- .../Sources/MultiplexedVideoNode.swift | 2 +- .../Components/StorageUsageScreen/BUILD | 2 + .../Sources/PieChartComponent.swift | 512 ++++--- .../Sources/StorageCategoriesComponent.swift | 44 +- .../StorageCategoryItemCompoment.swift | 10 +- .../StorageFileListPanelComponent.swift | 1000 ++++++++++++ .../StoragePeerListPanelComponent.swift | 183 ++- .../StorageUsagePanelContainerComponent.swift | 483 +++++- .../Sources/StorageUsageScreen.swift | 1353 +++++++++++++---- ...geUsageScreenSelectionPanelComponent.swift | 157 ++ .../TelegramUI/Sources/AppDelegate.swift | 2 +- .../ChatContextResultPeekContentNode.swift | 2 +- ...ListContextResultsChatInputPanelItem.swift | 2 +- .../Panes/PeerInfoVisualMediaPaneNode.swift | 2 +- .../Sources/PeerInfoGifPaneNode.swift | 4 +- 25 files changed, 3383 insertions(+), 654 deletions(-) create mode 100644 submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageFileListPanelComponent.swift create mode 100644 submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreenSelectionPanelComponent.swift diff --git a/submodules/CallListUI/Sources/CallListController.swift b/submodules/CallListUI/Sources/CallListController.swift index bcd23d8caa..c29f646cc4 100644 --- a/submodules/CallListUI/Sources/CallListController.swift +++ b/submodules/CallListUI/Sources/CallListController.swift @@ -308,7 +308,7 @@ public final class CallListController: TelegramBaseController { var cancelImpl: (() -> Void)? let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } let progressSignal = Signal { subscriber in - let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { cancelImpl?() })) strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) diff --git a/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift b/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift index ddb971bef5..ad2d5de24b 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift @@ -950,7 +950,9 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { } self.scrollNode.bounds = updatedBounds } - transition.animateHorizontalOffsetAdditive(node: self.scrollNode, offset: previousScrollBounds.minX - self.scrollNode.bounds.minX) + if abs(previousScrollBounds.minX - self.scrollNode.bounds.minX) > .ulpOfOne { + transition.animateHorizontalOffsetAdditive(node: self.scrollNode, offset: previousScrollBounds.minX - self.scrollNode.bounds.minX) + } self.previousSelectedAbsFrame = selectedFrame.offsetBy(dx: -self.scrollNode.bounds.minX, dy: 0.0) self.previousSelectedFrame = selectedFrame diff --git a/submodules/CheckNode/Sources/CheckNode.swift b/submodules/CheckNode/Sources/CheckNode.swift index 227c77c35b..029afd556d 100644 --- a/submodules/CheckNode/Sources/CheckNode.swift +++ b/submodules/CheckNode/Sources/CheckNode.swift @@ -321,6 +321,19 @@ public class CheckLayer: CALayer { self.isOpaque = false } + public override init(layer: Any) { + guard let layer = layer as? CheckLayer else { + preconditionFailure() + } + + self.theme = layer.theme + self.content = layer.content + + super.init(layer: layer) + + self.isOpaque = false + } + public init(theme: CheckNodeTheme, content: CheckNodeContent = .check) { self.theme = theme self.content = content diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift index efcbe348be..18860abb54 100644 --- a/submodules/ComponentFlow/Source/Base/Transition.swift +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -765,4 +765,36 @@ public struct Transition { } } } + + public func setBackgroundColor(view: UIView, color: UIColor, completion: ((Bool) -> Void)? = nil) { + self.setBackgroundColor(layer: view.layer, color: color, completion: completion) + } + + public func setBackgroundColor(layer: CALayer, color: UIColor, completion: ((Bool) -> Void)? = nil) { + if let current = layer.backgroundColor, current == color.cgColor { + completion?(true) + return + } + + switch self.animation { + case .none: + layer.backgroundColor = color.cgColor + completion?(true) + case let .curve(duration, curve): + let previousColor: CGColor = layer.backgroundColor ?? UIColor.clear.cgColor + layer.backgroundColor = color.cgColor + + layer.animate( + from: previousColor, + to: color.cgColor, + keyPath: "backgroundColor", + duration: duration, + delay: 0.0, + curve: curve, + removeOnCompletion: true, + additive: false, + completion: completion + ) + } + } } diff --git a/submodules/ComponentFlow/Source/Host/ComponentHostView.swift b/submodules/ComponentFlow/Source/Host/ComponentHostView.swift index affc53ed1c..2c8a3d87b6 100644 --- a/submodules/ComponentFlow/Source/Host/ComponentHostView.swift +++ b/submodules/ComponentFlow/Source/Host/ComponentHostView.swift @@ -155,6 +155,15 @@ public final class ComponentView { self.currentSize = size return size } + + public func updateEnvironment(transition: Transition, @EnvironmentBuilder environment: () -> Environment) -> CGSize? { + guard let currentComponent = self.currentComponent, let currentContainerSize = self.currentContainerSize else { + return nil + } + let size = self._update(transition: transition, component: currentComponent, maybeEnvironment: environment, updateEnvironment: true, forceUpdate: false, containerSize: currentContainerSize) + self.currentSize = size + return size + } private func _update(transition: Transition, component: AnyComponent, maybeEnvironment: () -> Environment, updateEnvironment: Bool, forceUpdate: Bool, containerSize: CGSize) -> CGSize { precondition(!self.isUpdating) diff --git a/submodules/DebugSettingsUI/Sources/DebugController.swift b/submodules/DebugSettingsUI/Sources/DebugController.swift index ed13290c23..6bd98f1dbb 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 reindexCache case resetBiometricsData(PresentationTheme) case resetWebViewCache(PresentationTheme) case optimizeDatabase(PresentationTheme) @@ -111,7 +112,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, .resetBiometricsData, .resetWebViewCache, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .playerEmbedding, .playlistPlayback, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .experimentalBackground, .inlineForums, .localTranscription, . enableReactionOverrides, .restorePurchases: + 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: return DebugControllerSection.experiments.rawValue case .preferredVideoCodec: return DebugControllerSection.videoExperiments.rawValue @@ -170,40 +171,42 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 21 case .reindexUnread: return 22 - case .resetBiometricsData: + case .reindexCache: return 23 - case .resetWebViewCache: + case .resetBiometricsData: return 24 - case .optimizeDatabase: + case .resetWebViewCache: return 25 - case .photoPreview: + case .optimizeDatabase: return 26 - case .knockoutWallpaper: + case .photoPreview: return 27 - case .experimentalCompatibility: + case .knockoutWallpaper: return 28 - case .enableDebugDataDisplay: + case .experimentalCompatibility: return 29 - case .acceleratedStickers: + case .enableDebugDataDisplay: return 30 - case .experimentalBackground: + case .acceleratedStickers: return 31 - case .inlineForums: + case .experimentalBackground: return 32 - case .localTranscription: + case .inlineForums: return 33 - case .enableReactionOverrides: + case .localTranscription: return 34 - case .restorePurchases: + case .enableReactionOverrides: return 35 - case .playerEmbedding: + case .restorePurchases: return 36 - case .playlistPlayback: + case .playerEmbedding: return 37 - case .voiceConference: + case .playlistPlayback: return 38 + case .voiceConference: + return 39 case let .preferredVideoCodec(index, _, _, _): - return 39 + index + return 40 + index case .disableVideoAspectScaling: return 100 case .enableVoipTcp: @@ -967,6 +970,46 @@ private enum DebugControllerEntry: ItemListNodeEntry { controller.dismiss() }) }) + 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 { + return + } + + var signal = context.engine.resources.reindexCacheInBackground(lowImpact: false) + + var cancelImpl: (() -> Void)? + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let progressSignal = Signal { subscriber in + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { + cancelImpl?() + })) + arguments.presentController(controller, nil) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } + } + } + |> runOn(Queue.mainQueue()) + |> delay(0.15, queue: Queue.mainQueue()) + let progressDisposable = progressSignal.start() + + let reindexDisposable = MetaDisposable() + + signal = signal + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + cancelImpl = { + reindexDisposable.set(nil) + } + reindexDisposable.set((signal + |> deliverOnMainQueue).start(completed: { + })) + }) case .resetBiometricsData: return ItemListActionItem(presentationData: presentationData, title: "Reset Biometrics Data", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { let _ = updatePresentationPasscodeSettingsInteractively(accountManager: arguments.sharedContext.accountManager, { settings in @@ -1210,6 +1253,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present entries.append(.resetHoles(presentationData.theme)) if isMainApp { entries.append(.reindexUnread(presentationData.theme)) + entries.append(.reindexCache) entries.append(.resetWebViewCache(presentationData.theme)) } entries.append(.optimizeDatabase(presentationData.theme)) diff --git a/submodules/Display/Source/CAAnimationUtils.swift b/submodules/Display/Source/CAAnimationUtils.swift index 77f1390c67..2da1a6b38d 100644 --- a/submodules/Display/Source/CAAnimationUtils.swift +++ b/submodules/Display/Source/CAAnimationUtils.swift @@ -34,6 +34,47 @@ private let completionKey = "CAAnimationUtils_completion" public let kCAMediaTimingFunctionSpring = "CAAnimationUtilsSpringCurve" public let kCAMediaTimingFunctionCustomSpringPrefix = "CAAnimationUtilsSpringCustomCurve" +private final class FrameRangeContext { + private var animationCount: Int = 0 + private var displayLink: CADisplayLink? + + init() { + } + + func add() { + self.animationCount += 1 + self.update() + } + + func remove() { + self.animationCount -= 1 + if self.animationCount < 0 { + self.animationCount = 0 + assertionFailure() + } + self.update() + } + + @objc func displayEvent() { + } + + private func update() { + if self.animationCount != 0 { + if self.displayLink == nil { + let displayLink = CADisplayLink(target: self, selector: #selector(self.displayEvent)) + self.displayLink = displayLink + displayLink.add(to: .main, forMode: .common) + displayLink.isPaused = false + } + } else if let displayLink = self.displayLink { + self.displayLink = nil + displayLink.invalidate() + } + } +} + +private let frameRangeContext = FrameRangeContext() + public extension CAAnimation { var completion: ((Bool) -> Void)? { get { @@ -56,7 +97,7 @@ private func adjustFrameRate(animation: CAAnimation) { if #available(iOS 15.0, *) { let maxFps = Float(UIScreen.main.maximumFramesPerSecond) if maxFps > 61.0 { - animation.preferredFrameRateRange = CAFrameRateRange(minimum: maxFps, maximum: maxFps, preferred: maxFps) + animation.preferredFrameRateRange = CAFrameRateRange(minimum: 60.0, maximum: maxFps, preferred: maxFps) } } } @@ -176,6 +217,8 @@ public extension CALayer { animationGroup.delegate = CALayerAnimationDelegate(animation: animationGroup, completion: completion) } + adjustFrameRate(animation: animationGroup) + self.add(animationGroup, forKey: key) } diff --git a/submodules/Display/Source/DisplayLinkAnimator.swift b/submodules/Display/Source/DisplayLinkAnimator.swift index ff3239a4b4..4eb0d836bf 100644 --- a/submodules/Display/Source/DisplayLinkAnimator.swift +++ b/submodules/Display/Source/DisplayLinkAnimator.swift @@ -81,16 +81,12 @@ public final class ConstantDisplayLinkAnimator { guard let displayLink = self.displayLink else { return } - if #available(iOS 10.0, *) { - let preferredFramesPerSecond: Int - if self.frameInterval == 1 { - preferredFramesPerSecond = 60 - } else { - preferredFramesPerSecond = 30 + if self.frameInterval == 1 { + if #available(iOS 15.0, *) { + self.displayLink?.preferredFrameRateRange = CAFrameRateRange(minimum: 60.0, maximum: 120.0, preferred: 120.0) } - displayLink.preferredFramesPerSecond = preferredFramesPerSecond } else { - displayLink.frameInterval = self.frameInterval + displayLink.preferredFramesPerSecond = 30 } } diff --git a/submodules/PhotoResources/Sources/PhotoResources.swift b/submodules/PhotoResources/Sources/PhotoResources.swift index b533cc03a1..7e3202b4cd 100644 --- a/submodules/PhotoResources/Sources/PhotoResources.swift +++ b/submodules/PhotoResources/Sources/PhotoResources.swift @@ -55,7 +55,7 @@ public func representationFetchRangeForDisplayAtSize(representation: TelegramMed return nil } -public func chatMessagePhotoDatas(postbox: Postbox, userLocation: MediaResourceUserLocation, photoReference: ImageMediaReference, fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0), autoFetchFullSize: Bool = false, tryAdditionalRepresentations: Bool = false, synchronousLoad: Bool = false, useMiniThumbnailIfAvailable: Bool = false, forceThumbnail: Bool = false) -> Signal, NoError> { +public func chatMessagePhotoDatas(postbox: Postbox, userLocation: MediaResourceUserLocation, photoReference: ImageMediaReference, fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0), autoFetchFullSize: Bool = false, tryAdditionalRepresentations: Bool = false, synchronousLoad: Bool = false, useMiniThumbnailIfAvailable: Bool = false, forceThumbnail: Bool = false, automaticFetch: Bool = true) -> Signal, NoError> { if !forceThumbnail, let progressiveRepresentation = progressiveImageRepresentation(photoReference.media.representations), progressiveRepresentation.progressiveSizes.count > 1 { enum SizeSource { case miniThumbnail(data: Data) @@ -129,10 +129,12 @@ public func chatMessagePhotoDatas(postbox: Postbox, userLocation: MediaResourceU } }) var fetchDisposable: Disposable? - if autoFetchFullSize { - fetchDisposable = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: .image, reference: photoReference.resourceReference(progressiveRepresentation.resource), range: (0 ..< Int64(largestByteSize), .default), statsCategory: .image).start() - } else if useMiniThumbnailIfAvailable { - fetchDisposable = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: .image, reference: photoReference.resourceReference(progressiveRepresentation.resource), range: (0 ..< Int64(thumbnailByteSize), .default), statsCategory: .image).start() + if automaticFetch { + if autoFetchFullSize { + fetchDisposable = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: .image, reference: photoReference.resourceReference(progressiveRepresentation.resource), range: (0 ..< Int64(largestByteSize), .default), statsCategory: .image).start() + } else if useMiniThumbnailIfAvailable { + fetchDisposable = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: .image, reference: photoReference.resourceReference(progressiveRepresentation.resource), range: (0 ..< Int64(thumbnailByteSize), .default), statsCategory: .image).start() + } } return ActionDisposable { @@ -1353,7 +1355,7 @@ public func avatarGalleryThumbnailPhoto(account: Account, representations: [Imag } } -public func mediaGridMessagePhoto(account: Account, userLocation: MediaResourceUserLocation, photoReference: ImageMediaReference, fullRepresentationSize: CGSize = CGSize(width: 127.0, height: 127.0), blurred: Bool = false, synchronousLoad: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { +public func mediaGridMessagePhoto(account: Account, userLocation: MediaResourceUserLocation, photoReference: ImageMediaReference, fullRepresentationSize: CGSize = CGSize(width: 127.0, height: 127.0), blurred: Bool = false, synchronousLoad: Bool = false, automaticFetch: Bool = true) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let useMiniThumbnailIfAvailable: Bool = fullRepresentationSize.width < 40.0 var updatedFullRepresentationSize = fullRepresentationSize if useMiniThumbnailIfAvailable, let largest = largestImageRepresentation(photoReference.media.representations) { @@ -1361,7 +1363,7 @@ public func mediaGridMessagePhoto(account: Account, userLocation: MediaResourceU updatedFullRepresentationSize = largest.dimensions.cgSize } } - let signal = chatMessagePhotoDatas(postbox: account.postbox, userLocation: userLocation, photoReference: photoReference, fullRepresentationSize: updatedFullRepresentationSize, autoFetchFullSize: true, tryAdditionalRepresentations: useMiniThumbnailIfAvailable, synchronousLoad: synchronousLoad, useMiniThumbnailIfAvailable: useMiniThumbnailIfAvailable, forceThumbnail: blurred) + let signal = chatMessagePhotoDatas(postbox: account.postbox, userLocation: userLocation, photoReference: photoReference, fullRepresentationSize: updatedFullRepresentationSize, autoFetchFullSize: true, tryAdditionalRepresentations: useMiniThumbnailIfAvailable, synchronousLoad: synchronousLoad, useMiniThumbnailIfAvailable: useMiniThumbnailIfAvailable, forceThumbnail: blurred, automaticFetch: automaticFetch) return signal |> map { value in @@ -1468,7 +1470,7 @@ public func gifPaneVideoThumbnail(account: Account, videoReference: FileMediaRef }, completed: { subscriber.putCompletion() }) - let fetched = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .gif, reference: videoReference.resourceReference(thumbnailResource)).start() + let fetched = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: videoReference.resourceReference(thumbnailResource)).start() return ActionDisposable { data.dispose() fetched.dispose() @@ -1847,10 +1849,10 @@ public func chatMessageWebFileCancelInteractiveFetch(account: Account, image: Te return account.postbox.mediaBox.cancelInteractiveResourceFetch(image.resource) } -public func chatWebpageSnippetFileData(account: Account, userLocation: MediaResourceUserLocation, mediaReference: AnyMediaReference, resource: MediaResource) -> Signal { +public func chatWebpageSnippetFileData(account: Account, userLocation: MediaResourceUserLocation, mediaReference: AnyMediaReference, resource: MediaResource, automaticFetch: Bool = true) -> Signal { let resourceData = account.postbox.mediaBox.resourceData(resource) - |> map { next in - return next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedIfSafe) + |> map { next in + return next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedIfSafe) } return Signal { subscriber in @@ -1861,7 +1863,16 @@ public func chatWebpageSnippetFileData(account: Account, userLocation: MediaReso }, completed: { subscriber.putCompletion() })) - disposable.add(fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: userLocation, userContentType: .image, reference: mediaReference.resourceReference(resource)).start()) + if automaticFetch { + var userContentType: MediaResourceUserContentType = .other + if let file = mediaReference.media as? TelegramMediaFile { + userContentType = MediaResourceUserContentType(file: file) + } else if let _ = mediaReference.media as? TelegramMediaImage { + userContentType = .image + } + + disposable.add(fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: userLocation, userContentType: userContentType, reference: mediaReference.resourceReference(resource)).start()) + } return disposable } } @@ -1889,8 +1900,8 @@ public func chatWebpageSnippetPhotoData(account: Account, userLocation: MediaRes } } -public func chatWebpageSnippetFile(account: Account, userLocation: MediaResourceUserLocation, mediaReference: AnyMediaReference, representation: TelegramMediaImageRepresentation) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let signal = chatWebpageSnippetFileData(account: account, userLocation: userLocation, mediaReference: mediaReference, resource: representation.resource) +public func chatWebpageSnippetFile(account: Account, userLocation: MediaResourceUserLocation, mediaReference: AnyMediaReference, representation: TelegramMediaImageRepresentation, automaticFetch: Bool = true) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + let signal = chatWebpageSnippetFileData(account: account, userLocation: userLocation, mediaReference: mediaReference, resource: representation.resource, automaticFetch: automaticFetch) return signal |> map { fullSizeData in return { arguments in @@ -1904,7 +1915,42 @@ public func chatWebpageSnippetFile(account: Account, userLocation: MediaResource } } - if let fullSizeImage = fullSizeImage { + var blurredImage: UIImage? + if fullSizeImage == nil { + var immediateThumbnailData: Data? + if let file = mediaReference.media as? TelegramMediaFile { + immediateThumbnailData = file.immediateThumbnailData + } else if let image = mediaReference.media as? TelegramMediaImage { + immediateThumbnailData = image.immediateThumbnailData + } + + if let decodedThumbnailData = immediateThumbnailData.flatMap(decodeTinyThumbnail), let imageSource = CGImageSourceCreateWithData(decodedThumbnailData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { + let thumbnailSize = CGSize(width: image.width, height: image.height) + let thumbnailContextSize = thumbnailSize.aspectFilled(CGSize(width: 20.0, height: 20.0)) + if let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) { + thumbnailContext.withFlippedContext { c in + c.interpolationQuality = .none + c.draw(image, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) + } + imageFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + + let thumbnailContext2Size = thumbnailSize.aspectFitted(CGSize(width: 100.0, height: 100.0)) + if let thumbnailContext2 = DrawingContext(size: thumbnailContext2Size, scale: 1.0) { + thumbnailContext2.withFlippedContext { c in + c.interpolationQuality = .none + if let image = thumbnailContext.generateImage()?.cgImage { + c.draw(image, in: CGRect(origin: CGPoint(), size: thumbnailContext2Size)) + } + } + imageFastBlur(Int32(thumbnailContext2Size.width), Int32(thumbnailContext2Size.height), Int32(thumbnailContext2.bytesPerRow), thumbnailContext2.bytes) + + blurredImage = thumbnailContext2.generateImage() + } + } + } + } + + if let fullSizeImage = fullSizeImage ?? (blurredImage?.cgImage) { guard let context = DrawingContext(size: arguments.drawingSize, clear: true) else { return nil } @@ -2455,9 +2501,9 @@ private func avatarGalleryPhotoDatas(account: Account, fileReference: FileMediaR if let _ = decodedThumbnailData { fetchedThumbnail = .complete() } else { - fetchedThumbnail = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .image, reference: representations[smallestIndex].reference) + fetchedThumbnail = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .avatar, reference: representations[smallestIndex].reference) } - let fetchedFullSize = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .image, reference: representations[largestIndex].reference) + let fetchedFullSize = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .avatar, reference: representations[largestIndex].reference) let thumbnail = Signal { subscriber in if let decodedThumbnailData = decodedThumbnailData { diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift index 8f6525bd8c..84da5e9505 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift @@ -113,7 +113,7 @@ private class GifVideoLayer: AVSampleBufferDisplayLayer { guard let file = self.file else { return } - let frameManager = SoftwareVideoLayerFrameManager(account: self.context.account, userLocation: self.userLocation, userContentType: .gif, fileReference: .savedGif(media: file), layerHolder: nil, layer: self) + let frameManager = SoftwareVideoLayerFrameManager(account: self.context.account, userLocation: self.userLocation, userContentType: .other, fileReference: .savedGif(media: file), layerHolder: nil, layer: self) self.frameManager = frameManager frameManager.started = { [weak self] in guard let strongSelf = self else { diff --git a/submodules/TelegramUI/Components/MultiplexedVideoNode/Sources/MultiplexedVideoNode.swift b/submodules/TelegramUI/Components/MultiplexedVideoNode/Sources/MultiplexedVideoNode.swift index 52d1183f1f..aec600797c 100644 --- a/submodules/TelegramUI/Components/MultiplexedVideoNode/Sources/MultiplexedVideoNode.swift +++ b/submodules/TelegramUI/Components/MultiplexedVideoNode/Sources/MultiplexedVideoNode.swift @@ -464,7 +464,7 @@ public final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate { layerHolder.layer.videoGravity = AVLayerVideoGravity.resizeAspectFill layerHolder.layer.frame = item.frame self.scrollNode.layer.addSublayer(layerHolder.layer) - let manager = SoftwareVideoLayerFrameManager(account: self.account, userLocation: .other, userContentType: .gif, fileReference: item.file.file, layerHolder: layerHolder) + let manager = SoftwareVideoLayerFrameManager(account: self.account, userLocation: .other, userContentType: .other, fileReference: item.file.file, layerHolder: layerHolder) self.visibleLayers[item.id] = (manager, layerHolder) self.visibleThumbnailLayers[item.id]?.ready = { [weak self] in if let strongSelf = self { diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/BUILD b/submodules/TelegramUI/Components/StorageUsageScreen/BUILD index 5e3883e0c2..538749ea59 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/BUILD +++ b/submodules/TelegramUI/Components/StorageUsageScreen/BUILD @@ -31,6 +31,8 @@ swift_library( "//submodules/ContextUI", "//submodules/AnimatedAvatarSetNode", "//submodules/AvatarNode", + "//submodules/PhotoResources", + "//submodules/SemanticStatusNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift index 09fbc4729a..d8e91a9dac 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift @@ -13,15 +13,88 @@ import MultilineTextComponent import EmojiStatusComponent import Postbox +private func interpolateChartData(start: PieChartComponent.ChartData, end: PieChartComponent.ChartData, progress: CGFloat) -> PieChartComponent.ChartData { + if start.items.count != end.items.count { + return start + } + + var result = end + for i in 0 ..< result.items.count { + result.items[i].value = (1.0 - progress) * start.items[i].value + progress * end.items[i].value + result.items[i].color = start.items[i].color.interpolateTo(end.items[i].color, fraction: progress) ?? end.items[i].color + } + + return result +} + +private func processChartData(data: PieChartComponent.ChartData) -> PieChartComponent.ChartData { + var data = data + + let minValue: Double = 0.01 + + var totalSum: CGFloat = 0.0 + for i in 0 ..< data.items.count { + if data.items[i].value > 0.00001 { + data.items[i].value = max(data.items[i].value, minValue) + } + totalSum += data.items[i].value + } + + var hasOneItem = false + for i in 0 ..< data.items.count { + if data.items[i].value != 0 && totalSum == data.items[i].value { + data.items[i].value = 1.0 + hasOneItem = true + break + } + } + + if !hasOneItem { + if abs(totalSum - 1.0) > 0.0001 { + let deltaValue = totalSum - 1.0 + + var availableSum: Double = 0.0 + for i in 0 ..< data.items.count { + let itemValue = data.items[i].value + let availableItemValue = max(0.0, itemValue - minValue) + if availableItemValue > 0.0 { + availableSum += availableItemValue + } + } + totalSum = 0.0 + let itemFraction = deltaValue / availableSum + for i in 0 ..< data.items.count { + let itemValue = data.items[i].value + let availableItemValue = max(0.0, itemValue - minValue) + if availableItemValue > 0.0 { + let itemDelta = availableItemValue * itemFraction + data.items[i].value -= itemDelta + } + totalSum += data.items[i].value + } + } + + if totalSum > 0.0 && totalSum < 1.0 - 0.0001 { + for i in 0 ..< data.items.count { + data.items[i].value /= totalSum + } + } + } + + return data +} + final class PieChartComponent: Component { struct ChartData: Equatable { struct Item: Equatable { var id: AnyHashable + var displayValue: Double var value: Double var color: UIColor - init(id: AnyHashable, value: Double, color: UIColor) { + init(id: AnyHashable, displayValue: Double, value: Double, color: UIColor) { self.id = id + self.displayValue = displayValue self.value = value self.color = color } @@ -55,96 +128,98 @@ final class PieChartComponent: Component { return true } - class View: UIView { - private var shapeLayers: [AnyHashable: SimpleShapeLayer] = [:] - private var labels: [AnyHashable: ComponentView] = [:] - var selectedKey: AnyHashable? + private final class ChartDataView: UIView { + private(set) var theme: PresentationTheme? + private(set) var data: ChartData? + private(set) var selectedKey: AnyHashable? - private weak var state: EmptyComponentState? + private var currentAnimation: (start: ChartData, end: ChartData, current: ChartData, progress: CGFloat)? + private var animator: DisplayLinkAnimator? + + private var labels: [AnyHashable: ComponentView] = [:] override init(frame: CGRect) { super.init(frame: frame) - self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + self.backgroundColor = nil + self.isOpaque = false } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - let point = recognizer.location(in: self) - for (key, layer) in self.shapeLayers { - if layer.frame.contains(point), let path = layer.path { - if path.contains(self.layer.convert(point, to: layer)) { - if self.selectedKey == key { - self.selectedKey = nil - } else { - self.selectedKey = key - } - - self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) - - break - } + deinit { + self.animator?.invalidate() + } + + func setItems(theme: PresentationTheme, data: ChartData, selectedKey: AnyHashable?, animated: Bool) { + let data = processChartData(data: data) + + if self.theme !== theme || self.data != data || self.selectedKey != selectedKey { + self.theme = theme + self.selectedKey = selectedKey + + if animated, let previous = self.data { + var initialState = previous + if let currentAnimation = self.currentAnimation { + initialState = currentAnimation.current } + self.currentAnimation = (initialState, data, initialState, 0.0) + self.animator?.invalidate() + self.animator = DisplayLinkAnimator(duration: 0.4, from: 0.0, to: 1.0, update: { [weak self] progress in + guard let self else { + return + } + let progress = listViewAnimationCurveSystem(progress) + if let currentAnimationValue = self.currentAnimation { + self.currentAnimation = (currentAnimationValue.start, currentAnimationValue.end, interpolateChartData(start: currentAnimationValue.start, end: currentAnimationValue.end, progress: progress), progress) + self.setNeedsDisplay() + } + }, completion: { [weak self] in + guard let self else { + return + } + self.currentAnimation = nil + self.setNeedsDisplay() + }) } + + self.data = data + + self.setNeedsDisplay() } } - func update(component: PieChartComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { - self.state = state + override func draw(_ rect: CGRect) { + guard let context = UIGraphicsGetCurrentContext() else { + return + } + guard let theme = self.theme, let data = self.currentAnimation?.current ?? self.data else { + return + } + if data.items.isEmpty { + return + } let innerDiameter: CGFloat = 100.0 let spacing: CGFloat = 2.0 let innerAngleSpacing: CGFloat = spacing / (innerDiameter * 0.5) - let minAngle: CGFloat = innerAngleSpacing * 2.0 + 2.0 / (innerDiameter * 0.5) + //let minAngle: CGFloat = innerAngleSpacing * 2.0 + 2.0 / (innerDiameter * 0.5) - var valueSum: Double = 0.0 - for item in component.chartData.items { - valueSum += item.value - } var angles: [Double] = [] - var totalAngle: Double = 0.0 - 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 > .ulpOfOne { - if angle < minAngle { - angle = minAngle - } - totalAngle += angle - } + for i in 0 ..< data.items.count { + let item = data.items[i] + let angle = item.value * CGFloat.pi * 2.0 angles.append(angle) } - if totalAngle > CGFloat.pi * 2.0 { - let deltaAngle = totalAngle - CGFloat.pi * 2.0 - - var availableAngleSum: Double = 0.0 - for i in 0 ..< angles.count { - let itemAngle = angles[i] - let availableItemAngle = max(0.0, itemAngle - minAngle) - if availableItemAngle > 0.0 { - availableAngleSum += availableItemAngle - } - } - let itemFraction = deltaAngle / availableAngleSum - for i in 0 ..< angles.count { - let availableItemAngle = max(0.0, angles[i] - minAngle) - if availableItemAngle > 0.0 { - let itemDelta = availableItemAngle * itemFraction - angles[i] -= itemDelta - } - } - } let diameter: CGFloat = 200.0 let reducedDiameter: CGFloat = 170.0 var startAngle: CGFloat = 0.0 - for i in 0 ..< component.chartData.items.count { - let item = component.chartData.items[i] + for i in 0 ..< data.items.count { + let item = data.items[i] let itemOuterDiameter: CGFloat if let selectedKey = self.selectedKey { @@ -157,21 +232,10 @@ final class PieChartComponent: Component { itemOuterDiameter = diameter } - let shapeLayerFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - diameter) / 2.0), y: 0.0), size: CGSize(width: diameter, height: diameter)) + let shapeLayerFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: diameter, height: diameter)) let angleSpacing: CGFloat = spacing / (itemOuterDiameter * 0.5) - let shapeLayer: SimpleShapeLayer - if let current = self.shapeLayers[item.id] { - shapeLayer = current - } else { - shapeLayer = SimpleShapeLayer() - self.shapeLayers[item.id] = shapeLayer - self.layer.insertSublayer(shapeLayer, at: 0) - } - - transition.setFrame(layer: shapeLayer, frame: shapeLayerFrame) - let angleValue: CGFloat = angles[i] let innerStartAngle = startAngle + innerAngleSpacing * 0.5 @@ -187,14 +251,17 @@ final class PieChartComponent: Component { path.addArc(center: CGPoint(x: diameter * 0.5, y: diameter * 0.5), radius: innerDiameter * 0.5, startAngle: innerEndAngle, endAngle: innerStartAngle, clockwise: true) path.addArc(center: CGPoint(x: diameter * 0.5, y: diameter * 0.5), radius: itemOuterDiameter * 0.5, startAngle: outerStartAngle, endAngle: outerEndAngle, clockwise: false) - transition.setShapeLayerPath(layer: shapeLayer, path: path) + context.addPath(path) + context.setFillColor(item.color.cgColor) + context.fillPath() startAngle += angleValue - shapeLayer.fillColor = item.color.cgColor - let fractionValue: Double = floor(item.value * 100.0 * 10.0) / 10.0 + let fractionValue: Double = floor(item.displayValue * 100.0 * 10.0) / 10.0 let fractionString: String - if abs(Double(Int(fractionValue)) - fractionValue) < 0.001 { + if fractionValue < 0.1 { + fractionString = "<0.1" + } else if abs(Double(Int(fractionValue)) - fractionValue) < 0.001 { fractionString = "\(Int(fractionValue))" } else { fractionString = "\(fractionValue)" @@ -207,16 +274,125 @@ final class PieChartComponent: Component { label = ComponentView() self.labels[item.id] = label } - 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)) + let labelSize = label.update(transition: .immediate, component: AnyComponent(Text(text: "\(fractionString)%", font: Font.with(size: 16.0, design: .round, weight: .semibold), color: theme.list.itemCheckColors.foregroundColor)), environment: {}, containerSize: CGSize(width: 100.0, height: 100.0)) var labelFrame: CGRect? - for step in 0 ... 6 { - let stepFraction: CGFloat = CGFloat(step) / 6.0 - let centerOffset: CGFloat = 0.5 * (1.0 - stepFraction) + 0.65 * stepFraction - + if angleValue >= 0.001 { + for step in 0 ... 20 { + let stepFraction: CGFloat = CGFloat(step) / 20.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 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 + } + } else { let midAngle: CGFloat = (innerStartAngle + innerEndAngle) * 0.5 - let centerDistance: CGFloat = (innerDiameter * 0.5 + (diameter * 0.5 - innerDiameter * 0.5) * centerOffset) + let centerDistance: CGFloat = (innerDiameter * 0.5 + (diameter * 0.5 - innerDiameter * 0.5) * 0.5) let relLabelCenter = CGPoint( x: cos(midAngle) * centerDistance, @@ -228,103 +404,14 @@ final class PieChartComponent: Component { 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 + let minSize = labelSize.aspectFitted(CGSize(width: 4.0, height: 4.0)) + labelFrame = CGRect(origin: CGPoint(x: labelCenter.x - minSize.width * 0.5, y: labelCenter.y - minSize.height * 0.5), size: minSize) } if let labelView = label.view, let labelFrame { + var animateIn: Bool = false if labelView.superview == nil { + animateIn = true self.addSubview(labelView) } @@ -338,11 +425,12 @@ final class PieChartComponent: Component { y: labelFrame.midY - shapeLayerFrame.midY ) + let labelAlpha: CGFloat if let selectedKey = self.selectedKey { if selectedKey == item.id { - transition.setAlpha(view: labelView, alpha: normalAlpha) + labelAlpha = normalAlpha } else { - transition.setAlpha(view: labelView, alpha: 0.0) + labelAlpha = 0.0 let reducedFactor: CGFloat = (reducedDiameter - innerDiameter) / (diameter - innerDiameter) let reducedDiameterFactor: CGFloat = reducedDiameter / diameter @@ -353,7 +441,16 @@ final class PieChartComponent: Component { relLabelCenter.y *= reducedDiameterFactor } } else { - transition.setAlpha(view: labelView, alpha: normalAlpha) + labelAlpha = normalAlpha + } + if labelView.alpha != labelAlpha { + let transition: Transition + if animateIn { + transition = .immediate + } else { + transition = Transition(animation: .curve(duration: 0.18, curve: .easeInOut)) + } + transition.setAlpha(view: labelView, alpha: labelAlpha) } let labelCenter = CGPoint( @@ -361,10 +458,61 @@ final class PieChartComponent: Component { y: shapeLayerFrame.midY + relLabelCenter.y ) - transition.setPosition(view: labelView, position: labelCenter) - transition.setScale(view: labelView, scale: labelScale) + labelView.center = labelCenter + labelView.transform = CGAffineTransformMakeScale(labelScale, labelScale) } } + } + } + + class View: UIView { + private let dataView: ChartDataView + private var labels: [AnyHashable: ComponentView] = [:] + var selectedKey: AnyHashable? + + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.dataView = ChartDataView() + + super.init(frame: frame) + + self.addSubview(self.dataView) + + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + let point = recognizer.location(in: self) + let _ = point + /*for (key, layer) in self.shapeLayers { + if layer.frame.contains(point), let path = layer.path { + if path.contains(self.layer.convert(point, to: layer)) { + if self.selectedKey == key { + self.selectedKey = nil + } else { + self.selectedKey = key + } + + self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) + + break + } + } + }*/ + } + } + + func update(component: PieChartComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.state = state + + transition.setFrame(view: self.dataView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - 200.0) / 2.0), y: 0.0), size: CGSize(width: 200.0, height: 200.0))) + self.dataView.setItems(theme: component.theme, data: component.chartData, selectedKey: self.selectedKey, animated: !transition.animation.isImmediate) return CGSize(width: availableSize.width, height: 200.0) } diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageCategoriesComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageCategoriesComponent.swift index 0e22d9aedb..89ec75d189 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageCategoriesComponent.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageCategoriesComponent.swift @@ -17,7 +17,7 @@ import SolidRoundedButtonComponent final class StorageCategoriesComponent: Component { struct CategoryData: Equatable { - var key: AnyHashable + var key: StorageUsageScreenComponent.Category var color: UIColor var title: String var size: Int64 @@ -25,7 +25,7 @@ final class StorageCategoriesComponent: Component { var isSelected: Bool var subcategories: [CategoryData] - init(key: AnyHashable, color: UIColor, title: String, size: Int64, sizeFraction: Double, isSelected: Bool, subcategories: [CategoryData]) { + init(key: StorageUsageScreenComponent.Category, color: UIColor, title: String, size: Int64, sizeFraction: Double, isSelected: Bool, subcategories: [CategoryData]) { self.key = key self.title = title self.color = color @@ -39,18 +39,27 @@ final class StorageCategoriesComponent: Component { let theme: PresentationTheme let strings: PresentationStrings let categories: [CategoryData] - let toggleCategorySelection: (AnyHashable) -> Void + let isOtherExpanded: Bool + let toggleCategorySelection: (StorageUsageScreenComponent.Category) -> Void + let toggleOtherExpanded: () -> Void + let clearAction: () -> Void init( theme: PresentationTheme, strings: PresentationStrings, categories: [CategoryData], - toggleCategorySelection: @escaping (AnyHashable) -> Void + isOtherExpanded: Bool, + toggleCategorySelection: @escaping (StorageUsageScreenComponent.Category) -> Void, + toggleOtherExpanded: @escaping () -> Void, + clearAction: @escaping () -> Void ) { self.theme = theme self.strings = strings self.categories = categories + self.isOtherExpanded = isOtherExpanded self.toggleCategorySelection = toggleCategorySelection + self.toggleOtherExpanded = toggleOtherExpanded + self.clearAction = clearAction } static func ==(lhs: StorageCategoriesComponent, rhs: StorageCategoriesComponent) -> Bool { @@ -63,14 +72,16 @@ final class StorageCategoriesComponent: Component { if lhs.categories != rhs.categories { return false } + if lhs.isOtherExpanded != rhs.isOtherExpanded { + return false + } return true } class View: UIView { - private var itemViews: [AnyHashable: ComponentView] = [:] + private var itemViews: [StorageUsageScreenComponent.Category: ComponentView] = [:] private let button = ComponentView() - private var expandedCategory: AnyHashable? private var component: StorageCategoriesComponent? private weak var state: EmptyComponentState? @@ -89,6 +100,8 @@ final class StorageCategoriesComponent: Component { self.component = component self.state = state + let expandedCategory: StorageUsageScreenComponent.Category? = component.isOtherExpanded ? .other : nil + var totalSelectedSize: Int64 = 0 var hasDeselected = false for category in component.categories { @@ -111,7 +124,7 @@ final class StorageCategoriesComponent: Component { var contentHeight: CGFloat = 0.0 - var validKeys = Set() + var validKeys = Set() for i in 0 ..< component.categories.count { let category = component.categories[i] validKeys.insert(category.key) @@ -134,7 +147,7 @@ final class StorageCategoriesComponent: Component { strings: component.strings, category: category, isExpandedLevel: false, - isExpanded: self.expandedCategory == category.key, + isExpanded: expandedCategory == category.key, hasNext: i != component.categories.count - 1, action: { [weak self] key, actionType in guard let self, let component = self.component else { @@ -144,12 +157,7 @@ final class StorageCategoriesComponent: Component { switch actionType { case .generic: if let category = component.categories.first(where: { $0.key == key }), !category.subcategories.isEmpty { - if self.expandedCategory == category.key { - self.expandedCategory = nil - } else { - self.expandedCategory = category.key - } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + component.toggleOtherExpanded() } else { component.toggleCategorySelection(key) } @@ -172,7 +180,7 @@ final class StorageCategoriesComponent: Component { contentHeight += itemSize.height } - var removeKeys: [AnyHashable] = [] + var removeKeys: [StorageUsageScreenComponent.Category] = [] for (key, itemView) in self.itemViews { if !validKeys.contains(key) { if let itemComponentView = itemView.view { @@ -221,7 +229,11 @@ final class StorageCategoriesComponent: Component { animationName: nil, iconPosition: .right, iconSpacing: 4.0, - action: { + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.clearAction() } )), environment: {}, diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageCategoryItemCompoment.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageCategoryItemCompoment.swift index 1a24727b9f..78ae0bc938 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageCategoryItemCompoment.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageCategoryItemCompoment.swift @@ -27,7 +27,7 @@ final class StorageCategoryItemComponent: Component { let isExpandedLevel: Bool let isExpanded: Bool let hasNext: Bool - let action: (AnyHashable, ActionType) -> Void + let action: (StorageUsageScreenComponent.Category, ActionType) -> Void init( theme: PresentationTheme, @@ -36,7 +36,7 @@ final class StorageCategoryItemComponent: Component { isExpandedLevel: Bool, isExpanded: Bool, hasNext: Bool, - action: @escaping (AnyHashable, ActionType) -> Void + action: @escaping (StorageUsageScreenComponent.Category, ActionType) -> Void ) { self.theme = theme self.strings = strings @@ -80,7 +80,7 @@ final class StorageCategoryItemComponent: Component { private let checkButtonArea: HighlightTrackingButton private let subcategoryClippingContainer: UIView - private var itemViews: [AnyHashable: ComponentView] = [:] + private var itemViews: [StorageUsageScreenComponent.Category: ComponentView] = [:] private var component: StorageCategoryItemComponent? @@ -306,7 +306,7 @@ final class StorageCategoryItemComponent: Component { self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height + ((component.isExpanded || component.hasNext) ? UIScreenPixel : 0.0))) - var validKeys = Set() + var validKeys = Set() if component.isExpanded { for i in 0 ..< component.category.subcategories.count { let category = component.category.subcategories[i] @@ -358,7 +358,7 @@ final class StorageCategoryItemComponent: Component { } } - var removeKeys: [AnyHashable] = [] + var removeKeys: [StorageUsageScreenComponent.Category] = [] for (key, itemView) in self.itemViews { if !validKeys.contains(key) { if let itemComponentView = itemView.view { diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageFileListPanelComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageFileListPanelComponent.swift new file mode 100644 index 0000000000..6ec155c72c --- /dev/null +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageFileListPanelComponent.swift @@ -0,0 +1,1000 @@ +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 +import PhotoResources +import SemanticStatusNode + +private let extensionImageCache = Atomic<[UInt32: UIImage]>(value: [:]) + +private let redColors: (UInt32, UInt32) = (0xed6b7b, 0xe63f45) +private let greenColors: (UInt32, UInt32) = (0x99de6f, 0x5fb84f) +private let blueColors: (UInt32, UInt32) = (0x72d5fd, 0x2a9ef1) +private let yellowColors: (UInt32, UInt32) = (0xffa24b, 0xed705c) + +private let extensionColorsMap: [String: (UInt32, UInt32)] = [ + "ppt": redColors, + "pptx": redColors, + "pdf": redColors, + "key": redColors, + + "xls": greenColors, + "xlsx": greenColors, + "csv": greenColors, + + "zip": yellowColors, + "rar": yellowColors, + "gzip": yellowColors, + "ai": yellowColors +] + +private func generateExtensionImage(colors: (UInt32, UInt32)) -> UIImage? { + return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.saveGState() + context.beginPath() + let _ = try? drawSvgPath(context, path: "M6,0 L26.7573593,0 C27.5530088,-8.52837125e-16 28.3160705,0.316070521 28.8786797,0.878679656 L39.1213203,11.1213203 C39.6839295,11.6839295 40,12.4469912 40,13.2426407 L40,34 C40,37.3137085 37.3137085,40 34,40 L6,40 C2.6862915,40 4.05812251e-16,37.3137085 0,34 L0,6 C-4.05812251e-16,2.6862915 2.6862915,6.08718376e-16 6,0 ") + context.clip() + + let gradientColors = [UIColor(rgb: colors.0).cgColor, UIColor(rgb: colors.1).cgColor] as CFArray + var locations: [CGFloat] = [0.0, 1.0] + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + + context.restoreGState() + + context.beginPath() + let _ = try? drawSvgPath(context, path: "M6,0 L26.7573593,0 C27.5530088,-8.52837125e-16 28.3160705,0.316070521 28.8786797,0.878679656 L39.1213203,11.1213203 C39.6839295,11.6839295 40,12.4469912 40,13.2426407 L40,34 C40,37.3137085 37.3137085,40 34,40 L6,40 C2.6862915,40 4.05812251e-16,37.3137085 0,34 L0,6 C-4.05812251e-16,2.6862915 2.6862915,6.08718376e-16 6,0 ") + context.clip() + + context.setFillColor(UIColor(rgb: 0xffffff, alpha: 0.2).cgColor) + context.translateBy(x: 40.0 - 14.0, y: 0.0) + let _ = try? drawSvgPath(context, path: "M-1,0 L14,0 L14,15 L14,14 C14,12.8954305 13.1045695,12 12,12 L4,12 C2.8954305,12 2,11.1045695 2,10 L2,2 C2,0.8954305 1.1045695,-2.02906125e-16 0,0 L-1,0 L-1,0 Z ") + }) +} + +private func extensionImage(fileExtension: String?) -> UIImage? { + let colors: (UInt32, UInt32) + if let fileExtension = fileExtension { + if let extensionColors = extensionColorsMap[fileExtension] { + colors = extensionColors + } else { + colors = blueColors + } + } else { + colors = blueColors + } + + if let cachedImage = (extensionImageCache.with { dict in + return dict[colors.0] + }) { + return cachedImage + } else if let image = generateExtensionImage(colors: colors) { + let _ = extensionImageCache.modify { dict in + var dict = dict + dict[colors.0] = image + return dict + } + return image + } else { + return nil + } +} +private let extensionFont = Font.with(size: 15.0, design: .round, weight: .bold) +private let mediumExtensionFont = Font.with(size: 14.0, design: .round, weight: .bold) +private let smallExtensionFont = Font.with(size: 12.0, design: .round, weight: .bold) + +private final class FileListItemComponent: Component { + enum Icon: Equatable { + case fileExtension(String) + case media(Media, TelegramMediaImageRepresentation) + case audio + + static func ==(lhs: Icon, rhs: Icon) -> Bool { + switch lhs { + case let .fileExtension(value): + if case .fileExtension(value) = rhs { + return true + } else { + return false + } + case let .media(media, representation): + if case let .media(rhsMedia, rhsRepresentation) = rhs { + if media.id != rhsMedia.id { + return false + } + if representation != rhsRepresentation { + return false + } + return true + } else { + return false + } + case .audio: + if case .audio = rhs { + return true + } else { + return false + } + } + } + } + + enum SelectionState: Equatable { + case none + case editing(isSelected: Bool) + } + + let context: AccountContext + let theme: PresentationTheme + let messageId: EngineMessage.Id + let title: String + let subtitle: String + let label: String + let icon: Icon + let sideInset: CGFloat + let selectionState: SelectionState + let hasNext: Bool + let action: (EngineMessage.Id) -> Void + + init( + context: AccountContext, + theme: PresentationTheme, + messageId: EngineMessage.Id, + title: String, + subtitle: String, + label: String, + icon: Icon, + sideInset: CGFloat, + selectionState: SelectionState, + hasNext: Bool, + action: @escaping (EngineMessage.Id) -> Void + ) { + self.context = context + self.theme = theme + self.messageId = messageId + self.title = title + self.subtitle = subtitle + self.label = label + self.icon = icon + self.sideInset = sideInset + self.selectionState = selectionState + self.hasNext = hasNext + self.action = action + } + + static func ==(lhs: FileListItemComponent, rhs: FileListItemComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.messageId != rhs.messageId { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.subtitle != rhs.subtitle { + return false + } + if lhs.label != rhs.label { + return false + } + if lhs.icon != rhs.icon { + return false + } + if lhs.sideInset != rhs.sideInset { + return false + } + if lhs.selectionState != rhs.selectionState { + return false + } + if lhs.hasNext != rhs.hasNext { + return false + } + return true + } + + final class View: HighlightTrackingButton { + private let title = ComponentView() + private let subtitle = ComponentView() + private let label = ComponentView() + private let separatorLayer: SimpleLayer + + private var iconView: UIImageView? + private var iconText: ComponentView? + + private var iconImageNode: TransformImageNode? + + private var semanticStatusNode: SemanticStatusNode? + + private var checkLayer: CheckLayer? + + private var highlightBackgroundFrame: CGRect? + private var highlightBackgroundLayer: SimpleLayer? + + private var component: FileListItemComponent? + + override init(frame: CGRect) { + self.separatorLayer = SimpleLayer() + + super.init(frame: frame) + + self.layer.addSublayer(self.separatorLayer) + + self.highligthedChanged = { [weak self] isHighlighted in + guard let self, let component = self.component, let highlightBackgroundFrame = self.highlightBackgroundFrame else { + return + } + + if isHighlighted, case .none = component.selectionState { + self.superview?.bringSubviewToFront(self) + + let highlightBackgroundLayer: SimpleLayer + if let current = self.highlightBackgroundLayer { + highlightBackgroundLayer = current + } else { + highlightBackgroundLayer = SimpleLayer() + self.highlightBackgroundLayer = highlightBackgroundLayer + self.layer.insertSublayer(highlightBackgroundLayer, above: self.separatorLayer) + highlightBackgroundLayer.backgroundColor = component.theme.list.itemHighlightedBackgroundColor.cgColor + } + highlightBackgroundLayer.frame = highlightBackgroundFrame + highlightBackgroundLayer.opacity = 1.0 + } else { + if let highlightBackgroundLayer = self.highlightBackgroundLayer { + self.highlightBackgroundLayer = nil + highlightBackgroundLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak highlightBackgroundLayer] _ in + highlightBackgroundLayer?.removeFromSuperlayer() + }) + } + } + } + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + guard let component = self.component else { + return + } + component.action(component.messageId) + } + + func update(component: FileListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let themeUpdated = self.component?.theme !== component.theme + + var hasSelectionUpdated = false + if let previousComponent = self.component { + switch previousComponent.selectionState { + case .none: + if case .none = component.selectionState { + } else { + hasSelectionUpdated = true + } + case .editing: + if case .editing = component.selectionState { + } else { + hasSelectionUpdated = true + } + } + } + + self.component = component + + let spacing: CGFloat = 1.0 + let height: CGFloat = 52.0 + var leftInset: CGFloat = 62.0 + component.sideInset + var iconLeftInset: CGFloat = component.sideInset + + if case let .editing(isSelected) = component.selectionState { + leftInset += 48.0 + iconLeftInset += 48.0 + + let checkSize: CGFloat = 22.0 + + let checkLayer: CheckLayer + if let current = self.checkLayer { + checkLayer = current + if themeUpdated { + checkLayer.theme = CheckNodeTheme(theme: component.theme, style: .plain) + } + checkLayer.setSelected(isSelected, animated: !transition.animation.isImmediate) + } else { + checkLayer = CheckLayer(theme: CheckNodeTheme(theme: component.theme, style: .plain)) + self.checkLayer = checkLayer + self.layer.addSublayer(checkLayer) + checkLayer.frame = CGRect(origin: CGPoint(x: -checkSize, y: floor((height - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize)) + checkLayer.setSelected(isSelected, animated: false) + checkLayer.setNeedsDisplay() + } + transition.setFrame(layer: checkLayer, frame: CGRect(origin: CGPoint(x: 20.0, y: floor((height - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize))) + } else { + if let checkLayer = self.checkLayer { + self.checkLayer = nil + transition.setPosition(layer: checkLayer, position: CGPoint(x: -checkLayer.bounds.width * 0.5, y: checkLayer.position.y), completion: { [weak checkLayer] _ in + checkLayer?.removeFromSuperlayer() + }) + } + } + + let rightInset: CGFloat = 16.0 + component.sideInset + + if case let .fileExtension(text) = component.icon { + let iconView: UIImageView + if let current = self.iconView { + iconView = current + } else { + iconView = UIImageView() + self.iconView = iconView + self.addSubview(iconView) + } + + let iconText: ComponentView + if let current = self.iconText { + iconText = current + } else { + iconText = ComponentView() + self.iconText = iconText + } + + if themeUpdated { + iconView.image = extensionImage(fileExtension: "mp3") + } + if let image = iconView.image { + let iconFrame = CGRect(origin: CGPoint(x: iconLeftInset + floor(( leftInset - iconLeftInset - image.size.width) / 2.0), y: floor((height - image.size.height) / 2.0)), size: image.size) + transition.setFrame(view: iconView, frame: iconFrame) + + let iconTextSize = iconText.update( + transition: .immediate, + component: AnyComponent(Text( + text: text, + font: text.count > 3 ? mediumExtensionFont : extensionFont, + color: .white + )), + environment: {}, + containerSize: CGSize(width: iconFrame.width - 4.0, height: 100.0) + ) + if let iconTextView = iconText.view { + if iconTextView.superview == nil { + self.addSubview(iconTextView) + } + transition.setFrame(view: iconTextView, frame: CGRect(origin: CGPoint(x: iconFrame.minX + floor((iconFrame.width - iconTextSize.width) / 2.0), y: iconFrame.maxY - iconTextSize.height - 4.0), size: iconTextSize)) + } + } + } else { + if let iconView = self.iconView { + self.iconView = nil + iconView.removeFromSuperview() + } + if let iconText = self.iconText { + self.iconText = nil + iconText.view?.removeFromSuperview() + } + } + + if case let .media(media, representation) = component.icon { + var resetImage = false + + let iconImageNode: TransformImageNode + if let current = self.iconImageNode { + iconImageNode = current + } else { + resetImage = true + + iconImageNode = TransformImageNode() + self.iconImageNode = iconImageNode + self.addSubview(iconImageNode.view) + } + + let iconSize = CGSize(width: 40.0, height: 40.0) + let imageSize: CGSize = representation.dimensions.cgSize + + if resetImage { + if let file = media as? TelegramMediaFile { + iconImageNode.setSignal(chatWebpageSnippetFile( + account: component.context.account, + userLocation: .peer(component.messageId.peerId), + mediaReference: FileMediaReference.standalone(media: file).abstract, + representation: representation, + automaticFetch: false + )) + } else if let image = media as? TelegramMediaImage { + iconImageNode.setSignal(mediaGridMessagePhoto( + account: component.context.account, + userLocation: .peer(component.messageId.peerId), + photoReference: ImageMediaReference.standalone(media: image), + automaticFetch: false + )) + } + } + + let iconFrame = CGRect(origin: CGPoint(x: iconLeftInset + floor((leftInset - iconLeftInset - iconSize.width) / 2.0), y: floor((height - iconSize.height) / 2.0)), size: iconSize) + transition.setFrame(view: iconImageNode.view, frame: iconFrame) + + let iconImageLayout = iconImageNode.asyncLayout() + let iconImageApply = iconImageLayout(TransformImageArguments( + corners: ImageCorners(radius: 8.0), + imageSize: imageSize, + boundingSize: iconSize, + intrinsicInsets: UIEdgeInsets() + )) + iconImageApply() + } else { + if let iconImageNode = self.iconImageNode { + self.iconImageNode = nil + iconImageNode.view.removeFromSuperview() + } + } + + if case .audio = component.icon { + let semanticStatusNode: SemanticStatusNode + if let current = self.semanticStatusNode { + semanticStatusNode = current + } else { + semanticStatusNode = SemanticStatusNode(backgroundNodeColor: .clear, foregroundNodeColor: .white) + self.semanticStatusNode = semanticStatusNode + self.addSubview(semanticStatusNode.view) + } + + let iconSize = CGSize(width: 40.0, height: 40.0) + let iconFrame = CGRect(origin: CGPoint(x: iconLeftInset + floor((leftInset - iconLeftInset - iconSize.width) / 2.0), y: floor((height - iconSize.height) / 2.0)), size: iconSize) + transition.setFrame(view: semanticStatusNode.view, frame: iconFrame) + + semanticStatusNode.backgroundNodeColor = component.theme.list.itemCheckColors.fillColor + semanticStatusNode.foregroundNodeColor = component.theme.list.itemCheckColors.foregroundColor + semanticStatusNode.overlayForegroundNodeColor = .white + semanticStatusNode.transitionToState(.play) + } else { + if let semanticStatusNode = self.semanticStatusNode { + self.semanticStatusNode = nil + semanticStatusNode.view.removeFromSuperview() + } + } + + let labelSize = self.label.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.label, font: Font.regular(17.0), textColor: component.theme.list.itemSecondaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) + ) + + let previousTitleFrame = self.title.view?.frame + var previousTitleContents: UIView? + if hasSelectionUpdated { + previousTitleContents = self.title.view?.snapshotView(afterScreenUpdates: false) + } + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.semibold(16.0), textColor: component.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset - labelSize.width - 4.0, height: 100.0) + ) + + let subtitleSize = self.subtitle.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.subtitle, font: Font.regular(14.0), textColor: component.theme.list.itemSecondaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset - labelSize.width - 4.0, height: 100.0) + ) + + let contentHeight = titleSize.height + spacing + subtitleSize.height + + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - contentHeight) / 2.0)), size: titleSize) + let subtitleFrame = CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY + spacing), size: subtitleSize) + + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + self.addSubview(titleView) + } + titleView.frame = titleFrame + if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x { + transition.animatePosition(view: titleView, from: CGPoint(x: previousTitleFrame.origin.x - titleFrame.origin.x, y: 0.0), to: CGPoint(), additive: true) + } + + if let previousTitleFrame, let previousTitleContents, previousTitleFrame.size != titleSize { + previousTitleContents.frame = CGRect(origin: previousTitleFrame.origin, size: previousTitleFrame.size) + self.addSubview(previousTitleContents) + + transition.setFrame(view: previousTitleContents, frame: CGRect(origin: titleFrame.origin, size: previousTitleFrame.size)) + transition.setAlpha(view: previousTitleContents, alpha: 0.0, completion: { [weak previousTitleContents] _ in + previousTitleContents?.removeFromSuperview() + }) + transition.animateAlpha(view: titleView, from: 0.0, to: 1.0) + } + } + if let subtitleView = self.subtitle.view { + if subtitleView.superview == nil { + subtitleView.isUserInteractionEnabled = false + self.addSubview(subtitleView) + } + transition.setFrame(view: subtitleView, frame: subtitleFrame) + } + if let labelView = self.label.view { + if labelView.superview == nil { + labelView.isUserInteractionEnabled = false + 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 + + self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height + ((component.hasNext) ? UIScreenPixel : 0.0))) + + return CGSize(width: availableSize.width, height: height) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class StorageFileListPanelComponent: Component { + typealias EnvironmentType = StorageUsagePanelEnvironment + + final class Item: Equatable { + let message: Message + let size: Int64 + + init( + message: Message, + size: Int64 + ) { + self.message = message + self.size = size + } + + static func ==(lhs: Item, rhs: Item) -> Bool { + if lhs.message.id != rhs.message.id { + return false + } + if lhs.size != rhs.size { + return false + } + return true + } + } + + final class Items: Equatable { + let items: [Item] + + init(items: [Item]) { + self.items = items + } + + static func ==(lhs: Items, rhs: Items) -> Bool { + if lhs === rhs { + return true + } + return lhs.items == rhs.items + } + } + + let context: AccountContext + let items: Items? + let selectionState: StorageUsageScreenComponent.SelectionState? + let peerAction: (EngineMessage.Id) -> Void + + init( + context: AccountContext, + items: Items?, + selectionState: StorageUsageScreenComponent.SelectionState?, + peerAction: @escaping (EngineMessage.Id) -> Void + ) { + self.context = context + self.items = items + self.selectionState = selectionState + self.peerAction = peerAction + } + + static func ==(lhs: StorageFileListPanelComponent, rhs: StorageFileListPanelComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.items != rhs.items { + return false + } + if lhs.selectionState != rhs.selectionState { + return false + } + return true + } + + private struct ItemLayout: Equatable { + let containerInsets: UIEdgeInsets + let containerWidth: CGFloat + let itemHeight: CGFloat + let itemCount: Int + + let contentHeight: CGFloat + + init( + containerInsets: UIEdgeInsets, + containerWidth: CGFloat, + itemHeight: CGFloat, + itemCount: Int + ) { + self.containerInsets = containerInsets + self.containerWidth = containerWidth + self.itemHeight = itemHeight + self.itemCount = itemCount + + self.contentHeight = containerInsets.top + containerInsets.bottom + CGFloat(itemCount) * itemHeight + } + + func visibleItems(for rect: CGRect) -> Range? { + 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: [EngineMessage.Id: ComponentView] = [:] + + private var ignoreScrolling: Bool = false + + private var component: StorageFileListPanelComponent? + 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.message.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 itemSelectionState: FileListItemComponent.SelectionState + if let selectionState = component.selectionState { + itemSelectionState = .editing(isSelected: selectionState.selectedMessages.contains(id)) + } else { + itemSelectionState = .none + } + + var isAudio: Bool = false + var isVoice: Bool = false + + let _ = isAudio + let _ = isVoice + + var title: String = "File" + + var subtitle = stringForFullDate(timestamp: item.message.timestamp, strings: environment.strings, dateTimeFormat: environment.dateTimeFormat) + + var extensionIconValue: String? + var imageIconValue: FileListItemComponent.Icon? + + for media in item.message.media { + if let file = media as? TelegramMediaFile { + let isInstantVideo = file.isInstantVideo + + for attribute in file.attributes { + if case let .Audio(voice, duration, titleValue, performer, _) = attribute { + let _ = duration + + isAudio = true + isVoice = voice + + title = titleValue ?? (file.fileName ?? "Unknown Track") + + if let performer = performer { + subtitle = performer + //descriptionString = "\(stringForDuration(Int32(duration))) • \(performer)" + } + + if !voice { + if file.fileName?.lowercased().hasSuffix(".ogg") == true { + //iconImage = .albumArt(file, SharedMediaPlaybackAlbumArt(thumbnailResource: ExternalMusicAlbumArtResource(file: .message(message: MessageReference(message), media: file), title: "", performer: "", isThumbnail: true), fullSizeResource: ExternalMusicAlbumArtResource(file: .message(message: MessageReference(message), media: file), title: "", performer: "", isThumbnail: false))) + } else { + //iconImage = .albumArt(file, SharedMediaPlaybackAlbumArt(thumbnailResource: ExternalMusicAlbumArtResource(file: .message(message: MessageReference(message), media: file), title: title ?? "", performer: performer ?? "", isThumbnail: true), fullSizeResource: ExternalMusicAlbumArtResource(file: .message(message: MessageReference(message), media: file), title: title ?? "", performer: performer ?? "", isThumbnail: false))) + } + } else { + title = "Audio" + } + } + } + + if isInstantVideo || isVoice { + /*var authorName: String + if let author = message.forwardInfo?.author { + if author.id == item.context.account.peerId { + authorName = item.presentationData.strings.DialogList_You + } else { + authorName = EnginePeer(author).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) + } + } else if let signature = message.forwardInfo?.authorSignature { + authorName = signature + } else if let author = message.author { + if author.id == item.context.account.peerId { + authorName = item.presentationData.strings.DialogList_You + } else { + authorName = EnginePeer(author).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) + } + } else { + authorName = " " + } + + if item.isGlobalSearchResult || item.isDownloadList { + let authorString = stringForFullAuthorName(message: EngineMessage(message), strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId) + if authorString.count > 1 { + globalAuthorTitle = authorString.last ?? "" + } + authorName = authorString.first ?? "" + } + + titleText = NSAttributedString(string: authorName, font: audioTitleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor) + let dateString = stringForFullDate(timestamp: message.timestamp, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat) + var descriptionString: String = "" + if let duration = file.duration { + if item.isGlobalSearchResult || item.isDownloadList || !item.displayFileInfo { + descriptionString = stringForDuration(Int32(duration)) + } else { + descriptionString = "\(stringForDuration(Int32(duration))) • \(dateString)" + } + } else { + if !(item.isGlobalSearchResult || item.isDownloadList) { + descriptionString = dateString + } + } + + descriptionText = NSAttributedString(string: descriptionString, font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor) + iconImage = .roundVideo(file)*/ + } else if !isAudio { + var fileName: String = file.fileName ?? "File" + if file.isVideo { + fileName = environment.strings.Message_Video + } + title = fileName + + var fileExtension: String? + if let range = fileName.range(of: ".", options: [.backwards]) { + fileExtension = fileName[range.upperBound...].lowercased() + } + extensionIconValue = fileExtension + + if let representation = smallestImageRepresentation(file.previewRepresentations) { + imageIconValue = .media(file, representation) + } + } + } else if let image = media as? TelegramMediaImage { + //TODO:localize + title = "Photo" + + if let representation = largestImageRepresentation(image.representations) { + imageIconValue = .media(image, representation) + } + } + } + + var icon: FileListItemComponent.Icon = .fileExtension(" ") + if let imageIconValue { + icon = imageIconValue + } else if isAudio { + icon = .audio + } else if let extensionIconValue { + icon = .fileExtension(extensionIconValue) + } + + let _ = itemView.update( + transition: itemTransition, + component: AnyComponent(FileListItemComponent( + context: component.context, + theme: environment.theme, + messageId: item.message.id, + title: title, + subtitle: subtitle, + label: dataSizeString(item.size, formatting: dataSizeFormatting), + icon: icon, + sideInset: environment.containerInsets.left, + selectionState: itemSelectionState, + hasNext: index != items.items.count - 1, + action: component.peerAction + )), + 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: [EngineMessage.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: StorageFileListPanelComponent, 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(FileListItemComponent( + context: component.context, + theme: environment.theme, + messageId: EngineMessage.Id(peerId: PeerId(namespace: PeerId.Namespace._internalFromInt32Value(0), id: PeerId.Id._internalFromInt64Value(0)), namespace: 0, id: 0), + title: "ABCDEF", + subtitle: "ABCDEF", + label: "1000", + icon: .fileExtension("file"), + sideInset: environment.containerInsets.left, + selectionState: .none, + hasNext: false, + action: { _ in + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 1000.0) + ) + + let itemLayout = ItemLayout( + containerInsets: environment.containerInsets, + containerWidth: availableSize.width, + itemHeight: measureItemSize.height, + itemCount: component.items?.items.count ?? 0 + ) + self.itemLayout = itemLayout + + self.ignoreScrolling = true + let contentOffset = self.scrollView.bounds.minY + transition.setPosition(view: self.scrollView, position: CGRect(origin: CGPoint(), size: availableSize).center) + var scrollBounds = self.scrollView.bounds + scrollBounds.size = availableSize + if !environment.isScrollable { + scrollBounds.origin = CGPoint() + } + transition.setBounds(view: self.scrollView, bounds: scrollBounds) + self.scrollView.isScrollEnabled = environment.isScrollable + let contentSize = CGSize(width: availableSize.width, height: itemLayout.contentHeight) + if self.scrollView.contentSize != contentSize { + self.scrollView.contentSize = contentSize + } + self.scrollView.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: transition) + + 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/StoragePeerListPanelComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerListPanelComponent.swift index 46fca7acc3..419e828be8 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerListPanelComponent.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StoragePeerListPanelComponent.swift @@ -19,13 +19,20 @@ import AvatarNode private let avatarFont = avatarPlaceholderFont(size: 15.0) private final class PeerListItemComponent: Component { + enum SelectionState: Equatable { + case none + case editing(isSelected: Bool) + } + let context: AccountContext let theme: PresentationTheme let sideInset: CGFloat let title: String let peer: EnginePeer? let label: String + let selectionState: SelectionState let hasNext: Bool + let action: (EnginePeer) -> Void init( context: AccountContext, @@ -34,7 +41,9 @@ private final class PeerListItemComponent: Component { title: String, peer: EnginePeer?, label: String, - hasNext: Bool + selectionState: SelectionState, + hasNext: Bool, + action: @escaping (EnginePeer) -> Void ) { self.context = context self.theme = theme @@ -42,7 +51,9 @@ private final class PeerListItemComponent: Component { self.title = title self.peer = peer self.label = label + self.selectionState = selectionState self.hasNext = hasNext + self.action = action } static func ==(lhs: PeerListItemComponent, rhs: PeerListItemComponent) -> Bool { @@ -64,6 +75,9 @@ private final class PeerListItemComponent: Component { if lhs.label != rhs.label { return false } + if lhs.selectionState != rhs.selectionState { + return false + } if lhs.hasNext != rhs.hasNext { return false } @@ -76,6 +90,11 @@ private final class PeerListItemComponent: Component { private let separatorLayer: SimpleLayer private let avatarNode: AvatarNode + private var checkLayer: CheckLayer? + + private var highlightBackgroundFrame: CGRect? + private var highlightBackgroundLayer: SimpleLayer? + private var component: PeerListItemComponent? override init(frame: CGRect) { @@ -87,23 +106,115 @@ private final class PeerListItemComponent: Component { self.layer.addSublayer(self.separatorLayer) self.layer.addSublayer(self.avatarNode.layer) + + self.highligthedChanged = { [weak self] isHighlighted in + guard let self, let component = self.component, let highlightBackgroundFrame = self.highlightBackgroundFrame else { + return + } + + if isHighlighted, case .none = component.selectionState { + self.superview?.bringSubviewToFront(self) + + let highlightBackgroundLayer: SimpleLayer + if let current = self.highlightBackgroundLayer { + highlightBackgroundLayer = current + } else { + highlightBackgroundLayer = SimpleLayer() + self.highlightBackgroundLayer = highlightBackgroundLayer + self.layer.insertSublayer(highlightBackgroundLayer, above: self.separatorLayer) + highlightBackgroundLayer.backgroundColor = component.theme.list.itemHighlightedBackgroundColor.cgColor + } + highlightBackgroundLayer.frame = highlightBackgroundFrame + highlightBackgroundLayer.opacity = 1.0 + } else { + if let highlightBackgroundLayer = self.highlightBackgroundLayer { + self.highlightBackgroundLayer = nil + highlightBackgroundLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak highlightBackgroundLayer] _ in + highlightBackgroundLayer?.removeFromSuperlayer() + }) + } + } + } + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + @objc private func pressed() { + guard let component = self.component, let peer = component.peer else { + return + } + component.action(peer) + } + func update(component: PeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme + + var hasSelectionUpdated = false + if let previousComponent = self.component { + switch previousComponent.selectionState { + case .none: + if case .none = component.selectionState { + } else { + hasSelectionUpdated = true + } + case .editing: + if case .editing = component.selectionState { + } else { + hasSelectionUpdated = true + } + } + } + self.component = component let height: CGFloat = 52.0 - let leftInset: CGFloat = 62.0 + component.sideInset + var leftInset: CGFloat = 62.0 + component.sideInset + var avatarLeftInset: CGFloat = component.sideInset + 10.0 + + if case let .editing(isSelected) = component.selectionState { + leftInset += 48.0 + avatarLeftInset += 48.0 + + let checkSize: CGFloat = 22.0 + + let checkLayer: CheckLayer + if let current = self.checkLayer { + checkLayer = current + if themeUpdated { + checkLayer.theme = CheckNodeTheme(theme: component.theme, style: .plain) + } + checkLayer.setSelected(isSelected, animated: !transition.animation.isImmediate) + } else { + checkLayer = CheckLayer(theme: CheckNodeTheme(theme: component.theme, style: .plain)) + self.checkLayer = checkLayer + self.layer.addSublayer(checkLayer) + checkLayer.frame = CGRect(origin: CGPoint(x: -checkSize, y: floor((height - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize)) + checkLayer.setSelected(isSelected, animated: false) + checkLayer.setNeedsDisplay() + } + transition.setFrame(layer: checkLayer, frame: CGRect(origin: CGPoint(x: 20.0, y: floor((height - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize))) + } else { + if let checkLayer = self.checkLayer { + self.checkLayer = nil + transition.setPosition(layer: checkLayer, position: CGPoint(x: -checkLayer.bounds.width * 0.5, y: checkLayer.position.y), completion: { [weak checkLayer] _ in + checkLayer?.removeFromSuperlayer() + }) + } + } + let rightInset: CGFloat = 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)) + let avatarFrame = CGRect(origin: CGPoint(x: avatarLeftInset, y: floor((height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize)) + if self.avatarNode.bounds.isEmpty { + self.avatarNode.frame = avatarFrame + } else { + transition.setFrame(layer: self.avatarNode.layer, frame: avatarFrame) + } if let peer = component.peer { self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, displayDimensions: CGSize(width: avatarSize, height: avatarSize)) } @@ -117,6 +228,12 @@ private final class PeerListItemComponent: Component { containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) ) + let previousTitleFrame = self.title.view?.frame + var previousTitleContents: UIView? + if hasSelectionUpdated { + previousTitleContents = self.title.view?.snapshotView(afterScreenUpdates: false) + } + let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( @@ -125,15 +242,31 @@ private final class PeerListItemComponent: Component { environment: {}, containerSize: CGSize(width: availableSize.width - leftInset - rightInset - labelSize.width - 4.0, height: 100.0) ) - + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - titleSize.height) / 2.0)), size: titleSize) if let titleView = self.title.view { if titleView.superview == nil { + titleView.isUserInteractionEnabled = false self.addSubview(titleView) } - transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: leftInset, y: floor((height - titleSize.height) / 2.0)), size: titleSize)) + titleView.frame = titleFrame + if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x { + transition.animatePosition(view: titleView, from: CGPoint(x: previousTitleFrame.origin.x - titleFrame.origin.x, y: 0.0), to: CGPoint(), additive: true) + } + + if let previousTitleFrame, let previousTitleContents, previousTitleFrame.size != titleSize { + previousTitleContents.frame = CGRect(origin: previousTitleFrame.origin, size: previousTitleFrame.size) + self.addSubview(previousTitleContents) + + transition.setFrame(view: previousTitleContents, frame: CGRect(origin: titleFrame.origin, size: previousTitleFrame.size)) + transition.setAlpha(view: previousTitleContents, alpha: 0.0, completion: { [weak previousTitleContents] _ in + previousTitleContents?.removeFromSuperview() + }) + transition.animateAlpha(view: titleView, from: 0.0, to: 1.0) + } } if let labelView = self.label.view { if labelView.superview == nil { + labelView.isUserInteractionEnabled = false self.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)) @@ -145,6 +278,8 @@ private final class PeerListItemComponent: Component { transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel))) self.separatorLayer.isHidden = !component.hasNext + self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height + ((component.hasNext) ? UIScreenPixel : 0.0))) + return CGSize(width: availableSize.width, height: height) } } @@ -201,13 +336,19 @@ final class StoragePeerListPanelComponent: Component { let context: AccountContext let items: Items? + let selectionState: StorageUsageScreenComponent.SelectionState? + let peerAction: (EnginePeer) -> Void init( context: AccountContext, - items: Items? + items: Items?, + selectionState: StorageUsageScreenComponent.SelectionState?, + peerAction: @escaping (EnginePeer) -> Void ) { self.context = context self.items = items + self.selectionState = selectionState + self.peerAction = peerAction } static func ==(lhs: StoragePeerListPanelComponent, rhs: StoragePeerListPanelComponent) -> Bool { @@ -217,6 +358,9 @@ final class StoragePeerListPanelComponent: Component { if lhs.items != rhs.items { return false } + if lhs.selectionState != rhs.selectionState { + return false + } return true } @@ -337,6 +481,13 @@ final class StoragePeerListPanelComponent: Component { self.visibleItems[id] = itemView } + let itemSelectionState: PeerListItemComponent.SelectionState + if let selectionState = component.selectionState { + itemSelectionState = .editing(isSelected: selectionState.selectedPeers.contains(id)) + } else { + itemSelectionState = .none + } + let _ = itemView.update( transition: itemTransition, component: AnyComponent(PeerListItemComponent( @@ -346,7 +497,9 @@ final class StoragePeerListPanelComponent: Component { title: item.peer.displayTitle(strings: environment.strings, displayOrder: .firstLast), peer: item.peer, label: dataSizeString(item.size, formatting: dataSizeFormatting), - hasNext: index != items.items.count - 1 + selectionState: itemSelectionState, + hasNext: index != items.items.count - 1, + action: component.peerAction )), environment: {}, containerSize: CGSize(width: itemLayout.containerWidth, height: itemLayout.itemHeight) @@ -392,7 +545,10 @@ final class StoragePeerListPanelComponent: Component { title: "ABCDEF", peer: nil, label: "1000", - hasNext: false + selectionState: .none, + hasNext: false, + action: { _ in + } )), environment: {}, containerSize: CGSize(width: availableSize.width, height: 1000.0) @@ -408,7 +564,14 @@ final class StoragePeerListPanelComponent: Component { 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) + var scrollBounds = self.scrollView.bounds + scrollBounds.size = availableSize + if !environment.isScrollable { + scrollBounds.origin = CGPoint() + } + transition.setBounds(view: self.scrollView, bounds: scrollBounds) + self.scrollView.isScrollEnabled = environment.isScrollable let contentSize = CGSize(width: availableSize.width, height: itemLayout.contentHeight) if self.scrollView.contentSize != contentSize { self.scrollView.contentSize = contentSize @@ -419,7 +582,7 @@ final class StoragePeerListPanelComponent: Component { transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: -deltaOffset), to: CGPoint(), additive: true) } self.ignoreScrolling = false - self.updateScrolling(transition: .immediate) + self.updateScrolling(transition: transition) return availableSize } diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsagePanelContainerComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsagePanelContainerComponent.swift index 2ee62f3215..41ff116578 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsagePanelContainerComponent.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsagePanelContainerComponent.swift @@ -5,19 +5,42 @@ import ComponentFlow import ComponentDisplayAdapters import TelegramPresentationData +final class StorageUsagePanelContainerEnvironment: Equatable { + let isScrollable: Bool + + init( + isScrollable: Bool + ) { + self.isScrollable = isScrollable + } + + static func ==(lhs: StorageUsagePanelContainerEnvironment, rhs: StorageUsagePanelContainerEnvironment) -> Bool { + if lhs.isScrollable != rhs.isScrollable { + return false + } + return true + } +} + final class StorageUsagePanelEnvironment: Equatable { let theme: PresentationTheme let strings: PresentationStrings + let dateTimeFormat: PresentationDateTimeFormat let containerInsets: UIEdgeInsets + let isScrollable: Bool init( theme: PresentationTheme, strings: PresentationStrings, - containerInsets: UIEdgeInsets + dateTimeFormat: PresentationDateTimeFormat, + containerInsets: UIEdgeInsets, + isScrollable: Bool ) { self.theme = theme self.strings = strings + self.dateTimeFormat = dateTimeFormat self.containerInsets = containerInsets + self.isScrollable = isScrollable } static func ==(lhs: StorageUsagePanelEnvironment, rhs: StorageUsagePanelEnvironment) -> Bool { @@ -27,9 +50,15 @@ final class StorageUsagePanelEnvironment: Equatable { if lhs.strings !== rhs.strings { return false } + if lhs.dateTimeFormat != rhs.dateTimeFormat { + return false + } if lhs.containerInsets != rhs.containerInsets { return false } + if lhs.isScrollable != rhs.isScrollable { + return false + } return true } } @@ -37,13 +66,16 @@ final class StorageUsagePanelEnvironment: Equatable { private final class StorageUsageHeaderItemComponent: CombinedComponent { let theme: PresentationTheme let title: String + let activityFraction: CGFloat init( theme: PresentationTheme, - title: String + title: String, + activityFraction: CGFloat ) { self.theme = theme self.title = title + self.activityFraction = activityFraction } static func ==(lhs: StorageUsageHeaderItemComponent, rhs: StorageUsageHeaderItemComponent) -> Bool { @@ -53,22 +85,38 @@ private final class StorageUsageHeaderItemComponent: CombinedComponent { if lhs.title != rhs.title { return false } + if lhs.activityFraction != rhs.activityFraction { + return false + } return true } static var body: Body { - let text = Child(Text.self) + let activeText = Child(Text.self) + let inactiveText = 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), + let activeText = activeText.update( + component: Text(text: context.component.title, font: Font.medium(14.0), color: context.component.theme.list.itemAccentColor), + availableSize: context.availableSize, + transition: .immediate + ) + let inactiveText = inactiveText.update( + component: Text(text: context.component.title, font: Font.medium(14.0), color: context.component.theme.list.itemSecondaryTextColor), availableSize: context.availableSize, transition: .immediate ) - context.add(text.position(CGPoint(x: text.size.width * 0.5, y: text.size.height * 0.5))) + context.add(activeText + .position(CGPoint(x: activeText.size.width * 0.5, y: activeText.size.height * 0.5)) + .opacity(context.component.activityFraction) + ) + context.add(inactiveText + .position(CGPoint(x: inactiveText.size.width * 0.5, y: inactiveText.size.height * 0.5)) + .opacity(1.0 - context.component.activityFraction) + ) - return text.size + return activeText.size } } } @@ -89,13 +137,22 @@ private final class StorageUsageHeaderComponent: Component { let theme: PresentationTheme let items: [Item] + let activeIndex: Int + let transitionFraction: CGFloat + let switchToPanel: (AnyHashable) -> Void init( theme: PresentationTheme, - items: [Item] + items: [Item], + activeIndex: Int, + transitionFraction: CGFloat, + switchToPanel: @escaping (AnyHashable) -> Void ) { self.theme = theme self.items = items + self.activeIndex = activeIndex + self.transitionFraction = transitionFraction + self.switchToPanel = switchToPanel } static func ==(lhs: StorageUsageHeaderComponent, rhs: StorageUsageHeaderComponent) -> Bool { @@ -105,6 +162,12 @@ private final class StorageUsageHeaderComponent: Component { if lhs.items != rhs.items { return false } + if lhs.activeIndex != rhs.activeIndex { + return false + } + if lhs.transitionFraction != rhs.transitionFraction { + return false + } return true } @@ -122,19 +185,46 @@ private final class StorageUsageHeaderComponent: Component { super.init(frame: frame) self.layer.addSublayer(self.activeItemLayer) + + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + let point = recognizer.location(in: self) + var closestId: (CGFloat, AnyHashable)? + if self.bounds.contains(point) { + for (id, item) in self.visibleItems { + if let itemView = item.view { + let distance: CGFloat = min(abs(point.x - itemView.frame.minX), abs(point.x - itemView.frame.maxX)) + if let closestIdValue = closestId { + if distance < closestIdValue.0 { + closestId = (distance, id) + } + } else { + closestId = (distance, id) + } + } + } + } + if let closestId = closestId, let component = self.component { + component.switchToPanel(closestId.1) + } + } + } + func update(component: StorageUsageHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme self.component = component var validIds = Set() - for item in component.items { + for i in 0 ..< component.items.count { + let item = component.items[i] validIds.insert(item.id) let itemView: ComponentView @@ -146,24 +236,59 @@ private final class StorageUsageHeaderComponent: Component { itemView = ComponentView() self.visibleItems[item.id] = itemView } + + let activeIndex: CGFloat = CGFloat(component.activeIndex) - component.transitionFraction + let activityDistance: CGFloat = abs(activeIndex - CGFloat(i)) + + let activityFraction: CGFloat + if activityDistance < 1.0 { + activityFraction = 1.0 - activityDistance + } else { + activityFraction = 0.0 + } + let itemSize = itemView.update( transition: itemTransition, component: AnyComponent(StorageUsageHeaderItemComponent( theme: component.theme, - title: item.title + title: item.title, + activityFraction: activityFraction )), environment: {}, containerSize: availableSize ) - let itemFrame = CGRect(origin: CGPoint(x: 34.0, y: floor((availableSize.height - itemSize.height) / 2.0)), size: itemSize) + + let itemHorizontalSpace = availableSize.width / CGFloat(component.items.count) + let itemX: CGFloat + if component.items.count == 1 { + itemX = 37.0 + } else { + itemX = itemHorizontalSpace * CGFloat(i) + floor((itemHorizontalSpace - itemSize.width) / 2.0) + } + + let itemFrame = CGRect(origin: CGPoint(x: itemX, y: floor((availableSize.height - itemSize.height) / 2.0)), size: itemSize) if let itemComponentView = itemView.view { if itemComponentView.superview == nil { self.addSubview(itemComponentView) + itemComponentView.isUserInteractionEnabled = false } itemTransition.setFrame(view: itemComponentView, frame: itemFrame) } - - 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 component.activeIndex < component.items.count { + let activeView = self.visibleItems[component.items[component.activeIndex].id]?.view + let nextIndex: Int + if component.transitionFraction > 0.0 { + nextIndex = max(0, component.activeIndex - 1) + } else { + nextIndex = min(component.items.count - 1, component.activeIndex + 1) + } + let nextView = self.visibleItems[component.items[nextIndex].id]?.view + if let activeView = activeView, let nextView = nextView { + let mergedFrame = activeView.frame.interpolate(to: nextView.frame, amount: abs(component.transitionFraction)) + transition.setFrame(layer: self.activeItemLayer, frame: CGRect(origin: CGPoint(x: mergedFrame.minX, y: availableSize.height - 3.0), size: CGSize(width: mergedFrame.width, height: 3.0))) + } } if themeUpdated { @@ -197,6 +322,8 @@ private final class StorageUsageHeaderComponent: Component { } final class StorageUsagePanelContainerComponent: Component { + typealias EnvironmentType = StorageUsagePanelContainerEnvironment + struct Item: Equatable { let id: AnyHashable let title: String @@ -215,17 +342,20 @@ final class StorageUsagePanelContainerComponent: Component { let theme: PresentationTheme let strings: PresentationStrings + let dateTimeFormat: PresentationDateTimeFormat let insets: UIEdgeInsets let items: [Item] init( theme: PresentationTheme, strings: PresentationStrings, + dateTimeFormat: PresentationDateTimeFormat, insets: UIEdgeInsets, items: [Item] ) { self.theme = theme self.strings = strings + self.dateTimeFormat = dateTimeFormat self.insets = insets self.items = items } @@ -237,6 +367,9 @@ final class StorageUsagePanelContainerComponent: Component { if lhs.strings !== rhs.strings { return false } + if lhs.dateTimeFormat != rhs.dateTimeFormat { + return false + } if lhs.insets != rhs.insets { return false } @@ -246,47 +379,211 @@ final class StorageUsagePanelContainerComponent: Component { return true } - class View: UIView { - private let topPanelBackgroundView: BlurredBackgroundView + class View: UIView, UIGestureRecognizerDelegate { + private let topPanelBackgroundView: UIView private let topPanelSeparatorLayer: SimpleLayer private let header = ComponentView() private var component: StorageUsagePanelContainerComponent? + private weak var state: EmptyComponentState? + private let panelsBackgroundLayer: SimpleLayer private var visiblePanels: [AnyHashable: ComponentView] = [:] + private var actualVisibleIds = Set() private var currentId: AnyHashable? + private var transitionFraction: CGFloat = 0.0 + private var animatingTransition: Bool = false override init(frame: CGRect) { - self.topPanelBackgroundView = BlurredBackgroundView(color: nil, enableBlur: true) + self.topPanelBackgroundView = UIView() self.topPanelSeparatorLayer = SimpleLayer() + self.panelsBackgroundLayer = SimpleLayer() + super.init(frame: frame) + self.layer.addSublayer(self.panelsBackgroundLayer) self.addSubview(self.topPanelBackgroundView) self.layer.addSublayer(self.topPanelSeparatorLayer) + + let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] point in + guard let self, let component = self.component, let currentId = self.currentId else { + return [] + } + guard let index = component.items.firstIndex(where: { $0.id == currentId }) else { + return [] + } + + /*if strongSelf.tabsContainerNode.bounds.contains(strongSelf.view.convert(point, to: strongSelf.tabsContainerNode.view)) { + return [] + }*/ + + if index == 0 { + return .left + } + return [.left, .right] + }) + panRecognizer.delegate = self + panRecognizer.delaysTouchesBegan = false + panRecognizer.cancelsTouchesInView = true + self.addGestureRecognizer(panRecognizer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - func update(component: StorageUsagePanelContainerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return false + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if let _ = otherGestureRecognizer as? InteractiveTransitionGestureRecognizer { + return false + } + if let _ = otherGestureRecognizer as? UIPanGestureRecognizer { + return true + } + return false + } + + @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .began: + func cancelContextGestures(view: UIView) { + if let gestureRecognizers = view.gestureRecognizers { + for gesture in gestureRecognizers { + if let gesture = gesture as? ContextGesture { + gesture.cancel() + } + } + } + for subview in view.subviews { + cancelContextGestures(view: subview) + } + } + + cancelContextGestures(view: self) + + //self.animatingTransition = true + case .changed: + guard let component = self.component, let currentId = self.currentId else { + return + } + guard let index = component.items.firstIndex(where: { $0.id == currentId }) else { + return + } + + let translation = recognizer.translation(in: self) + var transitionFraction = translation.x / self.bounds.width + if index <= 0 { + transitionFraction = min(0.0, transitionFraction) + } + if index >= component.items.count - 1 { + transitionFraction = max(0.0, transitionFraction) + } + self.transitionFraction = transitionFraction + self.state?.updated(transition: .immediate) + + // let nextKey = availablePanes[updatedIndex] + // print(transitionFraction) + //self.paneTransitionPromise.set(transitionFraction) + + //self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: .immediate) + //self.currentPaneUpdated?(false) + case .cancelled, .ended: + guard let component = self.component, let currentId = self.currentId else { + return + } + guard let index = component.items.firstIndex(where: { $0.id == currentId }) else { + return + } + + let translation = recognizer.translation(in: self) + let velocity = recognizer.velocity(in: self) + var directionIsToRight: Bool? + if abs(velocity.x) > 10.0 { + directionIsToRight = velocity.x < 0.0 + } else { + if abs(translation.x) > self.bounds.width / 2.0 { + directionIsToRight = translation.x > self.bounds.width / 2.0 + } + } + if let directionIsToRight = directionIsToRight { + var updatedIndex = index + if directionIsToRight { + updatedIndex = min(updatedIndex + 1, component.items.count - 1) + } else { + updatedIndex = max(updatedIndex - 1, 0) + } + self.currentId = component.items[updatedIndex].id + } + self.transitionFraction = 0.0 + + self.state?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring))) + + self.animatingTransition = false + //self.currentPaneUpdated?(false) + + //self.currentPaneStatusPromise.set(self.currentPane?.node.status ?? .single(nil)) + default: + break + } + } + + func update(component: StorageUsagePanelContainerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let environment = environment[StorageUsagePanelContainerEnvironment.self].value + let themeUpdated = self.component?.theme !== component.theme self.component = component + self.state = state if themeUpdated { - self.backgroundColor = component.theme.list.itemBlocksBackgroundColor - self.topPanelBackgroundView.updateColor(color: component.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.8), transition: .immediate) + self.panelsBackgroundLayer.backgroundColor = component.theme.list.itemBlocksBackgroundColor.cgColor self.topPanelSeparatorLayer.backgroundColor = component.theme.list.itemBlocksSeparatorColor.cgColor } - let topPanelFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: 44.0)) + let topPanelCoverHeight: CGFloat = 10.0 + + let topPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: -topPanelCoverHeight), size: CGSize(width: availableSize.width, height: 44.0)) transition.setFrame(view: self.topPanelBackgroundView, frame: topPanelFrame) - self.topPanelBackgroundView.update(size: topPanelFrame.size, transition: transition.containedViewLayoutTransition) + + let lockScrollingTransition: Transition + if themeUpdated { + lockScrollingTransition = transition + } else { + lockScrollingTransition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + } + lockScrollingTransition.setBackgroundColor(view: self.topPanelBackgroundView, color: environment.isScrollable ? component.theme.rootController.navigationBar.blurredBackgroundColor : component.theme.list.itemBlocksBackgroundColor) + + transition.setFrame(layer: self.panelsBackgroundLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: availableSize.width, height: availableSize.height - topPanelFrame.maxY))) transition.setFrame(layer: self.topPanelSeparatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: availableSize.width, height: UIScreenPixel))) + if let currentIdValue = self.currentId, !component.items.contains(where: { $0.id == currentIdValue }) { + self.currentId = nil + } + if self.currentId == nil { + self.currentId = component.items.first?.id + } + + var visibleIds = Set() + var currentIndex: Int? + if let currentId = self.currentId { + visibleIds.insert(currentId) + + if let index = component.items.firstIndex(where: { $0.id == currentId }) { + currentIndex = index + if index != 0 { + visibleIds.insert(component.items[index - 1].id) + } + if index != component.items.count - 1 { + visibleIds.insert(component.items[index + 1].id) + } + } + } + let _ = self.header.update( transition: transition, component: AnyComponent(StorageUsageHeaderComponent( @@ -296,6 +593,17 @@ final class StorageUsagePanelContainerComponent: Component { id: item.id, title: item.title ) + }, + activeIndex: currentIndex ?? 0, + transitionFraction: self.transitionFraction, + switchToPanel: { [weak self] id in + guard let self, let component = self.component else { + return + } + if component.items.contains(where: { $0.id == id }) { + self.currentId = id + self.state?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring))) + } } )), environment: {}, @@ -308,45 +616,114 @@ final class StorageUsagePanelContainerComponent: Component { 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) + dateTimeFormat: component.dateTimeFormat, + containerInsets: UIEdgeInsets(top: 0.0, left: component.insets.left, bottom: component.insets.bottom, right: component.insets.right), + isScrollable: environment.isScrollable ) + let centralPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: availableSize.width, height: availableSize.height - topPanelFrame.maxY)) + + if self.animatingTransition { + visibleIds = visibleIds.filter({ self.visiblePanels[$0] != nil }) + } + + self.actualVisibleIds = visibleIds + + for (id, _) in self.visiblePanels { + visibleIds.insert(id) + } + var validIds = Set() - if let currentId = self.currentId, let panelItem = component.items.first(where: { $0.id == currentId }) { - validIds.insert(panelItem.id) + if let currentIndex { + var anyAnchorOffset: CGFloat = 0.0 + for (id, panel) in self.visiblePanels { + guard let itemIndex = component.items.firstIndex(where: { $0.id == id }), let panelView = panel.view else { + continue + } + var itemFrame = centralPanelFrame.offsetBy(dx: self.transitionFraction * availableSize.width, dy: 0.0) + if itemIndex < currentIndex { + itemFrame.origin.x -= itemFrame.width + } else if itemIndex > currentIndex { + itemFrame.origin.x += itemFrame.width + } + + anyAnchorOffset = itemFrame.minX - panelView.frame.minX + + break + } - 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) + for id in visibleIds { + guard let itemIndex = component.items.firstIndex(where: { $0.id == id }) else { + continue + } + let panelItem = component.items[itemIndex] + + var itemFrame = centralPanelFrame.offsetBy(dx: self.transitionFraction * availableSize.width, dy: 0.0) + if itemIndex < currentIndex { + itemFrame.origin.x -= itemFrame.width + } else if itemIndex > currentIndex { + itemFrame.origin.x += itemFrame.width + } + + validIds.insert(panelItem.id) + + let panel: ComponentView + var panelTransition = transition + var animateInIfNeeded = false + if let current = self.visiblePanels[panelItem.id] { + panel = current + + if let panelView = panel.view, !panelView.bounds.isEmpty { + var wasHidden = false + if abs(panelView.frame.minX - availableSize.width) < .ulpOfOne || abs(panelView.frame.maxX - 0.0) < .ulpOfOne { + wasHidden = true + } + var isHidden = false + if abs(itemFrame.minX - availableSize.width) < .ulpOfOne || abs(itemFrame.maxX - 0.0) < .ulpOfOne { + isHidden = true + } + if wasHidden && isHidden { + panelTransition = .immediate + } + } + } else { + panelTransition = .immediate + animateInIfNeeded = true + + panel = ComponentView() + self.visiblePanels[panelItem.id] = panel + } + let _ = panel.update( + transition: panelTransition, + component: panelItem.panel, + environment: { + childEnvironment + }, + containerSize: centralPanelFrame.size + ) + if let panelView = panel.view { + if panelView.superview == nil { + self.insertSubview(panelView, belowSubview: self.topPanelBackgroundView) + } + + panelTransition.setFrame(view: panelView, frame: itemFrame, completion: { [weak self] _ in + guard let self else { + return + } + if !self.actualVisibleIds.contains(id) { + if let panel = self.visiblePanels[id] { + self.visiblePanels.removeValue(forKey: id) + panel.view?.removeFromSuperview() + } + } + }) + if animateInIfNeeded && anyAnchorOffset != 0.0 { + transition.animatePosition(view: panelView, from: CGPoint(x: -anyAnchorOffset, y: 0.0), to: CGPoint(), additive: true) + } } - panelTransition.setFrame(view: panelView, frame: CGRect(origin: CGPoint(), size: availableSize)) } } @@ -371,7 +748,7 @@ final class StorageUsagePanelContainerComponent: Component { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + 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 230f0db7a0..c4dc47be30 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift @@ -15,25 +15,53 @@ import Postbox import Markdown import ContextUI import AnimatedAvatarSetNode +import AvatarNode -private final class StorageUsageScreenComponent: Component { +private extension StorageUsageScreenComponent.Category { + init(_ category: StorageUsageStats.CategoryKey) { + switch category { + case .photos: + self = .photos + case .videos: + self = .videos + case .files: + self = .files + case .music: + self = .music + case .stickers: + self = .stickers + case .avatars: + self = .avatars + case .misc: + self = .misc + } + } +} + +final class StorageUsageScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let makeStorageUsageExceptionsScreen: (CacheStorageSettings.PeerStorageCategory) -> ViewController? + let peer: EnginePeer? init( context: AccountContext, - makeStorageUsageExceptionsScreen: @escaping (CacheStorageSettings.PeerStorageCategory) -> ViewController? + makeStorageUsageExceptionsScreen: @escaping (CacheStorageSettings.PeerStorageCategory) -> ViewController?, + peer: EnginePeer? ) { self.context = context self.makeStorageUsageExceptionsScreen = makeStorageUsageExceptionsScreen + self.peer = peer } static func ==(lhs: StorageUsageScreenComponent, rhs: StorageUsageScreenComponent) -> Bool { if lhs.context !== rhs.context { return false } + if lhs.peer != rhs.peer { + return false + } return true } @@ -46,7 +74,7 @@ private final class StorageUsageScreenComponent: Component { set(value) { var value = value if value.y > self.contentSize.height - self.bounds.height { - value.y = self.contentSize.height - self.bounds.height + value.y = max(0.0, self.contentSize.height - self.bounds.height) self.bounces = false } else { self.bounces = true @@ -59,10 +87,118 @@ private final class StorageUsageScreenComponent: Component { } private final class AnimationHint { - let isFirstStatsUpdate: Bool + enum Value { + case firstStatsUpdate + case clearedItems + } + let value: Value - init(isFirstStatsUpdate: Bool) { - self.isFirstStatsUpdate = isFirstStatsUpdate + init(value: Value) { + self.value = value + } + } + + final class SelectionState: Equatable { + let selectedPeers: Set + let selectedMessages: Set + + init( + selectedPeers: Set, + selectedMessages: Set + ) { + self.selectedPeers = selectedPeers + self.selectedMessages = selectedMessages + } + + static func ==(lhs: SelectionState, rhs: SelectionState) -> Bool { + if lhs.selectedPeers != rhs.selectedPeers { + return false + } + if lhs.selectedMessages != rhs.selectedMessages { + return false + } + return true + } + + func togglePeer(id: EnginePeer.Id) -> SelectionState { + var selectedPeers = self.selectedPeers + if selectedPeers.contains(id) { + selectedPeers.remove(id) + } else { + selectedPeers.insert(id) + } + + return SelectionState( + selectedPeers: selectedPeers, + selectedMessages: Set() + ) + } + + func toggleMessage(id: EngineMessage.Id) -> SelectionState { + var selectedMessages = self.selectedMessages + if selectedMessages.contains(id) { + selectedMessages.remove(id) + } else { + selectedMessages.insert(id) + } + + return SelectionState( + selectedPeers: Set(), + selectedMessages: selectedMessages + ) + } + } + + enum Category: Hashable { + case photos + case videos + case files + case music + case other + case stickers + case avatars + case misc + + var color: UIColor { + switch self { + case .photos: + return UIColor(rgb: 0x5AC8FA) + case .videos: + return UIColor(rgb: 0x3478F6) + case .files: + return UIColor(rgb: 0x34C759) + case .music: + return UIColor(rgb: 0xFF2D55) + case .other: + return UIColor(rgb: 0xC4C4C6) + case .stickers: + return UIColor(rgb: 0x5856D6) + case .avatars: + return UIColor(rgb: 0xAF52DE) + case .misc: + return UIColor(rgb: 0xFF9500) + } + } + + func title(strings: PresentationStrings) -> String { + switch self { + case .photos: + return "Photos" + case .videos: + return "Videos" + case .files: + return "Files" + case .music: + return "Music" + case .other: + return "Other" + case .stickers: + return "Stickers" + case .avatars: + return "Avatars" + case .misc: + return "Miscellaneous" + } } } @@ -70,13 +206,22 @@ private final class StorageUsageScreenComponent: Component { private let scrollView: ScrollViewImpl private var currentStats: AllStorageUsageStats? + private var currentMessages: [MessageId: Message] = [:] private var cacheSettings: CacheStorageSettings? private var peerItems: StoragePeerListPanelComponent.Items? + private var imageItems: StorageFileListPanelComponent.Items? + private var fileItems: StorageFileListPanelComponent.Items? + private var musicItems: StorageFileListPanelComponent.Items? - private var selectedCategories: Set = Set() + private var selectionState: SelectionState? + + private var selectedCategories: Set = Set() + private var isOtherCategoryExpanded: Bool = false private let navigationBackgroundView: BlurredBackgroundView private let navigationSeparatorLayer: SimpleLayer + private let navigationEditButton = ComponentView() + private let navigationDoneButton = ComponentView() private let headerView = ComponentView() private let headerOffsetContainer: UIView @@ -85,6 +230,8 @@ private final class StorageUsageScreenComponent: Component { private let headerProgressBackgroundLayer: SimpleLayer private let headerProgressForegroundLayer: SimpleLayer + private var chartAvatarNode: AvatarNode? + private let pieChartView = ComponentView() private let chartTotalLabel = ComponentView() private let categoriesView = ComponentView() @@ -95,7 +242,9 @@ private final class StorageUsageScreenComponent: Component { private var keepDurationSectionContainerView: UIView private var keepDurationItems: [AnyHashable: ComponentView] = [:] - private let panelContainer = ComponentView() + private let panelContainer = ComponentView() + + private var selectionPanel: ComponentView? private var component: StorageUsageScreenComponent? private weak var state: EmptyComponentState? @@ -109,6 +258,7 @@ private final class StorageUsageScreenComponent: Component { private var ignoreScrolling: Bool = false private var statsDisposable: Disposable? + private var messagesDisposable: Disposable? private var cacheSettingsDisposable: Disposable? override init(frame: CGRect) { @@ -163,6 +313,7 @@ private final class StorageUsageScreenComponent: Component { deinit { self.statsDisposable?.dispose() + self.messagesDisposable?.dispose() } func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { @@ -211,6 +362,8 @@ private final class StorageUsageScreenComponent: Component { private func updateScrolling(transition: Transition) { let scrollBounds = self.scrollView.bounds + let isLockedAtPanels = scrollBounds.maxY == self.scrollView.contentSize.height + if let headerView = self.headerView.view, let navigationMetrics = self.navigationMetrics { var headerOffset: CGFloat = scrollBounds.minY @@ -221,11 +374,20 @@ private final class StorageUsageScreenComponent: Component { headerOffset = min(headerOffset, minOffset) let animatedTransition = Transition(animation: .curve(duration: 0.18, curve: .easeInOut)) - if abs(headerOffset - minOffset) < 4.0 { - animatedTransition.setAlpha(view: self.navigationBackgroundView, alpha: 1.0) + let navigationBackgroundAlpha: CGFloat = abs(headerOffset - minOffset) < 4.0 ? 1.0 : 0.0 + + animatedTransition.setAlpha(view: self.navigationBackgroundView, alpha: navigationBackgroundAlpha) + + if let navigationEditButtonView = self.navigationEditButton.view { + animatedTransition.setAlpha(view: navigationEditButtonView, alpha: (self.selectionState == nil ? 1.0 : 0.0) * navigationBackgroundAlpha) + } + if let navigationDoneButtonView = self.navigationDoneButton.view { + animatedTransition.setAlpha(view: navigationDoneButtonView, alpha: (self.selectionState == nil ? 0.0 : 1.0) * navigationBackgroundAlpha) + } + + if abs(headerOffset - minOffset) < 4.0 && !isLockedAtPanels { animatedTransition.setAlpha(layer: self.navigationSeparatorLayer, alpha: 1.0) } else { - animatedTransition.setAlpha(view: self.navigationBackgroundView, alpha: 0.0) animatedTransition.setAlpha(layer: self.navigationSeparatorLayer, alpha: 0.0) } @@ -235,6 +397,13 @@ private final class StorageUsageScreenComponent: Component { transition.setBounds(view: self.headerOffsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: headerOffset), size: self.headerOffsetContainer.bounds.size)) } + + let _ = self.panelContainer.updateEnvironment( + transition: transition, + environment: { + StorageUsagePanelContainerEnvironment(isScrollable: isLockedAtPanels) + } + ) } func update(component: StorageUsageScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { @@ -264,51 +433,22 @@ private final class StorageUsageScreenComponent: Component { self.state?.updated(transition: .immediate) } }) - self.statsDisposable = (component.context.engine.resources.collectStorageUsageStats() - |> deliverOnMainQueue).start(next: { [weak self] stats in - guard let self else { - 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))) - }) + + self.reloadStats(firstTime: 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 { + if self.scrollView.bounds.minY > 0.0 && abs(self.scrollView.bounds.minY - (panelContainerView.frame.minY - navigationMetrics.navigationHeight)) <= UIScreenPixel { wasLockedAtPanels = true } } let animationHint = transition.userData(AnimationHint.self) - if let animationHint, animationHint.isFirstStatsUpdate { + if let animationHint, case .firstStatsUpdate = animationHint.value { var alphaTransition = transition - if animationHint.isFirstStatsUpdate { + if case .firstStatsUpdate = animationHint.value { alphaTransition = .easeInOut(duration: 0.25) } alphaTransition.setAlpha(view: self.scrollView, alpha: self.currentStats != nil ? 1.0 : 0.0) @@ -330,59 +470,57 @@ private final class StorageUsageScreenComponent: Component { transition.setFrame(view: self.navigationBackgroundView, frame: navigationFrame) transition.setFrame(layer: self.navigationSeparatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationFrame.maxY), size: CGSize(width: availableSize.width, height: UIScreenPixel))) - enum Category: Hashable { - case photos - case videos - case files - case music - case other - case stickers - case avatars - case misc - - var color: UIColor { - switch self { - case .photos: - return UIColor(rgb: 0x5AC8FA) - case .videos: - return UIColor(rgb: 0x3478F6) - case .files: - return UIColor(rgb: 0x34C759) - case .music: - return UIColor(rgb: 0xFF2D55) - case .other: - return UIColor(rgb: 0xC4C4C6) - case .stickers: - return UIColor(rgb: 0x5856D6) - case .avatars: - return UIColor(rgb: 0xAF52DE) - case .misc: - return UIColor(rgb: 0xFF9500) - } - } - - func title(strings: PresentationStrings) -> String { - switch self { - case .photos: - return "Photos" - case .videos: - return "Videos" - case .files: - return "Files" - case .music: - return "Music" - case .other: - return "Other" - case .stickers: - return "Stickers" - case .avatars: - return "Avatars" - case .misc: - return "Miscellaneous" + let navigationEditButtonSize = self.navigationEditButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent(Text(text: environment.strings.Common_Edit, font: Font.regular(17.0), color: environment.theme.rootController.navigationBar.accentTextColor)), + action: { [weak self] in + guard let self else { + return + } + if self.selectionState == nil { + self.selectionState = SelectionState( + selectedPeers: Set(), + selectedMessages: Set() + ) + self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + } } + ).minSize(CGSize(width: 16.0, height: environment.navigationHeight - environment.statusBarHeight))), + environment: {}, + containerSize: CGSize(width: 150.0, height: environment.navigationHeight - environment.statusBarHeight) + ) + if let navigationEditButtonView = self.navigationEditButton.view { + if navigationEditButtonView.superview == nil { + self.addSubview(navigationEditButtonView) } + transition.setFrame(view: navigationEditButtonView, frame: CGRect(origin: CGPoint(x: availableSize.width - 12.0 - navigationEditButtonSize.width, y: environment.statusBarHeight), size: navigationEditButtonSize)) } + let navigationDoneButtonSize = self.navigationDoneButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent(Text(text: environment.strings.Common_Done, font: Font.semibold(17.0), color: environment.theme.rootController.navigationBar.accentTextColor)), + action: { [weak self] in + guard let self else { + return + } + self.selectionState = nil + self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + } + ).minSize(CGSize(width: 16.0, height: environment.navigationHeight - environment.statusBarHeight))), + environment: {}, + containerSize: CGSize(width: 150.0, height: environment.navigationHeight - environment.statusBarHeight) + ) + if let navigationDoneButtonView = self.navigationDoneButton.view { + if navigationDoneButtonView.superview == nil { + self.addSubview(navigationDoneButtonView) + } + transition.setFrame(view: navigationDoneButtonView, frame: CGRect(origin: CGPoint(x: availableSize.width - 12.0 - navigationDoneButtonSize.width, y: environment.statusBarHeight), size: navigationDoneButtonSize)) + } + + let navigationRightButtonMaxWidth: CGFloat = max(navigationEditButtonSize.width, navigationDoneButtonSize.width) + self.backgroundColor = environment.theme.list.blocksBackgroundColor var contentHeight: CGFloat = 0.0 @@ -390,6 +528,86 @@ private final class StorageUsageScreenComponent: Component { let topInset: CGFloat = 19.0 let sideInset: CGFloat = 16.0 + environment.safeInsets.left + var bottomInset: CGFloat = environment.safeInsets.bottom + if let selectionState = self.selectionState { + let selectionPanel: ComponentView + var selectionPanelTransition = transition + if let current = self.selectionPanel { + selectionPanel = current + } else { + selectionPanelTransition = .immediate + selectionPanel = ComponentView() + self.selectionPanel = selectionPanel + } + + var selectedSize: Int64 = 0 + if let currentStats = self.currentStats { + for peerId in selectionState.selectedPeers { + if let stats = currentStats.peers[peerId] { + let peerSize = stats.stats.categories.values.reduce(0, { + $0 + $1.size + }) + selectedSize += peerSize + } + } + + let contextStats: StorageUsageStats + if let peer = component.peer { + contextStats = currentStats.peers[peer.id]?.stats ?? StorageUsageStats(categories: [:]) + } else { + contextStats = currentStats.totalStats + } + + for messageId in selectionState.selectedMessages { + for (_, category) in contextStats.categories { + if let messageSize = category.messages[messageId] { + selectedSize += messageSize + break + } + } + } + } + + //TODO:localize + let selectionPanelSize = selectionPanel.update( + transition: selectionPanelTransition, + component: AnyComponent(StorageUsageScreenSelectionPanelComponent( + theme: environment.theme, + title: "Clear Selected", + label: selectedSize == 0 ? nil : dataSizeString(Int(selectedSize), formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: ".")), + isEnabled: selectedSize != 0, + insets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: environment.safeInsets.bottom, right: sideInset), + action: { [weak self] in + guard let self, let selectionState = self.selectionState else { + return + } + self.requestClear(categories: Set(), peers: selectionState.selectedPeers, messages: selectionState.selectedMessages) + } + )), + environment: {}, + containerSize: availableSize + ) + if let selectionPanelView = selectionPanel.view { + var animateIn = false + if selectionPanelView.superview == nil { + self.addSubview(selectionPanelView) + animateIn = true + } + selectionPanelTransition.setFrame(view: selectionPanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - selectionPanelSize.height), size: selectionPanelSize)) + if animateIn { + transition.animatePosition(view: selectionPanelView, from: CGPoint(x: 0.0, y: selectionPanelSize.height), to: CGPoint(), additive: true) + } + } + bottomInset = selectionPanelSize.height + } else if let selectionPanel = self.selectionPanel { + self.selectionPanel = nil + if let selectionPanelView = selectionPanel.view { + transition.setPosition(view: selectionPanelView, position: CGPoint(x: selectionPanelView.center.x, y: availableSize.height + selectionPanelView.bounds.height * 0.5), completion: { [weak selectionPanelView] _ in + selectionPanelView?.removeFromSuperview() + }) + } + } + contentHeight += environment.statusBarHeight + topInset let chartOrder: [Category] = [ @@ -402,18 +620,40 @@ private final class StorageUsageScreenComponent: Component { .misc ] - if let animationHint, animationHint.isFirstStatsUpdate { - for category in chartOrder { - let _ = self.selectedCategories.insert(category) + 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)) + } } } var chartItems: [PieChartComponent.ChartData.Item] = [] var listCategories: [StorageCategoriesComponent.CategoryData] = [] + let otherCategories: [Category] = [ + .stickers, + .avatars, + .misc + ] + var totalSize: Int64 = 0 if let currentStats = self.currentStats { - for (_, value) in currentStats.totalStats.categories { + let contextStats: StorageUsageStats + if let peer = component.peer { + contextStats = currentStats.peers[peer.id]?.stats ?? StorageUsageStats(categories: [:]) + } else { + contextStats = currentStats.totalStats + } + + for (_, value) in contextStats.categories { totalSize += value.size } @@ -438,35 +678,45 @@ private final class StorageUsageScreenComponent: Component { continue } - if let categoryData = currentStats.totalStats.categories[mappedCategory] { - let categoryFraction: Double - if categoryData.size == 0 || totalSize == 0 { - categoryFraction = 0.0 - } else { - categoryFraction = Double(categoryData.size) / Double(totalSize) - } - chartItems.append(PieChartComponent.ChartData.Item(id: category, value: categoryFraction, color: category.color)) - + var categorySize: Int64 = 0 + if let categoryData = contextStats.categories[mappedCategory] { + categorySize = categoryData.size + } + + let categoryFraction: Double + if categorySize == 0 || totalSize == 0 { + categoryFraction = 0.0 + } else { + categoryFraction = Double(categorySize) / Double(totalSize) + } + + var categoryChartFraction: CGFloat = categoryFraction + if !self.selectedCategories.isEmpty && !self.selectedCategories.contains(category) { + categoryChartFraction = 0.0 + } + + var chartCategoryColor = category.color + if !self.isOtherCategoryExpanded && otherCategories.contains(category) { + chartCategoryColor = Category.other.color + } + + chartItems.append(PieChartComponent.ChartData.Item(id: category, displayValue: categoryFraction, value: categoryChartFraction, color: chartCategoryColor)) + + if categorySize != 0 { listCategories.append(StorageCategoriesComponent.CategoryData( - key: category, color: category.color, title: category.title(strings: environment.strings), size: categoryData.size, sizeFraction: categoryFraction, isSelected: self.selectedCategories.contains(category), subcategories: [])) + key: category, color: category.color, title: category.title(strings: environment.strings), size: categorySize, sizeFraction: categoryFraction, isSelected: self.selectedCategories.contains(category), subcategories: [])) } } } - let otherCategories: [Category] = [ - .stickers, - .avatars, - .misc - ] - var otherListCategories: [StorageCategoriesComponent.CategoryData] = [] for listCategory in listCategories { - if otherCategories.contains(where: { AnyHashable($0) == listCategory.key }) { + if otherCategories.contains(where: { $0 == listCategory.key }) { otherListCategories.append(listCategory) } } listCategories = listCategories.filter { item in - return !otherCategories.contains(where: { AnyHashable($0) == item.key }) + return !otherCategories.contains(where: { $0 == item.key }) } if !otherListCategories.isEmpty { var totalOtherSize: Int64 = 0 @@ -512,7 +762,7 @@ private final class StorageUsageScreenComponent: Component { transition: transition, component: AnyComponent(Text(text: "Storage Usage", font: Font.semibold(22.0), color: environment.theme.list.itemPrimaryTextColor)), environment: {}, - containerSize: CGSize(width: availableSize.width - 40.0 * 2.0, height: 100.0) + containerSize: CGSize(width: floor((availableSize.width - navigationRightButtonMaxWidth * 2.0) / 0.8), height: 100.0) ) let headerViewFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - headerViewSize.width) / 2.0), y: contentHeight), size: headerViewSize) if let headerComponentView = self.headerView.view { @@ -565,239 +815,364 @@ private final class StorageUsageScreenComponent: Component { contentHeight += 24.0 - let chartTotalLabelSize = self.chartTotalLabel.update( - transition: transition, - component: AnyComponent(Text(text: dataSizeString(Int(totalSize), formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: ".")), font: Font.with(size: 20.0, design: .round, weight: .bold), color: environment.theme.list.itemPrimaryTextColor)), environment: {}, containerSize: CGSize(width: 200.0, height: 200.0) - ) - if let chartTotalLabelView = self.chartTotalLabel.view { - if chartTotalLabelView.superview == nil { - 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)) - } - - self.categoriesView.parentState = state - let categoriesSize = self.categoriesView.update( - transition: transition, - component: AnyComponent(StorageCategoriesComponent( - theme: environment.theme, - strings: environment.strings, - categories: listCategories, - toggleCategorySelection: { [weak self] key in - guard let self else { - return - } - if key == AnyHashable(Category.other) { - let otherCategories: [Category] = [.stickers, .avatars, .misc] - if otherCategories.allSatisfy(self.selectedCategories.contains) { - for item in otherCategories { - self.selectedCategories.remove(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) { - self.selectedCategories.remove(key) - } else { - self.selectedCategories.insert(key) - } - } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) - } - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude) - ) - if let categoriesComponentView = self.categoriesView.view { - if categoriesComponentView.superview == nil { - self.scrollView.addSubview(categoriesComponentView) - } + if let peer = component.peer { + let avatarSize = CGSize(width: 72.0, height: 72.0) + let avatarFrame: CGRect = CGRect(origin: CGPoint(x: pieChartFrame.minX + floor((pieChartFrame.width - avatarSize.width) / 2.0), y: pieChartFrame.minY + floor((pieChartFrame.height - avatarSize.height) / 2.0)), size: avatarSize) - transition.setFrame(view: categoriesComponentView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: categoriesSize)) - } - contentHeight += categoriesSize.height - contentHeight += 8.0 - - //TODO:localize - let categoriesDescriptionSize = self.categoriesDescriptionView.update( - transition: transition, - component: AnyComponent(MultilineTextComponent(text: .markdown(text: "All media will stay in the Telegram cloud and can be re-downloaded if you need it again.", attributes: MarkdownAttributes( - body: body, - bold: bold, - link: body, - linkAttribute: { _ in nil } - )), maximumNumberOfLines: 0)), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 15.0 * 2.0, height: 10000.0) - ) - let categoriesDescriptionFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: categoriesDescriptionSize) - if let categoriesDescriptionComponentView = self.categoriesDescriptionView.view { - if categoriesDescriptionComponentView.superview == nil { - self.scrollView.addSubview(categoriesDescriptionComponentView) - } - transition.setFrame(view: categoriesDescriptionComponentView, frame: categoriesDescriptionFrame) - } - contentHeight += categoriesDescriptionSize.height - - contentHeight += 40.0 - - //TODO:localize - let keepDurationTitleSize = self.keepDurationTitleView.update( - transition: transition, - component: AnyComponent(MultilineTextComponent( - text: .markdown( - text: "KEEP MEDIA", attributes: MarkdownAttributes( - body: body, - bold: bold, - link: body, - linkAttribute: { _ in nil } - ) - ), - maximumNumberOfLines: 0 - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 15.0 * 2.0, height: 10000.0) - ) - let keepDurationTitleFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: keepDurationTitleSize) - if let keepDurationTitleComponentView = self.keepDurationTitleView.view { - if keepDurationTitleComponentView.superview == nil { - self.scrollView.addSubview(keepDurationTitleComponentView) - } - transition.setFrame(view: keepDurationTitleComponentView, frame: keepDurationTitleFrame) - } - contentHeight += keepDurationTitleSize.height - contentHeight += 8.0 - - var keepContentHeight: CGFloat = 0.0 - for i in 0 ..< 3 { - let item: ComponentView - if let current = self.keepDurationItems[i] { - item = current + let chartAvatarNode: AvatarNode + if let current = self.chartAvatarNode { + chartAvatarNode = current + transition.setFrame(view: chartAvatarNode.view, frame: avatarFrame) } else { - item = ComponentView() - self.keepDurationItems[i] = item + chartAvatarNode = AvatarNode(font: avatarPlaceholderFont(size: 17.0)) + self.chartAvatarNode = chartAvatarNode + self.scrollView.addSubview(chartAvatarNode.view) + chartAvatarNode.frame = avatarFrame + + chartAvatarNode.setPeer(context: component.context, theme: environment.theme, peer: peer, displayDimensions: avatarSize) } - - let mappedCategory: CacheStorageSettings.PeerStorageCategory - - //TODO:localize - let iconName: String - let title: String - switch i { - case 0: - iconName = "Settings/Menu/EditProfile" - title = "Private Chats" - mappedCategory = .privateChats - case 1: - iconName = "Settings/Menu/GroupChats" - title = "Group Chats" - mappedCategory = .groups - default: - iconName = "Settings/Menu/Channels" - title = "Channels" - mappedCategory = .channels - } - - let value = self.cacheSettings?.categoryStorageTimeout[mappedCategory] ?? Int32.max - let optionText: String - if value == Int32.max { - optionText = environment.strings.ClearCache_Forever - } else { - optionText = timeIntervalString(strings: environment.strings, value: value) - } - - let itemSize = item.update( + } else { + let chartTotalLabelSize = self.chartTotalLabel.update( transition: transition, - component: AnyComponent(StoragePeerTypeItemComponent( + component: AnyComponent(Text(text: dataSizeString(Int(totalSize), formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: ".")), font: Font.with(size: 20.0, design: .round, weight: .bold), color: environment.theme.list.itemPrimaryTextColor)), environment: {}, containerSize: CGSize(width: 200.0, height: 200.0) + ) + if let chartTotalLabelView = self.chartTotalLabel.view { + if chartTotalLabelView.superview == nil { + 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)) + } + } + + if !listCategories.isEmpty { + self.categoriesView.parentState = state + let categoriesSize = self.categoriesView.update( + transition: transition, + component: AnyComponent(StorageCategoriesComponent( theme: environment.theme, - iconName: iconName, - title: title, - value: optionText, - hasNext: i != 3 - 1, - action: { [weak self] sourceView in + strings: environment.strings, + categories: listCategories, + isOtherExpanded: self.isOtherCategoryExpanded, + toggleCategorySelection: { [weak self] key in guard let self else { return } - self.openKeepMediaCategory(mappedCategory: mappedCategory, sourceView: sourceView) + if key == Category.other { + let otherCategories: [Category] = [.stickers, .avatars, .misc] + if otherCategories.allSatisfy(self.selectedCategories.contains) { + for item in otherCategories { + self.selectedCategories.remove(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) { + self.selectedCategories.remove(key) + } else { + self.selectedCategories.insert(key) + } + } + self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + }, + toggleOtherExpanded: { [weak self] in + guard let self else { + return + } + + self.isOtherCategoryExpanded = !self.isOtherCategoryExpanded + self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + }, + clearAction: { [weak self] in + guard let self else { + return + } + self.requestClear(categories: self.selectedCategories, peers: Set(), messages: Set()) } )), environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude) ) - let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: keepContentHeight), size: itemSize) - if let itemView = item.view { - if itemView.superview == nil { - self.keepDurationSectionContainerView.addSubview(itemView) + if let categoriesComponentView = self.categoriesView.view { + if categoriesComponentView.superview == nil { + self.scrollView.addSubview(categoriesComponentView) } - transition.setFrame(view: itemView, frame: itemFrame) + + transition.setFrame(view: categoriesComponentView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: categoriesSize)) } - keepContentHeight += itemSize.height + contentHeight += categoriesSize.height + contentHeight += 8.0 + + + //TODO:localize + let categoriesDescriptionSize = self.categoriesDescriptionView.update( + transition: transition, + component: AnyComponent(MultilineTextComponent(text: .markdown(text: "All media will stay in the Telegram cloud and can be re-downloaded if you need it again.", attributes: MarkdownAttributes( + body: body, + bold: bold, + link: body, + linkAttribute: { _ in nil } + )), maximumNumberOfLines: 0)), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 15.0 * 2.0, height: 10000.0) + ) + let categoriesDescriptionFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: categoriesDescriptionSize) + if let categoriesDescriptionComponentView = self.categoriesDescriptionView.view { + if categoriesDescriptionComponentView.superview == nil { + self.scrollView.addSubview(categoriesDescriptionComponentView) + } + transition.setFrame(view: categoriesDescriptionComponentView, frame: categoriesDescriptionFrame) + } + contentHeight += categoriesDescriptionSize.height + contentHeight += 40.0 + } else { + self.categoriesView.view?.removeFromSuperview() + self.categoriesDescriptionView.view?.removeFromSuperview() } - self.keepDurationSectionContainerView.backgroundColor = environment.theme.list.itemBlocksBackgroundColor - transition.setFrame(view: self.keepDurationSectionContainerView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: keepContentHeight))) - contentHeight += keepContentHeight - contentHeight += 8.0 - //TODO:localize - let keepDurationDescriptionSize = self.keepDurationDescriptionView.update( - transition: transition, - component: AnyComponent(MultilineTextComponent( - text: .markdown( - text: "Photos, videos and other files from cloud chats that you have **not accessed** during this period will be removed from this device to save disk space.", attributes: MarkdownAttributes( - body: body, - bold: bold, - link: body, - linkAttribute: { _ in nil } - ) + if component.peer == nil { + //TODO:localize + let keepDurationTitleSize = self.keepDurationTitleView.update( + transition: transition, + component: AnyComponent(MultilineTextComponent( + text: .markdown( + text: "KEEP MEDIA", attributes: MarkdownAttributes( + body: body, + bold: bold, + link: body, + linkAttribute: { _ in nil } + ) + ), + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 15.0 * 2.0, height: 10000.0) + ) + let keepDurationTitleFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: keepDurationTitleSize) + if let keepDurationTitleComponentView = self.keepDurationTitleView.view { + if keepDurationTitleComponentView.superview == nil { + self.scrollView.addSubview(keepDurationTitleComponentView) + } + transition.setFrame(view: keepDurationTitleComponentView, frame: keepDurationTitleFrame) + } + contentHeight += keepDurationTitleSize.height + contentHeight += 8.0 + + + var keepContentHeight: CGFloat = 0.0 + for i in 0 ..< 3 { + let item: ComponentView + if let current = self.keepDurationItems[i] { + item = current + } else { + item = ComponentView() + self.keepDurationItems[i] = item + } + + let mappedCategory: CacheStorageSettings.PeerStorageCategory + + //TODO:localize + let iconName: String + let title: String + switch i { + case 0: + iconName = "Settings/Menu/EditProfile" + title = "Private Chats" + mappedCategory = .privateChats + case 1: + iconName = "Settings/Menu/GroupChats" + title = "Group Chats" + mappedCategory = .groups + default: + iconName = "Settings/Menu/Channels" + title = "Channels" + mappedCategory = .channels + } + + let value = self.cacheSettings?.categoryStorageTimeout[mappedCategory] ?? Int32.max + let optionText: String + if value == Int32.max { + optionText = environment.strings.ClearCache_Forever + } else { + optionText = timeIntervalString(strings: environment.strings, value: value) + } + + let itemSize = item.update( + transition: transition, + component: AnyComponent(StoragePeerTypeItemComponent( + theme: environment.theme, + iconName: iconName, + title: title, + value: optionText, + hasNext: i != 3 - 1, + action: { [weak self] sourceView in + guard let self else { + return + } + self.openKeepMediaCategory(mappedCategory: mappedCategory, sourceView: sourceView) + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: keepContentHeight), size: itemSize) + if let itemView = item.view { + if itemView.superview == nil { + self.keepDurationSectionContainerView.addSubview(itemView) + } + transition.setFrame(view: itemView, frame: itemFrame) + } + keepContentHeight += itemSize.height + } + self.keepDurationSectionContainerView.backgroundColor = environment.theme.list.itemBlocksBackgroundColor + transition.setFrame(view: self.keepDurationSectionContainerView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: keepContentHeight))) + contentHeight += keepContentHeight + contentHeight += 8.0 + + //TODO:localize + let keepDurationDescriptionSize = self.keepDurationDescriptionView.update( + transition: transition, + component: AnyComponent(MultilineTextComponent( + text: .markdown( + text: "Photos, videos and other files from cloud chats that you have **not accessed** during this period will be removed from this device to save disk space.", attributes: MarkdownAttributes( + body: body, + bold: bold, + link: body, + linkAttribute: { _ in nil } + ) + ), + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 15.0 * 2.0, height: 10000.0) + ) + let keepDurationDescriptionFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: keepDurationDescriptionSize) + if let keepDurationDescriptionComponentView = self.keepDurationDescriptionView.view { + if keepDurationDescriptionComponentView.superview == nil { + self.scrollView.addSubview(keepDurationDescriptionComponentView) + } + transition.setFrame(view: keepDurationDescriptionComponentView, frame: keepDurationDescriptionFrame) + } + contentHeight += keepDurationDescriptionSize.height + contentHeight += 40.0 + } + + var panelItems: [StorageUsagePanelContainerComponent.Item] = [] + if let peerItems = self.peerItems, !peerItems.items.isEmpty { + panelItems.append(StorageUsagePanelContainerComponent.Item( + id: "peers", + title: "Chats", + panel: AnyComponent(StoragePeerListPanelComponent( + context: component.context, + items: self.peerItems, + selectionState: self.selectionState, + peerAction: { [weak self] peer in + guard let self else { + return + } + if let selectionState = self.selectionState { + self.selectionState = selectionState.togglePeer(id: peer.id) + self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + } else { + self.openPeer(peer: peer) + } + } + )) + )) + } + if let imageItems = self.imageItems, !imageItems.items.isEmpty { + panelItems.append(StorageUsagePanelContainerComponent.Item( + id: "images", + title: "Media", + panel: AnyComponent(StorageFileListPanelComponent( + context: component.context, + items: self.imageItems, + selectionState: self.selectionState, + peerAction: { [weak self] messageId in + guard let self else { + return + } + if self.selectionState == nil { + self.selectionState = SelectionState(selectedPeers: Set(), selectedMessages: Set()) + } + self.selectionState = self.selectionState?.toggleMessage(id: messageId) + self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + } + )) + )) + } + if let fileItems = self.fileItems, !fileItems.items.isEmpty { + panelItems.append(StorageUsagePanelContainerComponent.Item( + id: "files", + title: "Files", + panel: AnyComponent(StorageFileListPanelComponent( + context: component.context, + items: self.fileItems, + selectionState: self.selectionState, + peerAction: { [weak self] messageId in + guard let self else { + return + } + if self.selectionState == nil { + self.selectionState = SelectionState(selectedPeers: Set(), selectedMessages: Set()) + } + self.selectionState = self.selectionState?.toggleMessage(id: messageId) + self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + } + )) + )) + } + if let musicItems = self.musicItems, !musicItems.items.isEmpty { + panelItems.append(StorageUsagePanelContainerComponent.Item( + id: "music", + title: "Music", + panel: AnyComponent(StorageFileListPanelComponent( + context: component.context, + items: self.musicItems, + selectionState: self.selectionState, + peerAction: { [weak self] messageId in + guard let self else { + return + } + if self.selectionState == nil { + self.selectionState = SelectionState(selectedPeers: Set(), selectedMessages: Set()) + } + self.selectionState = self.selectionState?.toggleMessage(id: messageId) + self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + } + )) + )) + } + + if !panelItems.isEmpty { + let panelContainerSize = self.panelContainer.update( + transition: transition, + component: AnyComponent(StorageUsagePanelContainerComponent( + theme: environment.theme, + strings: environment.strings, + dateTimeFormat: environment.dateTimeFormat, + insets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: bottomInset, right: environment.safeInsets.right), + items: panelItems) ), - maximumNumberOfLines: 0 - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 15.0 * 2.0, height: 10000.0) - ) - let keepDurationDescriptionFrame = CGRect(origin: CGPoint(x: sideInset + 15.0, y: contentHeight), size: keepDurationDescriptionSize) - if let keepDurationDescriptionComponentView = self.keepDurationDescriptionView.view { - if keepDurationDescriptionComponentView.superview == nil { - self.scrollView.addSubview(keepDurationDescriptionComponentView) + environment: { + StorageUsagePanelContainerEnvironment(isScrollable: wasLockedAtPanels) + }, + containerSize: CGSize(width: availableSize.width, height: availableSize.height - environment.navigationHeight) + ) + if let panelContainerView = self.panelContainer.view { + if panelContainerView.superview == nil { + self.scrollView.addSubview(panelContainerView) + } + transition.setFrame(view: panelContainerView, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: panelContainerSize)) } - transition.setFrame(view: keepDurationDescriptionComponentView, frame: keepDurationDescriptionFrame) + contentHeight += panelContainerSize.height + } else { + self.panelContainer.view?.removeFromSuperview() } - contentHeight += keepDurationDescriptionSize.height - contentHeight += 40.0 - - //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 @@ -827,6 +1202,312 @@ private final class StorageUsageScreenComponent: Component { return availableSize } + private func reloadStats(firstTime: Bool) { + if let controller = self.controller?() as? StorageUsageScreen { + controller.reloadParent?() + } + + guard let component = self.component else { + return + } + + self.statsDisposable = (component.context.engine.resources.collectStorageUsageStats() + |> deliverOnMainQueue).start(next: { [weak self] stats in + guard let self, let component = self.component else { + return + } + self.currentStats = stats + + var peerItems: [StoragePeerListPanelComponent.Item] = [] + + if component.peer == nil { + for item in stats.peers.values.sorted(by: { lhs, rhs in + let lhsSize: Int64 = lhs.stats.categories.values.reduce(0, { + $0 + $1.size + }) + let rhsSize: Int64 = rhs.stats.categories.values.reduce(0, { + $0 + $1.size + }) + return lhsSize > rhsSize + }) { + let itemSize: Int64 = item.stats.categories.values.reduce(0, { + $0 + $1.size + }) + peerItems.append(StoragePeerListPanelComponent.Item( + peer: item.peer, + size: itemSize + )) + } + } + + self.peerItems = StoragePeerListPanelComponent.Items(items: peerItems) + + self.state?.updated(transition: Transition(animation: .none).withUserData(AnimationHint(value: firstTime ? .firstStatsUpdate : .clearedItems))) + + class RenderResult { + var messages: [MessageId: Message] = [:] + var imageItems: [StorageFileListPanelComponent.Item] = [] + var fileItems: [StorageFileListPanelComponent.Item] = [] + 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 + let result = RenderResult() + + result.messages = messages + + var mergedMedia: [MessageId: Int64] = [:] + if let categoryStats = contextStats.categories[.photos] { + mergedMedia = categoryStats.messages + } + if let categoryStats = contextStats.categories[.videos] { + for (id, value) in categoryStats.messages { + mergedMedia[id] = value + } + } + + if !mergedMedia.isEmpty { + for (id, messageSize) in mergedMedia.sorted(by: { $0.value > $1.value }) { + if let message = messages[id] { + var matches = false + for media in message.media { + if media is TelegramMediaImage { + matches = true + break + } else if let file = media as? TelegramMediaFile { + if file.isVideo { + matches = true + break + } + } + } + + if matches { + result.imageItems.append(StorageFileListPanelComponent.Item( + message: message, + size: messageSize + )) + } + } + } + } + + if let categoryStats = contextStats.categories[.files] { + for (id, messageSize) in categoryStats.messages.sorted(by: { $0.value > $1.value }) { + if let message = messages[id] { + var matches = false + for media in message.media { + if media is TelegramMediaFile { + matches = true + } + } + + if matches { + result.fileItems.append(StorageFileListPanelComponent.Item( + message: message, + size: messageSize + )) + } + } + } + } + + if let categoryStats = contextStats.categories[.music] { + for (id, messageSize) in categoryStats.messages.sorted(by: { $0.value > $1.value }) { + if let message = messages[id] { + var matches = false + for media in message.media { + if media is TelegramMediaFile { + matches = true + } + } + + if matches { + result.musicItems.append(StorageFileListPanelComponent.Item( + message: message, + size: messageSize + )) + } + } + } + } + + return result + } + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self else { + return + } + + 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)) + }) + }) + } + + private func openPeer(peer: EnginePeer) { + guard let component = self.component else { + return + } + guard let controller = self.controller?() else { + return + } + + let childController = StorageUsageScreen(context: component.context, makeStorageUsageExceptionsScreen: component.makeStorageUsageExceptionsScreen, peer: peer) + childController.reloadParent = { [weak self] in + guard let self else { + return + } + self.reloadStats(firstTime: false) + } + controller.push(childController) + } + + private func requestClear(categories: Set, peers: Set, messages: Set) { + guard let component = self.component else { + return + } + let context = component.context + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let actionSheet = ActionSheetController(presentationData: presentationData) + //TODO:localizable + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: "Clear Selected", color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + + self?.commitClear(categories: categories, peers: peers, messages: messages) + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + self.controller?()?.present(actionSheet, in: .window(.root)) + } + + private func commitClear(categories: Set, peers: Set, messages: Set) { + guard let component = self.component else { + return + } + + if !categories.isEmpty { + let peerId: EnginePeer.Id? = component.peer?.id + + var mappedCategories: [StorageUsageStats.CategoryKey] = [] + for category in categories { + switch category { + case .photos: + mappedCategories.append(.photos) + case .videos: + mappedCategories.append(.videos) + case .files: + mappedCategories.append(.files) + case .music: + mappedCategories.append(.music) + case .other: + break + case .stickers: + mappedCategories.append(.stickers) + case .avatars: + mappedCategories.append(.avatars) + case .misc: + mappedCategories.append(.misc) + } + } + 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 { + return + } + var totalSize: Int64 = 0 + + let contextStats: StorageUsageStats + if let peer = component.peer { + contextStats = currentStats.peers[peer.id]?.stats ?? StorageUsageStats(categories: [:]) + } else { + contextStats = currentStats.totalStats + } + + for category in categories { + let mappedCategory: StorageUsageStats.CategoryKey + switch category { + case .photos: + mappedCategory = .photos + case .videos: + mappedCategory = .videos + case .files: + mappedCategory = .files + case .music: + mappedCategory = .music + case .other: + continue + case .stickers: + mappedCategory = .stickers + case .avatars: + mappedCategory = .avatars + case .misc: + mappedCategory = .misc + } + + if let value = contextStats.categories[mappedCategory] { + totalSize += value.size + } + } + + 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))) + }) + } else if !peers.isEmpty { + 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) + }) + } else if !messages.isEmpty { + var messageItems: [Message] = [] + for id in messages { + if let message = self.currentMessages[id] { + messageItems.append(message) + } + } + + 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) + }) + } + } + private func openKeepMediaCategory(mappedCategory: CacheStorageSettings.PeerStorageCategory, sourceView: StoragePeerTypeItemComponent.View) { guard let component = self.component else { return @@ -1035,12 +1716,16 @@ private final class StorageUsageScreenComponent: Component { public final class StorageUsageScreen: ViewControllerComponentContainer { private let context: AccountContext - public init(context: AccountContext, makeStorageUsageExceptionsScreen: @escaping (CacheStorageSettings.PeerStorageCategory) -> ViewController?) { + fileprivate var reloadParent: (() -> Void)? + + public init(context: AccountContext, makeStorageUsageExceptionsScreen: @escaping (CacheStorageSettings.PeerStorageCategory) -> ViewController?, peer: EnginePeer? = nil) { self.context = context - super.init(context: context, component: StorageUsageScreenComponent(context: context, makeStorageUsageExceptionsScreen: makeStorageUsageExceptionsScreen), navigationBarAppearance: .transparent) + super.init(context: context, component: StorageUsageScreenComponent(context: context, makeStorageUsageExceptionsScreen: makeStorageUsageExceptionsScreen, peer: peer), navigationBarAppearance: .transparent) - //self.navigationPresentation = .modal + if peer != nil { + self.navigationPresentation = .modal + } } required public init(coder aDecoder: NSCoder) { diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreenSelectionPanelComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreenSelectionPanelComponent.swift new file mode 100644 index 0000000000..edcaa66582 --- /dev/null +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreenSelectionPanelComponent.swift @@ -0,0 +1,157 @@ +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 SolidRoundedButtonComponent + +final class StorageUsageScreenSelectionPanelComponent: Component { + let theme: PresentationTheme + let title: String + let label: String? + let isEnabled: Bool + let insets: UIEdgeInsets + let action: () -> Void + + init( + theme: PresentationTheme, + title: String, + label: String?, + isEnabled: Bool, + insets: UIEdgeInsets, + action: @escaping () -> Void + ) { + self.theme = theme + self.title = title + self.label = label + self.isEnabled = isEnabled + self.insets = insets + self.action = action + } + + static func ==(lhs: StorageUsageScreenSelectionPanelComponent, rhs: StorageUsageScreenSelectionPanelComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.label != rhs.label { + return false + } + if lhs.isEnabled != rhs.isEnabled { + return false + } + if lhs.insets != rhs.insets { + return false + } + return true + } + + class View: UIView { + private let backgroundView: BlurredBackgroundView + private let separatorLayer: SimpleLayer + private let actionButton = ComponentView() + + private var component: StorageUsageScreenSelectionPanelComponent? + + override init(frame: CGRect) { + self.backgroundView = BlurredBackgroundView(color: nil, enableBlur: true) + self.separatorLayer = SimpleLayer() + + super.init(frame: frame) + + self.addSubview(self.backgroundView) + self.layer.addSublayer(self.separatorLayer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: StorageUsageScreenSelectionPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let themeUpdated = self.component?.theme !== component.theme + self.component = component + + let topInset: CGFloat = 8.0 + + let bottomInset: CGFloat + if component.insets.bottom == 0.0 { + bottomInset = topInset + } else { + bottomInset = component.insets.bottom + 10.0 + } + + let height: CGFloat = topInset + 50.0 + bottomInset + + if themeUpdated { + self.backgroundView.updateColor(color: component.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) + self.separatorLayer.backgroundColor = component.theme.rootController.navigationBar.separatorColor.cgColor + } + + + let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.height, height: height)) + transition.setFrame(view: self.backgroundView, frame: backgroundFrame) + self.backgroundView.update(size: backgroundFrame.size, transition: transition.containedViewLayoutTransition) + + transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: availableSize.width, height: UIScreenPixel))) + + let actionButtonSize = self.actionButton.update( + transition: transition, + component: AnyComponent(SolidRoundedButtonComponent( + title: component.title, + label: component.label, + theme: SolidRoundedButtonComponent.Theme( + backgroundColor: component.theme.list.itemCheckColors.fillColor, + backgroundColors: [], + foregroundColor: component.theme.list.itemCheckColors.foregroundColor + ), + font: .bold, + fontSize: 17.0, + height: 50.0, + cornerRadius: 10.0, + gloss: false, + isEnabled: component.isEnabled, + animationName: nil, + iconPosition: .right, + iconSpacing: 4.0, + action: { [weak self] in + guard let self else { + return + } + self.component?.action() + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - component.insets.left - component.insets.right, height: 50.0) + ) + if let actionButtonView = self.actionButton.view { + if actionButtonView.superview == nil { + self.addSubview(actionButtonView) + } + transition.setFrame(view: actionButtonView, frame: CGRect(origin: CGPoint(x: component.insets.left, y: topInset), size: actionButtonSize)) + } + + 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) + } +} diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 6a7327c266..f482bd96fa 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -1317,7 +1317,7 @@ private func extractAccountManagerState(records: AccountRecordsView = .complete() for (_, context, _) in activeAccounts.accounts { - signals = signals |> then(context.account.cleanupTasks()) + signals = signals |> then(context.account.cleanupTasks(lowImpact: false)) } disposable.set(signals.start(completed: { diff --git a/submodules/TelegramUI/Sources/ChatContextResultPeekContentNode.swift b/submodules/TelegramUI/Sources/ChatContextResultPeekContentNode.swift index 41582fb7ba..1bbde22823 100644 --- a/submodules/TelegramUI/Sources/ChatContextResultPeekContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatContextResultPeekContentNode.swift @@ -268,7 +268,7 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont let layerHolder = takeSampleBufferLayer() layerHolder.layer.videoGravity = AVLayerVideoGravity.resizeAspectFill self.layer.addSublayer(layerHolder.layer) - let manager = SoftwareVideoLayerFrameManager(account: self.account, userLocation: .other, userContentType: .gif, fileReference: videoFileReference, layerHolder: layerHolder) + let manager = SoftwareVideoLayerFrameManager(account: self.account, userLocation: .other, userContentType: .other, fileReference: videoFileReference, layerHolder: layerHolder) self.videoLayer = (thumbnailLayer, manager, layerHolder) thumbnailLayer.ready = { [weak self, weak thumbnailLayer, weak manager] in if let strongSelf = self, let thumbnailLayer = thumbnailLayer, let manager = manager { diff --git a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift index a35c27f63c..f86022e58a 100644 --- a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift +++ b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift @@ -399,7 +399,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode layerHolder.layer.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) strongSelf.layer.addSublayer(layerHolder.layer) - let manager = SoftwareVideoLayerFrameManager(account: item.account, userLocation: .other, userContentType: .gif, fileReference: .standalone(media: videoFile), layerHolder: layerHolder) + let manager = SoftwareVideoLayerFrameManager(account: item.account, userLocation: .other, userContentType: .other, fileReference: .standalone(media: videoFile), layerHolder: layerHolder) strongSelf.videoLayer = (thumbnailLayer, manager, layerHolder) thumbnailLayer.ready = { [weak thumbnailLayer, weak manager] in if let strongSelf = self, let thumbnailLayer = thumbnailLayer, let manager = manager { diff --git a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift index a5fff66eab..0ff7cd6a65 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift @@ -75,7 +75,7 @@ private final class FrameSequenceThumbnailNode: ASDisplayNode { let source = UniversalSoftwareVideoSource( mediaBox: self.context.account.postbox.mediaBox, userLocation: userLocation, - userContentType: .gif, + userContentType: .other, fileReference: self.file, automaticallyFetchHeader: true ) diff --git a/submodules/TelegramUI/Sources/PeerInfoGifPaneNode.swift b/submodules/TelegramUI/Sources/PeerInfoGifPaneNode.swift index 4b6f24b75f..a24126c45b 100644 --- a/submodules/TelegramUI/Sources/PeerInfoGifPaneNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfoGifPaneNode.swift @@ -65,7 +65,7 @@ private final class FrameSequenceThumbnailNode: ASDisplayNode { let source = UniversalSoftwareVideoSource( mediaBox: self.context.account.postbox.mediaBox, userLocation: userLocation, - userContentType: .gif, + userContentType: .other, fileReference: self.file, automaticallyFetchHeader: true ) @@ -301,7 +301,7 @@ private final class VisualMediaItemNode: ASDisplayNode { self.imageNode.layer.addSublayer(sampleBufferLayer.layer) } - self.videoLayerFrameManager = SoftwareVideoLayerFrameManager(account: self.context.account, userLocation: .peer(item.message.id.peerId), userContentType: .gif, fileReference: FileMediaReference.message(message: MessageReference(item.message), media: file), layerHolder: sampleBufferLayer) + self.videoLayerFrameManager = SoftwareVideoLayerFrameManager(account: self.context.account, userLocation: .peer(item.message.id.peerId), userContentType: .other, fileReference: FileMediaReference.message(message: MessageReference(item.message), media: file), layerHolder: sampleBufferLayer) self.videoLayerFrameManager?.start() } } else {