mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Storage management: add media grid view
This commit is contained in:
parent
0156554569
commit
8a348da927
@ -1371,7 +1371,7 @@ public func mediaGridMessagePhoto(account: Account, userLocation: MediaResourceU
|
||||
let fullSizeData = value._1
|
||||
let fullSizeComplete = value._3
|
||||
return { arguments in
|
||||
guard let context = DrawingContext(size: arguments.drawingSize, clear: true) else {
|
||||
guard let context = DrawingContext(size: arguments.drawingSize, opaque: arguments.corners.isEmpty && arguments.intrinsicInsets == .zero, clear: true) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -1951,7 +1951,7 @@ public func chatWebpageSnippetFile(account: Account, userLocation: MediaResource
|
||||
}
|
||||
|
||||
if let fullSizeImage = fullSizeImage ?? (blurredImage?.cgImage) {
|
||||
guard let context = DrawingContext(size: arguments.drawingSize, clear: true) else {
|
||||
guard let context = DrawingContext(size: arguments.drawingSize, opaque: arguments.corners.isEmpty && arguments.intrinsicInsets == .zero, clear: true) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -1980,7 +1980,7 @@ public func chatWebpageSnippetFile(account: Account, userLocation: MediaResource
|
||||
return context
|
||||
} else {
|
||||
if let emptyColor = arguments.emptyColor {
|
||||
guard let context = DrawingContext(size: arguments.drawingSize, clear: true) else {
|
||||
guard let context = DrawingContext(size: arguments.drawingSize, opaque: arguments.corners.isEmpty && arguments.intrinsicInsets == .zero, clear: true) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,550 @@
|
||||
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 badgeFont = Font.regular(12.0)
|
||||
private let videoIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat List/MiniThumbnailPlay"), color: .white)
|
||||
|
||||
private final class MediaGridLayer: SimpleLayer {
|
||||
enum SelectionState: Equatable {
|
||||
case none
|
||||
case editing(isSelected: Bool)
|
||||
}
|
||||
|
||||
private(set) var message: Message?
|
||||
private var disposable: Disposable?
|
||||
|
||||
private var size: CGSize?
|
||||
private var selectionState: SelectionState = .none
|
||||
private var theme: PresentationTheme?
|
||||
private var checkLayer: CheckLayer?
|
||||
private let badgeOverlay: SimpleLayer
|
||||
|
||||
override init() {
|
||||
self.badgeOverlay = SimpleLayer()
|
||||
self.badgeOverlay.contentsScale = UIScreenScale
|
||||
self.badgeOverlay.contentsGravity = .topRight
|
||||
|
||||
super.init()
|
||||
|
||||
self.isOpaque = true
|
||||
self.masksToBounds = true
|
||||
self.contentsGravity = .resizeAspectFill
|
||||
|
||||
self.addSublayer(self.badgeOverlay)
|
||||
}
|
||||
|
||||
override init(layer: Any) {
|
||||
self.badgeOverlay = SimpleLayer()
|
||||
|
||||
guard let other = layer as? MediaGridLayer else {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
super.init(layer: other)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable?.dispose()
|
||||
}
|
||||
|
||||
func prepareForReuse() {
|
||||
self.message = nil
|
||||
|
||||
if let disposable = self.disposable {
|
||||
self.disposable = nil
|
||||
disposable.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
func setup(context: AccountContext, strings: PresentationStrings, message: Message, size: Int64) {
|
||||
self.message = message
|
||||
|
||||
var isVideo = false
|
||||
var dimensions: CGSize?
|
||||
var signal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
|
||||
for media in message.media {
|
||||
if let file = media as? TelegramMediaFile, let representation = file.previewRepresentations.last {
|
||||
isVideo = file.isVideo
|
||||
signal = chatWebpageSnippetFile(
|
||||
account: context.account,
|
||||
userLocation: .peer(message.id.peerId),
|
||||
mediaReference: FileMediaReference.standalone(media: file).abstract,
|
||||
representation: representation,
|
||||
automaticFetch: false
|
||||
)
|
||||
dimensions = representation.dimensions.cgSize
|
||||
} else if let image = media as? TelegramMediaImage, let representation = image.representations.last {
|
||||
signal = mediaGridMessagePhoto(
|
||||
account: context.account,
|
||||
userLocation: .peer(message.id.peerId),
|
||||
photoReference: ImageMediaReference.standalone(media: image),
|
||||
automaticFetch: false
|
||||
)
|
||||
dimensions = representation.dimensions.cgSize
|
||||
}
|
||||
}
|
||||
|
||||
if let signal, let dimensions {
|
||||
self.disposable = (signal
|
||||
|> map { generator -> UIImage? in
|
||||
return generator(TransformImageArguments(corners: ImageCorners(radius: 0.0), imageSize: dimensions, boundingSize: CGSize(width: 100.0, height: 100.0), intrinsicInsets: UIEdgeInsets()))?.generateImage()
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { [weak self] image in
|
||||
guard let self, let image else {
|
||||
return
|
||||
}
|
||||
self.contents = image.cgImage
|
||||
})
|
||||
}
|
||||
|
||||
let text: String = dataSizeString(Int(size), formatting: DataSizeStringFormatting(strings: strings, decimalSeparator: "."))
|
||||
let attributedText = NSAttributedString(string: text, font: badgeFont, textColor: .white)
|
||||
let textBounds = attributedText.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
|
||||
let textSize = CGSize(width: ceil(textBounds.width), height: ceil(textBounds.height))
|
||||
let textLeftInset: CGFloat
|
||||
let textRightInset: CGFloat = 6.0
|
||||
if isVideo {
|
||||
textLeftInset = 18.0
|
||||
} else {
|
||||
textLeftInset = textRightInset
|
||||
}
|
||||
let badgeSize = CGSize(width: textLeftInset + textRightInset + textSize.width, height: 18.0)
|
||||
self.badgeOverlay.contents = generateImage(badgeSize, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(UIColor(white: 0.0, alpha: 0.5).cgColor)
|
||||
context.setBlendMode(.copy)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.height, height: size.height)))
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - size.height, y: 0.0), size: CGSize(width: size.height, height: size.height)))
|
||||
context.fill(CGRect(origin: CGPoint(x: size.height * 0.5, y: 0.0), size: CGSize(width: size.width - size.height, height: size.height)))
|
||||
context.setBlendMode(.normal)
|
||||
|
||||
UIGraphicsPushContext(context)
|
||||
|
||||
if isVideo, let videoIcon {
|
||||
videoIcon.draw(at: CGPoint(x: 2.0, y: floor((size.height - videoIcon.size.height) / 2.0)))
|
||||
}
|
||||
|
||||
attributedText.draw(in: textBounds.offsetBy(dx: textLeftInset, dy: UIScreenPixel + floor((size.height - textSize.height) * 0.5)))
|
||||
|
||||
UIGraphicsPopContext()
|
||||
})?.cgImage
|
||||
}
|
||||
|
||||
func updateSelection(size: CGSize, selectionState: SelectionState, theme: PresentationTheme, transition: Transition) {
|
||||
if self.size == size && self.selectionState == selectionState && self.theme === theme {
|
||||
return
|
||||
}
|
||||
|
||||
self.selectionState = selectionState
|
||||
self.size = size
|
||||
|
||||
let themeUpdated = self.theme !== theme
|
||||
self.theme = theme
|
||||
|
||||
switch selectionState {
|
||||
case .none:
|
||||
if let checkLayer = self.checkLayer {
|
||||
self.checkLayer = nil
|
||||
if !transition.animation.isImmediate {
|
||||
checkLayer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false)
|
||||
checkLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak checkLayer] _ in
|
||||
checkLayer?.removeFromSuperlayer()
|
||||
})
|
||||
} else {
|
||||
checkLayer.removeFromSuperlayer()
|
||||
}
|
||||
}
|
||||
case let .editing(isSelected):
|
||||
let checkWidth: CGFloat
|
||||
if size.width <= 60.0 {
|
||||
checkWidth = 22.0
|
||||
} else {
|
||||
checkWidth = 28.0
|
||||
}
|
||||
let checkSize = CGSize(width: checkWidth, height: checkWidth)
|
||||
let checkFrame = CGRect(origin: CGPoint(x: self.bounds.size.width - checkSize.width - 2.0, y: 2.0), size: checkSize)
|
||||
|
||||
if let checkLayer = self.checkLayer {
|
||||
if checkLayer.bounds.size != checkFrame.size {
|
||||
checkLayer.setNeedsDisplay()
|
||||
}
|
||||
transition.setFrame(layer: checkLayer, frame: checkFrame)
|
||||
if themeUpdated {
|
||||
checkLayer.theme = CheckNodeTheme(theme: theme, style: .overlay)
|
||||
}
|
||||
checkLayer.setSelected(isSelected, animated: !transition.animation.isImmediate)
|
||||
} else {
|
||||
let checkLayer = CheckLayer(theme: CheckNodeTheme(theme: theme, style: .overlay))
|
||||
self.checkLayer = checkLayer
|
||||
self.addSublayer(checkLayer)
|
||||
checkLayer.frame = checkFrame
|
||||
checkLayer.setSelected(isSelected, animated: false)
|
||||
checkLayer.setNeedsDisplay()
|
||||
|
||||
if !transition.animation.isImmediate {
|
||||
checkLayer.animateScale(from: 0.001, to: 1.0, duration: 0.2)
|
||||
checkLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.badgeOverlay.frame = CGRect(origin: CGPoint(x: size.width - 3.0, y: size.height - 3.0), size: CGSize(width: 0.0, height: 0.0))
|
||||
}
|
||||
}
|
||||
|
||||
private final class MediaGridLayerDataContext {
|
||||
|
||||
}
|
||||
|
||||
final class StorageMediaGridPanelComponent: Component {
|
||||
typealias EnvironmentType = StorageUsagePanelEnvironment
|
||||
|
||||
final class Item: Equatable {
|
||||
let message: 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: StorageMediaGridPanelComponent, rhs: StorageMediaGridPanelComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.items != rhs.items {
|
||||
return false
|
||||
}
|
||||
if lhs.selectionState != rhs.selectionState {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private struct ItemLayout: Equatable {
|
||||
var width: CGFloat
|
||||
var itemCount: Int
|
||||
var nativeItemSize: CGFloat
|
||||
let visibleItemSize: CGFloat
|
||||
|
||||
var itemInsets: UIEdgeInsets
|
||||
var itemSpacing: CGFloat
|
||||
var itemsPerRow: Int
|
||||
var contentSize: CGSize
|
||||
|
||||
init(
|
||||
width: CGFloat,
|
||||
containerInsets: UIEdgeInsets,
|
||||
itemCount: Int
|
||||
) {
|
||||
self.width = width
|
||||
self.itemCount = itemCount
|
||||
|
||||
let minItemsPerRow: Int = 3
|
||||
let itemSpacing: CGFloat = UIScreenPixel
|
||||
self.itemSpacing = itemSpacing
|
||||
let itemInsets: UIEdgeInsets = UIEdgeInsets(top: containerInsets.top, left: containerInsets.left, bottom: containerInsets.bottom, right: containerInsets.right)
|
||||
self.nativeItemSize = 120.0
|
||||
|
||||
self.itemInsets = itemInsets
|
||||
let itemHorizontalSpace = width - self.itemInsets.left - self.itemInsets.right
|
||||
self.itemsPerRow = max(minItemsPerRow, Int((itemHorizontalSpace + itemSpacing) / (self.nativeItemSize + itemSpacing)))
|
||||
let proposedItemSize = floor((itemHorizontalSpace - itemSpacing * (CGFloat(self.itemsPerRow) - 1.0)) / CGFloat(self.itemsPerRow))
|
||||
self.visibleItemSize = proposedItemSize
|
||||
|
||||
let numRows = (itemCount + (self.itemsPerRow - 1)) / self.itemsPerRow
|
||||
|
||||
self.contentSize = CGSize(
|
||||
width: width,
|
||||
height: self.itemInsets.top + self.itemInsets.bottom + CGFloat(numRows) * self.visibleItemSize + CGFloat(max(0, numRows - 1)) * self.itemSpacing
|
||||
)
|
||||
}
|
||||
|
||||
func frame(itemIndex: Int) -> CGRect {
|
||||
let row = itemIndex / self.itemsPerRow
|
||||
let column = itemIndex % self.itemsPerRow
|
||||
|
||||
var result = CGRect(
|
||||
origin: CGPoint(
|
||||
x: self.itemInsets.left + CGFloat(column) * (self.visibleItemSize + self.itemSpacing),
|
||||
y: self.itemInsets.top + CGFloat(row) * (self.visibleItemSize + self.itemSpacing)
|
||||
),
|
||||
size: CGSize(
|
||||
width: self.visibleItemSize,
|
||||
height: self.visibleItemSize
|
||||
)
|
||||
)
|
||||
if column == self.itemsPerRow - 1 {
|
||||
result.size.width = max(result.size.width, self.width - self.itemInsets.right - result.minX)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func visibleItems(for rect: CGRect) -> Range<Int>? {
|
||||
let offsetRect = rect.offsetBy(dx: -self.itemInsets.left, dy: -self.itemInsets.top)
|
||||
var minVisibleRow = Int(floor((offsetRect.minY - self.itemSpacing) / (self.visibleItemSize + self.itemSpacing)))
|
||||
minVisibleRow = max(0, minVisibleRow)
|
||||
let maxVisibleRow = Int(ceil((offsetRect.maxY - self.itemSpacing) / (self.visibleItemSize + self.itemSpacing)))
|
||||
|
||||
let minVisibleIndex = minVisibleRow * self.itemsPerRow
|
||||
let maxVisibleIndex = min(self.itemCount - 1, (maxVisibleRow + 1) * self.itemsPerRow - 1)
|
||||
|
||||
return maxVisibleIndex >= minVisibleIndex ? (minVisibleIndex ..< (maxVisibleIndex + 1)) : nil
|
||||
}
|
||||
}
|
||||
|
||||
class View: UIView, UIScrollViewDelegate {
|
||||
private let scrollView: UIScrollView
|
||||
|
||||
private var visibleLayers: [EngineMessage.Id: MediaGridLayer] = [:]
|
||||
private var layersAvailableForReuse: [MediaGridLayer] = []
|
||||
|
||||
private var ignoreScrolling: Bool = false
|
||||
|
||||
private var component: StorageMediaGridPanelComponent?
|
||||
private var environment: StorageUsagePanelEnvironment?
|
||||
private var itemLayout: ItemLayout?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.scrollView = UIScrollView()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.scrollView.delaysContentTouches = true
|
||||
self.scrollView.canCancelContentTouches = true
|
||||
self.scrollView.clipsToBounds = false
|
||||
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
||||
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
if #available(iOS 13.0, *) {
|
||||
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
|
||||
}
|
||||
self.scrollView.showsVerticalScrollIndicator = true
|
||||
self.scrollView.showsHorizontalScrollIndicator = false
|
||||
self.scrollView.alwaysBounceHorizontal = false
|
||||
self.scrollView.scrollsToTop = false
|
||||
self.scrollView.delegate = self
|
||||
self.scrollView.clipsToBounds = true
|
||||
self.addSubview(self.scrollView)
|
||||
|
||||
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
let point = recognizer.location(in: self.scrollView)
|
||||
for (id, itemLayer) in self.visibleLayers {
|
||||
if itemLayer.frame.contains(point) {
|
||||
component.peerAction(id)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 _ = environment
|
||||
|
||||
var validIds = Set<EngineMessage.Id>()
|
||||
|
||||
let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -100.0)
|
||||
if let visibleItems = itemLayout.visibleItems(for: visibleBounds) {
|
||||
for index in visibleItems.lowerBound ..< visibleItems.upperBound {
|
||||
if index >= items.items.count {
|
||||
continue
|
||||
}
|
||||
|
||||
let item = items.items[index]
|
||||
let id = item.message.id
|
||||
validIds.insert(id)
|
||||
}
|
||||
|
||||
var removeIds: [EngineMessage.Id] = []
|
||||
for (id, itemLayer) in self.visibleLayers {
|
||||
if !validIds.contains(id) {
|
||||
removeIds.append(id)
|
||||
itemLayer.isHidden = true
|
||||
self.layersAvailableForReuse.append(itemLayer)
|
||||
itemLayer.prepareForReuse()
|
||||
}
|
||||
}
|
||||
for id in removeIds {
|
||||
self.visibleLayers.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
for index in visibleItems.lowerBound ..< visibleItems.upperBound {
|
||||
if index >= items.items.count {
|
||||
continue
|
||||
}
|
||||
|
||||
let item = items.items[index]
|
||||
let id = item.message.id
|
||||
|
||||
var setupItemLayer = false
|
||||
|
||||
let itemLayer: MediaGridLayer
|
||||
if let current = self.visibleLayers[id] {
|
||||
itemLayer = current
|
||||
} else if !self.layersAvailableForReuse.isEmpty {
|
||||
setupItemLayer = true
|
||||
itemLayer = self.layersAvailableForReuse.removeLast()
|
||||
itemLayer.isHidden = false
|
||||
self.visibleLayers[id] = itemLayer
|
||||
} else {
|
||||
setupItemLayer = true
|
||||
itemLayer = MediaGridLayer()
|
||||
self.visibleLayers[id] = itemLayer
|
||||
self.scrollView.layer.addSublayer(itemLayer)
|
||||
}
|
||||
|
||||
let itemFrame = itemLayout.frame(itemIndex: index)
|
||||
itemLayer.frame = itemFrame
|
||||
|
||||
if setupItemLayer {
|
||||
itemLayer.setup(context: component.context, strings: environment.strings, message: item.message, size: item.size)
|
||||
}
|
||||
|
||||
let itemSelectionState: MediaGridLayer.SelectionState
|
||||
if let selectionState = component.selectionState {
|
||||
itemSelectionState = .editing(isSelected: selectionState.selectedMessages.contains(id))
|
||||
} else {
|
||||
itemSelectionState = .none
|
||||
}
|
||||
|
||||
itemLayer.updateSelection(size: itemFrame.size, selectionState: itemSelectionState, theme: environment.theme, transition: transition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: StorageMediaGridPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<StorageUsagePanelEnvironment>, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
|
||||
let environment = environment[StorageUsagePanelEnvironment.self].value
|
||||
self.environment = environment
|
||||
|
||||
|
||||
let itemLayout = ItemLayout(
|
||||
width: availableSize.width,
|
||||
containerInsets: environment.containerInsets,
|
||||
itemCount: component.items?.items.count ?? 0
|
||||
)
|
||||
self.itemLayout = itemLayout
|
||||
|
||||
self.ignoreScrolling = true
|
||||
let contentOffset = self.scrollView.bounds.minY
|
||||
transition.setPosition(view: self.scrollView, position: CGRect(origin: CGPoint(), size: availableSize).center)
|
||||
var scrollBounds = self.scrollView.bounds
|
||||
scrollBounds.size = availableSize
|
||||
if !environment.isScrollable {
|
||||
scrollBounds.origin = CGPoint()
|
||||
}
|
||||
transition.setBounds(view: self.scrollView, bounds: scrollBounds)
|
||||
self.scrollView.isScrollEnabled = environment.isScrollable
|
||||
let contentSize = CGSize(width: availableSize.width, height: itemLayout.contentSize.height)
|
||||
if self.scrollView.contentSize != contentSize {
|
||||
self.scrollView.contentSize = contentSize
|
||||
}
|
||||
self.scrollView.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<StorageUsagePanelEnvironment>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
@ -22,6 +22,70 @@ import AnimatedStickerNode
|
||||
import TelegramAnimatedStickerNode
|
||||
import TelegramStringFormatting
|
||||
|
||||
#if DEBUG
|
||||
import os.signpost
|
||||
|
||||
private class SignpostContext {
|
||||
enum EventType {
|
||||
case begin
|
||||
case end
|
||||
}
|
||||
|
||||
class OpaqueData {
|
||||
}
|
||||
|
||||
static var shared: SignpostContext? = {
|
||||
if #available(iOS 15.0, *) {
|
||||
return SignpostContextImpl()
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}()
|
||||
|
||||
func begin(name: StaticString) -> OpaqueData {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func end(name: StaticString, data: OpaqueData) {
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 15.0, *)
|
||||
private final class SignpostContextImpl: SignpostContext {
|
||||
final class OpaqueDataImpl: OpaqueData {
|
||||
let state: OSSignpostIntervalState
|
||||
let timestamp: Double
|
||||
|
||||
init(state: OSSignpostIntervalState, timestamp: Double) {
|
||||
self.state = state
|
||||
self.timestamp = timestamp
|
||||
}
|
||||
}
|
||||
|
||||
private let signpost = OSSignposter(subsystem: "org.telegram.Telegram-iOS", category: "StorageUsageScreen")
|
||||
private let id: OSSignpostID
|
||||
|
||||
override init() {
|
||||
self.id = self.signpost.makeSignpostID()
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
override func begin(name: StaticString) -> OpaqueData {
|
||||
let result = self.signpost.beginInterval(name, id: self.id)
|
||||
return OpaqueDataImpl(state: result, timestamp: CFAbsoluteTimeGetCurrent())
|
||||
}
|
||||
|
||||
override func end(name: StaticString, data: OpaqueData) {
|
||||
if let data = data as? OpaqueDataImpl {
|
||||
self.signpost.endInterval(name, data.state)
|
||||
print("Signpost \(name): \((CFAbsoluteTimeGetCurrent() - data.timestamp) * 1000.0) ms")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
private extension StorageUsageScreenComponent.Category {
|
||||
init(_ category: StorageUsageStats.CategoryKey) {
|
||||
switch category {
|
||||
@ -225,7 +289,7 @@ final class StorageUsageScreenComponent: Component {
|
||||
private var cacheSettingsExceptionCount: [CacheStorageSettings.PeerStorageCategory: Int32]?
|
||||
|
||||
private var peerItems: StoragePeerListPanelComponent.Items?
|
||||
private var imageItems: StorageFileListPanelComponent.Items?
|
||||
private var imageItems: StorageMediaGridPanelComponent.Items?
|
||||
private var fileItems: StorageFileListPanelComponent.Items?
|
||||
private var musicItems: StorageFileListPanelComponent.Items?
|
||||
|
||||
@ -629,11 +693,21 @@ final class StorageUsageScreenComponent: Component {
|
||||
return
|
||||
}
|
||||
if self.selectionState == nil {
|
||||
#if DEBUG
|
||||
let signpostState = SignpostContext.shared?.begin(name: "edit")
|
||||
#endif
|
||||
|
||||
self.selectionState = SelectionState(
|
||||
selectedPeers: Set(),
|
||||
selectedMessages: Set()
|
||||
)
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||
|
||||
#if DEBUG
|
||||
if let signpostState {
|
||||
SignpostContext.shared?.end(name: "edit", data: signpostState)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
).minSize(CGSize(width: 16.0, height: environment.navigationHeight - environment.statusBarHeight))),
|
||||
@ -1483,7 +1557,7 @@ final class StorageUsageScreenComponent: Component {
|
||||
panelItems.append(StorageUsagePanelContainerComponent.Item(
|
||||
id: "images",
|
||||
title: environment.strings.StorageManagement_TabMedia,
|
||||
panel: AnyComponent(StorageFileListPanelComponent(
|
||||
panel: AnyComponent(StorageMediaGridPanelComponent(
|
||||
context: component.context,
|
||||
items: self.imageItems,
|
||||
selectionState: self.selectionState,
|
||||
@ -1716,7 +1790,7 @@ final class StorageUsageScreenComponent: Component {
|
||||
|
||||
class RenderResult {
|
||||
var messages: [MessageId: Message] = [:]
|
||||
var imageItems: [StorageFileListPanelComponent.Item] = []
|
||||
var imageItems: [StorageMediaGridPanelComponent.Item] = []
|
||||
var fileItems: [StorageFileListPanelComponent.Item] = []
|
||||
var musicItems: [StorageFileListPanelComponent.Item] = []
|
||||
}
|
||||
@ -1755,7 +1829,7 @@ final class StorageUsageScreenComponent: Component {
|
||||
}
|
||||
|
||||
if matches {
|
||||
result.imageItems.append(StorageFileListPanelComponent.Item(
|
||||
result.imageItems.append(StorageMediaGridPanelComponent.Item(
|
||||
message: message,
|
||||
size: messageSize
|
||||
))
|
||||
@ -1846,7 +1920,7 @@ final class StorageUsageScreenComponent: Component {
|
||||
|
||||
self.currentMessages = result.messages
|
||||
|
||||
self.imageItems = StorageFileListPanelComponent.Items(items: result.imageItems)
|
||||
self.imageItems = StorageMediaGridPanelComponent.Items(items: result.imageItems)
|
||||
self.fileItems = StorageFileListPanelComponent.Items(items: result.fileItems)
|
||||
self.musicItems = StorageFileListPanelComponent.Items(items: result.musicItems)
|
||||
|
||||
@ -1905,6 +1979,7 @@ final class StorageUsageScreenComponent: Component {
|
||||
}
|
||||
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
|
||||
ActionSheetTextItem(title: presentationData.strings.StorageManagement_ClearConfirmationText, parseMarkdown: true),
|
||||
ActionSheetButtonItem(title: clearTitle, color: .destructive, action: { [weak self, weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user