mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-02 20:55:48 +00:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
6a15dbb4bb
@ -253,7 +253,7 @@
|
||||
"PUSH_CHAT_REACT_INVOICE" = "%2$@|%1$@ %3$@ to your invoice";
|
||||
"PUSH_CHAT_REACT_GIF" = "%2$@|%1$@ %3$@ to your GIF";
|
||||
|
||||
"PUSH_MESSAGE_SUGGEST_USERPIC" = "%1$@ suggested you new profile photo";
|
||||
"PUSH_MESSAGE_SUGGEST_USERPIC" = "%1$@|suggested you new profile photo";
|
||||
|
||||
|
||||
"PUSH_REMINDER_TITLE" = "🗓 Reminder";
|
||||
@ -8464,15 +8464,15 @@ Sorry for the inconvenience.";
|
||||
"UserInfo.SuggestPhotoTitle" = "Do you want to suggest a profile picture for %@?";
|
||||
"UserInfo.SetCustomPhotoTitle" = "Do you want to set a custom profile picture for %@?";
|
||||
|
||||
"UserInfo.SuggestPhoto.AlertPhotoText" = "Do you want to suggest %@ to set this photo for his/her profile?";
|
||||
"UserInfo.SuggestPhoto.AlertVideoText" = "Do you want to suggest %@ to set this video for his/her profile?";
|
||||
"UserInfo.SuggestPhoto.AlertPhotoText" = "Do you want to suggest %@ to set this photo for their profile?";
|
||||
"UserInfo.SuggestPhoto.AlertVideoText" = "Do you want to suggest %@ to set this video for their profile?";
|
||||
"UserInfo.SuggestPhoto.AlertSuggest" = "Suggest";
|
||||
|
||||
"UserInfo.SetCustomPhoto.AlertPhotoText" = "Do you want to set this photo for %@? Only you will see this photo and it will replace any photo %@ sets for themselves.";
|
||||
"UserInfo.SetCustomPhoto.AlertVideoText" = "Do you want to set this video for %@? Only you will see this video and it will replace any photo %@ sets for themselves.";
|
||||
"UserInfo.SetCustomPhoto.AlertPhotoText" = "Do you want to set this photo for %@? It will replace any photo %@ sets, but only you will see it.";
|
||||
"UserInfo.SetCustomPhoto.AlertVideoText" = "Do you want to set this video for %@? It will replace any photo %@ sets, but only you will see it.";
|
||||
"UserInfo.SetCustomPhoto.AlertSet" = "Set";
|
||||
"UserInfo.SetCustomPhoto.SuccessPhotoText" = "You will now always see this photo for **%@** account.";
|
||||
"UserInfo.SetCustomPhoto.SuccessVideoText" = "You will now always see this video for **%@** account.";
|
||||
"UserInfo.SetCustomPhoto.SuccessPhotoText" = "You will now always see this photo for **%@**.";
|
||||
"UserInfo.SetCustomPhoto.SuccessVideoText" = "You will now always see this video for **%@**.";
|
||||
|
||||
"UserInfo.CustomPhoto" = "photo set by you";
|
||||
"UserInfo.CustomVideo" = "video set by you";
|
||||
@ -8480,7 +8480,7 @@ Sorry for the inconvenience.";
|
||||
"UserInfo.PublicPhoto" = "public photo";
|
||||
"UserInfo.PublicVideo" = "public video";
|
||||
|
||||
"UserInfo.ResetToOriginalAlertText" = "Are you sure you want to reset to %@ original photo?";
|
||||
"UserInfo.ResetToOriginalAlertText" = "Are you sure you want to reset to the original photo from %@?";
|
||||
"UserInfo.ResetToOriginalAlertReset" = "Reset";
|
||||
|
||||
"Conversation.SuggestedPhotoTitle" = "Suggested Photo";
|
||||
@ -8511,18 +8511,18 @@ Sorry for the inconvenience.";
|
||||
"PhotoEditor.SetAsMyPhoto" = "Set as My Photo";
|
||||
"PhotoEditor.SetAsMyVideo" = "Set as My Video";
|
||||
|
||||
"Notification.BotWriteAllowed" = "You allowed this bot to message you when you added it in the attachment menu.";
|
||||
"Notification.BotWriteAllowed" = "You allowed this bot to message you when you added to your attachment menu.";
|
||||
|
||||
"Privacy.ProfilePhoto.SetPublicPhoto" = "Set Public Photo";
|
||||
"Privacy.ProfilePhoto.UpdatePublicPhoto" = "Update Public Photo";
|
||||
"Privacy.ProfilePhoto.RemovePublicPhoto" = "Remove Public Photo";
|
||||
"Privacy.ProfilePhoto.RemovePublicVideo" = "Remove Public Video";
|
||||
"Privacy.ProfilePhoto.PublicPhotoInfo" = "You can upload a public photo for those who are restricted from viewing your real profile photo.";
|
||||
"Privacy.ProfilePhoto.PublicPhotoSuccess" = "This photo is now set for those who are restricted from viewing your main photo.";
|
||||
"Privacy.ProfilePhoto.PublicVideoSuccess" = "This video is now set for those who are restricted from viewing your main photo.";
|
||||
"Privacy.ProfilePhoto.PublicPhotoSuccess" = "This photo will be shown to those who are restricted from viewing your main photo.";
|
||||
"Privacy.ProfilePhoto.PublicVideoSuccess" = "This video will be shown to those who are restricted from viewing your main photo.";
|
||||
|
||||
"Privacy.ProfilePhoto.CustomOverrideInfo" = "You can add users or entire groups which will not see your profile photo.";
|
||||
"Privacy.ProfilePhoto.CustomOverrideAddInfo" = "Add users or entire groups which will still see your profile photo.";
|
||||
"Privacy.ProfilePhoto.CustomOverrideInfo" = "You can add users or entire groups that will not see your profile photo.";
|
||||
"Privacy.ProfilePhoto.CustomOverrideAddInfo" = "Add users or entire groups that will still see your profile photo.";
|
||||
"Privacy.ProfilePhoto.CustomOverrideBothInfo" = "You can add users or entire groups as exceptions that will override the settings above.";
|
||||
|
||||
"WebApp.AddToAttachmentAllowMessages" = "Allow **%@** to send me messages";
|
||||
@ -8534,9 +8534,9 @@ Sorry for the inconvenience.";
|
||||
"GroupInfo.TitleMembers_1" = "%@ Member";
|
||||
"GroupInfo.TitleMembers_any" = "%@ Members";
|
||||
|
||||
"PeerInfo.HideMembersLimitedParticipantCountText_1" = "Only groups with more than **%d member** can have their member list hidden.";
|
||||
"PeerInfo.HideMembersLimitedParticipantCountText_any" = "Only groups with more than **%d members** can have their member list hidden.";
|
||||
"PeerInfo.HideMembersLimitedRights" = "You don't have permission to change this setting.";
|
||||
"PeerInfo.HideMembersLimitedParticipantCountText_1" = "Hiding members is available only for groups with more than **%d member**.";
|
||||
"PeerInfo.HideMembersLimitedParticipantCountText_any" = "Hiding members is available only for groups with more than **%d members**.";
|
||||
"PeerInfo.HideMembersLimitedRights" = "You don't have the permission to change this setting.";
|
||||
|
||||
"Privacy.Exceptions" = "EXCEPTIONS";
|
||||
"Privacy.ExceptionsCount_1" = "%@ EXCEPTION";
|
||||
@ -8578,7 +8578,7 @@ Sorry for the inconvenience.";
|
||||
"StorageManagement.DescriptionChatUsage" = "This chat uses %1$@% of your Telegram cache.";
|
||||
"StorageManagement.DescriptionAppUsage" = "Telegram uses %1$@% of your free disk space.";
|
||||
|
||||
"StorageManagement.ClearAll" = "Clear All Cache";
|
||||
"StorageManagement.ClearAll" = "Clear Entire Cache";
|
||||
"StorageManagement.ClearSelected" = "Clear Selected";
|
||||
|
||||
"StorageManagement.SectionPhotos" = "Photos";
|
||||
@ -8588,7 +8588,7 @@ Sorry for the inconvenience.";
|
||||
"StorageManagement.SectionOther" = "Other";
|
||||
"StorageManagement.SectionStickers" = "Stickers";
|
||||
"StorageManagement.SectionAvatars" = "Avatars";
|
||||
"StorageManagement.SectionMiscellaneous" = "Miscellaneous";
|
||||
"StorageManagement.SectionMiscellaneous" = "Misc";
|
||||
|
||||
"StorageManagement.SectionsDescription" = "All media will stay in the Telegram cloud and can be re-downloaded if you need it again.";
|
||||
|
||||
@ -8597,9 +8597,15 @@ Sorry for the inconvenience.";
|
||||
|
||||
"StorageManagement.AutoremoveSpaceDescription" = "If your cache size exceeds this limit, the oldest media will be deleted.";
|
||||
|
||||
"StorageManagement.ClearConfirmationText" = "Media and documents will stay in the cloud and can be re-downloaded if you need them again.";
|
||||
|
||||
"StorageManagement.TabChats" = "Chats";
|
||||
"StorageManagement.TabMedia" = "Media";
|
||||
"StorageManagement.TabFiles" = "Files";
|
||||
"StorageManagement.TabMusic" = "Music";
|
||||
|
||||
"ClearCache.Never" = "Never";
|
||||
|
||||
"GroupMembers.HideMembers" = "Hide Members";
|
||||
"GroupMembers.MembersHiddenOn" = "Switch this off to show the list of members in this group.";
|
||||
"GroupMembers.MembersHiddenOff" = "Switch this on to hide the list of members in this group. Admins will remain visible.";
|
||||
|
||||
@ -34,55 +34,6 @@ 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))
|
||||
|
||||
if #available(iOS 15.0, *) {
|
||||
let maxFps = Float(UIScreen.main.maximumFramesPerSecond)
|
||||
if maxFps > 61.0 {
|
||||
displayLink.preferredFrameRateRange = CAFrameRateRange(minimum: 60.0, maximum: maxFps, preferred: maxFps)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -103,18 +54,16 @@ public extension CAAnimation {
|
||||
|
||||
private func adjustFrameRate(animation: CAAnimation) {
|
||||
if #available(iOS 15.0, *) {
|
||||
if let animation = animation as? CABasicAnimation {
|
||||
if animation.keyPath == "opacity" {
|
||||
return
|
||||
}
|
||||
}
|
||||
let maxFps = Float(UIScreen.main.maximumFramesPerSecond)
|
||||
if maxFps > 61.0 {
|
||||
#if DEBUG
|
||||
//let _ = frameRangeContext.add()
|
||||
#endif
|
||||
|
||||
animation.preferredFrameRateRange = CAFrameRateRange(minimum: 30.0, maximum: maxFps, preferred: maxFps)
|
||||
var preferredFps: Float = maxFps
|
||||
if let animation = animation as? CABasicAnimation {
|
||||
if animation.keyPath == "opacity" {
|
||||
preferredFps = 60.0
|
||||
return
|
||||
}
|
||||
}
|
||||
animation.preferredFrameRateRange = CAFrameRateRange(minimum: 30.0, maximum: preferredFps, preferred: maxFps)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,153 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public final class SharedDisplayLinkDriver {
|
||||
public static let shared = SharedDisplayLinkDriver()
|
||||
|
||||
public final class Link {
|
||||
private let driver: SharedDisplayLinkDriver
|
||||
public let needsHighestFramerate: Bool
|
||||
let update: () -> Void
|
||||
var isValid: Bool = true
|
||||
public var isPaused: Bool = false {
|
||||
didSet {
|
||||
if self.isPaused != oldValue {
|
||||
driver.requestUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(driver: SharedDisplayLinkDriver, needsHighestFramerate: Bool, update: @escaping () -> Void) {
|
||||
self.driver = driver
|
||||
self.needsHighestFramerate = needsHighestFramerate
|
||||
self.update = update
|
||||
}
|
||||
|
||||
public func invalidate() {
|
||||
self.isValid = false
|
||||
}
|
||||
}
|
||||
|
||||
private final class RequestContext {
|
||||
weak var link: Link?
|
||||
|
||||
init(link: Link) {
|
||||
self.link = link
|
||||
}
|
||||
}
|
||||
|
||||
private var displayLink: CADisplayLink?
|
||||
private var hasRequestedHighestFramerate: Bool = false
|
||||
private var requests: [RequestContext] = []
|
||||
|
||||
private var isInForeground: Bool = false
|
||||
|
||||
private init() {
|
||||
let _ = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: nil, using: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.isInForeground = true
|
||||
self.update()
|
||||
})
|
||||
let _ = NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil, using: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.isInForeground = false
|
||||
self.update()
|
||||
})
|
||||
|
||||
switch UIApplication.shared.applicationState {
|
||||
case .active:
|
||||
self.isInForeground = true
|
||||
default:
|
||||
self.isInForeground = false
|
||||
}
|
||||
|
||||
self.update()
|
||||
}
|
||||
|
||||
private func requestUpdate() {
|
||||
self.update()
|
||||
}
|
||||
|
||||
private func update() {
|
||||
var hasActiveItems = false
|
||||
var needHighestFramerate = false
|
||||
for request in self.requests {
|
||||
if let link = request.link {
|
||||
needHighestFramerate = link.needsHighestFramerate
|
||||
if link.isValid && !link.isPaused {
|
||||
hasActiveItems = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.isInForeground && hasActiveItems {
|
||||
let displayLink: CADisplayLink
|
||||
if let current = self.displayLink {
|
||||
displayLink = current
|
||||
} else {
|
||||
displayLink = CADisplayLink(target: self, selector: #selector(self.displayLinkEvent))
|
||||
self.displayLink = displayLink
|
||||
displayLink.add(to: .main, forMode: .common)
|
||||
}
|
||||
if #available(iOS 15.0, *) {
|
||||
let frameRateRange: CAFrameRateRange
|
||||
if needHighestFramerate {
|
||||
frameRateRange = CAFrameRateRange(minimum: 30.0, maximum: 120.0, preferred: 120.0)
|
||||
} else {
|
||||
frameRateRange = .default
|
||||
}
|
||||
if displayLink.preferredFrameRateRange != frameRateRange {
|
||||
displayLink.preferredFrameRateRange = frameRateRange
|
||||
}
|
||||
}
|
||||
displayLink.isPaused = false
|
||||
} else {
|
||||
if let displayLink = self.displayLink {
|
||||
self.displayLink = nil
|
||||
displayLink.invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func displayLinkEvent() {
|
||||
var removeIndices: [Int]?
|
||||
for i in 0 ..< self.requests.count {
|
||||
if let link = self.requests[i].link, link.isValid {
|
||||
link.update()
|
||||
} else {
|
||||
if removeIndices == nil {
|
||||
removeIndices = [i]
|
||||
} else {
|
||||
removeIndices?.append(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
if let removeIndices = removeIndices {
|
||||
for index in removeIndices.reversed() {
|
||||
self.requests.remove(at: index)
|
||||
}
|
||||
|
||||
if self.requests.isEmpty {
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func add(needsHighestFramerate: Bool = true, _ update: @escaping () -> Void) -> Link {
|
||||
let link = Link(driver: self, needsHighestFramerate: needsHighestFramerate, update: update)
|
||||
self.requests.append(RequestContext(link: link))
|
||||
|
||||
self.update()
|
||||
|
||||
return link
|
||||
}
|
||||
}
|
||||
|
||||
public final class DisplayLinkTarget: NSObject {
|
||||
private let f: () -> Void
|
||||
|
||||
@ -14,7 +161,7 @@ public final class DisplayLinkTarget: NSObject {
|
||||
}
|
||||
|
||||
public final class DisplayLinkAnimator {
|
||||
private var displayLink: CADisplayLink!
|
||||
private var displayLink: SharedDisplayLinkDriver.Link?
|
||||
private let duration: Double
|
||||
private let fromValue: CGFloat
|
||||
private let toValue: CGFloat
|
||||
@ -32,21 +179,20 @@ public final class DisplayLinkAnimator {
|
||||
|
||||
self.startTime = CACurrentMediaTime()
|
||||
|
||||
self.displayLink = CADisplayLink(target: DisplayLinkTarget({ [weak self] in
|
||||
self.displayLink = SharedDisplayLinkDriver.shared.add { [weak self] in
|
||||
self?.tick()
|
||||
}), selector: #selector(DisplayLinkTarget.event))
|
||||
self.displayLink.isPaused = false
|
||||
self.displayLink.add(to: RunLoop.main, forMode: .common)
|
||||
}
|
||||
self.displayLink?.isPaused = false
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.displayLink.isPaused = true
|
||||
self.displayLink.invalidate()
|
||||
self.displayLink?.isPaused = true
|
||||
self.displayLink?.invalidate()
|
||||
}
|
||||
|
||||
public func invalidate() {
|
||||
self.displayLink.isPaused = true
|
||||
self.displayLink.invalidate()
|
||||
self.displayLink?.isPaused = true
|
||||
self.displayLink?.invalidate()
|
||||
}
|
||||
|
||||
@objc private func tick() {
|
||||
@ -60,14 +206,14 @@ public final class DisplayLinkAnimator {
|
||||
self.update(self.fromValue * CGFloat(1 - t) + self.toValue * CGFloat(t))
|
||||
if abs(t - 1.0) < Double.ulpOfOne {
|
||||
self.completed = true
|
||||
self.displayLink.isPaused = true
|
||||
self.displayLink?.isPaused = true
|
||||
self.completion()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class ConstantDisplayLinkAnimator {
|
||||
private var displayLink: CADisplayLink?
|
||||
private var displayLink: SharedDisplayLinkDriver.Link?
|
||||
private let update: () -> Void
|
||||
private var completed = false
|
||||
|
||||
@ -81,26 +227,16 @@ public final class ConstantDisplayLinkAnimator {
|
||||
guard let displayLink = self.displayLink else {
|
||||
return
|
||||
}
|
||||
if self.frameInterval == 1 {
|
||||
if #available(iOS 15.0, *) {
|
||||
self.displayLink?.preferredFrameRateRange = CAFrameRateRange(minimum: 60.0, maximum: 120.0, preferred: 120.0)
|
||||
}
|
||||
} else {
|
||||
displayLink.preferredFramesPerSecond = 30
|
||||
}
|
||||
let _ = displayLink
|
||||
}
|
||||
|
||||
public var isPaused: Bool = true {
|
||||
didSet {
|
||||
if self.isPaused != oldValue {
|
||||
if !self.isPaused && self.displayLink == nil {
|
||||
let displayLink = CADisplayLink(target: DisplayLinkTarget({ [weak self] in
|
||||
let displayLink = SharedDisplayLinkDriver.shared.add { [weak self] in
|
||||
self?.tick()
|
||||
}), selector: #selector(DisplayLinkTarget.event))
|
||||
/*if #available(iOS 15.0, *) {
|
||||
self.displayLink?.preferredFrameRateRange = CAFrameRateRange(minimum: 60.0, maximum: 120.0, preferred: 120.0)
|
||||
}*/
|
||||
displayLink.add(to: RunLoop.main, forMode: .common)
|
||||
}
|
||||
self.displayLink = displayLink
|
||||
self.updateDisplayLink()
|
||||
}
|
||||
|
||||
@ -2,7 +2,6 @@ import Foundation
|
||||
import UIKit
|
||||
|
||||
public class DisplayLinkDispatcher: NSObject {
|
||||
private var displayLink: CADisplayLink!
|
||||
private var blocksToDispatch: [() -> Void] = []
|
||||
private let limit: Int
|
||||
|
||||
@ -10,38 +9,13 @@ public class DisplayLinkDispatcher: NSObject {
|
||||
self.limit = limit
|
||||
|
||||
super.init()
|
||||
|
||||
if #available(iOS 10.0, *) {
|
||||
//self.displayLink.preferredFramesPerSecond = 60
|
||||
} else {
|
||||
self.displayLink = CADisplayLink(target: self, selector: #selector(self.run))
|
||||
self.displayLink.isPaused = true
|
||||
self.displayLink.add(to: RunLoop.main, forMode: .common)
|
||||
}
|
||||
}
|
||||
|
||||
public func dispatch(f: @escaping () -> Void) {
|
||||
if self.displayLink == nil {
|
||||
if Thread.isMainThread {
|
||||
f()
|
||||
} else {
|
||||
DispatchQueue.main.async(execute: f)
|
||||
}
|
||||
if Thread.isMainThread {
|
||||
f()
|
||||
} else {
|
||||
self.blocksToDispatch.append(f)
|
||||
self.displayLink.isPaused = false
|
||||
}
|
||||
}
|
||||
|
||||
@objc func run() {
|
||||
for _ in 0 ..< (self.limit == 0 ? 1000 : self.limit) {
|
||||
if self.blocksToDispatch.count == 0 {
|
||||
self.displayLink.isPaused = true
|
||||
break
|
||||
} else {
|
||||
let f = self.blocksToDispatch.removeFirst()
|
||||
f()
|
||||
}
|
||||
DispatchQueue.main.async(execute: f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -814,12 +814,6 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
}
|
||||
|
||||
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||
if #available(iOS 15.0, *) {
|
||||
if let scrollDisplayLink = self.scroller.value(forKey: "_scrollHeartbeat") as? CADisplayLink {
|
||||
let _ = scrollDisplayLink
|
||||
}
|
||||
}
|
||||
|
||||
self.isDragging = false
|
||||
if decelerate {
|
||||
self.lastContentOffsetTimestamp = CACurrentMediaTime()
|
||||
|
||||
@ -36,44 +36,6 @@ class DisplayLinkService {
|
||||
}
|
||||
}
|
||||
|
||||
// private init() {
|
||||
// displayLink.add(to: .main, forMode: .common)
|
||||
// displayLink.preferredFramesPerSecond = 60
|
||||
// displayLink.isPaused = true
|
||||
// }
|
||||
//
|
||||
// // MARK: - Display Link
|
||||
// private lazy var displayLink: CADisplayLink! = { CADisplayLink(target: self, selector: #selector(displayLinkDidFire)) } ()
|
||||
// private var previousTickTime = 0.0
|
||||
//
|
||||
// private func startDisplayLink() {
|
||||
// guard displayLink.isPaused else {
|
||||
// return
|
||||
// }
|
||||
// previousTickTime = CACurrentMediaTime()
|
||||
// displayLink.isPaused = false
|
||||
// }
|
||||
//
|
||||
// @objc private func displayLinkDidFire(_ displayLink: CADisplayLink) {
|
||||
// let currentTime = CACurrentMediaTime()
|
||||
// let delta = currentTime - previousTickTime
|
||||
// previousTickTime = currentTime
|
||||
// let allListners = listners.allObjects
|
||||
// var hasListners = false
|
||||
// for listner in allListners {
|
||||
// (listner as! DisplayLinkListner).update(delta: delta)
|
||||
// hasListners = true
|
||||
// }
|
||||
//
|
||||
// if !hasListners {
|
||||
// stopDisplayLink()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func stopDisplayLink() {
|
||||
// displayLink.isPaused = true
|
||||
// }
|
||||
|
||||
private init() {
|
||||
dispatchSourceTimer.schedule(deadline: .now() + 1.0 / 60, repeating: 1.0 / 60)
|
||||
dispatchSourceTimer.setEventHandler {
|
||||
|
||||
@ -145,7 +145,7 @@ open class ManagedAnimationNode: ASDisplayNode {
|
||||
public let intrinsicSize: CGSize
|
||||
|
||||
private let imageNode: ASImageNode
|
||||
private let displayLink: CADisplayLink
|
||||
private let displayLink: SharedDisplayLinkDriver.Link
|
||||
|
||||
public var imageUpdated: ((UIImage) -> Void)?
|
||||
public var image: UIImage? {
|
||||
@ -179,19 +179,14 @@ open class ManagedAnimationNode: ASDisplayNode {
|
||||
self.imageNode.frame = CGRect(origin: CGPoint(), size: self.intrinsicSize)
|
||||
|
||||
var displayLinkUpdate: (() -> Void)?
|
||||
self.displayLink = CADisplayLink(target: DisplayLinkTarget {
|
||||
self.displayLink = SharedDisplayLinkDriver.shared.add {
|
||||
displayLinkUpdate?()
|
||||
}, selector: #selector(DisplayLinkTarget.event))
|
||||
if #available(iOS 10.0, *) {
|
||||
self.displayLink.preferredFramesPerSecond = 60
|
||||
}
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.imageNode)
|
||||
|
||||
self.displayLink.add(to: RunLoop.main, forMode: .common)
|
||||
|
||||
displayLinkUpdate = { [weak self] in
|
||||
self?.updateAnimation()
|
||||
}
|
||||
@ -199,6 +194,7 @@ open class ManagedAnimationNode: ASDisplayNode {
|
||||
|
||||
open func advanceState() {
|
||||
guard !self.trackStack.isEmpty else {
|
||||
self.displayLink.isPaused = true
|
||||
return
|
||||
}
|
||||
|
||||
@ -211,6 +207,7 @@ open class ManagedAnimationNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
self.didTryAdvancingState = false
|
||||
self.displayLink.isPaused = false
|
||||
}
|
||||
|
||||
public func updateAnimation() {
|
||||
@ -219,6 +216,7 @@ open class ManagedAnimationNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
guard let state = self.state else {
|
||||
self.displayLink.isPaused = true
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@ -300,7 +300,7 @@ private final class MediaPlayerScrubbingBufferingNode: ASDisplayNode {
|
||||
public final class MediaPlayerScrubbingNode: ASDisplayNode {
|
||||
private var contentNodes: MediaPlayerScrubbingNodeContentNodes
|
||||
|
||||
private var displayLink: CADisplayLink?
|
||||
private var displayLink: SharedDisplayLinkDriver.Link?
|
||||
private var isInHierarchyValue: Bool = false
|
||||
|
||||
private var playbackStatusValue: MediaPlayerPlaybackStatus?
|
||||
@ -798,20 +798,9 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode {
|
||||
|
||||
if needsAnimation {
|
||||
if self.displayLink == nil {
|
||||
class DisplayLinkProxy: NSObject {
|
||||
var f: () -> Void
|
||||
init(_ f: @escaping () -> Void) {
|
||||
self.f = f
|
||||
}
|
||||
|
||||
@objc func displayLinkEvent() {
|
||||
self.f()
|
||||
}
|
||||
}
|
||||
let displayLink = CADisplayLink(target: DisplayLinkProxy({ [weak self] in
|
||||
let displayLink = SharedDisplayLinkDriver.shared.add { [weak self] in
|
||||
self?.updateProgress()
|
||||
}), selector: #selector(DisplayLinkProxy.displayLinkEvent))
|
||||
displayLink.add(to: .main, forMode: RunLoop.Mode.common)
|
||||
}
|
||||
self.displayLink = displayLink
|
||||
}
|
||||
self.displayLink?.isPaused = false
|
||||
|
||||
@ -359,7 +359,6 @@ private func channelMembersControllerEntries(context: AccountContext, presentati
|
||||
var displayHideMembers = false
|
||||
var canSetupHideMembers = false
|
||||
if let channel = view.peers[view.peerId] as? TelegramChannel, case .group = channel.info {
|
||||
//TODO:loc
|
||||
displayHideMembers = true
|
||||
canSetupHideMembers = channel.hasPermission(.banMembers)
|
||||
}
|
||||
@ -390,14 +389,13 @@ private func channelMembersControllerEntries(context: AccountContext, presentati
|
||||
isInteractive = false
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
entries.append(.hideMembers(text: "Hide Members", disabledReason: disabledReason, isInteractive: isInteractive, value: membersHidden))
|
||||
entries.append(.hideMembers(text: presentationData.strings.GroupMembers_HideMembers, disabledReason: disabledReason, isInteractive: isInteractive, value: membersHidden))
|
||||
|
||||
let infoText: String
|
||||
if membersHidden {
|
||||
infoText = "Switch this off to show the list of members in this group."
|
||||
infoText = presentationData.strings.GroupMembers_MembersHiddenOn
|
||||
} else {
|
||||
infoText = "Switch this on to hide the list of members in this group. Admins will remain visible."
|
||||
infoText = presentationData.strings.GroupMembers_MembersHiddenOff
|
||||
}
|
||||
entries.append(.hideMembersInfo(infoText))
|
||||
}
|
||||
|
||||
@ -1381,7 +1381,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
|
||||
}
|
||||
|
||||
@ -1962,7 +1962,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
|
||||
}
|
||||
|
||||
@ -1991,7 +1991,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
|
||||
}
|
||||
|
||||
|
||||
@ -57,6 +57,7 @@ public enum MediaResourceUserContentType: UInt8, Equatable {
|
||||
case file = 4
|
||||
case sticker = 6
|
||||
case avatar = 7
|
||||
case audioVideoMessage = 8
|
||||
}
|
||||
|
||||
public struct MediaResourceFetchParameters {
|
||||
|
||||
@ -230,6 +230,7 @@ public final class SqliteValueBox: ValueBox {
|
||||
}
|
||||
|
||||
func internalClose() {
|
||||
self.clearStatements()
|
||||
self.database = nil
|
||||
}
|
||||
|
||||
|
||||
@ -719,6 +719,7 @@ public final class StorageBox {
|
||||
let idKey = ValueBoxKey(length: 16 + 8)
|
||||
|
||||
let mainKey = ValueBoxKey(length: 16)
|
||||
var processedIds = Set<Data>()
|
||||
self.valueBox.scan(self.peerIdToIdTable, keys: { key in
|
||||
let peerId = key.getInt64(0)
|
||||
if peerId == 0 {
|
||||
@ -726,6 +727,10 @@ public final class StorageBox {
|
||||
}
|
||||
|
||||
let hashId = key.getData(8, length: 16)
|
||||
if processedIds.contains(hashId) {
|
||||
return true
|
||||
}
|
||||
processedIds.insert(hashId)
|
||||
|
||||
mainKey.setData(0, value: hashId)
|
||||
if let currentInfoValue = self.valueBox.get(self.hashIdToInfoTable, key: mainKey) {
|
||||
@ -749,7 +754,7 @@ public final class StorageBox {
|
||||
}
|
||||
|
||||
return true
|
||||
}, limit: 0)
|
||||
}, limit: 1)
|
||||
}
|
||||
}
|
||||
|
||||
@ -761,17 +766,13 @@ public final class StorageBox {
|
||||
return allStats
|
||||
}
|
||||
|
||||
func remove(peerId: Int64?, contentTypes: [UInt8]) -> [Data] {
|
||||
func remove(peerId: Int64?, contentTypes: [UInt8], includeIds: [Data], excludeIds: [Data]) -> [Data] {
|
||||
var resultIds: [Data] = []
|
||||
|
||||
self.valueBox.begin()
|
||||
|
||||
var scannedIds: [Data: Data] = [:]
|
||||
|
||||
for contentType in contentTypes {
|
||||
self.internalAddSize(contentType: contentType, delta: 0)
|
||||
}
|
||||
|
||||
self.valueBox.scan(self.hashIdToInfoTable, values: { key, value in
|
||||
let info = ItemInfo(buffer: value)
|
||||
if !contentTypes.contains(info.contentType) {
|
||||
@ -781,17 +782,24 @@ public final class StorageBox {
|
||||
return true
|
||||
})
|
||||
|
||||
for id in includeIds {
|
||||
scannedIds[md5Hash(id).data] = id
|
||||
}
|
||||
|
||||
let excludeIds = Set(excludeIds)
|
||||
|
||||
if let peerId = peerId {
|
||||
var filteredHashIds: [Data] = []
|
||||
self.valueBox.scan(self.idToReferenceTable, keys: { key in
|
||||
let id = key.getData(0, length: 16)
|
||||
if scannedIds[id] == nil {
|
||||
guard let realId = scannedIds[id] else {
|
||||
return true
|
||||
}
|
||||
if excludeIds.contains(realId) {
|
||||
return true
|
||||
}
|
||||
|
||||
let itemPeerId = key.getInt64(16)
|
||||
//let messageNamespace: UInt8 = key.getUInt8(16 + 8)
|
||||
//let messageId = key.getInt32(16 + 8 + 1)
|
||||
|
||||
if itemPeerId == peerId {
|
||||
filteredHashIds.append(id)
|
||||
@ -807,23 +815,20 @@ public final class StorageBox {
|
||||
}
|
||||
} else {
|
||||
for (hashId, id) in scannedIds {
|
||||
if excludeIds.contains(id) {
|
||||
continue
|
||||
}
|
||||
self.internalRemove(hashId: hashId)
|
||||
resultIds.append(id)
|
||||
}
|
||||
}
|
||||
|
||||
if let peerId = peerId {
|
||||
let _ = peerId
|
||||
} else {
|
||||
|
||||
}
|
||||
|
||||
self.valueBox.commit()
|
||||
|
||||
return Array(resultIds)
|
||||
}
|
||||
|
||||
func remove(peerIds: Set<PeerId>) -> [Data] {
|
||||
func remove(peerIds: Set<PeerId>, includeIds: [Data], excludeIds: [Data]) -> [Data] {
|
||||
var resultIds: [Data] = []
|
||||
|
||||
self.valueBox.begin()
|
||||
@ -833,7 +838,16 @@ public final class StorageBox {
|
||||
scannedIds.formUnion(self.allInternal(peerId: peerId))
|
||||
}
|
||||
|
||||
for id in includeIds {
|
||||
scannedIds.insert(id)
|
||||
}
|
||||
|
||||
let excludedIds = Set(excludeIds)
|
||||
|
||||
for id in scannedIds {
|
||||
if excludedIds.contains(id) {
|
||||
continue
|
||||
}
|
||||
self.internalRemove(hashId: md5Hash(id).data)
|
||||
resultIds.append(id)
|
||||
}
|
||||
@ -931,16 +945,16 @@ public final class StorageBox {
|
||||
}
|
||||
}
|
||||
|
||||
public func remove(peerId: PeerId?, contentTypes: [UInt8], completion: @escaping ([Data]) -> Void) {
|
||||
public func remove(peerId: PeerId?, contentTypes: [UInt8], includeIds: [Data], excludeIds: [Data], completion: @escaping ([Data]) -> Void) {
|
||||
self.impl.with { impl in
|
||||
let ids = impl.remove(peerId: peerId?.toInt64(), contentTypes: contentTypes)
|
||||
let ids = impl.remove(peerId: peerId?.toInt64(), contentTypes: contentTypes, includeIds: includeIds, excludeIds: excludeIds)
|
||||
completion(ids)
|
||||
}
|
||||
}
|
||||
|
||||
public func remove(peerIds: Set<PeerId>, completion: @escaping ([Data]) -> Void) {
|
||||
public func remove(peerIds: Set<PeerId>, includeIds: [Data], excludeIds: [Data], completion: @escaping ([Data]) -> Void) {
|
||||
self.impl.with { impl in
|
||||
let ids = impl.remove(peerIds: peerIds)
|
||||
let ids = impl.remove(peerIds: peerIds, includeIds: includeIds, excludeIds: excludeIds)
|
||||
completion(ids)
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,7 +39,95 @@ public func printOpenFiles() {
|
||||
}
|
||||
}
|
||||
|
||||
private func scanFiles(at path: String, olderThan minTimestamp: Int32, inodes: inout [InodeInfo]) -> ScanFilesResult {
|
||||
private final class TempScanDatabase {
|
||||
private let queue: Queue
|
||||
private let valueBox: SqliteValueBox
|
||||
|
||||
private let accessTimeTable: ValueBoxTable
|
||||
|
||||
private var nextId: Int32 = 0
|
||||
|
||||
private let accessTimeKey = ValueBoxKey(length: 4 + 4)
|
||||
private let accessInfoBuffer = WriteBuffer()
|
||||
|
||||
init?(queue: Queue, basePath: String) {
|
||||
self.queue = queue
|
||||
guard let valueBox = SqliteValueBox(basePath: basePath, queue: queue, isTemporary: true, isReadOnly: false, useCaches: true, removeDatabaseOnError: true, encryptionParameters: nil, upgradeProgress: { _ in }) else {
|
||||
return nil
|
||||
}
|
||||
self.valueBox = valueBox
|
||||
|
||||
self.accessTimeTable = ValueBoxTable(id: 2, keyType: .binary, compactValuesOnCreation: true)
|
||||
}
|
||||
|
||||
func begin() {
|
||||
self.valueBox.begin()
|
||||
}
|
||||
|
||||
func commit() {
|
||||
self.valueBox.commit()
|
||||
}
|
||||
|
||||
func dispose() {
|
||||
self.valueBox.internalClose()
|
||||
}
|
||||
|
||||
func add(pathBuffer: UnsafeMutablePointer<Int8>, pathSize: Int, size: Int64, timestamp: Int32) {
|
||||
let id = self.nextId
|
||||
self.nextId += 1
|
||||
|
||||
var size = size
|
||||
self.accessInfoBuffer.reset()
|
||||
self.accessInfoBuffer.write(&size, length: 8)
|
||||
self.accessInfoBuffer.write(pathBuffer, length: pathSize)
|
||||
|
||||
self.accessTimeKey.setInt32(0, value: timestamp)
|
||||
self.accessTimeKey.setInt32(4, value: id)
|
||||
self.valueBox.set(self.accessTimeTable, key: self.accessTimeKey, value: self.accessInfoBuffer)
|
||||
}
|
||||
|
||||
func topByAccessTime(_ f: (Int64, String) -> Bool) {
|
||||
var startKey = ValueBoxKey(length: 4)
|
||||
startKey.setInt32(0, value: 0)
|
||||
|
||||
let endKey = ValueBoxKey(length: 4)
|
||||
endKey.setInt32(0, value: Int32.max)
|
||||
|
||||
while true {
|
||||
var lastKey: ValueBoxKey?
|
||||
self.valueBox.range(self.accessTimeTable, start: startKey, end: endKey, values: { key, value in
|
||||
var result = true
|
||||
withExtendedLifetime(value, {
|
||||
let readBuffer = ReadBuffer(memoryBufferNoCopy: value)
|
||||
|
||||
var size: Int64 = 0
|
||||
readBuffer.read(&size, offset: 0, length: 8)
|
||||
|
||||
var pathData = Data(count: value.length - 8)
|
||||
pathData.withUnsafeMutableBytes { buffer -> Void in
|
||||
readBuffer.read(buffer.baseAddress!, offset: 0, length: buffer.count)
|
||||
}
|
||||
|
||||
if let path = String(data: pathData, encoding: .utf8) {
|
||||
result = f(size, path)
|
||||
}
|
||||
})
|
||||
|
||||
lastKey = key
|
||||
|
||||
return result
|
||||
}, limit: 512)
|
||||
|
||||
if let lastKey = lastKey {
|
||||
startKey = lastKey
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func scanFiles(at path: String, olderThan minTimestamp: Int32, includeSubdirectories: Bool, tempDatabase: TempScanDatabase) -> ScanFilesResult {
|
||||
var result = ScanFilesResult()
|
||||
|
||||
if let dp = opendir(path) {
|
||||
@ -63,21 +151,24 @@ private func scanFiles(at path: String, olderThan minTimestamp: Int32, inodes: i
|
||||
strncat(pathBuffer, "/", 1024)
|
||||
strncat(pathBuffer, &dirp.pointee.d_name.0, 1024)
|
||||
|
||||
//puts(pathBuffer)
|
||||
//puts("\n")
|
||||
|
||||
var value = stat()
|
||||
if stat(pathBuffer, &value) == 0 {
|
||||
if value.st_mtimespec.tv_sec < minTimestamp {
|
||||
unlink(pathBuffer)
|
||||
result.unlinkedCount += 1
|
||||
if (((value.st_mode) & S_IFMT) == S_IFDIR) {
|
||||
if includeSubdirectories {
|
||||
if let subPath = String(data: Data(bytes: pathBuffer, count: strnlen(pathBuffer, 1024)), encoding: .utf8) {
|
||||
let subResult = scanFiles(at: subPath, olderThan: minTimestamp, includeSubdirectories: true, tempDatabase: tempDatabase)
|
||||
result.totalSize += subResult.totalSize
|
||||
result.unlinkedCount += subResult.unlinkedCount
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result.totalSize += UInt64(value.st_size)
|
||||
inodes.append(InodeInfo(
|
||||
inode: value.st_ino,
|
||||
timestamp: Int32(clamping: value.st_mtimespec.tv_sec),
|
||||
size: UInt32(clamping: value.st_size)
|
||||
))
|
||||
if value.st_mtimespec.tv_sec < minTimestamp {
|
||||
unlink(pathBuffer)
|
||||
result.unlinkedCount += 1
|
||||
} else {
|
||||
result.totalSize += UInt64(value.st_size)
|
||||
tempDatabase.add(pathBuffer: pathBuffer, pathSize: strnlen(pathBuffer, 1024), size: Int64(value.st_size), timestamp: Int32(value.st_mtimespec.tv_sec))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -87,9 +178,8 @@ private func scanFiles(at path: String, olderThan minTimestamp: Int32, inodes: i
|
||||
return result
|
||||
}
|
||||
|
||||
private func mapFiles(paths: [String], inodes: inout [InodeInfo], removeSize: UInt64, mainStoragePath: String, storageBox: StorageBox) {
|
||||
/*private func mapFiles(paths: [String], inodes: inout [InodeInfo], removeSize: UInt64, mainStoragePath: String, storageBox: StorageBox) {
|
||||
var removedSize: UInt64 = 0
|
||||
|
||||
inodes.sort(by: { lhs, rhs in
|
||||
return lhs.timestamp < rhs.timestamp
|
||||
})
|
||||
@ -138,19 +228,25 @@ private func mapFiles(paths: [String], inodes: inout [InodeInfo], removeSize: UI
|
||||
|
||||
var value = stat()
|
||||
if stat(pathBuffer, &value) == 0 {
|
||||
if inodesToDelete.contains(value.st_ino) {
|
||||
if isMainPath {
|
||||
let nameLength = strnlen(&dirp.pointee.d_name.0, 1024)
|
||||
let nameData = Data(bytesNoCopy: &dirp.pointee.d_name.0, count: Int(nameLength), deallocator: .none)
|
||||
withExtendedLifetime(nameData, {
|
||||
if let fileName = String(data: nameData, encoding: .utf8) {
|
||||
if let idData = MediaBox.idForFileName(name: fileName).data(using: .utf8) {
|
||||
unlinkedResourceIds.append(idData)
|
||||
}
|
||||
}
|
||||
})
|
||||
if (((value.st_mode) & S_IFMT) == S_IFDIR) {
|
||||
if let subPath = String(data: Data(bytes: pathBuffer, count: strnlen(pathBuffer, 1024)), encoding: .utf8) {
|
||||
mapFiles(paths: <#T##[String]#>, inodes: &<#T##[InodeInfo]#>, removeSize: remov, mainStoragePath: mainStoragePath, storageBox: storageBox)
|
||||
}
|
||||
} else {
|
||||
if inodesToDelete.contains(value.st_ino) {
|
||||
if isMainPath {
|
||||
let nameLength = strnlen(&dirp.pointee.d_name.0, 1024)
|
||||
let nameData = Data(bytesNoCopy: &dirp.pointee.d_name.0, count: Int(nameLength), deallocator: .none)
|
||||
withExtendedLifetime(nameData, {
|
||||
if let fileName = String(data: nameData, encoding: .utf8) {
|
||||
if let idData = MediaBox.idForFileName(name: fileName).data(using: .utf8) {
|
||||
unlinkedResourceIds.append(idData)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
unlink(pathBuffer)
|
||||
}
|
||||
unlink(pathBuffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -161,7 +257,7 @@ private func mapFiles(paths: [String], inodes: inout [InodeInfo], removeSize: UI
|
||||
if !unlinkedResourceIds.isEmpty {
|
||||
storageBox.remove(ids: unlinkedResourceIds)
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
private final class TimeBasedCleanupImpl {
|
||||
private let queue: Queue
|
||||
@ -178,19 +274,6 @@ private final class TimeBasedCleanupImpl {
|
||||
private var gigabytesLimit: Int32?
|
||||
private let scheduledScanDisposable = MetaDisposable()
|
||||
|
||||
|
||||
private struct GeneralFile : Comparable, Equatable {
|
||||
let file: String
|
||||
let size: Int
|
||||
let timestamp:Int32
|
||||
static func == (lhs: GeneralFile, rhs: GeneralFile) -> Bool {
|
||||
return lhs.timestamp == rhs.timestamp && lhs.size == rhs.size && lhs.file == rhs.file
|
||||
}
|
||||
static func < (lhs: GeneralFile, rhs: GeneralFile) -> Bool {
|
||||
return lhs.timestamp < rhs.timestamp
|
||||
}
|
||||
}
|
||||
|
||||
init(queue: Queue, storageBox: StorageBox, generalPaths: [String], totalSizeBasedPath: String, shortLivedPaths: [String]) {
|
||||
self.queue = queue
|
||||
self.storageBox = storageBox
|
||||
@ -220,14 +303,22 @@ private final class TimeBasedCleanupImpl {
|
||||
let shortLivedPaths = self.shortLivedPaths
|
||||
let storageBox = self.storageBox
|
||||
let scanOnce = Signal<Never, NoError> { subscriber in
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
let queue = Queue(name: "TimeBasedCleanupScan", qos: .background)
|
||||
queue.async {
|
||||
let tempDirectory = TempBox.shared.tempDirectory()
|
||||
guard let tempDatabase = TempScanDatabase(queue: queue, basePath: tempDirectory.path) else {
|
||||
postboxLog("TimeBasedCleanup: couldn't create temp database at \(tempDirectory.path)")
|
||||
subscriber.putCompletion()
|
||||
return
|
||||
}
|
||||
tempDatabase.begin()
|
||||
|
||||
var removedShortLivedCount: Int = 0
|
||||
var removedGeneralCount: Int = 0
|
||||
let removedGeneralLimitCount: Int = 0
|
||||
|
||||
let startTime = CFAbsoluteTimeGetCurrent()
|
||||
|
||||
var inodes: [InodeInfo] = []
|
||||
var paths: [String] = []
|
||||
|
||||
let timestamp = Int32(Date().timeIntervalSince1970)
|
||||
@ -241,7 +332,7 @@ private final class TimeBasedCleanupImpl {
|
||||
let oldestShortLivedTimestamp = timestamp - shortLived
|
||||
let oldestGeneralTimestamp = timestamp - general
|
||||
for path in shortLivedPaths {
|
||||
let scanResult = scanFiles(at: path, olderThan: oldestShortLivedTimestamp, inodes: &inodes)
|
||||
let scanResult = scanFiles(at: path, olderThan: oldestShortLivedTimestamp, includeSubdirectories: true, tempDatabase: tempDatabase)
|
||||
if !paths.contains(path) {
|
||||
paths.append(path)
|
||||
}
|
||||
@ -251,7 +342,7 @@ private final class TimeBasedCleanupImpl {
|
||||
var totalLimitSize: UInt64 = 0
|
||||
|
||||
for path in generalPaths {
|
||||
let scanResult = scanFiles(at: path, olderThan: oldestGeneralTimestamp, inodes: &inodes)
|
||||
let scanResult = scanFiles(at: path, olderThan: oldestGeneralTimestamp, includeSubdirectories: true, tempDatabase: tempDatabase)
|
||||
if !paths.contains(path) {
|
||||
paths.append(path)
|
||||
}
|
||||
@ -259,7 +350,7 @@ private final class TimeBasedCleanupImpl {
|
||||
totalLimitSize += scanResult.totalSize
|
||||
}
|
||||
do {
|
||||
let scanResult = scanFiles(at: totalSizeBasedPath, olderThan: 0, inodes: &inodes)
|
||||
let scanResult = scanFiles(at: totalSizeBasedPath, olderThan: 0, includeSubdirectories: false, tempDatabase: tempDatabase)
|
||||
if !paths.contains(totalSizeBasedPath) {
|
||||
paths.append(totalSizeBasedPath)
|
||||
}
|
||||
@ -267,10 +358,40 @@ private final class TimeBasedCleanupImpl {
|
||||
totalLimitSize += scanResult.totalSize
|
||||
}
|
||||
|
||||
tempDatabase.commit()
|
||||
|
||||
var unlinkedResourceIds: [Data] = []
|
||||
|
||||
if totalLimitSize > bytesLimit {
|
||||
mapFiles(paths: paths, inodes: &inodes, removeSize: totalLimitSize - bytesLimit, mainStoragePath: totalSizeBasedPath, storageBox: storageBox)
|
||||
var remainingSize = Int64(totalLimitSize)
|
||||
tempDatabase.topByAccessTime { size, filePath in
|
||||
remainingSize -= size
|
||||
|
||||
unlink(filePath)
|
||||
|
||||
if (filePath as NSString).deletingLastPathComponent == totalSizeBasedPath {
|
||||
let fileName = (filePath as NSString).lastPathComponent
|
||||
if let idData = MediaBox.idForFileName(name: fileName).data(using: .utf8) {
|
||||
unlinkedResourceIds.append(idData)
|
||||
}
|
||||
}
|
||||
//let fileName = filePath.lastPathComponent
|
||||
|
||||
if remainingSize >= bytesLimit {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if !unlinkedResourceIds.isEmpty {
|
||||
storageBox.remove(ids: unlinkedResourceIds)
|
||||
}
|
||||
|
||||
tempDatabase.dispose()
|
||||
TempBox.shared.dispose(tempDirectory)
|
||||
|
||||
if removedShortLivedCount != 0 || removedGeneralCount != 0 || removedGeneralLimitCount != 0 {
|
||||
postboxLog("[TimeBasedCleanup] \(CFAbsoluteTimeGetCurrent() - startTime) s removed \(removedShortLivedCount) short-lived files, \(removedGeneralCount) general files, \(removedGeneralLimitCount) limit files")
|
||||
}
|
||||
|
||||
@ -401,7 +401,6 @@ public func storageUsageExceptionsScreen(
|
||||
}
|
||||
|
||||
subItems.append(.separator)
|
||||
//TODO:localize
|
||||
subItems.append(.action(ContextMenuActionItem(text: presentationData.strings.VoiceChat_RemovePeer, textColor: .destructive, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
|
||||
}, action: { _, f in
|
||||
|
||||
@ -284,29 +284,53 @@ func _internal_renderStorageUsageStatsMessages(account: Account, stats: StorageU
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_clearStorage(account: Account, peerId: EnginePeer.Id?, categories: [StorageUsageStats.CategoryKey]) -> Signal<Never, NoError> {
|
||||
func _internal_clearStorage(account: Account, peerId: EnginePeer.Id?, categories: [StorageUsageStats.CategoryKey], includeMessages: [Message], excludeMessages: [Message]) -> Signal<Never, NoError> {
|
||||
let mediaBox = account.postbox.mediaBox
|
||||
return Signal { subscriber in
|
||||
mediaBox.storageBox.remove(peerId: peerId, contentTypes: categories.map { item -> UInt8 in
|
||||
let mappedItem: MediaResourceUserContentType
|
||||
var includeResourceIds = Set<MediaResourceId>()
|
||||
for message in includeMessages {
|
||||
extractMediaResourceIds(message: message, resourceIds: &includeResourceIds)
|
||||
}
|
||||
var includeIds: [Data] = []
|
||||
for resourceId in includeResourceIds {
|
||||
if let data = resourceId.stringRepresentation.data(using: .utf8) {
|
||||
includeIds.append(data)
|
||||
}
|
||||
}
|
||||
|
||||
var excludeResourceIds = Set<MediaResourceId>()
|
||||
for message in excludeMessages {
|
||||
extractMediaResourceIds(message: message, resourceIds: &excludeResourceIds)
|
||||
}
|
||||
var excludeIds: [Data] = []
|
||||
for resourceId in excludeResourceIds {
|
||||
if let data = resourceId.stringRepresentation.data(using: .utf8) {
|
||||
excludeIds.append(data)
|
||||
}
|
||||
}
|
||||
|
||||
var mappedContentTypes: [UInt8] = []
|
||||
for item in categories {
|
||||
switch item {
|
||||
case .photos:
|
||||
mappedItem = .image
|
||||
mappedContentTypes.append(MediaResourceUserContentType.image.rawValue)
|
||||
case .videos:
|
||||
mappedItem = .video
|
||||
mappedContentTypes.append(MediaResourceUserContentType.video.rawValue)
|
||||
case .files:
|
||||
mappedItem = .file
|
||||
mappedContentTypes.append(MediaResourceUserContentType.file.rawValue)
|
||||
case .music:
|
||||
mappedItem = .audio
|
||||
mappedContentTypes.append(MediaResourceUserContentType.audio.rawValue)
|
||||
case .stickers:
|
||||
mappedItem = .sticker
|
||||
mappedContentTypes.append(MediaResourceUserContentType.sticker.rawValue)
|
||||
case .avatars:
|
||||
mappedItem = .avatar
|
||||
mappedContentTypes.append(MediaResourceUserContentType.avatar.rawValue)
|
||||
case .misc:
|
||||
mappedItem = .other
|
||||
mappedContentTypes.append(MediaResourceUserContentType.other.rawValue)
|
||||
mappedContentTypes.append(MediaResourceUserContentType.audioVideoMessage.rawValue)
|
||||
}
|
||||
return mappedItem.rawValue
|
||||
}, completion: { ids in
|
||||
}
|
||||
|
||||
mediaBox.storageBox.remove(peerId: peerId, contentTypes: mappedContentTypes, includeIds: includeIds, excludeIds: excludeIds, completion: { ids in
|
||||
var resourceIds: [MediaResourceId] = []
|
||||
for id in ids {
|
||||
if let value = String(data: id, encoding: .utf8) {
|
||||
@ -345,10 +369,32 @@ func _internal_clearStorage(account: Account, peerId: EnginePeer.Id?, categories
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_clearStorage(account: Account, peerIds: Set<EnginePeer.Id>) -> Signal<Never, NoError> {
|
||||
func _internal_clearStorage(account: Account, peerIds: Set<EnginePeer.Id>, includeMessages: [Message], excludeMessages: [Message]) -> Signal<Never, NoError> {
|
||||
let mediaBox = account.postbox.mediaBox
|
||||
return Signal { subscriber in
|
||||
mediaBox.storageBox.remove(peerIds: peerIds, completion: { ids in
|
||||
var includeResourceIds = Set<MediaResourceId>()
|
||||
for message in includeMessages {
|
||||
extractMediaResourceIds(message: message, resourceIds: &includeResourceIds)
|
||||
}
|
||||
var includeIds: [Data] = []
|
||||
for resourceId in includeResourceIds {
|
||||
if let data = resourceId.stringRepresentation.data(using: .utf8) {
|
||||
includeIds.append(data)
|
||||
}
|
||||
}
|
||||
|
||||
var excludeResourceIds = Set<MediaResourceId>()
|
||||
for message in excludeMessages {
|
||||
extractMediaResourceIds(message: message, resourceIds: &excludeResourceIds)
|
||||
}
|
||||
var excludeIds: [Data] = []
|
||||
for resourceId in excludeResourceIds {
|
||||
if let data = resourceId.stringRepresentation.data(using: .utf8) {
|
||||
excludeIds.append(data)
|
||||
}
|
||||
}
|
||||
|
||||
mediaBox.storageBox.remove(peerIds: peerIds, includeIds: includeIds, excludeIds: excludeIds, completion: { ids in
|
||||
var resourceIds: [MediaResourceId] = []
|
||||
for id in ids {
|
||||
if let value = String(data: id, encoding: .utf8) {
|
||||
@ -365,6 +411,47 @@ func _internal_clearStorage(account: Account, peerIds: Set<EnginePeer.Id>) -> Si
|
||||
}
|
||||
}
|
||||
|
||||
private func extractMediaResourceIds(message: Message, resourceIds: inout Set<MediaResourceId>) {
|
||||
for media in message.media {
|
||||
if let image = media as? TelegramMediaImage {
|
||||
for representation in image.representations {
|
||||
resourceIds.insert(representation.resource.id)
|
||||
}
|
||||
} else if let file = media as? TelegramMediaFile {
|
||||
for representation in file.previewRepresentations {
|
||||
resourceIds.insert(representation.resource.id)
|
||||
}
|
||||
resourceIds.insert(file.resource.id)
|
||||
} else if let webpage = media as? TelegramMediaWebpage {
|
||||
if case let .Loaded(content) = webpage.content {
|
||||
if let image = content.image {
|
||||
for representation in image.representations {
|
||||
resourceIds.insert(representation.resource.id)
|
||||
}
|
||||
}
|
||||
if let file = content.file {
|
||||
for representation in file.previewRepresentations {
|
||||
resourceIds.insert(representation.resource.id)
|
||||
}
|
||||
resourceIds.insert(file.resource.id)
|
||||
}
|
||||
}
|
||||
} else if let game = media as? TelegramMediaGame {
|
||||
if let image = game.image {
|
||||
for representation in image.representations {
|
||||
resourceIds.insert(representation.resource.id)
|
||||
}
|
||||
}
|
||||
if let file = game.file {
|
||||
for representation in file.previewRepresentations {
|
||||
resourceIds.insert(representation.resource.id)
|
||||
}
|
||||
resourceIds.insert(file.resource.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_clearStorage(account: Account, messages: [Message]) -> Signal<Never, NoError> {
|
||||
let mediaBox = account.postbox.mediaBox
|
||||
|
||||
@ -372,44 +459,7 @@ func _internal_clearStorage(account: Account, messages: [Message]) -> Signal<Nev
|
||||
DispatchQueue.global().async {
|
||||
var resourceIds = Set<MediaResourceId>()
|
||||
for message in messages {
|
||||
for media in message.media {
|
||||
if let image = media as? TelegramMediaImage {
|
||||
for representation in image.representations {
|
||||
resourceIds.insert(representation.resource.id)
|
||||
}
|
||||
} else if let file = media as? TelegramMediaFile {
|
||||
for representation in file.previewRepresentations {
|
||||
resourceIds.insert(representation.resource.id)
|
||||
}
|
||||
resourceIds.insert(file.resource.id)
|
||||
} else if let webpage = media as? TelegramMediaWebpage {
|
||||
if case let .Loaded(content) = webpage.content {
|
||||
if let image = content.image {
|
||||
for representation in image.representations {
|
||||
resourceIds.insert(representation.resource.id)
|
||||
}
|
||||
}
|
||||
if let file = content.file {
|
||||
for representation in file.previewRepresentations {
|
||||
resourceIds.insert(representation.resource.id)
|
||||
}
|
||||
resourceIds.insert(file.resource.id)
|
||||
}
|
||||
}
|
||||
} else if let game = media as? TelegramMediaGame {
|
||||
if let image = game.image {
|
||||
for representation in image.representations {
|
||||
resourceIds.insert(representation.resource.id)
|
||||
}
|
||||
}
|
||||
if let file = game.file {
|
||||
for representation in file.previewRepresentations {
|
||||
resourceIds.insert(representation.resource.id)
|
||||
}
|
||||
resourceIds.insert(file.resource.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
extractMediaResourceIds(message: message, resourceIds: &resourceIds)
|
||||
}
|
||||
|
||||
var removeIds: [Data] = []
|
||||
|
||||
@ -8,7 +8,9 @@ public typealias EngineTempBoxFile = TempBoxFile
|
||||
|
||||
public extension MediaResourceUserContentType {
|
||||
init(file: TelegramMediaFile) {
|
||||
if file.isMusic || file.isVoice {
|
||||
if file.isInstantVideo || file.isVoice {
|
||||
self = .audioVideoMessage
|
||||
} else if file.isMusic {
|
||||
self = .audio
|
||||
} else if file.isSticker || file.isAnimatedSticker {
|
||||
self = .sticker
|
||||
@ -231,12 +233,12 @@ public extension TelegramEngine {
|
||||
return _internal_renderStorageUsageStatsMessages(account: self.account, stats: stats, categories: categories, existingMessages: existingMessages)
|
||||
}
|
||||
|
||||
public func clearStorage(peerId: EnginePeer.Id?, categories: [StorageUsageStats.CategoryKey]) -> Signal<Never, NoError> {
|
||||
return _internal_clearStorage(account: self.account, peerId: peerId, categories: categories)
|
||||
public func clearStorage(peerId: EnginePeer.Id?, categories: [StorageUsageStats.CategoryKey], includeMessages: [Message], excludeMessages: [Message]) -> Signal<Never, NoError> {
|
||||
return _internal_clearStorage(account: self.account, peerId: peerId, categories: categories, includeMessages: includeMessages, excludeMessages: excludeMessages)
|
||||
}
|
||||
|
||||
public func clearStorage(peerIds: Set<EnginePeer.Id>) -> Signal<Never, NoError> {
|
||||
_internal_clearStorage(account: self.account, peerIds: peerIds)
|
||||
public func clearStorage(peerIds: Set<EnginePeer.Id>, includeMessages: [Message], excludeMessages: [Message]) -> Signal<Never, NoError> {
|
||||
_internal_clearStorage(account: self.account, peerIds: peerIds, includeMessages: includeMessages, excludeMessages: excludeMessages)
|
||||
}
|
||||
|
||||
public func clearStorage(messages: [Message]) -> Signal<Never, NoError> {
|
||||
|
||||
@ -38,6 +38,7 @@ swift_library(
|
||||
"//submodules/AnimatedStickerNode",
|
||||
"//submodules/TelegramAnimatedStickerNode",
|
||||
"//submodules/LegacyComponents",
|
||||
"//submodules/GalleryData",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -22,6 +22,7 @@ private func interpolateChartData(start: PieChartComponent.ChartData, end: PieCh
|
||||
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
|
||||
result.items[i].mergeFactor = (1.0 - progress) * start.items[i].mergeFactor + progress * end.items[i].mergeFactor
|
||||
}
|
||||
|
||||
return result
|
||||
@ -139,12 +140,16 @@ final class PieChartComponent: Component {
|
||||
var displayValue: Double
|
||||
var value: Double
|
||||
var color: UIColor
|
||||
var mergeable: Bool
|
||||
var mergeFactor: CGFloat
|
||||
|
||||
init(id: StorageUsageScreenComponent.Category, displayValue: Double, value: Double, color: UIColor) {
|
||||
init(id: StorageUsageScreenComponent.Category, displayValue: Double, value: Double, color: UIColor, mergeable: Bool, mergeFactor: CGFloat) {
|
||||
self.id = id
|
||||
self.displayValue = displayValue
|
||||
self.value = value
|
||||
self.color = color
|
||||
self.mergeable = mergeable
|
||||
self.mergeFactor = mergeFactor
|
||||
}
|
||||
}
|
||||
|
||||
@ -179,12 +184,12 @@ final class PieChartComponent: Component {
|
||||
private final class ChartDataView: UIView {
|
||||
private(set) var theme: PresentationTheme?
|
||||
private(set) var data: ChartData?
|
||||
private(set) var selectedKey: StorageUsageScreenComponent.Category?
|
||||
private(set) var selectedKey: AnyHashable?
|
||||
|
||||
private var currentAnimation: (start: ChartData, end: ChartData, current: ChartData, progress: CGFloat)?
|
||||
private var animator: DisplayLinkAnimator?
|
||||
|
||||
private var labels: [StorageUsageScreenComponent.Category: ChartLabel] = [:]
|
||||
private var labels: [AnyHashable: ChartLabel] = [:]
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
@ -201,7 +206,7 @@ final class PieChartComponent: Component {
|
||||
self.animator?.invalidate()
|
||||
}
|
||||
|
||||
func setItems(theme: PresentationTheme, data: ChartData, selectedKey: StorageUsageScreenComponent.Category?, animated: Bool) {
|
||||
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 {
|
||||
@ -253,7 +258,6 @@ final class PieChartComponent: Component {
|
||||
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)
|
||||
|
||||
var angles: [Double] = []
|
||||
for i in 0 ..< data.items.count {
|
||||
@ -265,13 +269,23 @@ final class PieChartComponent: Component {
|
||||
let diameter: CGFloat = 200.0
|
||||
let reducedDiameter: CGFloat = 170.0
|
||||
|
||||
let shapeLayerFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: diameter, height: diameter))
|
||||
|
||||
struct ItemAngleData {
|
||||
var angleValue: CGFloat
|
||||
var startAngle: CGFloat
|
||||
var endAngle: CGFloat
|
||||
}
|
||||
|
||||
var anglesData: [ItemAngleData] = []
|
||||
|
||||
var startAngle: CGFloat = 0.0
|
||||
for i in 0 ..< data.items.count {
|
||||
let item = data.items[i]
|
||||
|
||||
let itemOuterDiameter: CGFloat
|
||||
if let selectedKey = self.selectedKey {
|
||||
if selectedKey == item.id {
|
||||
if selectedKey == AnyHashable(item.id) {
|
||||
itemOuterDiameter = diameter
|
||||
} else {
|
||||
itemOuterDiameter = reducedDiameter
|
||||
@ -280,24 +294,54 @@ final class PieChartComponent: Component {
|
||||
itemOuterDiameter = 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 angleValue: CGFloat = angles[i]
|
||||
|
||||
var beforeSpacingFraction: CGFloat = 1.0
|
||||
var afterSpacingFraction: CGFloat = 1.0
|
||||
if item.mergeable {
|
||||
let previousItem: ChartData.Item
|
||||
if i == 0 {
|
||||
previousItem = data.items[data.items.count - 1]
|
||||
} else {
|
||||
previousItem = data.items[i - 1]
|
||||
}
|
||||
|
||||
let nextItem: ChartData.Item
|
||||
if i == data.items.count - 1 {
|
||||
nextItem = data.items[0]
|
||||
} else {
|
||||
nextItem = data.items[i + 1]
|
||||
}
|
||||
|
||||
if previousItem.mergeable {
|
||||
beforeSpacingFraction = item.mergeFactor * 1.0 + (1.0 - item.mergeFactor) * (-0.2)
|
||||
}
|
||||
if nextItem.mergeable {
|
||||
afterSpacingFraction = item.mergeFactor * 1.0 + (1.0 - item.mergeFactor) * (-0.2)
|
||||
}
|
||||
}
|
||||
|
||||
let innerStartAngle = startAngle + innerAngleSpacing * 0.5
|
||||
let arcInnerStartAngle = startAngle + innerAngleSpacing * 0.5 * beforeSpacingFraction
|
||||
|
||||
var innerEndAngle = startAngle + angleValue - innerAngleSpacing * 0.5
|
||||
innerEndAngle = max(innerEndAngle, innerStartAngle)
|
||||
var arcInnerEndAngle = startAngle + angleValue - innerAngleSpacing * 0.5 * afterSpacingFraction
|
||||
arcInnerEndAngle = max(arcInnerEndAngle, arcInnerStartAngle)
|
||||
|
||||
let outerStartAngle = startAngle + angleSpacing * 0.5
|
||||
let arcOuterStartAngle = startAngle + angleSpacing * 0.5 * beforeSpacingFraction
|
||||
var outerEndAngle = startAngle + angleValue - angleSpacing * 0.5
|
||||
outerEndAngle = max(outerEndAngle, outerStartAngle)
|
||||
var arcOuterEndAngle = startAngle + angleValue - angleSpacing * 0.5 * afterSpacingFraction
|
||||
arcOuterEndAngle = max(arcOuterEndAngle, arcOuterStartAngle)
|
||||
|
||||
let path = CGMutablePath()
|
||||
|
||||
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)
|
||||
path.addArc(center: CGPoint(x: diameter * 0.5, y: diameter * 0.5), radius: innerDiameter * 0.5, startAngle: arcInnerEndAngle, endAngle: arcInnerStartAngle, clockwise: true)
|
||||
path.addArc(center: CGPoint(x: diameter * 0.5, y: diameter * 0.5), radius: itemOuterDiameter * 0.5, startAngle: arcOuterStartAngle, endAngle: arcOuterEndAngle, clockwise: false)
|
||||
|
||||
context.addPath(path)
|
||||
context.setFillColor(item.color.cgColor)
|
||||
@ -305,7 +349,11 @@ final class PieChartComponent: Component {
|
||||
|
||||
startAngle += angleValue
|
||||
|
||||
let fractionValue: Double = floor(item.displayValue * 100.0 * 10.0) / 10.0
|
||||
anglesData.append(ItemAngleData(angleValue: angleValue, startAngle: innerStartAngle, endAngle: innerEndAngle))
|
||||
}
|
||||
|
||||
func updateItemLabel(id: AnyHashable, displayValue: Double, mergeFactor: CGFloat, angleData: ItemAngleData) {
|
||||
let fractionValue: Double = floor(displayValue * 100.0 * 10.0) / 10.0
|
||||
let fractionString: String
|
||||
if fractionValue < 0.1 {
|
||||
fractionString = "<0.1"
|
||||
@ -316,16 +364,20 @@ final class PieChartComponent: Component {
|
||||
}
|
||||
|
||||
let label: ChartLabel
|
||||
if let current = self.labels[item.id] {
|
||||
if let current = self.labels[id] {
|
||||
label = current
|
||||
} else {
|
||||
label = ChartLabel()
|
||||
self.labels[item.id] = label
|
||||
self.labels[id] = label
|
||||
}
|
||||
let labelSize = label.update(text: "\(fractionString)%")
|
||||
|
||||
var labelFrame: CGRect?
|
||||
|
||||
let angleValue = angleData.angleValue
|
||||
let innerStartAngle = angleData.startAngle
|
||||
let innerEndAngle = angleData.endAngle
|
||||
|
||||
if angleValue >= 0.001 {
|
||||
for step in 0 ... 20 {
|
||||
let stepFraction: CGFloat = CGFloat(step) / 20.0
|
||||
@ -472,7 +524,8 @@ final class PieChartComponent: Component {
|
||||
|
||||
var labelScale = labelFrame.width / labelSize.width
|
||||
|
||||
let normalAlpha: CGFloat = labelScale < 0.4 ? 0.0 : 1.0
|
||||
var normalAlpha: CGFloat = labelScale < 0.4 ? 0.0 : 1.0
|
||||
normalAlpha *= max(0.0, mergeFactor)
|
||||
|
||||
var relLabelCenter = CGPoint(
|
||||
x: labelFrame.midX - shapeLayerFrame.midX,
|
||||
@ -481,7 +534,7 @@ final class PieChartComponent: Component {
|
||||
|
||||
let labelAlpha: CGFloat
|
||||
if let selectedKey = self.selectedKey {
|
||||
if selectedKey == item.id {
|
||||
if selectedKey == id {
|
||||
labelAlpha = normalAlpha
|
||||
} else {
|
||||
labelAlpha = 0.0
|
||||
@ -499,7 +552,7 @@ final class PieChartComponent: Component {
|
||||
}
|
||||
if labelView.alpha != labelAlpha {
|
||||
let transition: Transition
|
||||
if animateIn {
|
||||
if animateIn || "".isEmpty {
|
||||
transition = .immediate
|
||||
} else {
|
||||
transition = Transition(animation: .curve(duration: 0.18, curve: .easeInOut))
|
||||
@ -516,6 +569,34 @@ final class PieChartComponent: Component {
|
||||
labelView.transform = CGAffineTransformMakeScale(labelScale, labelScale)
|
||||
}
|
||||
}
|
||||
|
||||
var mergedItem: (displayValue: Double, angleData: ItemAngleData, mergeFactor: CGFloat)?
|
||||
for i in 0 ..< data.items.count {
|
||||
let item = data.items[i]
|
||||
let angleData = anglesData[i]
|
||||
updateItemLabel(id: item.id, displayValue: item.displayValue, mergeFactor: item.mergeFactor, angleData: angleData)
|
||||
|
||||
if item.mergeable {
|
||||
if var currentMergedItem = mergedItem {
|
||||
currentMergedItem.displayValue += item.displayValue
|
||||
currentMergedItem.angleData.startAngle = min(currentMergedItem.angleData.startAngle, angleData.startAngle)
|
||||
currentMergedItem.angleData.endAngle = max(currentMergedItem.angleData.endAngle, angleData.endAngle)
|
||||
mergedItem = currentMergedItem
|
||||
} else {
|
||||
let invertedMergeFactor: CGFloat = 1.0 - max(0.0, item.mergeFactor)
|
||||
mergedItem = (item.displayValue, angleData, invertedMergeFactor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let mergedItem {
|
||||
updateItemLabel(id: "merged", displayValue: mergedItem.displayValue, mergeFactor: mergedItem.mergeFactor, angleData: mergedItem.angleData)
|
||||
} else {
|
||||
if let label = self.labels["merged"] {
|
||||
self.labels.removeValue(forKey: "merged")
|
||||
label.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -195,7 +195,6 @@ final class StorageCategoriesComponent: Component {
|
||||
self.itemViews.removeValue(forKey: key)
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
let clearTitle: String
|
||||
let label: String?
|
||||
if totalSelectedSize == 0 {
|
||||
|
||||
@ -151,6 +151,7 @@ private final class FileListItemComponent: Component {
|
||||
let selectionState: SelectionState
|
||||
let hasNext: Bool
|
||||
let action: (EngineMessage.Id) -> Void
|
||||
let contextAction: (EngineMessage.Id, ContextExtractedContentContainingView, ContextGesture) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
@ -163,7 +164,8 @@ private final class FileListItemComponent: Component {
|
||||
sideInset: CGFloat,
|
||||
selectionState: SelectionState,
|
||||
hasNext: Bool,
|
||||
action: @escaping (EngineMessage.Id) -> Void
|
||||
action: @escaping (EngineMessage.Id) -> Void,
|
||||
contextAction: @escaping (EngineMessage.Id, ContextExtractedContentContainingView, ContextGesture) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
@ -176,6 +178,7 @@ private final class FileListItemComponent: Component {
|
||||
self.selectionState = selectionState
|
||||
self.hasNext = hasNext
|
||||
self.action = action
|
||||
self.contextAction = contextAction
|
||||
}
|
||||
|
||||
static func ==(lhs: FileListItemComponent, rhs: FileListItemComponent) -> Bool {
|
||||
@ -212,7 +215,10 @@ private final class FileListItemComponent: Component {
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: HighlightTrackingButton {
|
||||
final class View: ContextControllerSourceView {
|
||||
private let extractedContainerView: ContextExtractedContentContainingView
|
||||
private let containerButton: HighlightTrackingButton
|
||||
|
||||
private let title = ComponentView<Empty>()
|
||||
private let subtitle = ComponentView<Empty>()
|
||||
private let label = ComponentView<Empty>()
|
||||
@ -227,19 +233,53 @@ private final class FileListItemComponent: Component {
|
||||
|
||||
private var checkLayer: CheckLayer?
|
||||
|
||||
private var isExtractedToContextMenu: Bool = false
|
||||
|
||||
private var highlightBackgroundFrame: CGRect?
|
||||
private var highlightBackgroundLayer: SimpleLayer?
|
||||
|
||||
private var component: FileListItemComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.extractedContainerView = ContextExtractedContentContainingView()
|
||||
self.containerButton = HighlightTrackingButton()
|
||||
|
||||
self.separatorLayer = SimpleLayer()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.layer.addSublayer(self.separatorLayer)
|
||||
|
||||
self.highligthedChanged = { [weak self] isHighlighted in
|
||||
self.addSubview(self.extractedContainerView)
|
||||
self.targetViewForActivationProgress = self.extractedContainerView.contentView
|
||||
|
||||
self.extractedContainerView.contentView.addSubview(self.containerButton)
|
||||
|
||||
self.extractedContainerView.isExtractedToContextPreviewUpdated = { [weak self] value in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
self.containerButton.clipsToBounds = value
|
||||
self.containerButton.backgroundColor = value ? component.theme.list.plainBackgroundColor : nil
|
||||
self.containerButton.layer.cornerRadius = value ? 10.0 : 0.0
|
||||
}
|
||||
self.extractedContainerView.willUpdateIsExtractedToContextPreview = { [weak self] value, transition in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.isExtractedToContextMenu = value
|
||||
|
||||
let mappedTransition: Transition
|
||||
if value {
|
||||
mappedTransition = Transition(transition)
|
||||
} else {
|
||||
mappedTransition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
|
||||
}
|
||||
self.state?.updated(transition: mappedTransition)
|
||||
}
|
||||
|
||||
self.containerButton.highligthedChanged = { [weak self] isHighlighted in
|
||||
guard let self, let component = self.component, let highlightBackgroundFrame = self.highlightBackgroundFrame else {
|
||||
return
|
||||
}
|
||||
@ -267,7 +307,15 @@ private final class FileListItemComponent: Component {
|
||||
}
|
||||
}
|
||||
}
|
||||
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||
self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||
|
||||
self.activated = { [weak self] gesture, _ in
|
||||
guard let self, let component = self.component else {
|
||||
gesture.cancel()
|
||||
return
|
||||
}
|
||||
component.contextAction(component.messageId, self.extractedContainerView, gesture)
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@ -301,9 +349,13 @@ private final class FileListItemComponent: Component {
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let contextInset: CGFloat = self.isExtractedToContextMenu ? 12.0 : 0.0
|
||||
|
||||
let spacing: CGFloat = 1.0
|
||||
let height: CGFloat = 52.0
|
||||
let verticalInset: CGFloat = 1.0
|
||||
var leftInset: CGFloat = 62.0 + component.sideInset
|
||||
var iconLeftInset: CGFloat = component.sideInset
|
||||
|
||||
@ -323,12 +375,12 @@ private final class FileListItemComponent: Component {
|
||||
} 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))
|
||||
self.containerButton.layer.addSublayer(checkLayer)
|
||||
checkLayer.frame = CGRect(origin: CGPoint(x: -checkSize, y: floor((height - verticalInset * 2.0 - 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: component.sideInset + 20.0, y: floor((height - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize)))
|
||||
transition.setFrame(layer: checkLayer, frame: CGRect(origin: CGPoint(x: component.sideInset + 20.0, y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize)))
|
||||
} else {
|
||||
if let checkLayer = self.checkLayer {
|
||||
self.checkLayer = nil
|
||||
@ -338,7 +390,7 @@ private final class FileListItemComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
let rightInset: CGFloat = 16.0 + component.sideInset
|
||||
let rightInset: CGFloat = contextInset * 2.0 + 16.0 + component.sideInset
|
||||
|
||||
if case let .fileExtension(text) = component.icon {
|
||||
let iconView: UIImageView
|
||||
@ -347,7 +399,7 @@ private final class FileListItemComponent: Component {
|
||||
} else {
|
||||
iconView = UIImageView()
|
||||
self.iconView = iconView
|
||||
self.addSubview(iconView)
|
||||
self.containerButton.addSubview(iconView)
|
||||
}
|
||||
|
||||
let iconText: ComponentView<Empty>
|
||||
@ -362,7 +414,7 @@ private final class FileListItemComponent: Component {
|
||||
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)
|
||||
let iconFrame = CGRect(origin: CGPoint(x: iconLeftInset + floor((leftInset - iconLeftInset - image.size.width) / 2.0), y: floor((height - verticalInset * 2.0 - image.size.height) / 2.0)), size: image.size)
|
||||
transition.setFrame(view: iconView, frame: iconFrame)
|
||||
|
||||
let iconTextSize = iconText.update(
|
||||
@ -377,7 +429,7 @@ private final class FileListItemComponent: Component {
|
||||
)
|
||||
if let iconTextView = iconText.view {
|
||||
if iconTextView.superview == nil {
|
||||
self.addSubview(iconTextView)
|
||||
self.containerButton.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))
|
||||
}
|
||||
@ -404,7 +456,7 @@ private final class FileListItemComponent: Component {
|
||||
|
||||
iconImageNode = TransformImageNode()
|
||||
self.iconImageNode = iconImageNode
|
||||
self.addSubview(iconImageNode.view)
|
||||
self.containerButton.addSubview(iconImageNode.view)
|
||||
}
|
||||
|
||||
let iconSize = CGSize(width: 40.0, height: 40.0)
|
||||
@ -429,7 +481,7 @@ private final class FileListItemComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
let iconFrame = CGRect(origin: CGPoint(x: iconLeftInset + floor((leftInset - iconLeftInset - iconSize.width) / 2.0), y: floor((height - iconSize.height) / 2.0)), size: iconSize)
|
||||
let iconFrame = CGRect(origin: CGPoint(x: iconLeftInset + floor((leftInset - iconLeftInset - iconSize.width) / 2.0), y: floor((height - verticalInset * 2.0 - iconSize.height) / 2.0)), size: iconSize)
|
||||
transition.setFrame(view: iconImageNode.view, frame: iconFrame)
|
||||
|
||||
let iconImageLayout = iconImageNode.asyncLayout()
|
||||
@ -454,11 +506,11 @@ private final class FileListItemComponent: Component {
|
||||
} else {
|
||||
semanticStatusNode = SemanticStatusNode(backgroundNodeColor: .clear, foregroundNodeColor: .white)
|
||||
self.semanticStatusNode = semanticStatusNode
|
||||
self.addSubview(semanticStatusNode.view)
|
||||
self.containerButton.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)
|
||||
let iconFrame = CGRect(origin: CGPoint(x: iconLeftInset + floor((leftInset - iconLeftInset - iconSize.width) / 2.0), y: floor((height - verticalInset * 2.0 - iconSize.height) / 2.0)), size: iconSize)
|
||||
transition.setFrame(view: semanticStatusNode.view, frame: iconFrame)
|
||||
|
||||
semanticStatusNode.backgroundNodeColor = component.theme.list.itemCheckColors.fillColor
|
||||
@ -483,7 +535,7 @@ private final class FileListItemComponent: Component {
|
||||
|
||||
let previousTitleFrame = self.title.view?.frame
|
||||
var previousTitleContents: UIView?
|
||||
if hasSelectionUpdated {
|
||||
if hasSelectionUpdated && !"".isEmpty {
|
||||
previousTitleContents = self.title.view?.snapshotView(afterScreenUpdates: false)
|
||||
}
|
||||
|
||||
@ -507,13 +559,13 @@ private final class FileListItemComponent: Component {
|
||||
|
||||
let contentHeight = titleSize.height + spacing + subtitleSize.height
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - contentHeight) / 2.0)), size: titleSize)
|
||||
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - verticalInset * 2.0 - 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)
|
||||
self.containerButton.addSubview(titleView)
|
||||
}
|
||||
titleView.frame = titleFrame
|
||||
if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x {
|
||||
@ -522,7 +574,7 @@ private final class FileListItemComponent: Component {
|
||||
|
||||
if let previousTitleFrame, let previousTitleContents, previousTitleFrame.size != titleSize {
|
||||
previousTitleContents.frame = CGRect(origin: previousTitleFrame.origin, size: previousTitleFrame.size)
|
||||
self.addSubview(previousTitleContents)
|
||||
self.containerButton.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
|
||||
@ -534,14 +586,14 @@ private final class FileListItemComponent: Component {
|
||||
if let subtitleView = self.subtitle.view {
|
||||
if subtitleView.superview == nil {
|
||||
subtitleView.isUserInteractionEnabled = false
|
||||
self.addSubview(subtitleView)
|
||||
self.containerButton.addSubview(subtitleView)
|
||||
}
|
||||
transition.setFrame(view: subtitleView, frame: subtitleFrame)
|
||||
}
|
||||
if let labelView = self.label.view {
|
||||
if labelView.superview == nil {
|
||||
labelView.isUserInteractionEnabled = false
|
||||
self.addSubview(labelView)
|
||||
self.containerButton.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))
|
||||
}
|
||||
@ -554,6 +606,14 @@ private final class FileListItemComponent: Component {
|
||||
|
||||
self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height + ((component.hasNext) ? UIScreenPixel : 0.0)))
|
||||
|
||||
let resultBounds = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height))
|
||||
transition.setFrame(view: self.extractedContainerView, frame: resultBounds)
|
||||
transition.setFrame(view: self.extractedContainerView.contentView, frame: resultBounds)
|
||||
self.extractedContainerView.contentRect = resultBounds
|
||||
|
||||
let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0))
|
||||
transition.setFrame(view: self.containerButton, frame: containerFrame)
|
||||
|
||||
return CGSize(width: availableSize.width, height: height)
|
||||
}
|
||||
}
|
||||
@ -611,18 +671,21 @@ final class StorageFileListPanelComponent: Component {
|
||||
let context: AccountContext
|
||||
let items: Items?
|
||||
let selectionState: StorageUsageScreenComponent.SelectionState?
|
||||
let peerAction: (EngineMessage.Id) -> Void
|
||||
let action: (EngineMessage.Id) -> Void
|
||||
let contextAction: (EngineMessage.Id, ContextExtractedContentContainingView, ContextGesture) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
items: Items?,
|
||||
selectionState: StorageUsageScreenComponent.SelectionState?,
|
||||
peerAction: @escaping (EngineMessage.Id) -> Void
|
||||
action: @escaping (EngineMessage.Id) -> Void,
|
||||
contextAction: @escaping (EngineMessage.Id, ContextExtractedContentContainingView, ContextGesture) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.items = items
|
||||
self.selectionState = selectionState
|
||||
self.peerAction = peerAction
|
||||
self.action = action
|
||||
self.contextAction = contextAction
|
||||
}
|
||||
|
||||
static func ==(lhs: StorageFileListPanelComponent, rhs: StorageFileListPanelComponent) -> Bool {
|
||||
@ -681,8 +744,14 @@ final class StorageFileListPanelComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
private final class ScrollViewImpl: UIScrollView {
|
||||
override func touchesShouldCancel(in view: UIView) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
class View: UIView, UIScrollViewDelegate {
|
||||
private let scrollView: UIScrollView
|
||||
private let scrollView: ScrollViewImpl
|
||||
|
||||
private let measureItem = ComponentView<Empty>()
|
||||
private var visibleItems: [EngineMessage.Id: ComponentView<Empty>] = [:]
|
||||
@ -694,7 +763,7 @@ final class StorageFileListPanelComponent: Component {
|
||||
private var itemLayout: ItemLayout?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.scrollView = UIScrollView()
|
||||
self.scrollView = ScrollViewImpl()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
@ -726,6 +795,10 @@ final class StorageFileListPanelComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||
cancelContextGestures(view: scrollView)
|
||||
}
|
||||
|
||||
private func updateScrolling(transition: Transition) {
|
||||
guard let component = self.component, let environment = self.environment, let items = component.items, let itemLayout = self.itemLayout else {
|
||||
return
|
||||
@ -898,7 +971,8 @@ final class StorageFileListPanelComponent: Component {
|
||||
sideInset: environment.containerInsets.left,
|
||||
selectionState: itemSelectionState,
|
||||
hasNext: index != items.items.count - 1,
|
||||
action: component.peerAction
|
||||
action: component.action,
|
||||
contextAction: component.contextAction
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: itemLayout.containerWidth, height: itemLayout.itemHeight)
|
||||
@ -949,6 +1023,8 @@ final class StorageFileListPanelComponent: Component {
|
||||
selectionState: .none,
|
||||
hasNext: false,
|
||||
action: { _ in
|
||||
},
|
||||
contextAction: { _, _, _ in
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
|
||||
@ -0,0 +1,664 @@
|
||||
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 action: (EngineMessage.Id) -> Void
|
||||
let contextAction: (EngineMessage.Id, UIView, CGRect, ContextGesture) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
items: Items?,
|
||||
selectionState: StorageUsageScreenComponent.SelectionState?,
|
||||
action: @escaping (EngineMessage.Id) -> Void,
|
||||
contextAction: @escaping (EngineMessage.Id, UIView, CGRect, ContextGesture) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.items = items
|
||||
self.selectionState = selectionState
|
||||
self.action = action
|
||||
self.contextAction = contextAction
|
||||
}
|
||||
|
||||
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: ContextControllerSourceView, 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?
|
||||
|
||||
private weak var currentGestureItemLayer: MediaGridLayer?
|
||||
|
||||
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(_:))))
|
||||
|
||||
self.shouldBegin = { [weak self] point in
|
||||
guard let self else {
|
||||
return false
|
||||
}
|
||||
|
||||
var itemLayer: MediaGridLayer?
|
||||
let scrollPoint = self.convert(point, to: self.scrollView)
|
||||
for (_, itemLayerValue) in self.visibleLayers {
|
||||
if itemLayerValue.frame.contains(scrollPoint) {
|
||||
itemLayer = itemLayerValue
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
guard let itemLayer else {
|
||||
return false
|
||||
}
|
||||
|
||||
self.currentGestureItemLayer = itemLayer
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
self.customActivationProgress = { [weak self] progress, update in
|
||||
guard let self, let itemLayer = self.currentGestureItemLayer else {
|
||||
return
|
||||
}
|
||||
|
||||
let targetContentRect = CGRect(origin: CGPoint(), size: itemLayer.bounds.size)
|
||||
|
||||
let scaleSide = itemLayer.bounds.width
|
||||
let minScale: CGFloat = max(0.7, (scaleSide - 15.0) / scaleSide)
|
||||
let currentScale = 1.0 * (1.0 - progress) + minScale * progress
|
||||
|
||||
let originalCenterOffsetX: CGFloat = itemLayer.bounds.width / 2.0 - targetContentRect.midX
|
||||
let scaledCenterOffsetX: CGFloat = originalCenterOffsetX * currentScale
|
||||
|
||||
let originalCenterOffsetY: CGFloat = itemLayer.bounds.height / 2.0 - targetContentRect.midY
|
||||
let scaledCenterOffsetY: CGFloat = originalCenterOffsetY * currentScale
|
||||
|
||||
let scaleMidX: CGFloat = scaledCenterOffsetX - originalCenterOffsetX
|
||||
let scaleMidY: CGFloat = scaledCenterOffsetY - originalCenterOffsetY
|
||||
|
||||
switch update {
|
||||
case .update:
|
||||
let sublayerTransform = CATransform3DTranslate(CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0), scaleMidX, scaleMidY, 0.0)
|
||||
itemLayer.transform = sublayerTransform
|
||||
case .begin:
|
||||
let sublayerTransform = CATransform3DTranslate(CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0), scaleMidX, scaleMidY, 0.0)
|
||||
itemLayer.transform = sublayerTransform
|
||||
case .ended:
|
||||
let sublayerTransform = CATransform3DTranslate(CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0), scaleMidX, scaleMidY, 0.0)
|
||||
let previousTransform = itemLayer.transform
|
||||
itemLayer.transform = sublayerTransform
|
||||
|
||||
itemLayer.animate(from: NSValue(caTransform3D: previousTransform), to: NSValue(caTransform3D: sublayerTransform), keyPath: "transform", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
self.activated = { [weak self] gesture, _ in
|
||||
guard let self, let component = self.component, let itemLayer = self.currentGestureItemLayer else {
|
||||
return
|
||||
}
|
||||
self.currentGestureItemLayer = nil
|
||||
guard let message = itemLayer.message else {
|
||||
return
|
||||
}
|
||||
let rect = self.convert(itemLayer.frame, from: self.scrollView)
|
||||
|
||||
component.contextAction(message.id, self, rect, gesture)
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
|
||||
var foundItemLayer: MediaGridLayer?
|
||||
for (_, itemLayer) in self.visibleLayers {
|
||||
if let message = itemLayer.message, message.id == messageId {
|
||||
foundItemLayer = itemLayer
|
||||
}
|
||||
}
|
||||
guard let itemLayer = foundItemLayer else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let itemFrame = self.convert(itemLayer.frame, from: self.scrollView)
|
||||
let proxyNode = ASDisplayNode()
|
||||
proxyNode.frame = itemFrame
|
||||
if let contents = itemLayer.contents {
|
||||
if let image = contents as? UIImage {
|
||||
proxyNode.contents = image.cgImage
|
||||
} else {
|
||||
proxyNode.contents = contents
|
||||
}
|
||||
}
|
||||
proxyNode.isHidden = true
|
||||
self.addSubnode(proxyNode)
|
||||
|
||||
let escapeNotification = EscapeNotification {
|
||||
proxyNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
return (proxyNode, proxyNode.bounds, {
|
||||
let view = UIView()
|
||||
view.frame = proxyNode.frame
|
||||
view.layer.contents = proxyNode.layer.contents
|
||||
escapeNotification.keep()
|
||||
return (view, nil)
|
||||
})
|
||||
}
|
||||
|
||||
@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.action(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)
|
||||
}
|
||||
}
|
||||
@ -18,6 +18,19 @@ import AvatarNode
|
||||
|
||||
private let avatarFont = avatarPlaceholderFont(size: 15.0)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
private final class PeerListItemComponent: Component {
|
||||
enum SelectionState: Equatable {
|
||||
case none
|
||||
@ -33,6 +46,7 @@ private final class PeerListItemComponent: Component {
|
||||
let selectionState: SelectionState
|
||||
let hasNext: Bool
|
||||
let action: (EnginePeer) -> Void
|
||||
let contextAction: (EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
@ -43,7 +57,8 @@ private final class PeerListItemComponent: Component {
|
||||
label: String,
|
||||
selectionState: SelectionState,
|
||||
hasNext: Bool,
|
||||
action: @escaping (EnginePeer) -> Void
|
||||
action: @escaping (EnginePeer) -> Void,
|
||||
contextAction: @escaping (EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
@ -54,6 +69,7 @@ private final class PeerListItemComponent: Component {
|
||||
self.selectionState = selectionState
|
||||
self.hasNext = hasNext
|
||||
self.action = action
|
||||
self.contextAction = contextAction
|
||||
}
|
||||
|
||||
static func ==(lhs: PeerListItemComponent, rhs: PeerListItemComponent) -> Bool {
|
||||
@ -84,7 +100,10 @@ private final class PeerListItemComponent: Component {
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: HighlightTrackingButton {
|
||||
final class View: ContextControllerSourceView {
|
||||
private let extractedContainerView: ContextExtractedContentContainingView
|
||||
private let containerButton: HighlightTrackingButton
|
||||
|
||||
private let title = ComponentView<Empty>()
|
||||
private let label = ComponentView<Empty>()
|
||||
private let separatorLayer: SimpleLayer
|
||||
@ -92,22 +111,58 @@ private final class PeerListItemComponent: Component {
|
||||
|
||||
private var checkLayer: CheckLayer?
|
||||
|
||||
private var isExtractedToContextMenu: Bool = false
|
||||
|
||||
private var highlightBackgroundFrame: CGRect?
|
||||
private var highlightBackgroundLayer: SimpleLayer?
|
||||
|
||||
private var component: PeerListItemComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.separatorLayer = SimpleLayer()
|
||||
|
||||
self.extractedContainerView = ContextExtractedContentContainingView()
|
||||
self.containerButton = HighlightTrackingButton()
|
||||
|
||||
self.avatarNode = AvatarNode(font: avatarFont)
|
||||
self.avatarNode.isLayerBacked = true
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.layer.addSublayer(self.separatorLayer)
|
||||
self.layer.addSublayer(self.avatarNode.layer)
|
||||
|
||||
self.highligthedChanged = { [weak self] isHighlighted in
|
||||
self.addSubview(self.extractedContainerView)
|
||||
self.targetViewForActivationProgress = self.extractedContainerView.contentView
|
||||
|
||||
self.extractedContainerView.contentView.addSubview(self.containerButton)
|
||||
|
||||
self.containerButton.layer.addSublayer(self.avatarNode.layer)
|
||||
|
||||
self.extractedContainerView.isExtractedToContextPreviewUpdated = { [weak self] value in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
self.containerButton.clipsToBounds = value
|
||||
self.containerButton.backgroundColor = value ? component.theme.list.plainBackgroundColor : nil
|
||||
self.containerButton.layer.cornerRadius = value ? 10.0 : 0.0
|
||||
}
|
||||
self.extractedContainerView.willUpdateIsExtractedToContextPreview = { [weak self] value, transition in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.isExtractedToContextMenu = value
|
||||
|
||||
let mappedTransition: Transition
|
||||
if value {
|
||||
mappedTransition = Transition(transition)
|
||||
} else {
|
||||
mappedTransition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
|
||||
}
|
||||
self.state?.updated(transition: mappedTransition)
|
||||
}
|
||||
|
||||
self.containerButton.highligthedChanged = { [weak self] isHighlighted in
|
||||
guard let self, let component = self.component, let highlightBackgroundFrame = self.highlightBackgroundFrame else {
|
||||
return
|
||||
}
|
||||
@ -135,7 +190,15 @@ private final class PeerListItemComponent: Component {
|
||||
}
|
||||
}
|
||||
}
|
||||
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||
self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||
|
||||
self.activated = { [weak self] gesture, _ in
|
||||
guard let self, let component = self.component, let peer = component.peer else {
|
||||
gesture.cancel()
|
||||
return
|
||||
}
|
||||
component.contextAction(peer, self.extractedContainerView, gesture)
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@ -169,8 +232,12 @@ private final class PeerListItemComponent: Component {
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let contextInset: CGFloat = self.isExtractedToContextMenu ? 12.0 : 0.0
|
||||
|
||||
let height: CGFloat = 52.0
|
||||
let verticalInset: CGFloat = 1.0
|
||||
var leftInset: CGFloat = 62.0 + component.sideInset
|
||||
var avatarLeftInset: CGFloat = component.sideInset + 10.0
|
||||
|
||||
@ -190,12 +257,12 @@ private final class PeerListItemComponent: Component {
|
||||
} 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))
|
||||
self.containerButton.layer.addSublayer(checkLayer)
|
||||
checkLayer.frame = CGRect(origin: CGPoint(x: -checkSize, y: floor((height - verticalInset * 2.0 - 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: component.sideInset + 20.0, y: floor((height - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize)))
|
||||
transition.setFrame(layer: checkLayer, frame: CGRect(origin: CGPoint(x: component.sideInset + 20.0, y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize)))
|
||||
} else {
|
||||
if let checkLayer = self.checkLayer {
|
||||
self.checkLayer = nil
|
||||
@ -205,11 +272,11 @@ private final class PeerListItemComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
let rightInset: CGFloat = 16.0 + component.sideInset
|
||||
let rightInset: CGFloat = contextInset * 2.0 + 16.0 + component.sideInset
|
||||
|
||||
let avatarSize: CGFloat = 40.0
|
||||
|
||||
let avatarFrame = CGRect(origin: CGPoint(x: avatarLeftInset, y: floor((height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))
|
||||
let avatarFrame = CGRect(origin: CGPoint(x: avatarLeftInset, y: floor((height - verticalInset * 2.0 - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))
|
||||
if self.avatarNode.bounds.isEmpty {
|
||||
self.avatarNode.frame = avatarFrame
|
||||
} else {
|
||||
@ -236,7 +303,7 @@ private final class PeerListItemComponent: Component {
|
||||
|
||||
let previousTitleFrame = self.title.view?.frame
|
||||
var previousTitleContents: UIView?
|
||||
if hasSelectionUpdated {
|
||||
if hasSelectionUpdated && !"".isEmpty {
|
||||
previousTitleContents = self.title.view?.snapshotView(afterScreenUpdates: false)
|
||||
}
|
||||
|
||||
@ -248,11 +315,11 @@ 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)
|
||||
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - verticalInset * 2.0 - titleSize.height) / 2.0)), size: titleSize)
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
titleView.isUserInteractionEnabled = false
|
||||
self.addSubview(titleView)
|
||||
self.containerButton.addSubview(titleView)
|
||||
}
|
||||
titleView.frame = titleFrame
|
||||
if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x {
|
||||
@ -273,9 +340,9 @@ private final class PeerListItemComponent: Component {
|
||||
if let labelView = self.label.view {
|
||||
if labelView.superview == nil {
|
||||
labelView.isUserInteractionEnabled = false
|
||||
self.addSubview(labelView)
|
||||
self.containerButton.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))
|
||||
transition.setFrame(view: labelView, frame: CGRect(origin: CGPoint(x: availableSize.width - rightInset - labelSize.width, y: floor((height - verticalInset * 2.0 - labelSize.height) / 2.0)), size: labelSize))
|
||||
}
|
||||
|
||||
if themeUpdated {
|
||||
@ -286,6 +353,14 @@ private final class PeerListItemComponent: Component {
|
||||
|
||||
self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height + ((component.hasNext) ? UIScreenPixel : 0.0)))
|
||||
|
||||
let resultBounds = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height))
|
||||
transition.setFrame(view: self.extractedContainerView, frame: resultBounds)
|
||||
transition.setFrame(view: self.extractedContainerView.contentView, frame: resultBounds)
|
||||
self.extractedContainerView.contentRect = resultBounds
|
||||
|
||||
let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0))
|
||||
transition.setFrame(view: self.containerButton, frame: containerFrame)
|
||||
|
||||
return CGSize(width: availableSize.width, height: height)
|
||||
}
|
||||
}
|
||||
@ -344,17 +419,20 @@ final class StoragePeerListPanelComponent: Component {
|
||||
let items: Items?
|
||||
let selectionState: StorageUsageScreenComponent.SelectionState?
|
||||
let peerAction: (EnginePeer) -> Void
|
||||
let contextAction: (EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
items: Items?,
|
||||
selectionState: StorageUsageScreenComponent.SelectionState?,
|
||||
peerAction: @escaping (EnginePeer) -> Void
|
||||
peerAction: @escaping (EnginePeer) -> Void,
|
||||
contextAction: @escaping (EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.items = items
|
||||
self.selectionState = selectionState
|
||||
self.peerAction = peerAction
|
||||
self.contextAction = contextAction
|
||||
}
|
||||
|
||||
static func ==(lhs: StoragePeerListPanelComponent, rhs: StoragePeerListPanelComponent) -> Bool {
|
||||
@ -413,8 +491,14 @@ final class StoragePeerListPanelComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
private final class ScrollViewImpl: UIScrollView {
|
||||
override func touchesShouldCancel(in view: UIView) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
class View: UIView, UIScrollViewDelegate {
|
||||
private let scrollView: UIScrollView
|
||||
private let scrollView: ScrollViewImpl
|
||||
|
||||
private let measureItem = ComponentView<Empty>()
|
||||
private var visibleItems: [EnginePeer.Id: ComponentView<Empty>] = [:]
|
||||
@ -426,7 +510,7 @@ final class StoragePeerListPanelComponent: Component {
|
||||
private var itemLayout: ItemLayout?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.scrollView = UIScrollView()
|
||||
self.scrollView = ScrollViewImpl()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
@ -458,6 +542,10 @@ final class StoragePeerListPanelComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||
cancelContextGestures(view: scrollView)
|
||||
}
|
||||
|
||||
private func updateScrolling(transition: Transition) {
|
||||
guard let component = self.component, let environment = self.environment, let items = component.items, let itemLayout = self.itemLayout else {
|
||||
return
|
||||
@ -505,7 +593,8 @@ final class StoragePeerListPanelComponent: Component {
|
||||
label: dataSizeString(item.size, formatting: dataSizeFormatting),
|
||||
selectionState: itemSelectionState,
|
||||
hasNext: index != items.items.count - 1,
|
||||
action: component.peerAction
|
||||
action: component.peerAction,
|
||||
contextAction: component.contextAction
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: itemLayout.containerWidth, height: itemLayout.itemHeight)
|
||||
@ -554,6 +643,8 @@ final class StoragePeerListPanelComponent: Component {
|
||||
selectionState: .none,
|
||||
hasNext: false,
|
||||
action: { _ in
|
||||
},
|
||||
contextAction: { _, _, _ in
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
|
||||
@ -345,19 +345,22 @@ final class StorageUsagePanelContainerComponent: Component {
|
||||
let dateTimeFormat: PresentationDateTimeFormat
|
||||
let insets: UIEdgeInsets
|
||||
let items: [Item]
|
||||
let currentPanelUpdated: (AnyHashable, Transition) -> Void
|
||||
|
||||
init(
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
dateTimeFormat: PresentationDateTimeFormat,
|
||||
insets: UIEdgeInsets,
|
||||
items: [Item]
|
||||
items: [Item],
|
||||
currentPanelUpdated: @escaping (AnyHashable, Transition) -> Void
|
||||
) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.dateTimeFormat = dateTimeFormat
|
||||
self.insets = insets
|
||||
self.items = items
|
||||
self.currentPanelUpdated = currentPanelUpdated
|
||||
}
|
||||
|
||||
static func ==(lhs: StorageUsagePanelContainerComponent, rhs: StorageUsagePanelContainerComponent) -> Bool {
|
||||
@ -439,6 +442,13 @@ final class StorageUsagePanelContainerComponent: Component {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
var currentPanelView: UIView? {
|
||||
guard let currentId = self.currentId, let panel = self.visiblePanels[currentId] else {
|
||||
return nil
|
||||
}
|
||||
return panel.view
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return false
|
||||
}
|
||||
@ -490,13 +500,6 @@ final class StorageUsagePanelContainerComponent: Component {
|
||||
}
|
||||
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
|
||||
@ -526,7 +529,11 @@ final class StorageUsagePanelContainerComponent: Component {
|
||||
}
|
||||
self.transitionFraction = 0.0
|
||||
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring)))
|
||||
let transition = Transition(animation: .curve(duration: 0.35, curve: .spring))
|
||||
if let currentId = self.currentId {
|
||||
self.state?.updated(transition: transition)
|
||||
component.currentPanelUpdated(currentId, transition)
|
||||
}
|
||||
|
||||
self.animatingTransition = false
|
||||
//self.currentPaneUpdated?(false)
|
||||
@ -608,7 +615,9 @@ final class StorageUsagePanelContainerComponent: Component {
|
||||
}
|
||||
if component.items.contains(where: { $0.id == id }) {
|
||||
self.currentId = id
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring)))
|
||||
let transition = Transition(animation: .curve(duration: 0.35, curve: .spring))
|
||||
self.state?.updated(transition: transition)
|
||||
component.currentPanelUpdated(id, transition)
|
||||
}
|
||||
}
|
||||
)),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -209,64 +209,8 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
|
||||
)
|
||||
}
|
||||
|
||||
private final class AnimationSupportContext {
|
||||
private let window: UIWindow
|
||||
private let testView: UIView
|
||||
private var animationCount: Int = 0
|
||||
private var displayLink: CADisplayLink?
|
||||
|
||||
init(window: UIWindow) {
|
||||
self.window = window
|
||||
self.testView = UIView()
|
||||
window.addSubview(self.testView)
|
||||
self.testView.frame = CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0))
|
||||
self.testView.backgroundColor = .black
|
||||
}
|
||||
|
||||
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() {
|
||||
self.testView.frame = CGRect(origin: CGPoint(x: self.testView.frame.minX == 0.0 ? 1.0 : 0.0, y: 0.0), size: self.testView.bounds.size)
|
||||
}
|
||||
|
||||
private func update() {
|
||||
if self.animationCount != 0 {
|
||||
if self.displayLink == nil {
|
||||
let displayLink = CADisplayLink(target: self, selector: #selector(self.displayEvent))
|
||||
|
||||
if #available(iOS 15.0, *) {
|
||||
let maxFps = Float(UIScreen.main.maximumFramesPerSecond)
|
||||
if maxFps > 61.0 {
|
||||
displayLink.preferredFrameRateRange = CAFrameRateRange(minimum: 60.0, maximum: maxFps, preferred: maxFps)
|
||||
}
|
||||
}
|
||||
|
||||
self.displayLink = displayLink
|
||||
displayLink.add(to: .main, forMode: .common)
|
||||
displayLink.isPaused = false
|
||||
}
|
||||
} else if let displayLink = self.displayLink {
|
||||
self.displayLink = nil
|
||||
displayLink.invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc(AppDelegate) class AppDelegate: UIResponder, UIApplicationDelegate, PKPushRegistryDelegate, UNUserNotificationCenterDelegate {
|
||||
@objc var window: UIWindow?
|
||||
private var animationSupportContext: AnimationSupportContext?
|
||||
var nativeWindow: (UIWindow & WindowHost)?
|
||||
var mainWindow: Window1!
|
||||
private var dataImportSplash: LegacyDataImportSplash?
|
||||
@ -362,9 +306,6 @@ private final class AnimationSupportContext {
|
||||
self.window = window
|
||||
self.nativeWindow = window
|
||||
|
||||
//self.animationSupportContext = AnimationSupportContext(window: window)
|
||||
//self.animationSupportContext?.add()
|
||||
|
||||
let clearNotificationsManager = ClearNotificationsManager(getNotificationIds: { completion in
|
||||
if #available(iOS 10.0, *) {
|
||||
UNUserNotificationCenter.current().getDeliveredNotifications(completionHandler: { notifications in
|
||||
@ -1380,6 +1321,8 @@ private final class AnimationSupportContext {
|
||||
}
|
||||
|
||||
disposable.set(signals.start(completed: {
|
||||
Logger.shared.log("App \(self.episodeId)", "Completed cleanup task")
|
||||
|
||||
task.setTaskCompleted(success: true)
|
||||
}))
|
||||
})
|
||||
|
||||
@ -263,7 +263,16 @@ public final class SharedWakeupManager {
|
||||
|
||||
func checkTasks() {
|
||||
var hasTasksForBackgroundExtension = false
|
||||
if self.inForeground || self.hasActiveAudioSession {
|
||||
|
||||
var hasActiveCalls = false
|
||||
for (_, _, tasks) in self.accountsAndTasks {
|
||||
if tasks.activeCalls {
|
||||
hasActiveCalls = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if self.inForeground || self.hasActiveAudioSession || hasActiveCalls {
|
||||
if let (completion, timer) = self.currentExternalCompletion {
|
||||
self.currentExternalCompletion = nil
|
||||
completion()
|
||||
|
||||
@ -78,12 +78,6 @@ extension CALayer {
|
||||
let animation = CABasicAnimation(keyPath: property.caLayerKeypath)
|
||||
animation.fromValue = keyframeValue
|
||||
animation.toValue = keyframeValue
|
||||
if #available(iOS 15.0, *) {
|
||||
let maxFps = Float(UIScreen.main.maximumFramesPerSecond)
|
||||
if maxFps > 61.0 {
|
||||
animation.preferredFrameRateRange = CAFrameRateRange(minimum: maxFps, maximum: maxFps, preferred: maxFps)
|
||||
}
|
||||
}
|
||||
return animation
|
||||
}
|
||||
|
||||
@ -141,12 +135,6 @@ extension CALayer {
|
||||
let calculationMode = try self.calculationMode(for: keyframes, context: context)
|
||||
|
||||
let animation = CAKeyframeAnimation(keyPath: property.caLayerKeypath)
|
||||
if #available(iOS 15.0, *) {
|
||||
let maxFps = Float(UIScreen.main.maximumFramesPerSecond)
|
||||
if maxFps > 61.0 {
|
||||
animation.preferredFrameRateRange = CAFrameRateRange(minimum: maxFps, maximum: maxFps, preferred: maxFps)
|
||||
}
|
||||
}
|
||||
|
||||
// Position animations define a `CGPath` curve that should be followed,
|
||||
// instead of animating directly between keyframe point values.
|
||||
|
||||
@ -113,12 +113,6 @@ final class MainThreadAnimationLayer: CALayer, RootAnimationLayer {
|
||||
let animation = CABasicAnimation(keyPath: event)
|
||||
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
|
||||
animation.fromValue = presentation()?.currentFrame
|
||||
if #available(iOS 15.0, *) {
|
||||
let maxFps = Float(UIScreen.main.maximumFramesPerSecond)
|
||||
if maxFps > 61.0 {
|
||||
animation.preferredFrameRateRange = CAFrameRateRange(minimum: maxFps, maximum: maxFps, preferred: maxFps)
|
||||
}
|
||||
}
|
||||
return animation
|
||||
}
|
||||
return super.action(forKey: event)
|
||||
|
||||
@ -412,14 +412,14 @@ final public class AnimationView: AnimationViewBase {
|
||||
self.f()
|
||||
}
|
||||
}
|
||||
self.workaroundDisplayLink = CADisplayLink(target: WorkaroundDisplayLinkTarget { [weak self] in
|
||||
/*self.workaroundDisplayLink = CADisplayLink(target: WorkaroundDisplayLinkTarget { [weak self] in
|
||||
let _ = self?.realtimeAnimationProgress
|
||||
}, selector: #selector(WorkaroundDisplayLinkTarget.update))
|
||||
if #available(iOS 15.0, *) {
|
||||
let maxFps = Float(UIScreen.main.maximumFramesPerSecond)
|
||||
self.workaroundDisplayLink?.preferredFrameRateRange = CAFrameRateRange(minimum: maxFps, maximum: maxFps, preferred: maxFps)
|
||||
}
|
||||
self.workaroundDisplayLink?.add(to: .main, forMode: .common)
|
||||
self.workaroundDisplayLink?.add(to: .main, forMode: .common)*/
|
||||
}
|
||||
} else {
|
||||
if let workaroundDisplayLink = self.workaroundDisplayLink {
|
||||
@ -1305,12 +1305,6 @@ final public class AnimationView: AnimationViewBase {
|
||||
layerAnimation.fillMode = CAMediaTimingFillMode.both
|
||||
layerAnimation.repeatCount = loopMode.caAnimationConfiguration.repeatCount
|
||||
layerAnimation.autoreverses = loopMode.caAnimationConfiguration.autoreverses
|
||||
if #available(iOS 15.0, *) {
|
||||
let maxFps = Float(UIScreen.main.maximumFramesPerSecond)
|
||||
if maxFps > 61.0 {
|
||||
layerAnimation.preferredFrameRateRange = CAFrameRateRange(minimum: maxFps, maximum: maxFps, preferred: maxFps)
|
||||
}
|
||||
}
|
||||
|
||||
layerAnimation.isRemovedOnCompletion = false
|
||||
if timeOffset != 0 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user