mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
1633 lines
84 KiB
Swift
1633 lines
84 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Display
|
|
import AsyncDisplayKit
|
|
import SwiftSignalKit
|
|
import Postbox
|
|
import TelegramCore
|
|
import TelegramPresentationData
|
|
import TelegramUIPreferences
|
|
import TelegramStringFormatting
|
|
import ItemListUI
|
|
import PresentationDataUtils
|
|
import OverlayStatusController
|
|
import AccountContext
|
|
import ItemListPeerItem
|
|
import DeleteChatPeerActionSheetItem
|
|
import UndoUI
|
|
import AnimatedStickerNode
|
|
import TelegramAnimatedStickerNode
|
|
import ContextUI
|
|
import AnimatedAvatarSetNode
|
|
|
|
private func totalDiskSpace() -> Int64 {
|
|
do {
|
|
let systemAttributes = try FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory() as String)
|
|
return (systemAttributes[FileAttributeKey.systemSize] as? NSNumber)?.int64Value ?? 0
|
|
} catch {
|
|
return 0
|
|
}
|
|
}
|
|
|
|
private func freeDiskSpace() -> Int64 {
|
|
do {
|
|
let systemAttributes = try FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory() as String)
|
|
return (systemAttributes[FileAttributeKey.systemFreeSize] as? NSNumber)?.int64Value ?? 0
|
|
} catch {
|
|
return 0
|
|
}
|
|
}
|
|
|
|
private final class StorageUsageControllerArguments {
|
|
let context: AccountContext
|
|
let updateKeepMediaTimeout: (Int32) -> Void
|
|
let updateMaximumCacheSize: (Int32) -> Void
|
|
let openClearAll: () -> Void
|
|
let openPeerMedia: (PeerId) -> Void
|
|
let clearPeerMedia: (PeerId) -> Void
|
|
let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void
|
|
let openCategoryMenu: (StorageUsageEntryTag) -> Void
|
|
|
|
init(context: AccountContext, updateKeepMediaTimeout: @escaping (Int32) -> Void, updateMaximumCacheSize: @escaping (Int32) -> Void, openClearAll: @escaping () -> Void, openPeerMedia: @escaping (PeerId) -> Void, clearPeerMedia: @escaping (PeerId) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, openCategoryMenu: @escaping (StorageUsageEntryTag) -> Void) {
|
|
self.context = context
|
|
self.updateKeepMediaTimeout = updateKeepMediaTimeout
|
|
self.updateMaximumCacheSize = updateMaximumCacheSize
|
|
self.openClearAll = openClearAll
|
|
self.openPeerMedia = openPeerMedia
|
|
self.clearPeerMedia = clearPeerMedia
|
|
self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions
|
|
self.openCategoryMenu = openCategoryMenu
|
|
}
|
|
}
|
|
|
|
private enum StorageUsageSection: Int32 {
|
|
case keepMedia
|
|
case maximumSize
|
|
case storage
|
|
case peers
|
|
}
|
|
|
|
private enum StorageUsageEntryTag: Hashable, ItemListItemTag {
|
|
case privateChats
|
|
case groups
|
|
case channels
|
|
|
|
public func isEqual(to other: ItemListItemTag) -> Bool {
|
|
if let other = other as? StorageUsageEntryTag, self == other {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
private enum StorageUsageEntry: ItemListNodeEntry {
|
|
case keepMediaHeader(PresentationTheme, String)
|
|
|
|
case keepMediaPrivateChats(title: String, text: String?, value: String)
|
|
case keepMediaGroups(title: String, text: String?, value: String)
|
|
case keepMediaChannels(title: String, text: String?, value: String)
|
|
|
|
case keepMedia(PresentationTheme, PresentationStrings, Int32)
|
|
case keepMediaInfo(PresentationTheme, String)
|
|
|
|
case maximumSizeHeader(PresentationTheme, String)
|
|
case maximumSize(PresentationTheme, PresentationStrings, Int32)
|
|
case maximumSizeInfo(PresentationTheme, String)
|
|
|
|
case storageHeader(PresentationTheme, String)
|
|
case storageUsage(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, [StorageUsageCategory])
|
|
case collecting(PresentationTheme, String)
|
|
case clearAll(PresentationTheme, String, Bool)
|
|
|
|
case peersHeader(PresentationTheme, String)
|
|
case peer(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, Peer, Peer?, String, Bool)
|
|
|
|
var section: ItemListSectionId {
|
|
switch self {
|
|
case .keepMediaHeader, .keepMedia, .keepMediaInfo, .keepMediaPrivateChats, .keepMediaGroups, .keepMediaChannels:
|
|
return StorageUsageSection.keepMedia.rawValue
|
|
case .maximumSizeHeader, .maximumSize, .maximumSizeInfo:
|
|
return StorageUsageSection.maximumSize.rawValue
|
|
case .storageHeader, .storageUsage, .collecting, .clearAll:
|
|
return StorageUsageSection.storage.rawValue
|
|
case .peersHeader, .peer:
|
|
return StorageUsageSection.peers.rawValue
|
|
}
|
|
}
|
|
|
|
var stableId: Int32 {
|
|
switch self {
|
|
case .keepMediaHeader:
|
|
return 0
|
|
case .keepMedia:
|
|
return 1
|
|
case .keepMediaPrivateChats:
|
|
return 2
|
|
case .keepMediaGroups:
|
|
return 3
|
|
case .keepMediaChannels:
|
|
return 4
|
|
case .keepMediaInfo:
|
|
return 5
|
|
case .maximumSizeHeader:
|
|
return 6
|
|
case .maximumSize:
|
|
return 7
|
|
case .maximumSizeInfo:
|
|
return 8
|
|
case .storageHeader:
|
|
return 9
|
|
case .storageUsage:
|
|
return 10
|
|
case .collecting:
|
|
return 11
|
|
case .clearAll:
|
|
return 12
|
|
case .peersHeader:
|
|
return 13
|
|
case let .peer(index, _, _, _, _, _, _, _, _):
|
|
return 14 + index
|
|
}
|
|
}
|
|
|
|
static func ==(lhs: StorageUsageEntry, rhs: StorageUsageEntry) -> Bool {
|
|
switch lhs {
|
|
case let .keepMediaHeader(lhsTheme, lhsText):
|
|
if case let .keepMediaHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .keepMedia(lhsTheme, lhsStrings, lhsValue):
|
|
if case let .keepMedia(rhsTheme, rhsStrings, rhsValue) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsValue == rhsValue {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .keepMediaInfo(lhsTheme, lhsText):
|
|
if case let .keepMediaInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .keepMediaPrivateChats(title, text, value):
|
|
if case .keepMediaPrivateChats(title, text, value) = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .keepMediaGroups(title, text, value):
|
|
if case .keepMediaGroups(title, text, value) = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .keepMediaChannels(title, text, value):
|
|
if case .keepMediaChannels(title, text, value) = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .maximumSizeHeader(lhsTheme, lhsText):
|
|
if case let .maximumSizeHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .maximumSize(lhsTheme, lhsStrings, lhsValue):
|
|
if case let .maximumSize(rhsTheme, rhsStrings, rhsValue) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsValue == rhsValue {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .maximumSizeInfo(lhsTheme, lhsText):
|
|
if case let .maximumSizeInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .storageHeader(lhsTheme, lhsText):
|
|
if case let .storageHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .storageUsage(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsCategories):
|
|
if case let .storageUsage(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsCategories) = rhs, lhsTheme === rhsTheme, lhsStrings == rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsCategories == rhsCategories {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .collecting(lhsTheme, lhsText):
|
|
if case let .collecting(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .clearAll(lhsTheme, lhsText, lhsEnabled):
|
|
if case let .clearAll(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .peersHeader(lhsTheme, lhsText):
|
|
if case let .peersHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .peer(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsNameOrder, lhsPeer, lhsChatPeer, lhsValue, lhsRevealed):
|
|
if case let .peer(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsNameOrder, rhsPeer, rhsChatPeer, rhsValue, rhsRevealed) = rhs {
|
|
if lhsIndex != rhsIndex {
|
|
return false
|
|
}
|
|
if lhsTheme !== rhsTheme {
|
|
return false
|
|
}
|
|
if lhsStrings !== rhsStrings {
|
|
return false
|
|
}
|
|
if lhsDateTimeFormat != rhsDateTimeFormat {
|
|
return false
|
|
}
|
|
if lhsNameOrder != rhsNameOrder {
|
|
return false
|
|
}
|
|
if !arePeersEqual(lhsPeer, rhsPeer) {
|
|
return false
|
|
}
|
|
if !arePeersEqual(lhsChatPeer, rhsChatPeer) {
|
|
return false
|
|
}
|
|
if lhsValue != rhsValue {
|
|
return false
|
|
}
|
|
if lhsRevealed != rhsRevealed {
|
|
return false
|
|
}
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
static func <(lhs: StorageUsageEntry, rhs: StorageUsageEntry) -> Bool {
|
|
return lhs.stableId < rhs.stableId
|
|
}
|
|
|
|
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
|
|
let arguments = arguments as! StorageUsageControllerArguments
|
|
switch self {
|
|
case let .keepMediaHeader(_, text):
|
|
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
|
case let .keepMediaPrivateChats(title, text, value):
|
|
return ItemListDisclosureItem(presentationData: presentationData, icon: UIImage(bundleImageName: "Settings/Menu/EditProfile")?.precomposed(), title: title, enabled: true, label: value, labelStyle: .text, additionalDetailLabel: text, sectionId: self.section, style: .blocks, disclosureStyle: .optionArrows, action: {
|
|
arguments.openCategoryMenu(.privateChats)
|
|
}, tag: StorageUsageEntryTag.privateChats)
|
|
case let .keepMediaGroups(title, text, value):
|
|
return ItemListDisclosureItem(presentationData: presentationData, icon: UIImage(bundleImageName: "Settings/Menu/GroupChats")?.precomposed(), title: title, enabled: true, label: value, labelStyle: .text, additionalDetailLabel: text, sectionId: self.section, style: .blocks, disclosureStyle: .optionArrows, action: {
|
|
arguments.openCategoryMenu(.groups)
|
|
}, tag: StorageUsageEntryTag.groups)
|
|
case let .keepMediaChannels(title, text, value):
|
|
return ItemListDisclosureItem(presentationData: presentationData, icon: UIImage(bundleImageName: "Settings/Menu/Channels")?.precomposed(), title: title, enabled: true, label: value, labelStyle: .text, additionalDetailLabel: text, sectionId: self.section, style: .blocks, disclosureStyle: .optionArrows, action: {
|
|
arguments.openCategoryMenu(.channels)
|
|
}, tag: StorageUsageEntryTag.channels)
|
|
case let .keepMedia(theme, strings, value):
|
|
return KeepMediaDurationPickerItem(theme: theme, strings: strings, value: value, sectionId: self.section, updated: { updatedValue in
|
|
arguments.updateKeepMediaTimeout(updatedValue)
|
|
})
|
|
case let .keepMediaInfo(_, text):
|
|
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
|
|
case let .maximumSizeHeader(_, text):
|
|
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
|
case let .maximumSize(theme, strings, value):
|
|
return MaximumCacheSizePickerItem(theme: theme, strings: strings, value: value, sectionId: self.section, updated: { updatedValue in
|
|
arguments.updateMaximumCacheSize(updatedValue)
|
|
})
|
|
case let .maximumSizeInfo(_, text):
|
|
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
|
|
case let .storageHeader(_, text):
|
|
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
|
case let .storageUsage(theme, strings, dateTimeFormat, categories):
|
|
return StorageUsageItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, categories: categories, sectionId: self.section)
|
|
case let .collecting(theme, text):
|
|
return CalculatingCacheSizeItem(theme: theme, title: text, sectionId: self.section, style: .blocks)
|
|
case let .clearAll(_, text, enabled):
|
|
return ItemListActionItem(presentationData: presentationData, title: text, kind: enabled ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: {
|
|
if enabled {
|
|
arguments.openClearAll()
|
|
}
|
|
})
|
|
case let .peersHeader(_, text):
|
|
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
|
case let .peer(_, _, strings, dateTimeFormat, nameDisplayOrder, peer, chatPeer, value, revealed):
|
|
let options: [ItemListPeerItemRevealOption] = [ItemListPeerItemRevealOption(type: .destructive, title: strings.ClearCache_Clear, action: {
|
|
arguments.clearPeerMedia(peer.id)
|
|
})]
|
|
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: EnginePeer(peer), aliasHandling: .threatSelfAsSaved, nameColor: chatPeer == nil ? .primary : .secret, presence: nil, text: .none, label: .disclosure(value), editing: ItemListPeerItemEditing(editable: true, editing: false, revealed: revealed), revealOptions: ItemListPeerItemRevealOptions(options: options), switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: {
|
|
let resolvedPeer = chatPeer ?? peer
|
|
arguments.openPeerMedia(resolvedPeer.id)
|
|
}, setPeerIdWithRevealedOptions: { peerId, fromPeerId in
|
|
arguments.setPeerIdWithRevealedOptions(peerId, fromPeerId)
|
|
}, removePeer: { _ in
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct StorageUsageState: Equatable {
|
|
var peerIdWithRevealedOptions: PeerId?
|
|
}
|
|
|
|
private func storageUsageControllerEntries(presentationData: PresentationData, cacheSettings: CacheStorageSettings, accountSpecificCacheSettings: AccountSpecificCacheStorageSettings, cacheStats: CacheUsageStatsResult?, state: StorageUsageState) -> [StorageUsageEntry] {
|
|
var entries: [StorageUsageEntry] = []
|
|
|
|
entries.append(.keepMediaHeader(presentationData.theme, presentationData.strings.Cache_KeepMedia.uppercased()))
|
|
|
|
let sections: [StorageUsageEntryTag] = [.privateChats, .groups, .channels]
|
|
for section in sections {
|
|
let mappedCategory: CacheStorageSettings.PeerStorageCategory
|
|
switch section {
|
|
case .privateChats:
|
|
mappedCategory = .privateChats
|
|
case .groups:
|
|
mappedCategory = .groups
|
|
case .channels:
|
|
mappedCategory = .channels
|
|
}
|
|
let value = cacheSettings.categoryStorageTimeout[mappedCategory] ?? Int32.max
|
|
|
|
let optionText: String
|
|
if value == Int32.max {
|
|
optionText = presentationData.strings.ClearCache_Forever
|
|
} else {
|
|
optionText = timeIntervalString(strings: presentationData.strings, value: value)
|
|
}
|
|
|
|
switch section {
|
|
case .privateChats:
|
|
entries.append(.keepMediaPrivateChats(title: presentationData.strings.Notifications_PrivateChats, text: nil, value: optionText))
|
|
case .groups:
|
|
entries.append(.keepMediaGroups(title: presentationData.strings.Notifications_GroupChats, text: nil, value: optionText))
|
|
case .channels:
|
|
entries.append(.keepMediaChannels(title: presentationData.strings.Notifications_Channels, text: nil, value: optionText))
|
|
}
|
|
}
|
|
|
|
//entries.append(.keepMedia(presentationData.theme, presentationData.strings, cacheSettings.defaultCacheStorageTimeout))
|
|
|
|
entries.append(.keepMediaInfo(presentationData.theme, presentationData.strings.Cache_KeepMediaHelp))
|
|
|
|
entries.append(.maximumSizeHeader(presentationData.theme, presentationData.strings.Cache_MaximumCacheSize.uppercased()))
|
|
entries.append(.maximumSize(presentationData.theme, presentationData.strings, cacheSettings.defaultCacheStorageLimitGigabytes))
|
|
entries.append(.maximumSizeInfo(presentationData.theme, presentationData.strings.Cache_MaximumCacheSizeHelp))
|
|
|
|
var addedHeader = false
|
|
|
|
entries.append(.storageHeader(presentationData.theme, presentationData.strings.ClearCache_StorageTitle(stringForDeviceType().uppercased()).string))
|
|
if let cacheStats = cacheStats, case let .result(stats) = cacheStats {
|
|
var peerSizes: Int64 = 0
|
|
var statsByPeerId: [(PeerId, Int64)] = []
|
|
var peerIndices: [PeerId: Int] = [:]
|
|
for (peerId, categories) in stats.media {
|
|
var updatedPeerId = peerId
|
|
if let group = stats.peers[peerId] as? TelegramGroup, let migrationReference = group.migrationReference, let channel = stats.peers[migrationReference.peerId] {
|
|
updatedPeerId = channel.id
|
|
}
|
|
var combinedSize: Int64 = 0
|
|
for (_, media) in categories {
|
|
for (_, size) in media {
|
|
combinedSize += size
|
|
}
|
|
}
|
|
if let index = peerIndices[updatedPeerId] {
|
|
statsByPeerId[index].1 += combinedSize
|
|
} else {
|
|
peerIndices[updatedPeerId] = statsByPeerId.count
|
|
statsByPeerId.append((updatedPeerId, combinedSize))
|
|
}
|
|
peerSizes += combinedSize
|
|
}
|
|
|
|
let telegramCacheSize = Int64(peerSizes + stats.otherSize + stats.cacheSize + stats.tempSize)
|
|
let totalTelegramSize = telegramCacheSize + stats.immutableSize
|
|
|
|
var categories: [StorageUsageCategory] = []
|
|
let totalSpace = max(totalDiskSpace(), 1)
|
|
let freeSpace = freeDiskSpace()
|
|
let otherAppsSpace = totalSpace - freeSpace - totalTelegramSize
|
|
|
|
let totalSpaceValue = CGFloat(totalSpace)
|
|
|
|
if telegramCacheSize > 0 {
|
|
categories.append(StorageUsageCategory(title: presentationData.strings.ClearCache_StorageCache, size: totalTelegramSize, fraction: CGFloat(totalTelegramSize) / totalSpaceValue, color: presentationData.theme.list.itemBarChart.color1))
|
|
} else {
|
|
categories.append(StorageUsageCategory(title: presentationData.strings.ClearCache_StorageServiceFiles, size: totalTelegramSize, fraction: CGFloat(totalTelegramSize) / totalSpaceValue, color: presentationData.theme.list.itemBarChart.color1))
|
|
}
|
|
categories.append(StorageUsageCategory(title: presentationData.strings.ClearCache_StorageOtherApps, size: otherAppsSpace, fraction: CGFloat(otherAppsSpace) / totalSpaceValue, color: presentationData.theme.list.itemBarChart.color2))
|
|
categories.append(StorageUsageCategory(title: presentationData.strings.ClearCache_StorageFree, size: freeSpace, fraction: CGFloat(freeSpace) / totalSpaceValue, color: presentationData.theme.list.itemBarChart.color3))
|
|
|
|
entries.append(.storageUsage(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, categories))
|
|
|
|
entries.append(.clearAll(presentationData.theme, presentationData.strings.ClearCache_ClearCache, telegramCacheSize > 0))
|
|
|
|
var index: Int32 = 0
|
|
for (peerId, size) in statsByPeerId.sorted(by: { $0.1 > $1.1 }) {
|
|
if size >= 32 * 1024 {
|
|
if let peer = stats.peers[peerId] {
|
|
if !addedHeader {
|
|
addedHeader = true
|
|
entries.append(.peersHeader(presentationData.theme, presentationData.strings.Cache_ByPeerHeader))
|
|
}
|
|
var mainPeer = peer
|
|
var chatPeer: Peer?
|
|
if let associatedPeerId = peer.associatedPeerId, let associatedPeer = stats.peers[associatedPeerId] {
|
|
chatPeer = mainPeer
|
|
mainPeer = associatedPeer
|
|
}
|
|
entries.append(.peer(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, mainPeer, chatPeer, dataSizeString(size, formatting: DataSizeStringFormatting(presentationData: presentationData)), state.peerIdWithRevealedOptions == peer.id))
|
|
index += 1
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
entries.append(.collecting(presentationData.theme, presentationData.strings.Cache_Indexing))
|
|
}
|
|
|
|
return entries
|
|
}
|
|
|
|
private func stringForCategory(strings: PresentationStrings, category: PeerCacheUsageCategory) -> String {
|
|
switch category {
|
|
case .image:
|
|
return strings.Cache_Photos
|
|
case .video:
|
|
return strings.Cache_Videos
|
|
case .audio:
|
|
return strings.Cache_Music
|
|
case .file:
|
|
return strings.Cache_Files
|
|
}
|
|
}
|
|
|
|
func cacheUsageStats(context: AccountContext) -> Signal<CacheUsageStatsResult?, NoError> {
|
|
let containerPath = context.sharedContext.applicationBindings.containerPath
|
|
let additionalPaths: [String] = [
|
|
NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true)[0],
|
|
containerPath + "/Documents/files",
|
|
containerPath + "/Documents/video",
|
|
containerPath + "/Documents/audio",
|
|
containerPath + "/Documents/mediacache",
|
|
containerPath + "/Documents/tempcache_v1/store",
|
|
]
|
|
return .single(nil)
|
|
|> then(context.engine.resources.collectCacheUsageStats(additionalCachePaths: additionalPaths, logFilesPath: context.sharedContext.applicationBindings.containerPath + "/telegram-data/logs")
|
|
|> map(Optional.init))
|
|
}
|
|
|
|
public func storageUsageController(context: AccountContext, cacheUsagePromise: Promise<CacheUsageStatsResult?>? = nil, isModal: Bool = false) -> ViewController {
|
|
let statePromise = ValuePromise(StorageUsageState(peerIdWithRevealedOptions: nil))
|
|
let stateValue = Atomic(value: StorageUsageState(peerIdWithRevealedOptions: nil))
|
|
let updateState: ((StorageUsageState) -> StorageUsageState) -> Void = { f in
|
|
statePromise.set(stateValue.modify { f($0) })
|
|
}
|
|
|
|
let cacheSettingsPromise = Promise<CacheStorageSettings>()
|
|
cacheSettingsPromise.set(context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.cacheStorageSettings])
|
|
|> map { sharedData -> CacheStorageSettings in
|
|
let cacheSettings: CacheStorageSettings
|
|
if let value = sharedData.entries[SharedDataKeys.cacheStorageSettings]?.get(CacheStorageSettings.self) {
|
|
cacheSettings = value
|
|
} else {
|
|
cacheSettings = CacheStorageSettings.defaultSettings
|
|
}
|
|
|
|
return cacheSettings
|
|
})
|
|
|
|
let accountSpecificCacheSettingsPromise = Promise<AccountSpecificCacheStorageSettings>()
|
|
let viewKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.accountSpecificCacheStorageSettings]))
|
|
accountSpecificCacheSettingsPromise.set(context.account.postbox.combinedView(keys: [viewKey])
|
|
|> map { views -> AccountSpecificCacheStorageSettings in
|
|
let cacheSettings: AccountSpecificCacheStorageSettings
|
|
if let view = views.views[viewKey] as? PreferencesView, let value = view.values[PreferencesKeys.accountSpecificCacheStorageSettings]?.get(AccountSpecificCacheStorageSettings.self) {
|
|
cacheSettings = value
|
|
} else {
|
|
cacheSettings = AccountSpecificCacheStorageSettings.defaultSettings
|
|
}
|
|
|
|
return cacheSettings
|
|
})
|
|
|
|
var presentControllerImpl: ((ViewController, PresentationContextType, Any?) -> Void)?
|
|
var pushControllerImpl: ((ViewController) -> Void)?
|
|
var findAutoremoveReferenceNode: ((StorageUsageEntryTag) -> ItemListDisclosureItemNode?)?
|
|
var presentInGlobalOverlay: ((ViewController) -> Void)?
|
|
|
|
var statsPromise: Promise<CacheUsageStatsResult?>
|
|
if let cacheUsagePromise = cacheUsagePromise {
|
|
statsPromise = cacheUsagePromise
|
|
} else {
|
|
statsPromise = Promise<CacheUsageStatsResult?>()
|
|
statsPromise.set(cacheUsageStats(context: context))
|
|
}
|
|
|
|
let resetStats: () -> Void = {
|
|
statsPromise.set(cacheUsageStats(context: context))
|
|
}
|
|
|
|
let actionDisposables = DisposableSet()
|
|
|
|
let clearDisposable = MetaDisposable()
|
|
actionDisposables.add(clearDisposable)
|
|
|
|
let arguments = StorageUsageControllerArguments(context: context, updateKeepMediaTimeout: { value in
|
|
let _ = updateCacheStorageSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in
|
|
var current = current
|
|
current.defaultCacheStorageTimeout = value
|
|
return current
|
|
}).start()
|
|
}, updateMaximumCacheSize: { value in
|
|
let _ = updateCacheStorageSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in
|
|
var current = current
|
|
current.defaultCacheStorageLimitGigabytes = value
|
|
return current
|
|
}).start()
|
|
}, openClearAll: {
|
|
let _ = (statsPromise.get()
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { [weak statsPromise] result in
|
|
if let result = result, case let .result(stats) = result {
|
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
|
let controller = ActionSheetController(presentationData: presentationData)
|
|
let dismissAction: () -> Void = { [weak controller] in
|
|
controller?.dismissAnimated()
|
|
}
|
|
|
|
var sizeIndex: [PeerCacheUsageCategory: (Bool, Int64)] = [:]
|
|
var otherSize: (Bool, Int64) = (true, 0)
|
|
|
|
for (_, categories) in stats.media {
|
|
for (category, media) in categories {
|
|
var combinedSize: Int64 = 0
|
|
for (_, size) in media {
|
|
combinedSize += size
|
|
}
|
|
if combinedSize != 0 {
|
|
sizeIndex[category] = (true, (sizeIndex[category]?.1 ?? 0) + combinedSize)
|
|
}
|
|
}
|
|
}
|
|
|
|
if stats.cacheSize + stats.otherSize + stats.tempSize > 10 * 1024 {
|
|
otherSize = (true, stats.cacheSize + stats.otherSize + stats.tempSize)
|
|
}
|
|
|
|
var itemIndex = 0
|
|
|
|
var selectedSize: Int64 = 0
|
|
let updateTotalSize: () -> Void = { [weak controller] in
|
|
controller?.updateItem(groupIndex: 0, itemIndex: itemIndex, { item in
|
|
let title: String
|
|
var filteredSize = sizeIndex.values.reduce(0, { $0 + ($1.0 ? $1.1 : 0) })
|
|
if otherSize.0 {
|
|
filteredSize += otherSize.1
|
|
}
|
|
selectedSize = filteredSize
|
|
|
|
if filteredSize == 0 {
|
|
title = presentationData.strings.Cache_ClearNone
|
|
} else {
|
|
title = presentationData.strings.Cache_Clear("\(dataSizeString(filteredSize, formatting: DataSizeStringFormatting(presentationData: presentationData)))").string
|
|
}
|
|
|
|
if let item = item as? ActionSheetButtonItem {
|
|
return ActionSheetButtonItem(title: title, color: filteredSize != 0 ? .accent : .disabled, enabled: filteredSize != 0, action: item.action)
|
|
}
|
|
return item
|
|
})
|
|
}
|
|
|
|
let toggleCheck: (PeerCacheUsageCategory?, Int) -> Void = { [weak controller] category, itemIndex in
|
|
if let category = category {
|
|
if let (value, size) = sizeIndex[category] {
|
|
sizeIndex[category] = (!value, size)
|
|
}
|
|
} else {
|
|
otherSize = (!otherSize.0, otherSize.1)
|
|
}
|
|
controller?.updateItem(groupIndex: 0, itemIndex: itemIndex + 1, { item in
|
|
if let item = item as? ActionSheetCheckboxItem {
|
|
return ActionSheetCheckboxItem(title: item.title, label: item.label, value: !item.value, action: item.action)
|
|
}
|
|
return item
|
|
})
|
|
updateTotalSize()
|
|
}
|
|
var items: [ActionSheetItem] = []
|
|
|
|
let validCategories: [PeerCacheUsageCategory] = [.image, .video, .audio, .file]
|
|
|
|
var totalSize: Int64 = 0
|
|
|
|
items.append(ActionSheetTextItem(title: presentationData.strings.ClearCache_ClearDescription))
|
|
|
|
for categoryId in validCategories {
|
|
if let (_, size) = sizeIndex[categoryId] {
|
|
let categorySize: Int64 = size
|
|
totalSize += categorySize
|
|
let index = itemIndex
|
|
items.append(ActionSheetCheckboxItem(title: stringForCategory(strings: presentationData.strings, category: categoryId), label: dataSizeString(categorySize, formatting: DataSizeStringFormatting(presentationData: presentationData)), value: true, action: { value in
|
|
toggleCheck(categoryId, index)
|
|
}))
|
|
itemIndex += 1
|
|
}
|
|
}
|
|
|
|
if otherSize.1 != 0 {
|
|
totalSize += otherSize.1
|
|
let index = itemIndex
|
|
items.append(ActionSheetCheckboxItem(title: presentationData.strings.Localization_LanguageOther, label: dataSizeString(otherSize.1, formatting: DataSizeStringFormatting(presentationData: presentationData)), value: true, action: { value in
|
|
toggleCheck(nil, index)
|
|
}))
|
|
itemIndex += 1
|
|
}
|
|
selectedSize = totalSize
|
|
|
|
if !items.isEmpty {
|
|
var cancelImpl: (() -> Void)?
|
|
|
|
items.append(ActionSheetButtonItem(title: presentationData.strings.Cache_Clear("\(dataSizeString(totalSize, formatting: DataSizeStringFormatting(presentationData: presentationData)))").string, action: { [weak controller] in
|
|
if let statsPromise = statsPromise {
|
|
let clearCategories = sizeIndex.keys.filter({ sizeIndex[$0]!.0 })
|
|
|
|
var clearMediaIds = Set<MediaId>()
|
|
|
|
var media = stats.media
|
|
for (peerId, categories) in stats.media {
|
|
var categories = categories
|
|
for category in clearCategories {
|
|
if let contents = categories[category] {
|
|
for (mediaId, _) in contents {
|
|
clearMediaIds.insert(mediaId)
|
|
}
|
|
}
|
|
categories.removeValue(forKey: category)
|
|
}
|
|
|
|
media[peerId] = categories
|
|
}
|
|
|
|
var clearResourceIds = Set<MediaResourceId>()
|
|
for id in clearMediaIds {
|
|
if let ids = stats.mediaResourceIds[id] {
|
|
for resourceId in ids {
|
|
clearResourceIds.insert(resourceId)
|
|
}
|
|
}
|
|
}
|
|
|
|
var updatedOtherPaths = stats.otherPaths
|
|
var updatedOtherSize = stats.otherSize
|
|
var updatedCacheSize = stats.cacheSize
|
|
var updatedTempPaths = stats.tempPaths
|
|
var updatedTempSize = stats.tempSize
|
|
|
|
var signal: Signal<Float, NoError> = context.engine.resources.clearCachedMediaResources(mediaResourceIds: clearResourceIds)
|
|
if otherSize.0 {
|
|
let removeTempFiles: Signal<Float, NoError> = Signal { subscriber in
|
|
let fileManager = FileManager.default
|
|
var count: Int = 0
|
|
let totalCount = stats.tempPaths.count
|
|
|
|
let reportProgress: (Int) -> Void = { count in
|
|
Queue.mainQueue().async {
|
|
subscriber.putNext(min(1.0, Float(count) / Float(totalCount)))
|
|
}
|
|
}
|
|
|
|
if totalCount == 0 {
|
|
subscriber.putNext(1.0)
|
|
subscriber.putCompletion()
|
|
return EmptyDisposable
|
|
}
|
|
|
|
for path in stats.tempPaths {
|
|
let _ = try? fileManager.removeItem(atPath: path)
|
|
count += 1
|
|
reportProgress(count)
|
|
}
|
|
|
|
subscriber.putCompletion()
|
|
return EmptyDisposable
|
|
} |> runOn(Queue.concurrentDefaultQueue())
|
|
signal = (signal |> map { $0 * 0.7 })
|
|
|> then(context.account.postbox.mediaBox.removeOtherCachedResources(paths: stats.otherPaths) |> map { 0.7 + 0.2 * $0 })
|
|
|> then(removeTempFiles |> map { 0.9 + 0.1 * $0 })
|
|
}
|
|
|
|
if otherSize.0 {
|
|
updatedOtherPaths = []
|
|
updatedOtherSize = 0
|
|
updatedCacheSize = 0
|
|
updatedTempPaths = []
|
|
updatedTempSize = 0
|
|
}
|
|
|
|
let progressPromise = ValuePromise<Float>(0.0)
|
|
let overlayNode = StorageUsageClearProgressOverlayNode(presentationData: presentationData)
|
|
overlayNode.setProgressSignal(progressPromise.get())
|
|
controller?.setItemGroupOverlayNode(groupIndex: 0, node: overlayNode)
|
|
|
|
let resultStats = CacheUsageStats(media: media, mediaResourceIds: stats.mediaResourceIds, peers: stats.peers, otherSize: updatedOtherSize, otherPaths: updatedOtherPaths, cacheSize: updatedCacheSize, tempPaths: updatedTempPaths, tempSize: updatedTempSize, immutableSize: stats.immutableSize)
|
|
|
|
cancelImpl = {
|
|
clearDisposable.set(nil)
|
|
resetStats()
|
|
}
|
|
statsPromise.set(.single(.result(resultStats)))
|
|
clearDisposable.set((signal
|
|
|> deliverOnMainQueue).start(next: { progress in
|
|
progressPromise.set(progress)
|
|
}, completed: {
|
|
statsPromise.set(.single(.result(resultStats)))
|
|
progressPromise.set(1.0)
|
|
Queue.mainQueue().after(1.0) {
|
|
dismissAction()
|
|
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(selectedSize, formatting: DataSizeStringFormatting(presentationData: presentationData)))", stringForDeviceType()).string), elevatedLayout: false, action: { _ in return false }), .current, nil)
|
|
}
|
|
}))
|
|
}
|
|
}))
|
|
|
|
controller.setItemGroups([
|
|
ActionSheetItemGroup(items: items),
|
|
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: {
|
|
cancelImpl?()
|
|
dismissAction()
|
|
})])
|
|
])
|
|
presentControllerImpl?(controller, .window(.root), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
|
}
|
|
}
|
|
})
|
|
}, openPeerMedia: { peerId in
|
|
let _ = (statsPromise.get() |> take(1) |> deliverOnMainQueue).start(next: { [weak statsPromise] result in
|
|
if let result = result, case let .result(stats) = result {
|
|
var additionalPeerId: PeerId?
|
|
if var categories = stats.media[peerId], let peer = stats.peers[peerId] {
|
|
if let channel = peer as? TelegramChannel, case .group = channel.info {
|
|
for (_, peer) in stats.peers {
|
|
if let group = peer as? TelegramGroup, let migrationReference = group.migrationReference, migrationReference.peerId == peerId {
|
|
if let additionalCategories = stats.media[group.id] {
|
|
additionalPeerId = group.id
|
|
categories.merge(additionalCategories, uniquingKeysWith: { lhs, rhs in
|
|
return lhs.merging(rhs, uniquingKeysWith: { lhs, rhs in
|
|
return lhs + rhs
|
|
})
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
|
let controller = ActionSheetController(presentationData: presentationData)
|
|
let dismissAction: () -> Void = { [weak controller] in
|
|
controller?.dismissAnimated()
|
|
}
|
|
|
|
var sizeIndex: [PeerCacheUsageCategory: (Bool, Int64)] = [:]
|
|
|
|
var itemIndex = 1
|
|
|
|
var selectedSize: Int64 = 0
|
|
let updateTotalSize: () -> Void = { [weak controller] in
|
|
controller?.updateItem(groupIndex: 0, itemIndex: itemIndex, { item in
|
|
let title: String
|
|
let filteredSize = sizeIndex.values.reduce(0, { $0 + ($1.0 ? $1.1 : 0) })
|
|
selectedSize = filteredSize
|
|
|
|
if filteredSize == 0 {
|
|
title = presentationData.strings.Cache_ClearNone
|
|
} else {
|
|
title = presentationData.strings.Cache_Clear("\(dataSizeString(filteredSize, formatting: DataSizeStringFormatting(presentationData: presentationData)))").string
|
|
}
|
|
|
|
if let item = item as? ActionSheetButtonItem {
|
|
return ActionSheetButtonItem(title: title, color: filteredSize != 0 ? .accent : .disabled, enabled: filteredSize != 0, action: item.action)
|
|
}
|
|
return item
|
|
})
|
|
}
|
|
|
|
let toggleCheck: (PeerCacheUsageCategory, Int) -> Void = { [weak controller] category, itemIndex in
|
|
if let (value, size) = sizeIndex[category] {
|
|
sizeIndex[category] = (!value, size)
|
|
}
|
|
controller?.updateItem(groupIndex: 0, itemIndex: itemIndex, { item in
|
|
if let item = item as? ActionSheetCheckboxItem {
|
|
return ActionSheetCheckboxItem(title: item.title, label: item.label, value: !item.value, action: item.action)
|
|
}
|
|
return item
|
|
})
|
|
updateTotalSize()
|
|
}
|
|
var items: [ActionSheetItem] = []
|
|
|
|
items.append(DeleteChatPeerActionSheetItem(context: context, peer: EnginePeer(peer), chatPeer: EnginePeer(peer), action: .clearCache, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder))
|
|
|
|
let validCategories: [PeerCacheUsageCategory] = [.image, .video, .audio, .file]
|
|
|
|
var totalSize: Int64 = 0
|
|
|
|
for categoryId in validCategories {
|
|
if let media = categories[categoryId] {
|
|
var categorySize: Int64 = 0
|
|
for (_, size) in media {
|
|
categorySize += size
|
|
}
|
|
sizeIndex[categoryId] = (true, categorySize)
|
|
totalSize += categorySize
|
|
if categorySize > 1024 {
|
|
let index = itemIndex
|
|
items.append(ActionSheetCheckboxItem(title: stringForCategory(strings: presentationData.strings, category: categoryId), label: dataSizeString(categorySize, formatting: DataSizeStringFormatting(presentationData: presentationData)), value: true, action: { value in
|
|
toggleCheck(categoryId, index)
|
|
}))
|
|
itemIndex += 1
|
|
}
|
|
}
|
|
}
|
|
selectedSize = totalSize
|
|
|
|
if !items.isEmpty {
|
|
var cancelImpl: (() -> Void)?
|
|
|
|
items.append(ActionSheetButtonItem(title: presentationData.strings.Cache_Clear("\(dataSizeString(totalSize, formatting: DataSizeStringFormatting(presentationData: presentationData)))").string, action: { [weak controller] in
|
|
if let statsPromise = statsPromise {
|
|
let clearCategories = sizeIndex.keys.filter({ sizeIndex[$0]!.0 })
|
|
var clearMediaIds = Set<MediaId>()
|
|
|
|
var media = stats.media
|
|
if var categories = media[peerId] {
|
|
for category in clearCategories {
|
|
if let contents = categories[category] {
|
|
for (mediaId, _) in contents {
|
|
clearMediaIds.insert(mediaId)
|
|
}
|
|
}
|
|
categories.removeValue(forKey: category)
|
|
}
|
|
|
|
media[peerId] = categories
|
|
}
|
|
if let additionalPeerId = additionalPeerId {
|
|
if var categories = media[additionalPeerId] {
|
|
for category in clearCategories {
|
|
if let contents = categories[category] {
|
|
for (mediaId, _) in contents {
|
|
clearMediaIds.insert(mediaId)
|
|
}
|
|
}
|
|
categories.removeValue(forKey: category)
|
|
}
|
|
|
|
media[additionalPeerId] = categories
|
|
}
|
|
}
|
|
|
|
var clearResourceIds = Set<MediaResourceId>()
|
|
for id in clearMediaIds {
|
|
if let ids = stats.mediaResourceIds[id] {
|
|
for resourceId in ids {
|
|
clearResourceIds.insert(resourceId)
|
|
}
|
|
}
|
|
}
|
|
|
|
let signal = context.engine.resources.clearCachedMediaResources(mediaResourceIds: clearResourceIds)
|
|
|
|
let progressPromise = ValuePromise<Float>(0.0)
|
|
let overlayNode = StorageUsageClearProgressOverlayNode(presentationData: presentationData)
|
|
overlayNode.setProgressSignal(progressPromise.get())
|
|
controller?.setItemGroupOverlayNode(groupIndex: 0, node: overlayNode)
|
|
|
|
let resultStats = CacheUsageStats(media: media, mediaResourceIds: stats.mediaResourceIds, peers: stats.peers, otherSize: stats.otherSize, otherPaths: stats.otherPaths, cacheSize: stats.cacheSize, tempPaths: stats.tempPaths, tempSize: stats.tempSize, immutableSize: stats.immutableSize)
|
|
|
|
cancelImpl = {
|
|
clearDisposable.set(nil)
|
|
resetStats()
|
|
}
|
|
clearDisposable.set((signal
|
|
|> deliverOnMainQueue).start(next: { progress in
|
|
progressPromise.set(progress)
|
|
}, completed: {
|
|
statsPromise.set(.single(.result(resultStats)))
|
|
progressPromise.set(1.0)
|
|
Queue.mainQueue().after(1.0) {
|
|
dismissAction()
|
|
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(selectedSize, formatting: DataSizeStringFormatting(presentationData: presentationData)))", stringForDeviceType()).string), elevatedLayout: false, action: { _ in return false }), .current, nil)
|
|
}
|
|
}))
|
|
}
|
|
}))
|
|
|
|
controller.setItemGroups([
|
|
ActionSheetItemGroup(items: items),
|
|
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: {
|
|
cancelImpl?()
|
|
dismissAction()
|
|
})])
|
|
])
|
|
presentControllerImpl?(controller, .window(.root), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}, clearPeerMedia: { peerId in
|
|
let _ = (statsPromise.get() |> take(1) |> deliverOnMainQueue).start(next: { [weak statsPromise] result in
|
|
if let result = result, case let .result(stats) = result {
|
|
var additionalPeerId: PeerId?
|
|
if var categories = stats.media[peerId], let peer = stats.peers[peerId] {
|
|
if let channel = peer as? TelegramChannel, case .group = channel.info {
|
|
for (_, peer) in stats.peers {
|
|
if let group = peer as? TelegramGroup, let migrationReference = group.migrationReference, migrationReference.peerId == peerId {
|
|
if let additionalCategories = stats.media[group.id] {
|
|
additionalPeerId = group.id
|
|
categories.merge(additionalCategories, uniquingKeysWith: { lhs, rhs in
|
|
return lhs.merging(rhs, uniquingKeysWith: { lhs, rhs in
|
|
return lhs + rhs
|
|
})
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var sizeIndex: [PeerCacheUsageCategory: (Bool, Int64)] = [:]
|
|
let validCategories: [PeerCacheUsageCategory] = [.image, .video, .audio, .file]
|
|
|
|
var totalSize: Int64 = 0
|
|
|
|
for categoryId in validCategories {
|
|
if let media = categories[categoryId] {
|
|
var categorySize: Int64 = 0
|
|
for (_, size) in media {
|
|
categorySize += size
|
|
}
|
|
sizeIndex[categoryId] = (true, categorySize)
|
|
totalSize += categorySize
|
|
}
|
|
}
|
|
|
|
if let statsPromise = statsPromise {
|
|
let clearCategories = sizeIndex.keys.filter({ sizeIndex[$0]!.0 })
|
|
var clearMediaIds = Set<MediaId>()
|
|
|
|
var media = stats.media
|
|
if var categories = media[peerId] {
|
|
for category in clearCategories {
|
|
if let contents = categories[category] {
|
|
for (mediaId, _) in contents {
|
|
clearMediaIds.insert(mediaId)
|
|
}
|
|
}
|
|
categories.removeValue(forKey: category)
|
|
}
|
|
|
|
media[peerId] = categories
|
|
}
|
|
if let additionalPeerId = additionalPeerId {
|
|
if var categories = media[additionalPeerId] {
|
|
for category in clearCategories {
|
|
if let contents = categories[category] {
|
|
for (mediaId, _) in contents {
|
|
clearMediaIds.insert(mediaId)
|
|
}
|
|
}
|
|
categories.removeValue(forKey: category)
|
|
}
|
|
|
|
media[additionalPeerId] = categories
|
|
}
|
|
}
|
|
|
|
var clearResourceIds = Set<MediaResourceId>()
|
|
for id in clearMediaIds {
|
|
if let ids = stats.mediaResourceIds[id] {
|
|
for resourceId in ids {
|
|
clearResourceIds.insert(resourceId)
|
|
}
|
|
}
|
|
}
|
|
|
|
var signal = context.engine.resources.clearCachedMediaResources(mediaResourceIds: clearResourceIds)
|
|
|
|
let resultStats = CacheUsageStats(media: media, mediaResourceIds: stats.mediaResourceIds, peers: stats.peers, otherSize: stats.otherSize, otherPaths: stats.otherPaths, cacheSize: stats.cacheSize, tempPaths: stats.tempPaths, tempSize: stats.tempSize, immutableSize: stats.immutableSize)
|
|
|
|
var cancelImpl: (() -> Void)?
|
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
|
let progressSignal = Signal<Never, NoError> { subscriber in
|
|
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
|
cancelImpl?()
|
|
}))
|
|
presentControllerImpl?(controller, .window(.root), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
|
return ActionDisposable { [weak controller] in
|
|
Queue.mainQueue().async() {
|
|
controller?.dismiss()
|
|
}
|
|
}
|
|
}
|
|
|> runOn(Queue.mainQueue())
|
|
|> delay(0.15, queue: Queue.mainQueue())
|
|
let progressDisposable = progressSignal.start()
|
|
|
|
signal = signal
|
|
|> afterDisposed {
|
|
Queue.mainQueue().async {
|
|
progressDisposable.dispose()
|
|
}
|
|
}
|
|
cancelImpl = {
|
|
clearDisposable.set(nil)
|
|
resetStats()
|
|
}
|
|
clearDisposable.set((signal
|
|
|> deliverOnMainQueue).start(completed: {
|
|
statsPromise.set(.single(.result(resultStats)))
|
|
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(totalSize, formatting: DataSizeStringFormatting(presentationData: presentationData)))", stringForDeviceType()).string), elevatedLayout: false, action: { _ in return false }), .current, nil)
|
|
}))
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
updateState { state in
|
|
var state = state
|
|
state.peerIdWithRevealedOptions = nil
|
|
return state
|
|
}
|
|
}, setPeerIdWithRevealedOptions: { peerId, fromPeerId in
|
|
updateState { state in
|
|
if (peerId == nil && fromPeerId == state.peerIdWithRevealedOptions) || (peerId != nil && fromPeerId == nil) {
|
|
var state = state
|
|
state.peerIdWithRevealedOptions = peerId
|
|
return state
|
|
} else {
|
|
return state
|
|
}
|
|
}
|
|
}, openCategoryMenu: { category in
|
|
let mappedCategory: CacheStorageSettings.PeerStorageCategory
|
|
switch category {
|
|
case .privateChats:
|
|
mappedCategory = .privateChats
|
|
case .groups:
|
|
mappedCategory = .groups
|
|
case .channels:
|
|
mappedCategory = .channels
|
|
}
|
|
|
|
let viewKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.accountSpecificCacheStorageSettings]))
|
|
let accountSpecificSettings: Signal<AccountSpecificCacheStorageSettings, NoError> = context.account.postbox.combinedView(keys: [viewKey])
|
|
|> map { views -> AccountSpecificCacheStorageSettings in
|
|
let cacheSettings: AccountSpecificCacheStorageSettings
|
|
if let view = views.views[viewKey] as? PreferencesView, let value = view.values[PreferencesKeys.accountSpecificCacheStorageSettings]?.get(AccountSpecificCacheStorageSettings.self) {
|
|
cacheSettings = value
|
|
} else {
|
|
cacheSettings = AccountSpecificCacheStorageSettings.defaultSettings
|
|
}
|
|
|
|
return cacheSettings
|
|
}
|
|
|> distinctUntilChanged
|
|
|
|
let peerExceptions: Signal<[(peer: FoundPeer, value: Int32)], NoError> = accountSpecificSettings
|
|
|> mapToSignal { accountSpecificSettings -> Signal<[(peer: FoundPeer, value: Int32)], NoError> in
|
|
return context.account.postbox.transaction { transaction -> [(peer: FoundPeer, value: Int32)] in
|
|
var result: [(peer: FoundPeer, value: Int32)] = []
|
|
|
|
for item in accountSpecificSettings.peerStorageTimeoutExceptions {
|
|
let peerId = item.key
|
|
let value = item.value
|
|
|
|
guard let peer = transaction.getPeer(peerId) else {
|
|
continue
|
|
}
|
|
let peerCategory: CacheStorageSettings.PeerStorageCategory
|
|
var subscriberCount: Int32?
|
|
if peer is TelegramUser {
|
|
peerCategory = .privateChats
|
|
} else if peer is TelegramGroup {
|
|
peerCategory = .groups
|
|
|
|
if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedGroupData {
|
|
subscriberCount = (cachedData.participants?.participants.count).flatMap(Int32.init)
|
|
}
|
|
} else if let channel = peer as? TelegramChannel {
|
|
if case .group = channel.info {
|
|
peerCategory = .groups
|
|
} else {
|
|
peerCategory = .channels
|
|
}
|
|
if peerCategory == mappedCategory {
|
|
if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData {
|
|
subscriberCount = cachedData.participantsSummary.memberCount
|
|
}
|
|
}
|
|
} else {
|
|
continue
|
|
}
|
|
|
|
if peerCategory != mappedCategory {
|
|
continue
|
|
}
|
|
|
|
result.append((peer: FoundPeer(peer: peer, subscribers: subscriberCount), value: value))
|
|
}
|
|
|
|
return result.sorted(by: { lhs, rhs in
|
|
if lhs.value != rhs.value {
|
|
return lhs.value < rhs.value
|
|
}
|
|
return lhs.peer.peer.debugDisplayTitle < rhs.peer.peer.debugDisplayTitle
|
|
})
|
|
}
|
|
}
|
|
|
|
let _ = (combineLatest(
|
|
cacheSettingsPromise.get() |> take(1),
|
|
peerExceptions |> take(1)
|
|
)
|
|
|> deliverOnMainQueue).start(next: { cacheSettings, peerExceptions in
|
|
let currentValue: Int32 = cacheSettings.categoryStorageTimeout[mappedCategory] ?? Int32.max
|
|
|
|
let applyValue: (Int32) -> Void = { value in
|
|
let _ = updateCacheStorageSettingsInteractively(accountManager: context.sharedContext.accountManager, { cacheSettings in
|
|
var cacheSettings = cacheSettings
|
|
cacheSettings.categoryStorageTimeout[mappedCategory] = value
|
|
return cacheSettings
|
|
}).start()
|
|
}
|
|
|
|
var subItems: [ContextMenuItem] = []
|
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
|
|
|
var presetValues: [Int32] = [
|
|
Int32.max,
|
|
31 * 24 * 60 * 60,
|
|
7 * 24 * 60 * 60,
|
|
1 * 24 * 60 * 60
|
|
]
|
|
if currentValue != 0 && !presetValues.contains(currentValue) {
|
|
presetValues.append(currentValue)
|
|
presetValues.sort(by: >)
|
|
}
|
|
|
|
for value in presetValues {
|
|
let optionText: String
|
|
if value == Int32.max {
|
|
optionText = presentationData.strings.ClearCache_Forever
|
|
} else {
|
|
optionText = timeIntervalString(strings: presentationData.strings, value: value)
|
|
}
|
|
subItems.append(.action(ContextMenuActionItem(text: optionText, icon: { theme in
|
|
if currentValue == value {
|
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
|
|
} else {
|
|
return nil
|
|
}
|
|
}, action: { _, f in
|
|
applyValue(value)
|
|
f(.default)
|
|
})))
|
|
}
|
|
|
|
subItems.append(.separator)
|
|
|
|
if peerExceptions.isEmpty {
|
|
let exceptionsText = presentationData.strings.GroupInfo_Permissions_AddException
|
|
subItems.append(.action(ContextMenuActionItem(text: exceptionsText, icon: { theme in
|
|
if case .privateChats = category {
|
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddUser"), color: theme.contextMenu.primaryColor)
|
|
} else {
|
|
return generateTintedImage(image: UIImage(bundleImageName: "Location/CreateGroupIcon"), color: theme.contextMenu.primaryColor)
|
|
}
|
|
}, action: { _, f in
|
|
f(.default)
|
|
|
|
pushControllerImpl?(storageUsageExceptionsScreen(context: context, category: mappedCategory))
|
|
})))
|
|
} else {
|
|
subItems.append(.custom(MultiplePeerAvatarsContextItem(context: context, peers: peerExceptions.prefix(3).map { EnginePeer($0.peer.peer) }, action: { c, _ in
|
|
c.dismiss(completion: {
|
|
|
|
})
|
|
pushControllerImpl?(storageUsageExceptionsScreen(context: context, category: mappedCategory))
|
|
}), false))
|
|
}
|
|
|
|
if let sourceNode = findAutoremoveReferenceNode?(category) {
|
|
let items: Signal<ContextController.Items, NoError> = .single(ContextController.Items(content: .list(subItems)))
|
|
let source: ContextContentSource = .reference(StorageUsageContextReferenceContentSource(sourceView: sourceNode.labelNode.view))
|
|
|
|
let contextController = ContextController(
|
|
account: context.account,
|
|
presentationData: presentationData,
|
|
source: source,
|
|
items: items,
|
|
gesture: nil
|
|
)
|
|
sourceNode.updateHasContextMenu(hasContextMenu: true)
|
|
contextController.dismissed = { [weak sourceNode] in
|
|
sourceNode?.updateHasContextMenu(hasContextMenu: false)
|
|
}
|
|
presentInGlobalOverlay?(contextController)
|
|
}
|
|
})
|
|
})
|
|
|
|
var dismissImpl: (() -> Void)?
|
|
|
|
let signal = combineLatest(context.sharedContext.presentationData, cacheSettingsPromise.get(), accountSpecificCacheSettingsPromise.get(), statsPromise.get(), statePromise.get()) |> deliverOnMainQueue
|
|
|> map { presentationData, cacheSettings, accountSpecificCacheSettings, cacheStats, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
|
let leftNavigationButton = isModal ? ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
|
|
dismissImpl?()
|
|
}) : nil
|
|
|
|
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Cache_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
|
|
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: storageUsageControllerEntries(presentationData: presentationData, cacheSettings: cacheSettings, accountSpecificCacheSettings: accountSpecificCacheSettings, cacheStats: cacheStats, state: state), style: .blocks, emptyStateItem: nil, animateChanges: false)
|
|
|
|
return (controllerState, (listState, arguments))
|
|
} |> afterDisposed {
|
|
actionDisposables.dispose()
|
|
}
|
|
|
|
let controller = ItemListController(context: context, state: signal)
|
|
if isModal {
|
|
controller.navigationPresentation = .modal
|
|
controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
|
|
}
|
|
presentControllerImpl = { [weak controller] c, contextType, a in
|
|
controller?.present(c, in: contextType, with: a)
|
|
}
|
|
pushControllerImpl = { [weak controller] c in
|
|
controller?.push(c)
|
|
}
|
|
presentInGlobalOverlay = { [weak controller] c in
|
|
controller?.presentInGlobalOverlay(c, with: nil)
|
|
}
|
|
findAutoremoveReferenceNode = { [weak controller] category in
|
|
guard let controller else {
|
|
return nil
|
|
}
|
|
|
|
let targetTag: StorageUsageEntryTag = category
|
|
var resultItemNode: ItemListItemNode?
|
|
controller.forEachItemNode { itemNode in
|
|
if let itemNode = itemNode as? ItemListItemNode {
|
|
if let tag = itemNode.tag, tag.isEqual(to: targetTag) {
|
|
resultItemNode = itemNode
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
if let resultItemNode = resultItemNode as? ItemListDisclosureItemNode {
|
|
return resultItemNode
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
dismissImpl = { [weak controller] in
|
|
controller?.dismiss()
|
|
}
|
|
return controller
|
|
}
|
|
|
|
private class StorageUsageClearProgressOverlayNode: ASDisplayNode, ActionSheetGroupOverlayNode {
|
|
private let presentationData: PresentationData
|
|
|
|
private let animationNode: AnimatedStickerNode
|
|
private let progressTextNode: ImmediateTextNode
|
|
private let descriptionTextNode: ImmediateTextNode
|
|
private let progressBackgroundNode: ASDisplayNode
|
|
private let progressForegroundNode: ASDisplayNode
|
|
|
|
private let progressDisposable = MetaDisposable()
|
|
|
|
private var validLayout: CGSize?
|
|
|
|
init(presentationData: PresentationData) {
|
|
self.presentationData = presentationData
|
|
|
|
self.animationNode = DefaultAnimatedStickerNodeImpl()
|
|
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "ClearCache"), width: 256, height: 256, playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
|
|
self.animationNode.visibility = true
|
|
|
|
self.progressTextNode = ImmediateTextNode()
|
|
self.progressTextNode.textAlignment = .center
|
|
|
|
self.descriptionTextNode = ImmediateTextNode()
|
|
self.descriptionTextNode.textAlignment = .center
|
|
self.descriptionTextNode.maximumNumberOfLines = 0
|
|
|
|
self.progressBackgroundNode = ASDisplayNode()
|
|
self.progressBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.controlAccentColor.withMultipliedAlpha(0.2)
|
|
self.progressBackgroundNode.cornerRadius = 3.0
|
|
|
|
self.progressForegroundNode = ASDisplayNode()
|
|
self.progressForegroundNode.backgroundColor = self.presentationData.theme.actionSheet.controlAccentColor
|
|
self.progressForegroundNode.cornerRadius = 3.0
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.animationNode)
|
|
self.addSubnode(self.progressTextNode)
|
|
self.addSubnode(self.descriptionTextNode)
|
|
self.addSubnode(self.progressBackgroundNode)
|
|
self.addSubnode(self.progressForegroundNode)
|
|
}
|
|
|
|
deinit {
|
|
self.progressDisposable.dispose()
|
|
}
|
|
|
|
func setProgressSignal(_ signal: Signal<Float, NoError>) {
|
|
self.progressDisposable.set((signal
|
|
|> deliverOnMainQueue).start(next: { [weak self] progress in
|
|
if let strongSelf = self {
|
|
strongSelf.setProgress(progress)
|
|
}
|
|
}))
|
|
}
|
|
|
|
private var progress: Float = 0.0
|
|
private func setProgress(_ progress: Float) {
|
|
self.progress = progress
|
|
|
|
if let size = self.validLayout {
|
|
self.updateLayout(size: size, transition: .animated(duration: 0.5, curve: .linear))
|
|
}
|
|
}
|
|
|
|
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
|
|
self.validLayout = size
|
|
|
|
let inset: CGFloat = 24.0
|
|
let progressHeight: CGFloat = 6.0
|
|
let spacing: CGFloat = 16.0
|
|
|
|
let progressFrame = CGRect(x: inset, y: size.height - inset - progressHeight, width: size.width - inset * 2.0, height: progressHeight)
|
|
self.progressBackgroundNode.frame = progressFrame
|
|
let progressForegroundFrame = CGRect(x: inset, y: size.height - inset - progressHeight, width: floorToScreenPixels(progressFrame.width * CGFloat(self.progress)), height: progressHeight)
|
|
if !self.progressForegroundNode.frame.origin.x.isZero {
|
|
transition.updateFrame(node: self.progressForegroundNode, frame: progressForegroundFrame, beginWithCurrentState: true)
|
|
} else {
|
|
self.progressForegroundNode.frame = progressForegroundFrame
|
|
}
|
|
|
|
self.descriptionTextNode.attributedText = NSAttributedString(string: self.presentationData.strings.ClearCache_KeepOpenedDescription, font: Font.regular(15.0), textColor: self.presentationData.theme.actionSheet.secondaryTextColor)
|
|
let descriptionTextSize = self.descriptionTextNode.updateLayout(CGSize(width: size.width - inset * 3.0, height: size.height))
|
|
var descriptionTextFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - descriptionTextSize.width) / 2.0), y: progressFrame.minY - spacing - 9.0 - descriptionTextSize.height), size: descriptionTextSize)
|
|
|
|
self.progressTextNode.attributedText = NSAttributedString(string: self.presentationData.strings.ClearCache_Progress(Int(progress * 100.0)).string, font: Font.with(size: 17.0, design: .regular, weight: .semibold, traits: [.monospacedNumbers]), textColor: self.presentationData.theme.actionSheet.primaryTextColor)
|
|
let progressTextSize = self.progressTextNode.updateLayout(size)
|
|
var progressTextFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - progressTextSize.width) / 2.0), y: descriptionTextFrame.minY - spacing - progressTextSize.height), size: progressTextSize)
|
|
|
|
let availableHeight = progressTextFrame.minY
|
|
if availableHeight < 100.0 {
|
|
let offset = availableHeight / 2.0 - spacing
|
|
descriptionTextFrame = descriptionTextFrame.offsetBy(dx: 0.0, dy: -offset)
|
|
progressTextFrame = progressTextFrame.offsetBy(dx: 0.0, dy: -offset)
|
|
self.animationNode.alpha = 0.0
|
|
} else {
|
|
self.animationNode.alpha = 1.0
|
|
}
|
|
|
|
self.progressTextNode.frame = progressTextFrame
|
|
self.descriptionTextNode.frame = descriptionTextFrame
|
|
|
|
let imageSide = min(160.0, availableHeight - 30.0)
|
|
let imageSize = CGSize(width: imageSide, height: imageSide)
|
|
|
|
let animationFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floorToScreenPixels((progressTextFrame.minY - imageSize.height) / 2.0)), size: imageSize)
|
|
self.animationNode.frame = animationFrame
|
|
self.animationNode.updateLayout(size: imageSize)
|
|
}
|
|
}
|
|
|
|
private final class StorageUsageContextReferenceContentSource: ContextReferenceContentSource {
|
|
private let sourceView: UIView
|
|
|
|
init(sourceView: UIView) {
|
|
self.sourceView = sourceView
|
|
}
|
|
|
|
func transitionInfo() -> ContextControllerReferenceViewInfo? {
|
|
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, insets: UIEdgeInsets(top: -4.0, left: 0.0, bottom: -4.0, right: 0.0))
|
|
}
|
|
}
|
|
|
|
final class MultiplePeerAvatarsContextItem: ContextMenuCustomItem {
|
|
fileprivate let context: AccountContext
|
|
fileprivate let peers: [EnginePeer]
|
|
fileprivate let action: (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void
|
|
|
|
init(context: AccountContext, peers: [EnginePeer], action: @escaping (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void) {
|
|
self.context = context
|
|
self.peers = peers
|
|
self.action = action
|
|
}
|
|
|
|
func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode {
|
|
return MultiplePeerAvatarsContextItemNode(presentationData: presentationData, item: self, getController: getController, actionSelected: actionSelected)
|
|
}
|
|
}
|
|
|
|
private final class MultiplePeerAvatarsContextItemNode: ASDisplayNode, ContextMenuCustomNode, ContextActionNodeProtocol {
|
|
private let item: MultiplePeerAvatarsContextItem
|
|
private var presentationData: PresentationData
|
|
private let getController: () -> ContextControllerProtocol?
|
|
private let actionSelected: (ContextMenuActionResult) -> Void
|
|
|
|
private let backgroundNode: ASDisplayNode
|
|
private let highlightedBackgroundNode: ASDisplayNode
|
|
private let textNode: ImmediateTextNode
|
|
|
|
private let avatarsNode: AnimatedAvatarSetNode
|
|
private let avatarsContext: AnimatedAvatarSetContext
|
|
|
|
private let buttonNode: HighlightTrackingButtonNode
|
|
|
|
private var pointerInteraction: PointerInteraction?
|
|
|
|
init(presentationData: PresentationData, item: MultiplePeerAvatarsContextItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) {
|
|
self.item = item
|
|
self.presentationData = presentationData
|
|
self.getController = getController
|
|
self.actionSelected = actionSelected
|
|
|
|
let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize)
|
|
|
|
self.backgroundNode = ASDisplayNode()
|
|
self.backgroundNode.isAccessibilityElement = false
|
|
self.backgroundNode.backgroundColor = presentationData.theme.contextMenu.itemBackgroundColor
|
|
self.highlightedBackgroundNode = ASDisplayNode()
|
|
self.highlightedBackgroundNode.isAccessibilityElement = false
|
|
self.highlightedBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor
|
|
self.highlightedBackgroundNode.alpha = 0.0
|
|
|
|
self.textNode = ImmediateTextNode()
|
|
self.textNode.isAccessibilityElement = false
|
|
self.textNode.isUserInteractionEnabled = false
|
|
self.textNode.displaysAsynchronously = false
|
|
self.textNode.attributedText = NSAttributedString(string: " ", font: textFont, textColor: presentationData.theme.contextMenu.primaryColor)
|
|
self.textNode.maximumNumberOfLines = 1
|
|
|
|
self.buttonNode = HighlightTrackingButtonNode()
|
|
self.buttonNode.isAccessibilityElement = true
|
|
self.buttonNode.accessibilityLabel = presentationData.strings.VoiceChat_StopRecording
|
|
|
|
self.avatarsNode = AnimatedAvatarSetNode()
|
|
self.avatarsContext = AnimatedAvatarSetContext()
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.backgroundNode)
|
|
self.addSubnode(self.highlightedBackgroundNode)
|
|
self.addSubnode(self.textNode)
|
|
self.addSubnode(self.avatarsNode)
|
|
self.addSubnode(self.buttonNode)
|
|
|
|
self.buttonNode.highligthedChanged = { [weak self] highligted in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if highligted {
|
|
strongSelf.highlightedBackgroundNode.alpha = 1.0
|
|
} else {
|
|
strongSelf.highlightedBackgroundNode.alpha = 0.0
|
|
strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
|
}
|
|
}
|
|
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
|
|
self.buttonNode.isUserInteractionEnabled = true
|
|
}
|
|
|
|
deinit {
|
|
}
|
|
|
|
override func didLoad() {
|
|
super.didLoad()
|
|
|
|
self.pointerInteraction = PointerInteraction(node: self.buttonNode, style: .hover, willEnter: { [weak self] in
|
|
if let strongSelf = self {
|
|
strongSelf.highlightedBackgroundNode.alpha = 0.75
|
|
}
|
|
}, willExit: { [weak self] in
|
|
if let strongSelf = self {
|
|
strongSelf.highlightedBackgroundNode.alpha = 0.0
|
|
}
|
|
})
|
|
}
|
|
|
|
private var validLayout: (calculatedWidth: CGFloat, size: CGSize)?
|
|
|
|
func updateLayout(constrainedWidth: CGFloat, constrainedHeight: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) {
|
|
let sideInset: CGFloat = 14.0
|
|
let verticalInset: CGFloat = 12.0
|
|
|
|
let rightTextInset: CGFloat = sideInset + 36.0
|
|
|
|
let calculatedWidth = min(constrainedWidth, 250.0)
|
|
|
|
let textFont = Font.regular(self.presentationData.listsFontSize.baseDisplaySize)
|
|
let text: String = self.presentationData.strings.CacheEvictionMenu_CategoryExceptions(Int32(self.item.peers.count))
|
|
self.textNode.attributedText = NSAttributedString(string: text, font: textFont, textColor: self.presentationData.theme.contextMenu.primaryColor)
|
|
|
|
let textSize = self.textNode.updateLayout(CGSize(width: calculatedWidth - sideInset - rightTextInset, height: .greatestFiniteMagnitude))
|
|
|
|
let combinedTextHeight = textSize.height
|
|
return (CGSize(width: calculatedWidth, height: verticalInset * 2.0 + combinedTextHeight), { size, transition in
|
|
self.validLayout = (calculatedWidth: calculatedWidth, size: size)
|
|
let verticalOrigin = floor((size.height - combinedTextHeight) / 2.0)
|
|
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalOrigin), size: textSize)
|
|
transition.updateFrameAdditive(node: self.textNode, frame: textFrame)
|
|
|
|
let avatarsContent: AnimatedAvatarSetContext.Content
|
|
|
|
let avatarsPeers: [EnginePeer] = self.item.peers
|
|
|
|
avatarsContent = self.avatarsContext.update(peers: avatarsPeers, animated: false)
|
|
|
|
let avatarsSize = self.avatarsNode.update(context: self.item.context, content: avatarsContent, itemSize: CGSize(width: 24.0, height: 24.0), customSpacing: 10.0, animated: false, synchronousLoad: true)
|
|
self.avatarsNode.frame = CGRect(origin: CGPoint(x: size.width - sideInset - 12.0 - avatarsSize.width, y: floor((size.height - avatarsSize.height) / 2.0)), size: avatarsSize)
|
|
|
|
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
|
|
transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
|
|
transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
|
|
})
|
|
}
|
|
|
|
func updateTheme(presentationData: PresentationData) {
|
|
self.presentationData = presentationData
|
|
|
|
self.backgroundNode.backgroundColor = presentationData.theme.contextMenu.itemBackgroundColor
|
|
self.highlightedBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor
|
|
|
|
let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize)
|
|
|
|
self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: textFont, textColor: presentationData.theme.contextMenu.primaryColor)
|
|
}
|
|
|
|
@objc private func buttonPressed() {
|
|
self.performAction()
|
|
}
|
|
|
|
private var actionTemporarilyDisabled: Bool = false
|
|
|
|
func canBeHighlighted() -> Bool {
|
|
return self.isActionEnabled
|
|
}
|
|
|
|
func updateIsHighlighted(isHighlighted: Bool) {
|
|
self.setIsHighlighted(isHighlighted)
|
|
}
|
|
|
|
func performAction() {
|
|
if self.actionTemporarilyDisabled {
|
|
return
|
|
}
|
|
self.actionTemporarilyDisabled = true
|
|
Queue.mainQueue().async { [weak self] in
|
|
self?.actionTemporarilyDisabled = false
|
|
}
|
|
|
|
guard let controller = self.getController() else {
|
|
return
|
|
}
|
|
self.item.action(controller, { [weak self] result in
|
|
self?.actionSelected(result)
|
|
})
|
|
}
|
|
|
|
var isActionEnabled: Bool {
|
|
return true
|
|
}
|
|
|
|
func setIsHighlighted(_ value: Bool) {
|
|
if value {
|
|
self.highlightedBackgroundNode.alpha = 1.0
|
|
} else {
|
|
self.highlightedBackgroundNode.alpha = 0.0
|
|
}
|
|
}
|
|
|
|
func actionNode(at point: CGPoint) -> ContextActionNodeProtocol {
|
|
return self
|
|
}
|
|
}
|