Swiftgram/submodules/SettingsUI/Sources/Data and Storage/SaveIncomingMediaController.swift
2023-01-13 21:14:45 +04:00

733 lines
31 KiB
Swift

import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AccountContext
import ItemListPeerActionItem
import ItemListAvatarAndNameInfoItem
import ItemListPeerItem
private enum MediaType {
case photo
case video
}
private final class SaveIncomingMediaControllerArguments {
let context: AccountContext
let toggle: (MediaType) -> Void
let updateMaximumVideoSize: (Int64) -> Void
let openAddException: () -> Void
let openPeerMenu: (EnginePeer) -> Void
let setPeerIdWithRevealedOptions: (EnginePeer.Id?, EnginePeer.Id?) -> Void
let deletePeer: (EnginePeer.Id) -> Void
let deleteAllExceptions: () -> Void
init(context: AccountContext, toggle: @escaping (MediaType) -> Void, updateMaximumVideoSize: @escaping (Int64) -> Void, openAddException: @escaping () -> Void, openPeerMenu: @escaping (EnginePeer) -> Void, setPeerIdWithRevealedOptions: @escaping (EnginePeer.Id?, EnginePeer.Id?) -> Void, deletePeer: @escaping (EnginePeer.Id) -> Void, deleteAllExceptions: @escaping () -> Void) {
self.context = context
self.toggle = toggle
self.updateMaximumVideoSize = updateMaximumVideoSize
self.openAddException = openAddException
self.openPeerMenu = openPeerMenu
self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions
self.deletePeer = deletePeer
self.deleteAllExceptions = deleteAllExceptions
}
}
enum SaveIncomingMediaSection: ItemListSectionId {
case peer
case mediaTypes
case videoSize
case exceptions
case deleteAllExceptions
}
private enum SaveIncomingMediaEntry: ItemListNodeEntry {
enum StableId: Hashable {
case peer
case typesHeader
case typePhotos
case typeVideos
case typesInfo
case videoSizeHeader
case videoSize
case videoInfo
case exceptionsHeader
case addException
case exceptionItem(EnginePeer.Id)
case deleteAllExceptions
}
case peer(peer: EnginePeer, presence: EnginePeer.Presence?)
case typesHeader(String)
case typePhotos(String, Bool)
case typeVideos(String, Bool)
case typesInfo(String)
case videoSizeHeader(String)
case videoSize(decimalSeparator: String, text: String, value: Int64)
case videoInfo(String)
case exceptionsHeader(String)
case addException(String)
case exceptionItem(index: Int, peer: EnginePeer, label: String)
case deleteAllExceptions(String)
var section: ItemListSectionId {
switch self {
case .peer:
return SaveIncomingMediaSection.peer.rawValue
case .typesHeader, .typePhotos, .typeVideos, .typesInfo:
return SaveIncomingMediaSection.mediaTypes.rawValue
case .videoSizeHeader, .videoSize, .videoInfo:
return SaveIncomingMediaSection.videoSize.rawValue
case .exceptionsHeader, .addException, .exceptionItem:
return SaveIncomingMediaSection.exceptions.rawValue
case .deleteAllExceptions:
return SaveIncomingMediaSection.deleteAllExceptions.rawValue
}
}
var stableId: StableId {
switch self {
case .peer:
return .peer
case .typesHeader:
return .typesHeader
case .typePhotos:
return .typePhotos
case .typeVideos:
return .typeVideos
case .typesInfo:
return .typesInfo
case .videoSizeHeader:
return .videoSizeHeader
case .videoSize:
return .videoSize
case .videoInfo:
return .videoInfo
case .exceptionsHeader:
return .exceptionsHeader
case .addException:
return .addException
case let .exceptionItem(_, peer, _):
return .exceptionItem(peer.id)
case .deleteAllExceptions:
return .deleteAllExceptions
}
}
var sortIndex: Int {
switch self {
case .peer:
return 0
case .typesHeader:
return 1
case .typePhotos:
return 2
case .typeVideos:
return 3
case .typesInfo:
return 4
case .videoSizeHeader:
return 5
case .videoSize:
return 6
case .videoInfo:
return 7
case .exceptionsHeader:
return 8
case .addException:
return 9
case let .exceptionItem(index, _, _):
return 100 + index
case .deleteAllExceptions:
return 100000
}
}
static func <(lhs: SaveIncomingMediaEntry, rhs: SaveIncomingMediaEntry) -> Bool {
return lhs.sortIndex < rhs.sortIndex
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! SaveIncomingMediaControllerArguments
switch self {
case let .peer(peer, presence):
return ItemListAvatarAndNameInfoItem(
accountContext: arguments.context,
presentationData: presentationData,
dateTimeFormat: PresentationDateTimeFormat(),
mode: .generic,
peer: peer,
presence: presence,
memberCount: nil,
state: ItemListAvatarAndNameInfoItemState(),
sectionId: self.section,
style: .blocks(withTopInset: true, withExtendedBottomInset: false),
editingNameUpdated: { _ in
},
avatarTapped: {
}
)
case let .typesHeader(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .typePhotos(title, value):
return ItemListSwitchItem(presentationData: presentationData, icon: UIImage(bundleImageName: "Settings/Menu/DataPhotos"), title: title, value: value, sectionId: self.section, style: .blocks, updated: { _ in
arguments.toggle(.photo)
})
case let .typeVideos(title, value):
return ItemListSwitchItem(presentationData: presentationData, icon: UIImage(bundleImageName: "Settings/Menu/DataVideo"), title: title, value: value, sectionId: self.section, style: .blocks, updated: { _ in
arguments.toggle(.video)
})
case let .typesInfo(text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
case let .videoSizeHeader(title):
return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section)
case let .videoSize(decimalSeparator, text, size):
return AutodownloadSizeLimitItem(theme: presentationData.theme, strings: presentationData.strings, decimalSeparator: decimalSeparator, text: text, value: size, range: nil/*2 * 1024 * 1024 ..< (4 * 1024 * 1024 * 1024)*/, sectionId: self.section, updated: { value in
arguments.updateMaximumVideoSize(value)
})
case let .videoInfo(text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
case let .exceptionsHeader(title):
return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section)
case let .addException(title):
let icon: UIImage? = PresentationResourcesItemList.createGroupIcon(presentationData.theme)
return ItemListPeerActionItem(presentationData: presentationData, icon: icon, title: title, alwaysPlain: false, sectionId: self.section, editing: false, action: {
arguments.openAddException()
})
case let .exceptionItem(_, peer, label):
return ItemListPeerItem(
presentationData: presentationData,
dateTimeFormat: PresentationDateTimeFormat(),
nameDisplayOrder: .firstLast,
context: arguments.context,
peer: peer,
height: .generic,
aliasHandling: .threatSelfAsSaved,
nameColor: .primary,
presence: nil,
text: .text(label, .secondary),
label: .none,
editing: ItemListPeerItemEditing(editable: true, editing: false, revealed: false),
revealOptions: ItemListPeerItemRevealOptions(options: [ItemListPeerItemRevealOption(type: .destructive, title: presentationData.strings.Common_Delete, action: {
arguments.deletePeer(peer.id)
})]),
switchValue: nil,
enabled: true,
selectable: true,
sectionId: self.section,
action: {
arguments.openPeerMenu(peer)
},
setPeerIdWithRevealedOptions: { lhs, rhs in
arguments.setPeerIdWithRevealedOptions(lhs, rhs)
},
removePeer: { id in
arguments.deletePeer(id)
}
)
/*return ItemListDisclosureItem(presentationData: presentationData, icon: nil, context: arguments.context, iconPeer: peer, title: peer.displayTitle(strings: presentationData.strings, displayOrder: .firstLast), enabled: true, titleFont: .bold, label: label, labelStyle: .detailText, additionalDetailLabel: nil, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: {
arguments.openPeerMenu(peer)
}, tag: nil)*/
case let .deleteAllExceptions(title):
return ItemListActionItem(presentationData: presentationData, title: title, kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: {
arguments.deleteAllExceptions()
})
}
}
}
private func saveIncomingMediaControllerEntries(presentationData: PresentationData, scope: SaveIncomingMediaScope, state: SaveIncomingMediaControllerState, peer: EnginePeer?, peerPresence: EnginePeer.Presence?, configuration: MediaAutoSaveConfiguration, exceptions: [MediaAutoSaveSettings.ExceptionItem], autosaveExceptionPeers: [EnginePeer.Id: EnginePeer?]) -> [SaveIncomingMediaEntry] {
var entries: [SaveIncomingMediaEntry] = []
if case .peer = scope, let peer {
entries.append(.peer(peer: peer, presence: peerPresence))
}
entries.append(.typesHeader("SAVE TO CAMERA ROLL"))
//TODO:localize
entries.append(.typePhotos("Photos", configuration.photo))
entries.append(.typeVideos("Videos", configuration.video))
entries.append(.typesInfo("Automatically save all new media from private chats to your Cameral Roll."))
if configuration.video {
let sizeText: String
if configuration.maximumVideoSize == Int64.max {
sizeText = autodownloadDataSizeString(1536 * 1024 * 1024, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
} else {
sizeText = autodownloadDataSizeString(configuration.maximumVideoSize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
}
let text = presentationData.strings.AutoDownloadSettings_UpTo(sizeText).string
entries.append(.videoSizeHeader("MAXIMUM VIDEO SIZE"))
entries.append(.videoSize(decimalSeparator: presentationData.dateTimeFormat.decimalSeparator, text: text, value: configuration.maximumVideoSize))
entries.append(.videoInfo("All downloaded videos in private chats less than 100 MB will be saved to Cameral Roll."))
}
if case let .peerType(peerType) = scope {
var filteredExceptions: [(EnginePeer, MediaAutoSaveConfiguration)] = []
for exception in exceptions {
guard let maybeExceptionPeer = autosaveExceptionPeers[exception.id], let exceptionPeer = maybeExceptionPeer else {
continue
}
let peerTypeValue: AutomaticSaveIncomingPeerType
switch exceptionPeer {
case .user, .secretChat:
peerTypeValue = .privateChats
case .legacyGroup:
peerTypeValue = .groups
case let .channel(channel):
if case .broadcast = channel.info {
peerTypeValue = .channels
} else {
peerTypeValue = .groups
}
}
if peerTypeValue == peerType {
filteredExceptions.append((exceptionPeer, exception.configuration))
}
}
if filteredExceptions.isEmpty {
//TODO:localize
entries.append(.exceptionsHeader("EXCEPTIONS"))
} else {
entries.append(.exceptionsHeader(presentationData.strings.Notifications_CategoryExceptions(Int32(filteredExceptions.count)).uppercased()))
}
entries.append(.addException("Add Exception"))
var index = 0
for (exceptionPeer, exceptionConfiguration) in filteredExceptions {
var label = ""
if exceptionConfiguration.photo {
if !label.isEmpty {
label.append(", ")
}
label.append("Photos")
} else {
if !label.isEmpty {
label.append(", ")
}
label.append("No Photos")
}
if exceptionConfiguration.video {
if !label.isEmpty {
label.append(", ")
}
label.append("Videos up to \(dataSizeString(Int(configuration.maximumVideoSize), formatting: DataSizeStringFormatting(presentationData: presentationData)))")
} else {
if !label.isEmpty {
label.append(", ")
}
label.append("No Videos")
}
entries.append(.exceptionItem(index: index, peer: exceptionPeer, label: label))
index += 1
}
if !filteredExceptions.isEmpty {
entries.append(.deleteAllExceptions("Delete All Exceptions"))
}
}
return entries
}
enum SaveIncomingMediaScope {
case peer(EnginePeer.Id)
case addPeer(id: EnginePeer.Id, completion: (MediaAutoSaveConfiguration) -> Void)
case peerType(AutomaticSaveIncomingPeerType)
}
private struct SaveIncomingMediaControllerState: Equatable {
var pendingConfiguration: MediaAutoSaveConfiguration = .default
var peerIdWithOptions: EnginePeer.Id?
}
func saveIncomingMediaController(context: AccountContext, scope: SaveIncomingMediaScope) -> ViewController {
let stateValue = Atomic(value: SaveIncomingMediaControllerState())
let statePromise = ValuePromise<SaveIncomingMediaControllerState>(stateValue.with { $0 })
let updateState: ((SaveIncomingMediaControllerState) -> SaveIncomingMediaControllerState) -> Void = { f in
var changed = false
let value = stateValue.modify { current in
let updated = f(current)
if updated != current {
changed = true
}
return updated
}
if changed {
statePromise.set(value)
}
}
var pushController: ((ViewController) -> Void)?
var dismiss: (() -> Void)?
let arguments = SaveIncomingMediaControllerArguments(
context: context,
toggle: { type in
if case .addPeer = scope {
updateState { state in
var state = state
switch type {
case .photo:
state.pendingConfiguration.photo = !state.pendingConfiguration.photo
case .video:
state.pendingConfiguration.video = !state.pendingConfiguration.video
}
return state
}
} else {
let _ = updateMediaAutoSaveSettingsInteractively(account: context.account, { settings in
var settings = settings
switch scope {
case let .peer(peerId):
if let index = settings.exceptions.firstIndex(where: { $0.id == peerId }) {
switch type {
case .photo:
settings.exceptions[index].configuration.photo = !settings.exceptions[index].configuration.photo
case .video:
settings.exceptions[index].configuration.video = !settings.exceptions[index].configuration.video
}
}
case .addPeer:
break
case let .peerType(peerType):
let mappedType: MediaAutoSaveSettings.PeerType
switch peerType {
case .privateChats:
mappedType = .users
case .groups:
mappedType = .groups
case .channels:
mappedType = .channels
}
var current = settings.configurations[mappedType] ?? .default
switch type {
case .photo:
current.photo = !current.photo
case .video:
current.video = !current.video
}
settings.configurations[mappedType] = current
}
return settings
}).start()
}
},
updateMaximumVideoSize: { value in
if case .addPeer = scope {
updateState { state in
var state = state
state.pendingConfiguration.maximumVideoSize = value
return state
}
} else {
let _ = updateMediaAutoSaveSettingsInteractively(account: context.account, { settings in
var settings = settings
switch scope {
case let .peer(peerId):
if let index = settings.exceptions.firstIndex(where: { $0.id == peerId }) {
settings.exceptions[index].configuration.maximumVideoSize = value
}
case .addPeer:
break
case let .peerType(peerType):
let mappedType: MediaAutoSaveSettings.PeerType
switch peerType {
case .privateChats:
mappedType = .users
case .groups:
mappedType = .groups
case .channels:
mappedType = .channels
}
var current = settings.configurations[mappedType] ?? .default
current.maximumVideoSize = value
settings.configurations[mappedType] = current
}
return settings
}).start()
}
},
openAddException: {
guard case let .peerType(peerType) = scope else {
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var filter: ChatListNodePeersFilter = [.excludeRecent, .doNotSearchMessages, .removeSearchHeader]
switch peerType {
case .groups:
filter.insert(.onlyGroups)
case .privateChats:
filter.insert(.onlyPrivateChats)
filter.insert(.excludeSecretChats)
case .channels:
filter.insert(.onlyChannels)
}
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: filter, hasContactSelector: false, title: presentationData.strings.Notifications_AddExceptionTitle))
controller.peerSelected = { [weak controller] peer, _ in
let peerId = peer.id
let preferencesKey: PostboxViewKey = .preferences(keys: Set([ApplicationSpecificPreferencesKeys.mediaAutoSaveSettings]))
let preferences = context.account.postbox.combinedView(keys: [preferencesKey])
|> map { views -> MediaAutoSaveSettings in
guard let view = views.views[preferencesKey] as? PreferencesView else {
return .default
}
return view.values[ApplicationSpecificPreferencesKeys.mediaAutoSaveSettings]?.get(MediaAutoSaveSettings.self) ?? MediaAutoSaveSettings.default
}
let _ = (preferences
|> take(1)
|> deliverOnMainQueue).start(next: { settings in
if settings.exceptions.contains(where: { $0.id == peerId }) {
guard let controller = controller, let navigationController = controller.navigationController as? NavigationController else {
return
}
var controllers = navigationController.viewControllers
controllers = controllers.filter { item in
if item === controller {
return false
}
return true
}
controllers.append(saveIncomingMediaController(context: context, scope: .peer(peerId)))
navigationController.setViewControllers(controllers, animated: true)
} else {
var dismissAll: (() -> Void)?
let exceptionController = saveIncomingMediaController(context: context, scope: .addPeer(id: peerId, completion: { configuration in
let _ = updateMediaAutoSaveSettingsInteractively(account: context.account, { settings in
var settings = settings
settings.exceptions.removeAll(where: { $0.id == peerId })
settings.exceptions.insert(MediaAutoSaveSettings.ExceptionItem(id: peerId, configuration: configuration), at: 0)
return settings
}).start()
dismissAll?()
}))
controller?.push(exceptionController)
dismissAll = { [weak exceptionController] in
guard let exceptionController = exceptionController else {
return
}
guard let navigationController = exceptionController.navigationController as? NavigationController else {
return
}
var controllers = navigationController.viewControllers
controllers = controllers.filter { item in
if item === exceptionController || item === controller {
return false
}
return true
}
navigationController.setViewControllers(controllers, animated: true)
}
}
})
}
pushController?(controller)
},
openPeerMenu: { peer in
pushController?(saveIncomingMediaController(context: context, scope: .peer(peer.id)))
},
setPeerIdWithRevealedOptions: { itemId, fromItemId in
updateState { state in
var state = state
if (itemId == nil && fromItemId == state.peerIdWithOptions) || (itemId != nil && fromItemId == nil) {
state.peerIdWithOptions = itemId
}
return state
}
},
deletePeer: { id in
let _ = updateMediaAutoSaveSettingsInteractively(account: context.account, { settings in
var settings = settings
settings.exceptions.removeAll(where: { $0.id == id })
return settings
}).start()
},
deleteAllExceptions: {
let _ = updateMediaAutoSaveSettingsInteractively(account: context.account, { settings in
var settings = settings
settings.exceptions.removeAll()
return settings
}).start()
}
)
let preferencesKey: PostboxViewKey = .preferences(keys: Set([ApplicationSpecificPreferencesKeys.mediaAutoSaveSettings]))
let preferences = context.account.postbox.combinedView(keys: [preferencesKey])
|> map { views -> MediaAutoSaveSettings in
guard let view = views.views[preferencesKey] as? PreferencesView else {
return .default
}
return view.values[ApplicationSpecificPreferencesKeys.mediaAutoSaveSettings]?.get(MediaAutoSaveSettings.self) ?? MediaAutoSaveSettings.default
}
let peer: Signal<(EnginePeer?, EnginePeer.Presence?), NoError>
switch scope {
case let .peer(id):
peer = context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.Peer(id: id),
TelegramEngine.EngineData.Item.Peer.Presence(id: id)
)
case let .addPeer(id, _):
peer = context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.Peer(id: id),
TelegramEngine.EngineData.Item.Peer.Presence(id: id)
)
default:
peer = .single((nil, nil))
}
let autosaveExceptionPeers: Signal<[EnginePeer.Id: EnginePeer?], NoError> = preferences
|> mapToSignal { mediaAutoSaveSettings -> Signal<[EnginePeer.Id: EnginePeer?], NoError> in
let peerIds = mediaAutoSaveSettings.exceptions.map(\.id)
return context.engine.data.get(EngineDataMap(
peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))
))
}
struct StoredState {
var entryCount: Int
var hasVideo: Bool
}
let previousState = Atomic<StoredState?>(value: nil)
let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, statePromise.get(), preferences, peer, autosaveExceptionPeers)
|> deliverOnMainQueue
|> map { presentationData, state, mediaAutoSaveSettings, peer, autosaveExceptionPeers -> (ItemListControllerState, (ItemListNodeState, Any)) in
var rightButton: ItemListNavigationButton?
switch scope {
case .peer, .addPeer:
rightButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: {
switch scope {
case let .addPeer(_, completion):
let configuration = stateValue.with({ $0 }).pendingConfiguration
completion(configuration)
default:
dismiss?()
}
})
default:
break
}
let configuration: MediaAutoSaveConfiguration
var exceptions: [MediaAutoSaveSettings.ExceptionItem] = []
let title: String
switch scope {
case let .peer(id):
//TODO:localize
if let data = mediaAutoSaveSettings.exceptions.first(where: { $0.id == id }) {
configuration = data.configuration
} else {
configuration = .default
}
title = "Exception"
case .addPeer:
configuration = state.pendingConfiguration
title = "Add Exception"
case let .peerType(peerType):
exceptions = mediaAutoSaveSettings.exceptions
switch peerType {
case .privateChats:
configuration = mediaAutoSaveSettings.configurations[.users] ?? .default
title = "Private Chats"
case .groups:
configuration = mediaAutoSaveSettings.configurations[.groups] ?? .default
title = "Groups"
case .channels:
configuration = mediaAutoSaveSettings.configurations[.channels] ?? .default
title = "Channels"
}
}
let entries = saveIncomingMediaControllerEntries(presentationData: presentationData, scope: scope, state: state, peer: peer.0, peerPresence: peer.1, configuration: configuration, exceptions: exceptions, autosaveExceptionPeers: autosaveExceptionPeers)
var animateChanges = false
let storedState = StoredState(
entryCount: entries.count,
hasVideo: entries.contains(where: { entry in
switch entry {
case .videoSize:
return true
default:
return false
}
})
)
if let previous = previousState.swap(storedState) {
if previous.entryCount > storedState.entryCount || previous.hasVideo != storedState.hasVideo {
animateChanges = true
}
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: nil, rightNavigationButton: rightButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, emptyStateItem: nil, animateChanges: animateChanges)
return (controllerState, (listState, arguments))
}
let controller = ItemListController(context: context, state: signal)
switch scope {
case .peer, .addPeer:
controller.navigationPresentation = .modal
default:
break
}
pushController = { [weak controller] c in
controller?.push(c)
}
dismiss = { [weak controller] in
controller?.dismiss()
}
return controller
}