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) } let title: String if peer.peer.id == arguments.context.account.peerId { title = presentationData.strings.DialogList_SavedMessages } else { title = EnginePeer(peer.peer).displayTitle(strings: presentationData.strings, displayOrder: .firstLast) } return ItemListDisclosureItem(presentationData: presentationData, icon: nil, context: arguments.context, iconPeer: EnginePeer(peer.peer), title: title, 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() 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 = 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 == 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(.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 for i in 0 ..< settings.peerStorageTimeoutExceptions.count { if settings.peerStorageTimeoutExceptions[i].key == peerId { settings.peerStorageTimeoutExceptions.remove(at: i) break } } settings.peerStorageTimeoutExceptions.append(AccountSpecificCacheStorageSettings.Value(key: peerId, value: 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 { var found = false for i in 0 ..< settings.peerStorageTimeoutExceptions.count { if settings.peerStorageTimeoutExceptions[i].key == peerId { found = true settings.peerStorageTimeoutExceptions[i] = AccountSpecificCacheStorageSettings.Value(key: peerId, value: value) break } } if !found { settings.peerStorageTimeoutExceptions.append(AccountSpecificCacheStorageSettings.Value(key: peerId, value: value)) } } else { for i in 0 ..< settings.peerStorageTimeoutExceptions.count { if settings.peerStorageTimeoutExceptions[i].key == peerId { settings.peerStorageTimeoutExceptions.remove(at: i) break } } } 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) 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 = .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)) } }