Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2022-12-29 16:51:22 +04:00
commit 6a15dbb4bb
30 changed files with 2258 additions and 621 deletions

View File

@ -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.";

View File

@ -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)
}
}
}

View File

@ -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()
}

View File

@ -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)
}
}
}

View File

@ -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()

View File

@ -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 {

View File

@ -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
}

View File

@ -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

View File

@ -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))
}

View File

@ -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
}

View File

@ -57,6 +57,7 @@ public enum MediaResourceUserContentType: UInt8, Equatable {
case file = 4
case sticker = 6
case avatar = 7
case audioVideoMessage = 8
}
public struct MediaResourceFetchParameters {

View File

@ -230,6 +230,7 @@ public final class SqliteValueBox: ValueBox {
}
func internalClose() {
self.clearStatements()
self.database = nil
}

View File

@ -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)
}
}

View File

@ -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")
}

View File

@ -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

View File

@ -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] = []

View File

@ -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> {

View File

@ -38,6 +38,7 @@ swift_library(
"//submodules/AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode",
"//submodules/LegacyComponents",
"//submodules/GalleryData",
],
visibility = [
"//visibility:public",

View File

@ -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()
}
}
}
}

View File

@ -195,7 +195,6 @@ final class StorageCategoriesComponent: Component {
self.itemViews.removeValue(forKey: key)
}
//TODO:localize
let clearTitle: String
let label: String?
if totalSelectedSize == 0 {

View File

@ -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: {},

View File

@ -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)
}
}

View File

@ -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: {},

View File

@ -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)
}
}
)),

View File

@ -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)
}))
})

View File

@ -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()

View File

@ -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.

View File

@ -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)

View File

@ -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 {