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 fullSizeData = value._1
|
||||||
let fullSizeComplete = value._3
|
let fullSizeComplete = value._3
|
||||||
return { arguments in
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1951,7 +1951,7 @@ public func chatWebpageSnippetFile(account: Account, userLocation: MediaResource
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let fullSizeImage = fullSizeImage ?? (blurredImage?.cgImage) {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1980,7 +1980,7 @@ public func chatWebpageSnippetFile(account: Account, userLocation: MediaResource
|
|||||||
return context
|
return context
|
||||||
} else {
|
} else {
|
||||||
if let emptyColor = arguments.emptyColor {
|
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
|
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 TelegramAnimatedStickerNode
|
||||||
import TelegramStringFormatting
|
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 {
|
private extension StorageUsageScreenComponent.Category {
|
||||||
init(_ category: StorageUsageStats.CategoryKey) {
|
init(_ category: StorageUsageStats.CategoryKey) {
|
||||||
switch category {
|
switch category {
|
||||||
@ -225,7 +289,7 @@ final class StorageUsageScreenComponent: Component {
|
|||||||
private var cacheSettingsExceptionCount: [CacheStorageSettings.PeerStorageCategory: Int32]?
|
private var cacheSettingsExceptionCount: [CacheStorageSettings.PeerStorageCategory: Int32]?
|
||||||
|
|
||||||
private var peerItems: StoragePeerListPanelComponent.Items?
|
private var peerItems: StoragePeerListPanelComponent.Items?
|
||||||
private var imageItems: StorageFileListPanelComponent.Items?
|
private var imageItems: StorageMediaGridPanelComponent.Items?
|
||||||
private var fileItems: StorageFileListPanelComponent.Items?
|
private var fileItems: StorageFileListPanelComponent.Items?
|
||||||
private var musicItems: StorageFileListPanelComponent.Items?
|
private var musicItems: StorageFileListPanelComponent.Items?
|
||||||
|
|
||||||
@ -629,11 +693,21 @@ final class StorageUsageScreenComponent: Component {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if self.selectionState == nil {
|
if self.selectionState == nil {
|
||||||
|
#if DEBUG
|
||||||
|
let signpostState = SignpostContext.shared?.begin(name: "edit")
|
||||||
|
#endif
|
||||||
|
|
||||||
self.selectionState = SelectionState(
|
self.selectionState = SelectionState(
|
||||||
selectedPeers: Set(),
|
selectedPeers: Set(),
|
||||||
selectedMessages: Set()
|
selectedMessages: Set()
|
||||||
)
|
)
|
||||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
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))),
|
).minSize(CGSize(width: 16.0, height: environment.navigationHeight - environment.statusBarHeight))),
|
||||||
@ -1483,7 +1557,7 @@ final class StorageUsageScreenComponent: Component {
|
|||||||
panelItems.append(StorageUsagePanelContainerComponent.Item(
|
panelItems.append(StorageUsagePanelContainerComponent.Item(
|
||||||
id: "images",
|
id: "images",
|
||||||
title: environment.strings.StorageManagement_TabMedia,
|
title: environment.strings.StorageManagement_TabMedia,
|
||||||
panel: AnyComponent(StorageFileListPanelComponent(
|
panel: AnyComponent(StorageMediaGridPanelComponent(
|
||||||
context: component.context,
|
context: component.context,
|
||||||
items: self.imageItems,
|
items: self.imageItems,
|
||||||
selectionState: self.selectionState,
|
selectionState: self.selectionState,
|
||||||
@ -1716,7 +1790,7 @@ final class StorageUsageScreenComponent: Component {
|
|||||||
|
|
||||||
class RenderResult {
|
class RenderResult {
|
||||||
var messages: [MessageId: Message] = [:]
|
var messages: [MessageId: Message] = [:]
|
||||||
var imageItems: [StorageFileListPanelComponent.Item] = []
|
var imageItems: [StorageMediaGridPanelComponent.Item] = []
|
||||||
var fileItems: [StorageFileListPanelComponent.Item] = []
|
var fileItems: [StorageFileListPanelComponent.Item] = []
|
||||||
var musicItems: [StorageFileListPanelComponent.Item] = []
|
var musicItems: [StorageFileListPanelComponent.Item] = []
|
||||||
}
|
}
|
||||||
@ -1755,7 +1829,7 @@ final class StorageUsageScreenComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if matches {
|
if matches {
|
||||||
result.imageItems.append(StorageFileListPanelComponent.Item(
|
result.imageItems.append(StorageMediaGridPanelComponent.Item(
|
||||||
message: message,
|
message: message,
|
||||||
size: messageSize
|
size: messageSize
|
||||||
))
|
))
|
||||||
@ -1846,7 +1920,7 @@ final class StorageUsageScreenComponent: Component {
|
|||||||
|
|
||||||
self.currentMessages = result.messages
|
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.fileItems = StorageFileListPanelComponent.Items(items: result.fileItems)
|
||||||
self.musicItems = StorageFileListPanelComponent.Items(items: result.musicItems)
|
self.musicItems = StorageFileListPanelComponent.Items(items: result.musicItems)
|
||||||
|
|
||||||
@ -1905,6 +1979,7 @@ final class StorageUsageScreenComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
|
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
|
||||||
|
ActionSheetTextItem(title: presentationData.strings.StorageManagement_ClearConfirmationText, parseMarkdown: true),
|
||||||
ActionSheetButtonItem(title: clearTitle, color: .destructive, action: { [weak self, weak actionSheet] in
|
ActionSheetButtonItem(title: clearTitle, color: .destructive, action: { [weak self, weak actionSheet] in
|
||||||
actionSheet?.dismissAnimated()
|
actionSheet?.dismissAnimated()
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user