mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-23 06:35:51 +00:00
[WIP] Modern cache eviction
This commit is contained in:
@@ -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