diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index cd4bba858c..7e9500c499 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -8934,3 +8934,5 @@ Sorry for the inconvenience."; "MediaPicker.VoiceOver.Camera" = "Camera"; "ChatList.ReadAll" = "Read All"; + +"ChatList.ClearSavedMessagesConfirmation" = "Are you sure you want to delete all your saved messages?"; diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index f2ce5cba6a..9c8e096f03 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -2177,6 +2177,18 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController reorderedFilterIdsValue = reorderedFilterIds } + let completion = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.chatListDisplayNode.isReorderingFilters = false + strongSelf.isReorderingTabsValue.set(false) + (strongSelf.parent as? TabBarController)?.updateIsTabBarEnabled(true, transition: .animated(duration: 0.2, curve: .easeInOut)) + strongSelf.searchContentNode?.setIsEnabled(true, animated: true) + if let layout = strongSelf.validLayout { + strongSelf.updateLayout(layout: layout, transition: .animated(duration: 0.2, curve: .easeInOut)) + } + } if let reorderedFilterIds = reorderedFilterIdsValue { let _ = (self.context.engine.peers.updateChatListFiltersInteractively { stateFilters in var updatedFilters: [ChatListFilter] = [] @@ -2199,18 +2211,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return } strongSelf.reloadFilters(firstUpdate: { - guard let strongSelf = self else { - return - } - strongSelf.chatListDisplayNode.isReorderingFilters = false - strongSelf.isReorderingTabsValue.set(false) - (strongSelf.parent as? TabBarController)?.updateIsTabBarEnabled(true, transition: .animated(duration: 0.2, curve: .easeInOut)) - strongSelf.searchContentNode?.setIsEnabled(true, animated: true) - if let layout = strongSelf.validLayout { - strongSelf.updateLayout(layout: layout, transition: .animated(duration: 0.2, curve: .easeInOut)) - } + completion() }) }) + } else { + completion() } } diff --git a/submodules/ChatListUI/Sources/ChatListSelection.swift b/submodules/ChatListUI/Sources/ChatListSelection.swift index 37a39d649b..b7ed5c2aab 100644 --- a/submodules/ChatListUI/Sources/ChatListSelection.swift +++ b/submodules/ChatListUI/Sources/ChatListSelection.swift @@ -32,7 +32,7 @@ func chatListSelectionOptions(context: AccountContext, peerIds: Set, fil return context.engine.data.subscribe(TelegramEngine.EngineData.Item.Messages.TotalReadCounters()) |> map { readCounters -> ChatListSelectionOptions in var hasUnread = false - if readCounters.count(for: .filtered, in: .chats, with: .all) != 0 { + if readCounters.count(for: .raw, in: .chats, with: .all) != 0 { hasUnread = true } return ChatListSelectionOptions(read: .all(enabled: hasUnread), delete: false) diff --git a/submodules/DeleteChatPeerActionSheetItem/Sources/DeleteChatPeerActionSheetItem.swift b/submodules/DeleteChatPeerActionSheetItem/Sources/DeleteChatPeerActionSheetItem.swift index 6fa0341561..90cc87378e 100644 --- a/submodules/DeleteChatPeerActionSheetItem/Sources/DeleteChatPeerActionSheetItem.swift +++ b/submodules/DeleteChatPeerActionSheetItem/Sources/DeleteChatPeerActionSheetItem.swift @@ -129,7 +129,7 @@ private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode { } case let .clearHistory(canClearCache): if peer.id == context.account.peerId { - text = PresentationStrings.FormattedString(string: strings.ChatList_DeleteSavedMessagesConfirmation, ranges: []) + text = PresentationStrings.FormattedString(string: strings.ChatList_ClearSavedMessagesConfirmation, ranges: []) } else if case .user = peer { text = strings.ChatList_ClearChatConfirmation(peer.displayTitle(strings: strings, displayOrder: nameOrder)) } else { diff --git a/submodules/Display/Source/TextNode.swift b/submodules/Display/Source/TextNode.swift index 9af6b7612f..0a79dd2cd7 100644 --- a/submodules/Display/Source/TextNode.swift +++ b/submodules/Display/Source/TextNode.swift @@ -1514,7 +1514,7 @@ open class TextNode: ASDisplayNode { if glyphCount == 2, let font = attributes["NSFont"] as? UIFont, font.fontName.contains("ColorEmoji"), let string = layout.attributedString { let range = CTRunGetStringRange(run) - if range.location < string.length && (range.location + range.length) < string.length { + if range.location < string.length && (range.location + range.length) <= string.length { let substring = string.attributedSubstring(from: NSMakeRange(range.location, range.length)).string let heart = Unicode.Scalar(0x2764)! diff --git a/submodules/SettingsUI/Sources/Data and Storage/NetworkUsageStatsController.swift b/submodules/SettingsUI/Sources/Data and Storage/NetworkUsageStatsController.swift deleted file mode 100644 index aba9c011ec..0000000000 --- a/submodules/SettingsUI/Sources/Data and Storage/NetworkUsageStatsController.swift +++ /dev/null @@ -1,435 +0,0 @@ -import Foundation -import UIKit -import Display -import SwiftSignalKit -import Postbox -import TelegramCore -import TelegramPresentationData -import ItemListUI -import PresentationDataUtils -import AccountContext - -private enum NetworkUsageControllerSection { - case cellular - case wifi -} - -private final class NetworkUsageStatsControllerArguments { - let resetStatistics: (NetworkUsageControllerSection) -> Void - - init(resetStatistics: @escaping (NetworkUsageControllerSection) -> Void) { - self.resetStatistics = resetStatistics - } -} - -private enum NetworkUsageStatsSection: Int32 { - case messages - case image - case video - case audio - case file - case call - case total - case reset -} - -private enum NetworkUsageStatsEntry: ItemListNodeEntry { - case messagesHeader(PresentationTheme, String) - case messagesSent(PresentationTheme, String, String) - case messagesReceived(PresentationTheme, String, String) - - case imageHeader(PresentationTheme, String) - case imageSent(PresentationTheme, String, String) - case imageReceived(PresentationTheme, String, String) - - case videoHeader(PresentationTheme, String) - case videoSent(PresentationTheme, String, String) - case videoReceived(PresentationTheme, String, String) - - case audioHeader(PresentationTheme, String) - case audioSent(PresentationTheme, String, String) - case audioReceived(PresentationTheme, String, String) - - case fileHeader(PresentationTheme, String) - case fileSent(PresentationTheme, String, String) - case fileReceived(PresentationTheme, String, String) - - case callHeader(PresentationTheme, String) - case callSent(PresentationTheme, String, String) - case callReceived(PresentationTheme, String, String) - - case reset(PresentationTheme, NetworkUsageControllerSection, String) - case resetTimestamp(PresentationTheme, String) - - var section: ItemListSectionId { - switch self { - case .messagesHeader, .messagesSent, .messagesReceived: - return NetworkUsageStatsSection.messages.rawValue - case .imageHeader, .imageSent, .imageReceived: - return NetworkUsageStatsSection.image.rawValue - case .videoHeader, .videoSent, .videoReceived: - return NetworkUsageStatsSection.video.rawValue - case .audioHeader, .audioSent, .audioReceived: - return NetworkUsageStatsSection.audio.rawValue - case .fileHeader, .fileSent, .fileReceived: - return NetworkUsageStatsSection.file.rawValue - case .callHeader, .callSent, .callReceived: - return NetworkUsageStatsSection.call.rawValue - case .reset, .resetTimestamp: - return NetworkUsageStatsSection.reset.rawValue - } - } - - var stableId: Int32 { - switch self { - case .messagesHeader: - return 0 - case .messagesSent: - return 1 - case .messagesReceived: - return 2 - case .imageHeader: - return 3 - case .imageSent: - return 4 - case .imageReceived: - return 5 - case .videoHeader: - return 6 - case .videoSent: - return 7 - case .videoReceived: - return 8 - case .audioHeader: - return 9 - case .audioSent: - return 10 - case .audioReceived: - return 11 - case .fileHeader: - return 12 - case .fileSent: - return 13 - case .fileReceived: - return 14 - case .callHeader: - return 15 - case .callSent: - return 16 - case .callReceived: - return 17 - case .reset: - return 18 - case .resetTimestamp: - return 19 - } - } - - static func ==(lhs: NetworkUsageStatsEntry, rhs: NetworkUsageStatsEntry) -> Bool { - switch lhs { - case let .messagesHeader(lhsTheme, lhsText): - if case let .messagesHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .messagesSent(lhsTheme, lhsText, lhsValue): - if case let .messagesSent(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { - return true - } else { - return false - } - case let .messagesReceived(lhsTheme, lhsText, lhsValue): - if case let .messagesReceived(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { - return true - } else { - return false - } - case let .imageHeader(lhsTheme, lhsText): - if case let .imageHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .imageSent(lhsTheme, lhsText, lhsValue): - if case let .imageSent(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { - return true - } else { - return false - } - case let .imageReceived(lhsTheme, lhsText, lhsValue): - if case let .imageReceived(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { - return true - } else { - return false - } - case let .videoHeader(lhsTheme, lhsText): - if case let .videoHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .videoSent(lhsTheme, lhsText, lhsValue): - if case let .videoSent(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { - return true - } else { - return false - } - case let .videoReceived(lhsTheme, lhsText, lhsValue): - if case let .videoReceived(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { - return true - } else { - return false - } - case let .audioHeader(lhsTheme, lhsText): - if case let .audioHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .audioSent(lhsTheme, lhsText, lhsValue): - if case let .audioSent(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { - return true - } else { - return false - } - case let .audioReceived(lhsTheme, lhsText, lhsValue): - if case let .audioReceived(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { - return true - } else { - return false - } - case let .fileHeader(lhsTheme, lhsText): - if case let .fileHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .fileSent(lhsTheme, lhsText, lhsValue): - if case let .fileSent(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { - return true - } else { - return false - } - case let .fileReceived(lhsTheme, lhsText, lhsValue): - if case let .fileReceived(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { - return true - } else { - return false - } - case let .callHeader(lhsTheme, lhsText): - if case let .callHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - case let .callSent(lhsTheme, lhsText, lhsValue): - if case let .callSent(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { - return true - } else { - return false - } - case let .callReceived(lhsTheme, lhsText, lhsValue): - if case let .callReceived(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { - return true - } else { - return false - } - case let .reset(lhsTheme, lhsSection, lhsText): - if case let .reset(rhsTheme, rhsSection, rhsText) = rhs, lhsTheme === rhsTheme, lhsSection == rhsSection, lhsText == rhsText { - return true - } else { - return false - } - case let .resetTimestamp(lhsTheme, lhsText): - if case let .resetTimestamp(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { - return true - } else { - return false - } - } - } - - static func <(lhs: NetworkUsageStatsEntry, rhs: NetworkUsageStatsEntry) -> Bool { - return lhs.stableId < rhs.stableId - } - - func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { - let arguments = arguments as! NetworkUsageStatsControllerArguments - switch self { - case let .messagesHeader(_, text): - return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .messagesSent(_, text, value): - return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) - case let .messagesReceived(_, text, value): - return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) - case let .imageHeader(_, text): - return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .imageSent(_, text, value): - return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) - case let .imageReceived(_, text, value): - return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) - case let .videoHeader(_, text): - return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .videoSent(_, text, value): - return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) - case let .videoReceived(_, text, value): - return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) - case let .audioHeader(_, text): - return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .audioSent(_, text, value): - return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) - case let .audioReceived(_, text, value): - return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) - case let .fileHeader(_, text): - return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .fileSent(_, text, value): - return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) - case let .fileReceived(_, text, value): - return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) - case let .callHeader(_, text): - return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .callSent(_, text, value): - return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) - case let .callReceived(_, text, value): - return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) - case let .reset(_, section, text): - return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { - arguments.resetStatistics(section) - }) - case let .resetTimestamp(_, text): - return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) - } - } -} - -private func networkUsageStatsControllerEntries(presentationData: PresentationData, section: NetworkUsageControllerSection, stats: NetworkUsageStats) -> [NetworkUsageStatsEntry] { - var entries: [NetworkUsageStatsEntry] = [] - - let formatting = DataSizeStringFormatting(presentationData: presentationData) - switch section { - case .cellular: - entries.append(.messagesHeader(presentationData.theme, presentationData.strings.NetworkUsageSettings_GeneralDataSection)) - entries.append(.messagesSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(stats.generic.cellular.outgoing, formatting: formatting))) - entries.append(.messagesReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(stats.generic.cellular.incoming, formatting: formatting))) - - entries.append(.imageHeader(presentationData.theme, presentationData.strings.NetworkUsageSettings_MediaImageDataSection)) - entries.append(.imageSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(stats.image.cellular.outgoing, formatting: formatting))) - entries.append(.imageReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(stats.image.cellular.incoming, formatting: formatting))) - - entries.append(.videoHeader(presentationData.theme, presentationData.strings.NetworkUsageSettings_MediaVideoDataSection)) - entries.append(.videoSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(stats.video.cellular.outgoing, formatting: formatting))) - entries.append(.videoReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(stats.video.cellular.incoming, formatting: formatting))) - - entries.append(.audioHeader(presentationData.theme, presentationData.strings.NetworkUsageSettings_MediaAudioDataSection)) - entries.append(.audioSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(stats.audio.cellular.outgoing, formatting: formatting))) - entries.append(.audioReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(stats.audio.cellular.incoming, formatting: formatting))) - - entries.append(.fileHeader(presentationData.theme, presentationData.strings.NetworkUsageSettings_MediaDocumentDataSection)) - entries.append(.fileSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(stats.file.cellular.outgoing, formatting: formatting))) - entries.append(.fileReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(stats.file.cellular.incoming, formatting: formatting))) - - entries.append(.callHeader(presentationData.theme, presentationData.strings.NetworkUsageSettings_CallDataSection)) - entries.append(.callSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(stats.call.cellular.outgoing, formatting: formatting))) - entries.append(.callReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(stats.call.cellular.incoming, formatting: formatting))) - - entries.append(.reset(presentationData.theme, section, presentationData.strings.NetworkUsageSettings_ResetStats)) - - if stats.resetCellularTimestamp != 0 { - let formatter = DateFormatter() - formatter.dateFormat = "E, d MMM yyyy HH:mm" - let dateStringPlain = formatter.string(from: Date(timeIntervalSince1970: Double(stats.resetCellularTimestamp))) - - entries.append(.resetTimestamp(presentationData.theme, presentationData.strings.NetworkUsageSettings_CellularUsageSince(dateStringPlain).string)) - } - case .wifi: - entries.append(.messagesHeader(presentationData.theme, presentationData.strings.NetworkUsageSettings_GeneralDataSection)) - entries.append(.messagesSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(stats.generic.wifi.outgoing, formatting: formatting))) - entries.append(.messagesReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(stats.generic.wifi.incoming, formatting: formatting))) - - entries.append(.imageHeader(presentationData.theme, presentationData.strings.NetworkUsageSettings_MediaImageDataSection)) - entries.append(.imageSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(stats.image.wifi.outgoing, formatting: formatting))) - entries.append(.imageReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(stats.image.wifi.incoming, formatting: formatting))) - - entries.append(.videoHeader(presentationData.theme, presentationData.strings.NetworkUsageSettings_MediaVideoDataSection)) - entries.append(.videoSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(stats.video.wifi.outgoing, formatting: formatting))) - entries.append(.videoReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(stats.video.wifi.incoming, formatting: formatting))) - - entries.append(.audioHeader(presentationData.theme, presentationData.strings.NetworkUsageSettings_MediaAudioDataSection)) - entries.append(.audioSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(stats.audio.wifi.outgoing, formatting: formatting))) - entries.append(.audioReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(stats.audio.wifi.incoming, formatting: formatting))) - - entries.append(.fileHeader(presentationData.theme, presentationData.strings.NetworkUsageSettings_MediaDocumentDataSection)) - entries.append(.fileSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(stats.file.wifi.outgoing, formatting: formatting))) - entries.append(.fileReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(stats.file.wifi.incoming, formatting: formatting))) - - entries.append(.callHeader(presentationData.theme, presentationData.strings.NetworkUsageSettings_CallDataSection)) - entries.append(.callSent(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesSent, dataSizeString(stats.call.wifi.outgoing, formatting: formatting))) - entries.append(.callReceived(presentationData.theme, presentationData.strings.NetworkUsageSettings_BytesReceived, dataSizeString(stats.call.wifi.incoming, formatting: formatting))) - - entries.append(.reset(presentationData.theme, section, presentationData.strings.NetworkUsageSettings_ResetStats)) - if stats.resetWifiTimestamp != 0 { - let formatter = DateFormatter() - formatter.dateFormat = "E, d MMM yyyy HH:mm" - let dateStringPlain = formatter.string(from: Date(timeIntervalSince1970: Double(stats.resetWifiTimestamp))) - - entries.append(.resetTimestamp(presentationData.theme, presentationData.strings.NetworkUsageSettings_WifiUsageSince(dateStringPlain).string)) - } - } - - return entries -} - -func networkUsageStatsController(context: AccountContext) -> ViewController { - let section = ValuePromise(.cellular) - let stats = Promise() - stats.set(accountNetworkUsageStats(account: context.account, reset: [])) - - var presentControllerImpl: ((ViewController) -> Void)? - - let arguments = NetworkUsageStatsControllerArguments(resetStatistics: { [weak stats] section in - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = ActionSheetController(presentationData: presentationData) - let dismissAction: () -> Void = { [weak controller] in - controller?.dismissAnimated() - } - controller.setItemGroups([ - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.NetworkUsageSettings_ResetStats, color: .destructive, action: { - dismissAction() - - let reset: ResetNetworkUsageStats - switch section { - case .wifi: - reset = .wifi - case .cellular: - reset = .cellular - } - stats?.set(accountNetworkUsageStats(account: context.account, reset: reset)) - }), - ]), - ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) - ]) - presentControllerImpl?(controller) - }) - - let signal = combineLatest(context.sharedContext.presentationData, section.get(), stats.get()) |> deliverOnMainQueue - |> map { presentationData, section, stats -> (ItemListControllerState, (ItemListNodeState, Any)) in - - let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .sectionControl([presentationData.strings.NetworkUsageSettings_Cellular, presentationData.strings.NetworkUsageSettings_Wifi], 0), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: networkUsageStatsControllerEntries(presentationData: presentationData, section: section, stats: stats), style: .blocks, emptyStateItem: nil, animateChanges: false) - - return (controllerState, (listState, arguments)) - } - - let controller = ItemListController(context: context, state: signal) - controller.titleControlValueChanged = { [weak section] index in - section?.set(index == 0 ? .cellular : .wifi) - } - - presentControllerImpl = { [weak controller] c in - controller?.present(c, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - } - - return controller -} diff --git a/submodules/SettingsUI/Sources/Data and Storage/StorageUsageController.swift b/submodules/SettingsUI/Sources/Data and Storage/StorageUsageController.swift deleted file mode 100644 index 222a382743..0000000000 --- a/submodules/SettingsUI/Sources/Data and Storage/StorageUsageController.swift +++ /dev/null @@ -1,1632 +0,0 @@ -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 { - 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? = 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() - 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() - 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 - if let cacheUsagePromise = cacheUsagePromise { - statsPromise = cacheUsagePromise - } else { - statsPromise = Promise() - 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() - - 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() - 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 = context.engine.resources.clearCachedMediaResources(mediaResourceIds: clearResourceIds) - if otherSize.0 { - let removeTempFiles: Signal = 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(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() - - 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() - 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(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() - - 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() - 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 { 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 = 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 = .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) { - 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 - } -} diff --git a/submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift b/submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift index cf57cc61cf..5422f7abe9 100644 --- a/submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift +++ b/submodules/SettingsUI/Sources/DeleteAccountOptionsController.swift @@ -16,6 +16,7 @@ import UrlHandling import AccountUtils import PremiumUI import PasswordSetupUI +import StorageUsageScreen private struct DeleteAccountOptionsArguments { let changePhoneNumber: () -> Void @@ -276,7 +277,9 @@ public func deleteAccountOptionsController(context: AccountContext, navigationCo }, clearCache: { addAppLogEvent(postbox: context.account.postbox, type: "deactivate.options_clear_cache_tap") - pushControllerImpl?(storageUsageController(context: context)) + pushControllerImpl?(StorageUsageScreen(context: context, makeStorageUsageExceptionsScreen: { category in + return storageUsageExceptionsScreen(context: context, category: category) + })) dismissImpl?() }, clearSyncedContacts: { addAppLogEvent(postbox: context.account.postbox, type: "deactivate.options_clear_contacts_tap") diff --git a/submodules/SettingsUI/Sources/LogoutOptionsController.swift b/submodules/SettingsUI/Sources/LogoutOptionsController.swift index 34bcce2f3c..d6750a91a0 100644 --- a/submodules/SettingsUI/Sources/LogoutOptionsController.swift +++ b/submodules/SettingsUI/Sources/LogoutOptionsController.swift @@ -15,6 +15,7 @@ import PresentationDataUtils import UrlHandling import AccountUtils import PremiumUI +import StorageUsageScreen private struct LogoutOptionsItemArguments { let addAccount: () -> Void @@ -181,7 +182,9 @@ public func logoutOptionsController(context: AccountContext, navigationControlle }) dismissImpl?() }, clearCache: { - pushControllerImpl?(storageUsageController(context: context)) + pushControllerImpl?(StorageUsageScreen(context: context, makeStorageUsageExceptionsScreen: { category in + return storageUsageExceptionsScreen(context: context, category: category) + })) dismissImpl?() }, changePhoneNumber: { let introController = PrivacyIntroController(context: context, mode: .changePhoneNumber(phoneNumber), proceedAction: { diff --git a/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift b/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift index 735b3e8a89..bca956bec6 100644 --- a/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift +++ b/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift @@ -20,6 +20,7 @@ import InstantPageCache import NotificationPeerExceptionController import QrCodeUI import PremiumUI +import StorageUsageScreen enum SettingsSearchableItemIcon { case profile @@ -681,16 +682,54 @@ private func dataSearchableItems(context: AccountContext) -> [SettingsSearchable presentDataSettings(context, present, nil) }), SettingsSearchableItem(id: .data(1), title: strings.ChatSettings_Cache, alternate: synonyms(strings.SettingsSearch_Synonyms_Data_Storage_Title), icon: icon, breadcrumbs: [strings.Settings_ChatSettings], present: { context, _, present in - present(.push, storageUsageController(context: context)) + let controller = StorageUsageScreen(context: context, makeStorageUsageExceptionsScreen: { category in + return storageUsageExceptionsScreen(context: context, category: category) + }) + present(.push, controller) }), SettingsSearchableItem(id: .data(2), title: strings.Cache_KeepMedia, alternate: synonyms(strings.SettingsSearch_Synonyms_Data_Storage_KeepMedia), icon: icon, breadcrumbs: [strings.Settings_ChatSettings, strings.ChatSettings_Cache], present: { context, _, present in - present(.push, storageUsageController(context: context)) + let controller = StorageUsageScreen(context: context, makeStorageUsageExceptionsScreen: { category in + return storageUsageExceptionsScreen(context: context, category: category) + }) + present(.push, controller) }), SettingsSearchableItem(id: .data(3), title: strings.Cache_ClearCache, alternate: synonyms(strings.SettingsSearch_Synonyms_Data_Storage_ClearCache), icon: icon, breadcrumbs: [strings.Settings_ChatSettings, strings.ChatSettings_Cache], present: { context, _, present in - present(.push, storageUsageController(context: context)) + let controller = StorageUsageScreen(context: context, makeStorageUsageExceptionsScreen: { category in + return storageUsageExceptionsScreen(context: context, category: category) + }) + present(.push, controller) }), SettingsSearchableItem(id: .data(4), title: strings.NetworkUsageSettings_Title, alternate: synonyms(strings.SettingsSearch_Synonyms_Data_NetworkUsage), icon: icon, breadcrumbs: [strings.Settings_ChatSettings], present: { context, _, present in - present(.push, networkUsageStatsController(context: context)) + let mediaAutoDownloadSettings = context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.automaticMediaDownloadSettings]) + |> map { sharedData -> MediaAutoDownloadSettings in + var automaticMediaDownloadSettings: MediaAutoDownloadSettings + if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.automaticMediaDownloadSettings]?.get(MediaAutoDownloadSettings.self) { + automaticMediaDownloadSettings = value + } else { + automaticMediaDownloadSettings = .defaultSettings + } + return automaticMediaDownloadSettings + } + + let _ = (combineLatest( + accountNetworkUsageStats(account: context.account, reset: []), + mediaAutoDownloadSettings + ) + |> take(1) + |> deliverOnMainQueue).start(next: { stats, mediaAutoDownloadSettings in + var stats = stats + + if stats.resetWifiTimestamp == 0 { + var value = stat() + if stat(context.account.basePath, &value) == 0 { + stats.resetWifiTimestamp = Int32(value.st_ctimespec.tv_sec) + } + } + + present(.push, DataUsageScreen(context: context, stats: stats, mediaAutoDownloadSettings: mediaAutoDownloadSettings, makeAutodownloadSettingsController: { isCellular in + return autodownloadMediaConnectionTypeController(context: context, connectionType: isCellular ? .cellular : .wifi) + })) + }) }), SettingsSearchableItem(id: .data(5), title: strings.ChatSettings_AutoDownloadUsingCellular, alternate: synonyms(strings.SettingsSearch_Synonyms_Data_AutoDownloadUsingCellular), icon: icon, breadcrumbs: [strings.Settings_ChatSettings, strings.ChatSettings_AutoDownloadTitle], present: { context, _, present in present(.push, autodownloadMediaConnectionTypeController(context: context, connectionType: .cellular)) diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 1c77f474d8..c25bc350cf 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -89,6 +89,7 @@ import ChatListHeaderComponent import ChatControllerInteraction import FeaturedStickersScreen import ChatEntityKeyboardInputNode +import StorageUsageScreen #if DEBUG import os.signpost @@ -11704,6 +11705,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self?.beginClearHistory(type: type) } + let context = self.context let _ = (self.context.engine.data.get( TelegramEngine.EngineData.Item.Peer.ParticipantCount(id: peerId), TelegramEngine.EngineData.Item.Peer.CanDeleteHistory(id: peerId) @@ -11890,9 +11892,19 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G default: text = strongSelf.presentationData.strings.ChatList_DeleteForCurrentUser } - items.append(ActionSheetButtonItem(title: text, color: .destructive, action: { [weak actionSheet] in + items.append(ActionSheetButtonItem(title: text, color: .destructive, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() - beginClear(.forLocalPeer) + if mainPeer.id == context.account.peerId, let strongSelf = self { + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationText, actions: [ + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { + }), + TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteSavedMessagesConfirmationAction, action: { + beginClear(.forLocalPeer) + }) + ], parseMarkdown: true), in: .window(.root)) + } else { + beginClear(.forLocalPeer) + } })) } } @@ -12160,7 +12172,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) }) if let strongSelf = self { - let controller = storageUsageController(context: strongSelf.context, isModal: true) + let context = strongSelf.context + let controller = StorageUsageScreen(context: context, makeStorageUsageExceptionsScreen: { category in + return storageUsageExceptionsScreen(context: context, category: category) + }) strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } })) @@ -16974,7 +16989,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G actionSheet?.dismissAnimated() if let strongSelf = self, !presented { presented = true - strongSelf.push(storageUsageController(context: strongSelf.context, isModal: true)) + let context = strongSelf.context + strongSelf.push(StorageUsageScreen(context: context, makeStorageUsageExceptionsScreen: { category in + return storageUsageExceptionsScreen(context: context, category: category) + })) } })) diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index f9c9d4b288..3dcc0b19b8 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -2461,8 +2461,10 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { if textView.inputView !== updatedInputView { textView.inputView = updatedInputView if textView.isFirstResponder { - if self.chatPresentationInterfaceStateRequiresInputFocus(chatPresentationInterfaceState) { - if let validLayout = self.validLayout, let inputHeight = validLayout.0.inputHeight, inputHeight > 100.0 { + if self.chatPresentationInterfaceStateRequiresInputFocus(chatPresentationInterfaceState), let validLayout = self.validLayout { + if case .compact = validLayout.0.metrics.widthClass { + waitForKeyboardLayout = true + } else if let inputHeight = validLayout.0.inputHeight, inputHeight > 100.0 { waitForKeyboardLayout = true } } diff --git a/submodules/TelegramUI/Sources/CheckDiskSpace.swift b/submodules/TelegramUI/Sources/CheckDiskSpace.swift index 135ee38241..593e02ec0e 100644 --- a/submodules/TelegramUI/Sources/CheckDiskSpace.swift +++ b/submodules/TelegramUI/Sources/CheckDiskSpace.swift @@ -5,6 +5,7 @@ import AccountContext import AlertUI import PresentationDataUtils import SettingsUI +import StorageUsageScreen private func totalDiskSpace() -> Int64 { do { @@ -31,7 +32,9 @@ func checkAvailableDiskSpace(context: AccountContext, threshold: Int64 = 100 * 1 let presentationData = context.sharedContext.currentPresentationData.with { $0 } let controller = textAlertController(context: context, title: nil, text: presentationData.strings.Cache_LowDiskSpaceText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { - push(storageUsageController(context: context, isModal: true)) + push(StorageUsageScreen(context: context, makeStorageUsageExceptionsScreen: { category in + return storageUsageExceptionsScreen(context: context, category: category) + })) }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) push(controller) diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift index 61e9109124..471343418a 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift @@ -199,7 +199,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection let rightNavigationButton = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.rightNavigationButtonPressed)) self.rightNavigationButton = rightNavigationButton self.navigationItem.rightBarButtonItem = self.rightNavigationButton - rightNavigationButton.isEnabled = count != 0 || self.params.alwaysEnabled + rightNavigationButton.isEnabled = true //count != 0 || self.params.alwaysEnabled case .channelCreation: self.titleView.title = CounterContollerTitle(title: self.presentationData.strings.GroupInfo_AddParticipantTitle, counter: "") let rightNavigationButton = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.rightNavigationButtonPressed)) diff --git a/submodules/TelegramUI/Sources/OpenUrl.swift b/submodules/TelegramUI/Sources/OpenUrl.swift index 91c303fc4c..33acd873bc 100644 --- a/submodules/TelegramUI/Sources/OpenUrl.swift +++ b/submodules/TelegramUI/Sources/OpenUrl.swift @@ -907,7 +907,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur let _ = (settings |> deliverOnMainQueue).start(next: { settings in if settings.defaultWebBrowser == nil { - if isCompact { + if !"".isEmpty && isCompact { let controller = BrowserScreen(context: context, subject: .webPage(url: parsedUrl.absoluteString)) navigationController?.pushViewController(controller) } else { diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 4d9a33de25..d5f6328059 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -4564,7 +4564,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate canSetupAutoremoveTimeout = true } } else if let user = chatPeer as? TelegramUser { - if user.id != strongSelf.context.account.peerId && user.botInfo == nil { + if user.id != strongSelf.context.account.peerId { canSetupAutoremoveTimeout = true } } else if let channel = chatPeer as? TelegramChannel { diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 7324e9ae08..d5d67f16ee 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1239,8 +1239,9 @@ public final class SharedAccountContextImpl: SharedAccountContext { guard let navigationController = self.mainWindow?.viewController as? NavigationController else { return } - - let controller = storageUsageController(context: context, isModal: true) + let controller = StorageUsageScreen(context: context, makeStorageUsageExceptionsScreen: { category in + return storageUsageExceptionsScreen(context: context, category: category) + }) navigationController.pushViewController(controller) }