Swiftgram/submodules/SettingsUI/Sources/Data and Storage/SaveIncomingMediaController.swift
2024-05-25 02:29:19 +04:00

747 lines
32 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(
itemContext: .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, height: .generic, 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(presentationData.strings.Autosave_TypesSection))
entries.append(.typePhotos(presentationData.strings.Autosave_TypePhoto, configuration.photo))
entries.append(.typeVideos(presentationData.strings.Autosave_TypeVideo, configuration.video))
entries.append(.typesInfo(presentationData.strings.Autosave_TypesInfo))
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(presentationData.strings.Autosave_VideoSizeSection))
entries.append(.videoSize(decimalSeparator: presentationData.dateTimeFormat.decimalSeparator, text: text, value: configuration.maximumVideoSize))
entries.append(.videoInfo(presentationData.strings.Autosave_VideoInfo(sizeText).string))
}
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 {
entries.append(.exceptionsHeader(presentationData.strings.Autosave_ExceptionsSection))
} else {
entries.append(.exceptionsHeader(presentationData.strings.Notifications_CategoryExceptions(Int32(filteredExceptions.count)).uppercased()))
}
entries.append(.addException(presentationData.strings.Autosave_AddException))
var index = 0
for (exceptionPeer, exceptionConfiguration) in filteredExceptions {
var label = ""
if exceptionConfiguration.photo {
if !label.isEmpty {
label.append(", ")
}
label.append(presentationData.strings.Settings_AutosaveMediaPhoto)
} else {
if !label.isEmpty {
label.append(", ")
}
label.append(presentationData.strings.Settings_AutosaveMediaNoPhoto)
}
if exceptionConfiguration.video {
if !label.isEmpty {
label.append(", ")
}
label.append(presentationData.strings.Settings_AutosaveMediaVideo(dataSizeString(Int(exceptionConfiguration.maximumVideoSize), formatting: DataSizeStringFormatting(presentationData: presentationData))).string)
} else {
if !label.isEmpty {
label.append(", ")
}
label.append(presentationData.strings.Settings_AutosaveMediaNoVideo)
}
entries.append(.exceptionItem(index: index, peer: exceptionPeer, label: label))
index += 1
}
if !filteredExceptions.isEmpty {
entries.append(.deleteAllExceptions(presentationData.strings.Autosave_DeleteAllExceptions))
}
}
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 presentControllerImpl: ((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 presentationData = context.sharedContext.currentPresentationData.with { $0 }
let actionSheet = ActionSheetController(presentationData: presentationData)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Autosave_DeleteAllExceptions, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
let _ = updateMediaAutoSaveSettingsInteractively(account: context.account, { settings in
var settings = settings
settings.exceptions.removeAll()
return settings
}).start()
})
]), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
presentControllerImpl?(actionSheet)
}
)
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):
if let data = mediaAutoSaveSettings.exceptions.first(where: { $0.id == id }) {
configuration = data.configuration
} else {
configuration = .default
}
title = presentationData.strings.Autosave_Exception
case .addPeer:
configuration = state.pendingConfiguration
title = presentationData.strings.Autosave_AddException
case let .peerType(peerType):
exceptions = mediaAutoSaveSettings.exceptions
switch peerType {
case .privateChats:
configuration = mediaAutoSaveSettings.configurations[.users] ?? .default
title = presentationData.strings.Notifications_PrivateChats
case .groups:
configuration = mediaAutoSaveSettings.configurations[.groups] ?? .default
title = presentationData.strings.Notifications_GroupChats
case .channels:
configuration = mediaAutoSaveSettings.configurations[.channels] ?? .default
title = presentationData.strings.Notifications_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)
}
presentControllerImpl = { [weak controller] c in
controller?.present(c, in: .window(.root))
}
dismiss = { [weak controller] in
controller?.dismiss()
}
return controller
}