WinterGram/submodules/SettingsUI/Sources/Data and Storage/WinterGramDeletedMessagesController.swift

587 lines
30 KiB
Swift

import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import ItemListPeerActionItem
import ItemListPeerItem
import AccountContext
import TelegramStringFormatting
import UndoUI
import ComponentFlow
import StorageUsageScreen
private func generatePieChartImage(size: CGSize, values: [(color: UIColor, fraction: CGFloat)], theme: PresentationTheme) -> UIImage? {
return generateImage(size, contextGenerator: { targetSize, context in
context.clear(CGRect(origin: .zero, size: targetSize))
let center = CGPoint(x: targetSize.width / 2.0, y: targetSize.height / 2.0)
let outerRadius = min(targetSize.width, targetSize.height) / 2.0 - 2.0
let innerRadius = outerRadius * 0.52
let separatorAngle: CGFloat = 0.03
var startAngle: CGFloat = -CGFloat.pi / 2.0
let total = values.reduce(0.0) { $0 + max(0.0, $1.fraction) }
guard total > 0.0 else {
let emptyColor = theme.list.itemAccentColor.withAlphaComponent(0.25)
context.setFillColor(emptyColor.cgColor)
context.fillEllipse(in: CGRect(x: center.x - outerRadius, y: center.y - outerRadius, width: outerRadius * 2.0, height: outerRadius * 2.0))
context.setFillColor(theme.list.blocksBackgroundColor.cgColor)
context.fillEllipse(in: CGRect(x: center.x - innerRadius, y: center.y - innerRadius, width: innerRadius * 2.0, height: innerRadius * 2.0))
return
}
for (color, fraction) in values {
let rawSweep = (fraction / total) * CGFloat.pi * 2.0
let sweep = max(0.0, rawSweep - separatorAngle)
let endAngle = startAngle + sweep
let path = CGMutablePath()
path.addArc(center: center, radius: outerRadius, startAngle: startAngle, endAngle: endAngle, clockwise: false)
path.addArc(center: center, radius: innerRadius, startAngle: endAngle, endAngle: startAngle, clockwise: true)
path.closeSubpath()
context.addPath(path)
context.setFillColor(color.cgColor)
context.fillPath()
startAngle += rawSweep
}
}, opaque: false)
}
private let categoryColors: [WinterGramDeletedMessageCategory: UIColor] = [
.text: UIColor(rgb: 0x34C759),
.photo: UIColor(rgb: 0x007AFF),
.video: UIColor(rgb: 0xFF2D55),
.voice: UIColor(rgb: 0xFF9500),
.videoMessage: UIColor(rgb: 0xAF52DE),
.music: UIColor(rgb: 0x5856D6),
.sticker: UIColor(rgb: 0xFFCC00),
.other: UIColor(rgb: 0x8E8E93)
]
private func categoryTitle(_ category: WinterGramDeletedMessageCategory, _ strings: PresentationStrings) -> String {
switch category {
case .text:
return strings.WinterGram_DeletedMessages_Text
case .photo:
return strings.WinterGram_DeletedMessages_Photos
case .video:
return strings.WinterGram_DeletedMessages_Videos
case .voice:
return strings.WinterGram_DeletedMessages_Voice
case .videoMessage:
return strings.WinterGram_DeletedMessages_VideoMessages
case .music:
return strings.WinterGram_DeletedMessages_Music
case .sticker:
return strings.WinterGram_DeletedMessages_Stickers
case .other:
return strings.WinterGram_DeletedMessages_Other
}
}
private final class WinterGramDeletedMessagesChartItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData
let chartData: PieChartComponent.ChartData
let totalText: String
let sectionId: ItemListSectionId
init(presentationData: ItemListPresentationData, chartData: PieChartComponent.ChartData, totalText: String, sectionId: ItemListSectionId) {
self.presentationData = presentationData
self.chartData = chartData
self.totalText = totalText
self.sectionId = sectionId
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = WinterGramDeletedMessagesChartItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? WinterGramDeletedMessagesChartItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
private final class WinterGramDeletedMessagesChartItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let chartHost: ComponentHostView<Empty>
private let totalLabelNode: ImmediateTextNode
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.chartHost = ComponentHostView<Empty>()
self.totalLabelNode = ImmediateTextNode()
self.totalLabelNode.displaysAsynchronously = false
self.totalLabelNode.isUserInteractionEnabled = false
super.init(layerBacked: false)
self.addSubnode(self.totalLabelNode)
}
func asyncLayout() -> (_ item: WinterGramDeletedMessagesChartItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
return { item, params, neighbors in
let imageSize = CGSize(width: 240.0, height: 240.0)
let contentSize = CGSize(width: params.width, height: imageSize.height + 24.0)
let insets = itemListNeighborsGroupedInsets(neighbors, params)
let separatorHeight = UIScreenPixel
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
let theme = item.presentationData.theme
return (layout, { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.backgroundNode.backgroundColor = theme.list.itemBlocksBackgroundColor
strongSelf.topStripeNode.backgroundColor = theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = theme.list.itemBlocksSeparatorColor
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = params.leftInset + 16.0
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
if strongSelf.chartHost.superview == nil {
strongSelf.view.addSubview(strongSelf.chartHost)
}
// Keep the centre total label above the pie.
strongSelf.view.bringSubviewToFront(strongSelf.totalLabelNode.view)
let chartSize = strongSelf.chartHost.update(
transition: .immediate,
component: AnyComponent(PieChartComponent(
theme: theme,
strings: item.presentationData.strings,
emptyColor: theme.list.itemAccentColor.withAlphaComponent(0.25),
chartData: item.chartData
)),
environment: {},
containerSize: imageSize
)
let chartFrame = CGRect(origin: CGPoint(x: floor((params.width - chartSize.width) / 2.0), y: 12.0), size: chartSize)
strongSelf.chartHost.frame = chartFrame
// Total size in the donut centre, matching the native Storage Usage screen.
strongSelf.totalLabelNode.attributedText = NSAttributedString(string: item.totalText, font: Font.semibold(20.0), textColor: theme.list.itemPrimaryTextColor)
let totalLabelSize = strongSelf.totalLabelNode.updateLayout(CGSize(width: chartFrame.width, height: 44.0))
strongSelf.totalLabelNode.frame = CGRect(origin: CGPoint(x: chartFrame.minX + floor((chartFrame.width - totalLabelSize.width) / 2.0), y: chartFrame.minY + floor((chartFrame.height - totalLabelSize.height) / 2.0)), size: totalLabelSize)
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}
private enum WinterGramDeletedMessagesControllerSection: Int32 {
case overview
case chart
case categories
case topChats
case delete
}
private enum WinterGramDeletedMessagesControllerEntry: ItemListNodeEntry {
case overviewHeader(PresentationTheme, String)
case overviewInfo(PresentationTheme, String)
case chart(PresentationTheme, PieChartComponent.ChartData, String)
case categoryHeader(PresentationTheme, String)
case category(PresentationTheme, WinterGramDeletedMessageCategory, Int64, Int, Bool, Bool, Double)
case topChatsHeader(PresentationTheme, String)
case topChat(PresentationTheme, EnginePeer, Int, Int64, Int32)
case delete(PresentationTheme, String, Bool)
var section: ItemListSectionId {
switch self {
case .overviewHeader, .overviewInfo:
return WinterGramDeletedMessagesControllerSection.overview.rawValue
case .chart:
return WinterGramDeletedMessagesControllerSection.chart.rawValue
case .categoryHeader, .category:
return WinterGramDeletedMessagesControllerSection.categories.rawValue
case .topChatsHeader, .topChat:
return WinterGramDeletedMessagesControllerSection.topChats.rawValue
case .delete:
return WinterGramDeletedMessagesControllerSection.delete.rawValue
}
}
var stableId: Int32 {
switch self {
case .overviewHeader:
return 0
case .overviewInfo:
return 1
case .chart:
return 2
case .categoryHeader:
return 3
case let .category(_, category, _, _, _, _, _):
return 10 + category.rawValue
case .topChatsHeader:
return 200
case let .topChat(_, _, _, _, index):
return 210 + index
case .delete:
return 1000
}
}
static func ==(lhs: WinterGramDeletedMessagesControllerEntry, rhs: WinterGramDeletedMessagesControllerEntry) -> Bool {
switch lhs {
case let .overviewHeader(lhsTheme, lhsText):
if case let .overviewHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .overviewInfo(lhsTheme, lhsText):
if case let .overviewInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .chart(lhsTheme, lhsData, lhsTotal):
if case let .chart(rhsTheme, rhsData, rhsTotal) = rhs, lhsTheme === rhsTheme, lhsData == rhsData, lhsTotal == rhsTotal {
return true
} else {
return false
}
case let .categoryHeader(lhsTheme, lhsText):
if case let .categoryHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .category(lhsTheme, lhsCategory, lhsSize, lhsCount, lhsChecked, lhsIsLast, lhsFraction):
if case let .category(rhsTheme, rhsCategory, rhsSize, rhsCount, rhsChecked, rhsIsLast, rhsFraction) = rhs, lhsTheme === rhsTheme, lhsCategory == rhsCategory, lhsSize == rhsSize, lhsCount == rhsCount, lhsChecked == rhsChecked, lhsIsLast == rhsIsLast, lhsFraction == rhsFraction {
return true
} else {
return false
}
case let .topChatsHeader(lhsTheme, lhsText):
if case let .topChatsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .topChat(lhsTheme, lhsPeer, lhsCount, lhsSize, lhsIndex):
if case let .topChat(rhsTheme, rhsPeer, rhsCount, rhsSize, rhsIndex) = rhs, lhsTheme === rhsTheme, lhsPeer == rhsPeer, lhsCount == rhsCount, lhsSize == rhsSize, lhsIndex == rhsIndex {
return true
} else {
return false
}
case let .delete(lhsTheme, lhsText, lhsEnabled):
if case let .delete(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled {
return true
} else {
return false
}
}
}
static func <(lhs: WinterGramDeletedMessagesControllerEntry, rhs: WinterGramDeletedMessagesControllerEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! WinterGramDeletedMessagesControllerArguments
switch self {
case let .overviewHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .overviewInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section, style: .blocks)
case let .chart(_, chartData, totalText):
return WinterGramDeletedMessagesChartItem(presentationData: presentationData, chartData: chartData, totalText: totalText, sectionId: self.section)
case let .categoryHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .category(_, category, size, _, checked, isLast, fraction):
let title = categoryTitle(category, presentationData.strings)
return winterGramDeletedMessagesCategoryItem(
presentationData: presentationData,
category: category,
color: categoryColors[category] ?? presentationData.theme.list.itemAccentColor,
title: title,
size: size,
fraction: fraction,
checked: checked,
isLast: isLast,
sectionId: self.section,
toggle: {
arguments.toggleCategory(category)
}
)
case let .topChatsHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .topChat(_, peer, count, size, _):
let sizeFormatting = DataSizeStringFormatting(strings: presentationData.strings, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
let subtitle = "\(count)\(dataSizeString(size, formatting: sizeFormatting))"
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: arguments.context, peer: peer, presence: nil, text: .text(subtitle, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: nil), switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: {
arguments.openChat(peer)
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in })
case let .delete(_, text, enabled):
return ItemListPeerActionItem(presentationData: presentationData, style: .blocks, icon: PresentationResourcesItemList.deleteIconImage(presentationData.theme), title: text, sectionId: self.section, color: enabled ? .destructive : .disabled, editing: false, action: enabled ? {
arguments.deleteSelected()
} : nil)
}
}
}
private func generateFilledCircleImage(diameter: CGFloat, color: UIColor) -> UIImage? {
return generateImage(CGSize(width: diameter, height: diameter), contextGenerator: { size, context in
context.clear(CGRect(origin: .zero, size: size))
context.setFillColor(color.cgColor)
context.fillEllipse(in: CGRect(origin: .zero, size: size))
}, opaque: false)
}
private final class WinterGramDeletedMessagesControllerArguments {
let context: AccountContext
let toggleCategory: (WinterGramDeletedMessageCategory) -> Void
let deleteSelected: () -> Void
let openChat: (EnginePeer) -> Void
init(context: AccountContext, toggleCategory: @escaping (WinterGramDeletedMessageCategory) -> Void, deleteSelected: @escaping () -> Void, openChat: @escaping (EnginePeer) -> Void) {
self.context = context
self.toggleCategory = toggleCategory
self.deleteSelected = deleteSelected
self.openChat = openChat
}
}
private func winterGramDeletedMessagesControllerEntries(stats: WinterGramDeletedMessagesStats, topPeers: [EnginePeer], selectedCategories: Set<WinterGramDeletedMessageCategory>, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, theme: PresentationTheme) -> [WinterGramDeletedMessagesControllerEntry] {
let sizeFormatting = DataSizeStringFormatting(strings: strings, decimalSeparator: dateTimeFormat.decimalSeparator)
var entries: [WinterGramDeletedMessagesControllerEntry] = []
entries.append(.overviewHeader(theme, strings.WinterGram_DeletedMessages_Title.uppercased()))
// Empty state: when nothing has been saved yet, drop the (otherwise empty) pie chart, the category
// checklist and the delete action they read as a "cheap" empty ring and just explain where saved
// deletions come from, matching the calm empty states of the native Storage Usage screen.
if stats.totalCount == 0 {
entries.append(.overviewInfo(theme, strings.WinterGram_DeletedAndEditedMessagesAreKeptLocallyOnThisDeviceOnly))
return entries
}
entries.append(.overviewInfo(theme, "\(strings.WinterGram_DeletedMessages_Total): \(stats.totalCount)\(dataSizeString(stats.totalSize, formatting: sizeFormatting))"))
let chartTotal = stats.categories.reduce(Int64(0)) { $0 + max(0, $1.size) }
var chartItems: [PieChartComponent.ChartData.Item] = []
for stat in stats.categories where stat.size > 0 {
let color = categoryColors[stat.category] ?? theme.list.itemAccentColor
let fraction = chartTotal > 0 ? Double(stat.size) / Double(chartTotal) : 0.0
chartItems.append(PieChartComponent.ChartData.Item(
id: AnyHashable(stat.category),
displayValue: fraction,
displaySize: stat.size,
value: fraction,
color: color,
particle: nil,
title: categoryTitle(stat.category, strings),
mergeable: false,
mergeFactor: 1.0
))
}
let chartTotalText = dataSizeString(chartTotal, formatting: sizeFormatting)
entries.append(.chart(theme, PieChartComponent.ChartData(items: chartItems), chartTotalText))
entries.append(.categoryHeader(theme, strings.WinterGram_DeletedMessages_SelectTypes.uppercased()))
let visibleCategories = stats.categories.filter { $0.count > 0 }
for (index, stat) in visibleCategories.enumerated() {
let fraction = chartTotal > 0 ? Double(stat.size) / Double(chartTotal) : 0.0
entries.append(.category(theme, stat.category, stat.size, stat.count, selectedCategories.contains(stat.category), index == visibleCategories.count - 1, fraction))
}
if !stats.topChats.isEmpty {
entries.append(.topChatsHeader(theme, strings.WinterGram_DeletedMessages_TopChats.uppercased()))
var topChatIndex: Int32 = 0
for stat in stats.topChats {
if let peer = topPeers.first(where: { $0.id == EnginePeer.Id(stat.peerId) }) {
entries.append(.topChat(theme, peer, stat.count, stat.size, topChatIndex))
topChatIndex += 1
}
}
}
return entries
}
public func winterGramDeletedMessagesController(context: AccountContext) -> ViewController {
var presentControllerImpl: ((ViewController, Any?) -> Void)?
let selectedCategories = Atomic<Set<WinterGramDeletedMessageCategory>>(value: Set(WinterGramDeletedMessageCategory.allCases))
let selectedCategoriesPromise = ValuePromise<Set<WinterGramDeletedMessageCategory>>(Set(WinterGramDeletedMessageCategory.allCases), ignoreRepeated: true)
let arguments = WinterGramDeletedMessagesControllerArguments(context: context, toggleCategory: { category in
let selected = selectedCategories.modify { selected in
var selected = selected
if selected.contains(category) {
selected.remove(category)
} else {
selected.insert(category)
}
return selected
}
selectedCategoriesPromise.set(selected)
}, deleteSelected: {
let selected = selectedCategories.with { $0 }
guard !selected.isEmpty else {
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let actionSheet = ActionSheetController(presentationData: presentationData)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetTextItem(title: presentationData.strings.WinterGram_DeletedMessages_ConfirmDelete),
ActionSheetButtonItem(title: presentationData.strings.Common_Delete, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
let _ = (winterGramClearDeletedMessages(postbox: context.account.postbox, categories: selected)
|> deliverOnMainQueue).start(next: { freedSize in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let sizeFormatting = DataSizeStringFormatting(strings: presentationData.strings, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.WinterGram_DeletedMessages_Deleted(dataSizeString(freedSize, formatting: sizeFormatting)).string, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return false }), nil)
selectedCategoriesPromise.set(Set(WinterGramDeletedMessageCategory.allCases))
let _ = selectedCategories.swap(Set(WinterGramDeletedMessageCategory.allCases))
})
})
]), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
presentControllerImpl?(actionSheet, nil)
}, openChat: { peer in
if let navigationController = context.sharedContext.mainWindow?.viewController as? NavigationController {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer)))
}
})
let statsSignal = winterGramDeletedMessagesStats(postbox: context.account.postbox)
let topPeersSignal = statsSignal
|> map { stats -> [EnginePeer.Id] in
return stats.topChats.map { EnginePeer.Id($0.peerId) }
}
|> distinctUntilChanged
|> mapToSignal { peerIds -> Signal<[EnginePeer], NoError> in
guard !peerIds.isEmpty else {
return .single([])
}
return context.engine.data.subscribe(
EngineDataMap(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:)))
)
|> map { peerMap -> [EnginePeer] in
return peerMap.values.compactMap { $0 }
}
}
let signal = combineLatest(queue: .mainQueue(),
context.sharedContext.presentationData,
statsSignal,
selectedCategoriesPromise.get(),
topPeersSignal
)
|> map { presentationData, stats, selectedCategories, topPeers -> (ItemListControllerState, (ItemListNodeState, Any)) in
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.WinterGram_DeletedMessages_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
let canDelete = !selectedCategories.isEmpty
let sizeFormatting = DataSizeStringFormatting(strings: presentationData.strings, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
let selectedSize = stats.categories.reduce(Int64(0)) { partial, stat in
return selectedCategories.contains(stat.category) ? partial + max(0, stat.size) : partial
}
var footerTitle = presentationData.strings.WinterGram_DeletedMessages_DeleteSelected
if canDelete && selectedSize > 0 {
footerTitle += " " + dataSizeString(selectedSize, formatting: sizeFormatting)
}
// No saved deletions no destructive footer (matches the empty state from the entries builder).
let footerItem: WinterGramDeletedMessagesFooterItem? = stats.totalCount == 0 ? nil : WinterGramDeletedMessagesFooterItem(theme: presentationData.theme, title: footerTitle, isEnabled: canDelete, action: {
arguments.deleteSelected()
})
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: winterGramDeletedMessagesControllerEntries(stats: stats, topPeers: topPeers, selectedCategories: selectedCategories, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, theme: presentationData.theme), style: .blocks, footerItem: footerItem, animateChanges: true)
return (controllerState, (listState, arguments))
}
let controller = ItemListController(context: context, state: signal)
presentControllerImpl = { [weak controller] c, a in
controller?.present(c, in: .window(.root), with: a)
}
return controller
}