mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-24 07:05:35 +00:00
[WIP] Modern cache eviction
This commit is contained in:
@@ -17,6 +17,8 @@ import DeleteChatPeerActionSheetItem
|
||||
import UndoUI
|
||||
import AnimatedStickerNode
|
||||
import TelegramAnimatedStickerNode
|
||||
import ContextUI
|
||||
import AnimatedAvatarSetNode
|
||||
|
||||
private func totalDiskSpace() -> Int64 {
|
||||
do {
|
||||
@@ -44,8 +46,9 @@ private final class StorageUsageControllerArguments {
|
||||
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) {
|
||||
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
|
||||
@@ -53,6 +56,7 @@ private final class StorageUsageControllerArguments {
|
||||
self.openPeerMedia = openPeerMedia
|
||||
self.clearPeerMedia = clearPeerMedia
|
||||
self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions
|
||||
self.openCategoryMenu = openCategoryMenu
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,8 +67,27 @@ private enum StorageUsageSection: Int32 {
|
||||
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)
|
||||
|
||||
@@ -82,43 +105,49 @@ private enum StorageUsageEntry: ItemListNodeEntry {
|
||||
|
||||
var section: ItemListSectionId {
|
||||
switch self {
|
||||
case .keepMediaHeader, .keepMedia, .keepMediaInfo:
|
||||
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
|
||||
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 .keepMediaInfo:
|
||||
return 2
|
||||
case .maximumSizeHeader:
|
||||
return 3
|
||||
case .maximumSize:
|
||||
return 4
|
||||
case .maximumSizeInfo:
|
||||
return 5
|
||||
case .storageHeader:
|
||||
return 6
|
||||
case .storageUsage:
|
||||
return 7
|
||||
case .collecting:
|
||||
return 8
|
||||
case .clearAll:
|
||||
return 9
|
||||
case .peersHeader:
|
||||
return 10
|
||||
case let .peer(index, _, _, _, _, _, _, _, _):
|
||||
return 11 + index
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,6 +171,24 @@ private enum StorageUsageEntry: ItemListNodeEntry {
|
||||
} 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
|
||||
@@ -235,6 +282,18 @@ private enum StorageUsageEntry: ItemListNodeEntry {
|
||||
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)
|
||||
@@ -279,18 +338,46 @@ private enum StorageUsageEntry: ItemListNodeEntry {
|
||||
}
|
||||
|
||||
private struct StorageUsageState: Equatable {
|
||||
let peerIdWithRevealedOptions: PeerId?
|
||||
|
||||
func withUpdatedPeerIdWithRevealedOptions(_ peerIdWithRevealedOptions: PeerId?) -> StorageUsageState {
|
||||
return StorageUsageState(peerIdWithRevealedOptions: peerIdWithRevealedOptions)
|
||||
}
|
||||
var peerIdWithRevealedOptions: PeerId?
|
||||
}
|
||||
|
||||
private func storageUsageControllerEntries(presentationData: PresentationData, cacheSettings: CacheStorageSettings, cacheStats: CacheUsageStatsResult?, state: StorageUsageState) -> [StorageUsageEntry] {
|
||||
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()))
|
||||
entries.append(.keepMedia(presentationData.theme, presentationData.strings, cacheSettings.defaultCacheStorageTimeout))
|
||||
|
||||
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()))
|
||||
@@ -420,7 +507,24 @@ public func storageUsageController(context: AccountContext, cacheUsagePromise: P
|
||||
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 {
|
||||
@@ -441,11 +545,15 @@ public func storageUsageController(context: AccountContext, cacheUsagePromise: P
|
||||
|
||||
let arguments = StorageUsageControllerArguments(context: context, updateKeepMediaTimeout: { value in
|
||||
let _ = updateCacheStorageSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in
|
||||
return current.withUpdatedDefaultCacheStorageTimeout(value)
|
||||
var current = current
|
||||
current.defaultCacheStorageTimeout = value
|
||||
return current
|
||||
}).start()
|
||||
}, updateMaximumCacheSize: { value in
|
||||
let _ = updateCacheStorageSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in
|
||||
return current.withUpdatedDefaultCacheStorageLimitGigabytes(value)
|
||||
var current = current
|
||||
current.defaultCacheStorageLimitGigabytes = value
|
||||
return current
|
||||
}).start()
|
||||
}, openClearAll: {
|
||||
let _ = (statsPromise.get()
|
||||
@@ -957,28 +1065,197 @@ public func storageUsageController(context: AccountContext, cacheUsagePromise: P
|
||||
})
|
||||
|
||||
updateState { state in
|
||||
return state.withUpdatedPeerIdWithRevealedOptions(nil)
|
||||
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) {
|
||||
return state.withUpdatedPeerIdWithRevealedOptions(peerId)
|
||||
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 (peerId, value) in accountSpecificSettings.peerStorageTimeoutExceptions {
|
||||
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(), statsPromise.get(), statePromise.get()) |> deliverOnMainQueue
|
||||
|> map { presentationData, cacheSettings, cacheStats, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||
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, cacheStats: cacheStats, state: state), style: .blocks, emptyStateItem: nil, 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 {
|
||||
@@ -993,6 +1270,34 @@ public func storageUsageController(context: AccountContext, cacheUsagePromise: P
|
||||
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()
|
||||
}
|
||||
@@ -1110,3 +1415,215 @@ private class StorageUsageClearProgressOverlayNode: ASDisplayNode, ActionSheetGr
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,486 @@
|
||||
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 UndoUI
|
||||
import ContextUI
|
||||
import ItemListPeerActionItem
|
||||
|
||||
private enum StorageUsageExceptionsEntryTag: Hashable, ItemListItemTag {
|
||||
case peer(EnginePeer.Id)
|
||||
|
||||
public func isEqual(to other: ItemListItemTag) -> Bool {
|
||||
if let other = other as? StorageUsageExceptionsEntryTag, self == other {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class StorageUsageExceptionsScreenArguments {
|
||||
let context: AccountContext
|
||||
let openAddException: () -> Void
|
||||
let openPeerMenu: (EnginePeer.Id, Int32) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
openAddException: @escaping () -> Void,
|
||||
openPeerMenu: @escaping (EnginePeer.Id, Int32) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.openAddException = openAddException
|
||||
self.openPeerMenu = openPeerMenu
|
||||
}
|
||||
}
|
||||
|
||||
private enum StorageUsageExceptionsSection: Int32 {
|
||||
case add
|
||||
case items
|
||||
}
|
||||
|
||||
private enum StorageUsageExceptionsEntry: ItemListNodeEntry {
|
||||
enum SortIndex: Equatable, Comparable {
|
||||
case index(Int)
|
||||
case peer(index: Int, peerId: EnginePeer.Id)
|
||||
|
||||
static func <(lhs: SortIndex, rhs: SortIndex) -> Bool {
|
||||
switch lhs {
|
||||
case let .index(index):
|
||||
if case let .index(rhsIndex) = rhs {
|
||||
return index < rhsIndex
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
case let .peer(index, peerId):
|
||||
if case let .peer(rhsIndex, rhsPeerId) = rhs {
|
||||
if index != rhsIndex {
|
||||
return index < rhsIndex
|
||||
} else {
|
||||
return peerId < rhsPeerId
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum StableId: Hashable {
|
||||
case index(Int)
|
||||
case peer(EnginePeer.Id)
|
||||
}
|
||||
|
||||
case addException(String)
|
||||
case exceptionsHeader(String)
|
||||
case peer(index: Int, peer: FoundPeer, value: Int32)
|
||||
|
||||
var section: ItemListSectionId {
|
||||
switch self {
|
||||
case .addException:
|
||||
return StorageUsageExceptionsSection.add.rawValue
|
||||
case .exceptionsHeader, .peer:
|
||||
return StorageUsageExceptionsSection.items.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
var stableId: StableId {
|
||||
switch self {
|
||||
case .addException:
|
||||
return .index(0)
|
||||
case .exceptionsHeader:
|
||||
return .index(1)
|
||||
case let .peer(_, peer, _):
|
||||
return .peer(peer.peer.id)
|
||||
}
|
||||
}
|
||||
|
||||
var sortIndex: SortIndex {
|
||||
switch self {
|
||||
case .addException:
|
||||
return .index(0)
|
||||
case .exceptionsHeader:
|
||||
return .index(1)
|
||||
case let .peer(index, peer, _):
|
||||
return .peer(index: index, peerId: peer.peer.id)
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: StorageUsageExceptionsEntry, rhs: StorageUsageExceptionsEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .addException(text):
|
||||
if case .addException(text) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .exceptionsHeader(text):
|
||||
if case .exceptionsHeader(text) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .peer(index, peer, value):
|
||||
if case .peer(index, peer, value) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func <(lhs: StorageUsageExceptionsEntry, rhs: StorageUsageExceptionsEntry) -> Bool {
|
||||
return lhs.sortIndex < rhs.sortIndex
|
||||
}
|
||||
|
||||
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
|
||||
let arguments = arguments as! StorageUsageExceptionsScreenArguments
|
||||
switch self {
|
||||
case let .addException(text):
|
||||
let icon: UIImage? = PresentationResourcesItemList.createGroupIcon(presentationData.theme)
|
||||
return ItemListPeerActionItem(presentationData: presentationData, icon: icon, title: text, alwaysPlain: false, sectionId: self.section, editing: false, action: {
|
||||
arguments.openAddException()
|
||||
})
|
||||
case let .exceptionsHeader(text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .peer(_, peer, value):
|
||||
var additionalDetailLabel: String?
|
||||
if let subscribers = peer.subscribers {
|
||||
additionalDetailLabel = presentationData.strings.VoiceChat_Panel_Members(subscribers)
|
||||
}
|
||||
let optionText: String
|
||||
if value == Int32.max {
|
||||
optionText = presentationData.strings.ClearCache_Forever
|
||||
} else {
|
||||
optionText = timeIntervalString(strings: presentationData.strings, value: value)
|
||||
}
|
||||
|
||||
return ItemListDisclosureItem(presentationData: presentationData, icon: nil, context: arguments.context, iconPeer: EnginePeer(peer.peer), title: EnginePeer(peer.peer).displayTitle(strings: presentationData.strings, displayOrder: .firstLast), enabled: true, titleFont: .bold, label: optionText, labelStyle: .text, additionalDetailLabel: additionalDetailLabel, sectionId: self.section, style: .blocks, disclosureStyle: .optionArrows, action: {
|
||||
arguments.openPeerMenu(peer.peer.id, value)
|
||||
}, tag: StorageUsageExceptionsEntryTag.peer(peer.peer.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct StorageUsageExceptionsState: Equatable {
|
||||
}
|
||||
|
||||
private func storageUsageExceptionsScreenEntries(
|
||||
presentationData: PresentationData,
|
||||
peerExceptions: [(peer: FoundPeer, value: Int32)],
|
||||
state: StorageUsageExceptionsState
|
||||
) -> [StorageUsageExceptionsEntry] {
|
||||
var entries: [StorageUsageExceptionsEntry] = []
|
||||
|
||||
entries.append(.addException(presentationData.strings.Notification_Exceptions_AddException))
|
||||
|
||||
if !peerExceptions.isEmpty {
|
||||
entries.append(.exceptionsHeader(presentationData.strings.Notifications_CategoryExceptions(Int32(peerExceptions.count)).uppercased()))
|
||||
|
||||
var index = 100
|
||||
for item in peerExceptions {
|
||||
entries.append(.peer(index: index, peer: item.peer, value: item.value))
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
public func storageUsageExceptionsScreen(
|
||||
context: AccountContext,
|
||||
category: CacheStorageSettings.PeerStorageCategory,
|
||||
isModal: Bool = false
|
||||
) -> ViewController {
|
||||
let statePromise = ValuePromise(StorageUsageExceptionsState())
|
||||
let stateValue = Atomic(value: StorageUsageExceptionsState())
|
||||
let updateState: ((StorageUsageExceptionsState) -> StorageUsageExceptionsState) -> Void = { f in
|
||||
statePromise.set(stateValue.modify { f($0) })
|
||||
}
|
||||
let _ = updateState
|
||||
|
||||
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 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 (peerId, value) in accountSpecificSettings.peerStorageTimeoutExceptions {
|
||||
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 == category {
|
||||
if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData {
|
||||
subscriberCount = cachedData.participantsSummary.memberCount
|
||||
}
|
||||
}
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
if peerCategory != category {
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var presentControllerImpl: ((ViewController, PresentationContextType, Any?) -> Void)?
|
||||
let _ = presentControllerImpl
|
||||
var pushControllerImpl: ((ViewController) -> Void)?
|
||||
|
||||
var findPeerReferenceNode: ((EnginePeer.Id) -> ItemListDisclosureItemNode?)?
|
||||
let _ = findPeerReferenceNode
|
||||
|
||||
var presentInGlobalOverlay: ((ViewController) -> Void)?
|
||||
let _ = presentInGlobalOverlay
|
||||
|
||||
let actionDisposables = DisposableSet()
|
||||
|
||||
let clearDisposable = MetaDisposable()
|
||||
actionDisposables.add(clearDisposable)
|
||||
|
||||
let arguments = StorageUsageExceptionsScreenArguments(
|
||||
context: context,
|
||||
openAddException: {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
var filter: ChatListNodePeersFilter = [.excludeRecent, .doNotSearchMessages, .removeSearchHeader]
|
||||
switch category {
|
||||
case .groups:
|
||||
filter.insert(.onlyGroups)
|
||||
case .privateChats:
|
||||
filter.insert(.onlyPrivateChats)
|
||||
filter.insert(.excludeSavedMessages)
|
||||
filter.insert(.excludeSecretChats)
|
||||
case .channels:
|
||||
filter.insert(.onlyChannels)
|
||||
}
|
||||
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: filter, hasContactSelector: false, title: presentationData.strings.Notifications_AddExceptionTitle))
|
||||
controller.peerSelected = { [weak controller] peer, _ in
|
||||
let peerId = peer.id
|
||||
|
||||
let _ = updateAccountSpecificCacheStorageSettingsInteractively(postbox: context.account.postbox, { settings in
|
||||
var settings = settings
|
||||
|
||||
settings.peerStorageTimeoutExceptions[peerId] = Int32.max
|
||||
|
||||
return settings
|
||||
}).start()
|
||||
|
||||
controller?.dismiss()
|
||||
}
|
||||
pushControllerImpl?(controller)
|
||||
},
|
||||
openPeerMenu: { peerId, currentValue in
|
||||
let applyValue: (Int32?) -> Void = { value in
|
||||
let _ = updateAccountSpecificCacheStorageSettingsInteractively(postbox: context.account.postbox, { settings in
|
||||
var settings = settings
|
||||
|
||||
if let value = value {
|
||||
settings.peerStorageTimeoutExceptions[peerId] = value
|
||||
} else {
|
||||
settings.peerStorageTimeoutExceptions.removeValue(forKey: peerId)
|
||||
}
|
||||
|
||||
return settings
|
||||
}).start()
|
||||
}
|
||||
|
||||
var subItems: [ContextMenuItem] = []
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let presetValues: [Int32] = [
|
||||
Int32.max,
|
||||
31 * 24 * 60 * 60,
|
||||
7 * 24 * 60 * 60,
|
||||
1 * 24 * 60 * 60
|
||||
]
|
||||
|
||||
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)
|
||||
//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
|
||||
f(.default)
|
||||
|
||||
applyValue(nil)
|
||||
})))
|
||||
|
||||
if let sourceNode = findPeerReferenceNode?(peerId) {
|
||||
let items: Signal<ContextController.Items, NoError> = .single(ContextController.Items(content: .list(subItems)))
|
||||
let source: ContextContentSource = .reference(StorageUsageExceptionsContextReferenceContentSource(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)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
let _ = cacheSettingsPromise
|
||||
|
||||
var dismissImpl: (() -> Void)?
|
||||
|
||||
let signal = combineLatest(queue: .mainQueue(),
|
||||
context.sharedContext.presentationData,
|
||||
peerExceptions,
|
||||
statePromise.get()
|
||||
)
|
||||
|> deliverOnMainQueue
|
||||
|> map { presentationData, peerExceptions, 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.Notifications_ExceptionsTitle), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
|
||||
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: storageUsageExceptionsScreenEntries(presentationData: presentationData, peerExceptions: peerExceptions, 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)
|
||||
}
|
||||
findPeerReferenceNode = { [weak controller] peerId in
|
||||
guard let controller else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let targetTag: StorageUsageExceptionsEntryTag = .peer(peerId)
|
||||
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 final class StorageUsageExceptionsContextReferenceContentSource: 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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user