import Foundation import UIKit import Display import AsyncDisplayKit import Postbox import TelegramCore import SwiftSignalKit import AccountContext import TelegramPresentationData import TelegramUIPreferences import TelegramStringFormatting import MergeLists import Photos import PhotosUI import LegacyComponents import LegacyMediaPickerUI import AttachmentUI import ContextUI import WebSearchUI import SparseItemGrid import UndoUI import PresentationDataUtils import MoreButtonNode import CameraScreen import MediaEditor final class MediaPickerInteraction { let downloadManager: AssetDownloadManager let openMedia: (PHFetchResult, Int, UIImage?) -> Void let openSelectedMedia: (TGMediaSelectableItem, UIImage?) -> Void let openDraft: (MediaEditorDraft, UIImage?) -> Void let toggleSelection: (TGMediaSelectableItem, Bool, Bool) -> Bool let sendSelected: (TGMediaSelectableItem?, Bool, Int32?, Bool, @escaping () -> Void) -> Void let schedule: () -> Void let dismissInput: () -> Void let selectionState: TGMediaSelectionContext? let editingState: TGMediaEditingContext var hiddenMediaId: String? init(downloadManager: AssetDownloadManager, openMedia: @escaping (PHFetchResult, Int, UIImage?) -> Void, openSelectedMedia: @escaping (TGMediaSelectableItem, UIImage?) -> Void, openDraft: @escaping (MediaEditorDraft, UIImage?) -> Void, toggleSelection: @escaping (TGMediaSelectableItem, Bool, Bool) -> Bool, sendSelected: @escaping (TGMediaSelectableItem?, Bool, Int32?, Bool, @escaping () -> Void) -> Void, schedule: @escaping () -> Void, dismissInput: @escaping () -> Void, selectionState: TGMediaSelectionContext?, editingState: TGMediaEditingContext) { self.downloadManager = downloadManager self.openMedia = openMedia self.openSelectedMedia = openSelectedMedia self.openDraft = openDraft self.toggleSelection = toggleSelection self.sendSelected = sendSelected self.schedule = schedule self.dismissInput = dismissInput self.selectionState = selectionState self.editingState = editingState } } private struct MediaPickerGridEntry: Comparable, Identifiable { let stableId: Int let content: MediaPickerGridItemContent let selectable: Bool let stories: Bool static func <(lhs: MediaPickerGridEntry, rhs: MediaPickerGridEntry) -> Bool { return lhs.stableId < rhs.stableId } func item(context: AccountContext, interaction: MediaPickerInteraction, theme: PresentationTheme, strings: PresentationStrings) -> MediaPickerGridItem { return MediaPickerGridItem(content: self.content, interaction: interaction, theme: theme, strings: strings, selectable: self.selectable, enableAnimations: context.sharedContext.energyUsageSettings.fullTranslucency, stories: self.stories) } } private struct MediaPickerGridTransaction { let deletions: [Int] let insertions: [GridNodeInsertItem] let updates: [GridNodeUpdateItem] let scrollToItem: GridNodeScrollToItem? init(previousList: [MediaPickerGridEntry], list: [MediaPickerGridEntry], context: AccountContext, interaction: MediaPickerInteraction, theme: PresentationTheme, strings: PresentationStrings, scrollToItem: GridNodeScrollToItem?) { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: previousList, rightList: list) self.deletions = deleteIndices self.insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(context: context, interaction: interaction, theme: theme, strings: strings), previousIndex: $0.2) } self.updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, interaction: interaction, theme: theme, strings: strings)) } self.scrollToItem = scrollToItem } init(clearList: [MediaPickerGridEntry]) { var deletions: [Int] = [] var i = 0 for _ in clearList { deletions.append(i) i += 1 } self.deletions = deletions self.insertions = [] self.updates = [] self.scrollToItem = nil } } struct Month: Equatable { var packedValue: Int32 init(packedValue: Int32) { self.packedValue = packedValue } init(localTimestamp: Int32) { var time: time_t = time_t(localTimestamp) var timeinfo: tm = tm() gmtime_r(&time, &timeinfo) let year = UInt32(max(timeinfo.tm_year, 0)) let month = UInt32(max(timeinfo.tm_mon, 0)) self.packedValue = Int32(bitPattern: year | (month << 16)) } var year: Int32 { return Int32(bitPattern: (UInt32(bitPattern: self.packedValue) >> 0) & 0xffff) } var month: Int32 { return Int32(bitPattern: (UInt32(bitPattern: self.packedValue) >> 16) & 0xffff) } } private var savedStoriesContentOffset: CGFloat? public final class MediaPickerScreen: ViewController, AttachmentContainable { public enum Subject { public enum Media: Equatable { case image(UIImage) case video(URL) var asset: TGMediaSelectableItem { switch self { case let .image(image): return image case let .video(url): return TGCameraCapturedVideo(url: url) } } var identifier: String { switch self { case let .image(image): return image.uniqueIdentifier case let .video(url): return url.absoluteString } } } public enum AssetsMode: Equatable { case `default` case wallpaper case story case addImage case createSticker } case assets(PHAssetCollection?, AssetsMode) case media([Media]) } private let context: AccountContext private var presentationData: PresentationData private var presentationDataDisposable: Disposable? private let updatedPresentationData: (initial: PresentationData, signal: Signal)? fileprivate var interaction: MediaPickerInteraction? private let peer: EnginePeer? private let isScheduledMessages: Bool private let threadTitle: String? private let chatLocation: ChatLocation? private let bannedSendPhotos: (Int32, Bool)? private let bannedSendVideos: (Int32, Bool)? private let canBoostToUnrestrict: Bool private let subject: Subject private let saveEditedPhotos: Bool private let titleView: MediaPickerTitleView private let moreButtonNode: MoreButtonNode public weak var webSearchController: WebSearchController? public var openCamera: ((TGAttachmentCameraView?) -> Void)? public var presentSchedulePicker: (Bool, @escaping (Int32) -> Void) -> Void = { _, _ in } public var presentTimerPicker: (@escaping (Int32) -> Void) -> Void = { _ in } public var presentWebSearch: (MediaGroupsScreen, Bool) -> Void = { _, _ in } public var getCaptionPanelView: () -> TGCaptionPanelView? = { return nil } public var openBoost: () -> Void = { } public var customSelection: ((MediaPickerScreen, Any) -> Void)? = nil public var createFromScratch: () -> Void = {} public var presentFilePicker: () -> Void = {} private var completed = false public var legacyCompletion: (_ signals: [Any], _ silently: Bool, _ scheduleTime: Int32?, @escaping (String) -> UIView?, @escaping () -> Void) -> Void = { _, _, _, _, _ in } public var requestAttachmentMenuExpansion: () -> Void = { } public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in } public var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in } public var cancelPanGesture: () -> Void = { } public var isContainerPanning: () -> Bool = { return false } public var isContainerExpanded: () -> Bool = { return false } private let selectedCollection = Promise(nil) var dismissAll: () -> Void = { } private class Node: ViewControllerTracingNode, ASGestureRecognizerDelegate { enum DisplayMode { case all case selected } enum State { case noAccess(cameraAccess: AVAuthorizationStatus?) case assets(fetchResult: PHFetchResult?, preload: Bool, drafts: [MediaEditorDraft], mediaAccess: PHAuthorizationStatus, cameraAccess: AVAuthorizationStatus?) case media([Subject.Media]) } private weak var controller: MediaPickerScreen? private var presentationData: PresentationData fileprivate let mediaAssetsContext: MediaAssetsContext private var requestedMediaAccess = false private var requestedCameraAccess = false private let containerNode: ASDisplayNode private let backgroundNode: NavigationBackgroundNode fileprivate let gridNode: GridNode fileprivate var cameraView: TGAttachmentCameraView? private var cameraActivateAreaNode: AccessibilityAreaNode private var placeholderNode: MediaPickerPlaceholderNode? private var manageNode: MediaPickerManageNode? private var scrollingArea: SparseItemGridScrollingArea private var isFastScrolling = false private var selectionNode: MediaPickerSelectedListNode? private var nextStableId: Int = 1 private var currentEntries: [MediaPickerGridEntry] = [] private var enqueuedTransactions: [MediaPickerGridTransaction] = [] private var state: State? private var preloadPromise = ValuePromise(true) private var itemsDisposable: Disposable? private var selectionChangedDisposable: Disposable? private var itemsDimensionsUpdatedDisposable: Disposable? private var hiddenMediaDisposable: Disposable? fileprivate let hiddenMediaId = Promise(nil) private var selectionGesture: MediaPickerGridSelectionGesture? private var fastScrollContentOffset = ValuePromise(ignoreRepeated: true) private var fastScrollDisposable: Disposable? private var didSetReady = false private let _ready = Promise() var ready: Promise { return self._ready } fileprivate var isSuspended = false private var hasGallery = false private var isCameraPreviewVisible = true private var validLayout: (ContainerViewLayout, CGFloat)? init(controller: MediaPickerScreen) { self.controller = controller self.presentationData = controller.presentationData var assetType: PHAssetMediaType? if case let .assets(_, mode) = controller.subject, [.wallpaper, .addImage, .createSticker].contains(mode) { assetType = .image } let mediaAssetsContext = MediaAssetsContext(assetType: assetType) self.mediaAssetsContext = mediaAssetsContext self.containerNode = ASDisplayNode() self.backgroundNode = NavigationBackgroundNode(color: self.presentationData.theme.rootController.tabBar.backgroundColor) self.backgroundNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor self.gridNode = GridNode() self.scrollingArea = SparseItemGridScrollingArea() self.cameraActivateAreaNode = AccessibilityAreaNode() self.cameraActivateAreaNode.accessibilityLabel = "Camera" self.cameraActivateAreaNode.accessibilityTraits = [.button] super.init() if case .assets(nil, .default) = controller.subject { } else if case .assets(nil, .story) = controller.subject, (savedStoriesContentOffset ?? 0.0).isZero { } else { self.preloadPromise.set(false) } self.addSubnode(self.containerNode) self.containerNode.addSubnode(self.backgroundNode) self.containerNode.addSubnode(self.gridNode) self.containerNode.addSubnode(self.scrollingArea) let selectedCollection = controller.selectedCollection.get() let preloadPromise = self.preloadPromise let updatedState: Signal switch controller.subject { case let .assets(collection, mode): let drafts: Signal<[MediaEditorDraft], NoError> if mode == .story { drafts = storyDrafts(engine: controller.context.engine) } else { drafts = .single([]) } updatedState = combineLatest(mediaAssetsContext.mediaAccess(), mediaAssetsContext.cameraAccess()) |> mapToSignal { mediaAccess, cameraAccess -> Signal in if case .notDetermined = mediaAccess { return .single(.assets(fetchResult: nil, preload: false, drafts: [], mediaAccess: mediaAccess, cameraAccess: cameraAccess)) } else if [.restricted, .denied].contains(mediaAccess) { return .single(.noAccess(cameraAccess: cameraAccess)) } else { return selectedCollection |> mapToSignal { selectedCollection in let collection = selectedCollection ?? collection if let collection { return combineLatest(mediaAssetsContext.fetchAssets(collection), preloadPromise.get()) |> map { fetchResult, preload in return .assets(fetchResult: fetchResult, preload: preload, drafts: [], mediaAccess: mediaAccess, cameraAccess: selectedCollection != nil ? nil : cameraAccess) } } else { return combineLatest(mediaAssetsContext.recentAssets(), preloadPromise.get(), drafts) |> map { fetchResult, preload, drafts in return .assets(fetchResult: fetchResult, preload: preload, drafts: drafts, mediaAccess: mediaAccess, cameraAccess: cameraAccess) } } } } } case let .media(media): updatedState = .single(.media(media)) } self.itemsDisposable = (updatedState |> deliverOnMainQueue).start(next: { [weak self] state in guard let strongSelf = self else { return } strongSelf.updateState(state) }) self.gridNode.scrollingInitiated = { [weak self] in self?.dismissInput() } self.gridNode.visibleContentOffsetChanged = { [weak self] _ in self?.updateNavigation(transition: .immediate) } self.hiddenMediaDisposable = (self.hiddenMediaId.get() |> deliverOnMainQueue).start(next: { [weak self] id in if let strongSelf = self { strongSelf.controller?.interaction?.hiddenMediaId = id strongSelf.gridNode.forEachItemNode { itemNode in if let itemNode = itemNode as? MediaPickerGridItemNode { itemNode.updateHiddenMedia() } } strongSelf.selectionNode?.updateHiddenMedia() } }) if let selectionState = self.controller?.interaction?.selectionState { func selectionChangedSignal(selectionState: TGMediaSelectionContext) -> Signal { return Signal { subscriber in let disposable = selectionState.selectionChangedSignal()?.start(next: { next in if let next = next as? TGMediaSelectionChange { subscriber.putNext(next.animated) } }, completed: {}) return ActionDisposable { disposable?.dispose() } } } self.selectionChangedDisposable = (selectionChangedSignal(selectionState: selectionState) |> deliverOnMainQueue).start(next: { [weak self] animated in if let strongSelf = self { strongSelf.updateSelectionState(animated: animated) } }) } if let editingState = self.controller?.interaction?.editingState { func itemsDimensionsUpdatedSignal(editingState: TGMediaEditingContext) -> Signal { return Signal { subscriber in let disposable = editingState.cropAdjustmentsUpdatedSignal()?.start(next: { next in subscriber.putNext(Void()) }, completed: {}) return ActionDisposable { disposable?.dispose() } } } self.itemsDimensionsUpdatedDisposable = (itemsDimensionsUpdatedSignal(editingState: editingState) |> deliverOnMainQueue).start(next: { [weak self] _ in if let strongSelf = self { strongSelf.updateSelectionState() } }) } } deinit { self.itemsDisposable?.dispose() self.hiddenMediaDisposable?.dispose() self.selectionChangedDisposable?.dispose() self.itemsDimensionsUpdatedDisposable?.dispose() self.fastScrollDisposable?.dispose() self.currentAssetDownloadDisposable.dispose() } override func didLoad() { super.didLoad() guard let controller = self.controller else { return } self.gridNode.scrollView.alwaysBounceVertical = true self.gridNode.scrollView.showsVerticalScrollIndicator = false if case let .assets(_, mode) = controller.subject, [.wallpaper, .story, .addImage, .createSticker].contains(mode) { } else { let selectionGesture = MediaPickerGridSelectionGesture() selectionGesture.delegate = self.wrappedGestureRecognizerDelegate selectionGesture.began = { [weak self] in self?.controller?.cancelPanGesture() } selectionGesture.updateIsScrollEnabled = { [weak self] isEnabled in self?.gridNode.scrollView.isScrollEnabled = isEnabled } selectionGesture.itemAt = { [weak self] point in if let strongSelf = self, let itemNode = strongSelf.gridNode.itemNodeAtPoint(point) as? MediaPickerGridItemNode, let selectableItem = itemNode.selectableItem { return (selectableItem, strongSelf.controller?.interaction?.selectionState?.isIdentifierSelected(selectableItem.uniqueIdentifier) ?? false) } else { return nil } } selectionGesture.updateSelection = { [weak self] asset, selected in if let strongSelf = self { strongSelf.controller?.interaction?.selectionState?.setItem(asset, selected: selected, animated: true, sender: nil) } } selectionGesture.sideInset = 44.0 self.gridNode.view.addGestureRecognizer(selectionGesture) self.selectionGesture = selectionGesture } if let controller = self.controller, case let .assets(collection, _) = controller.subject, collection != nil { self.gridNode.view.interactiveTransitionGestureRecognizerTest = { point -> Bool in return point.x > 44.0 } self.selectionGesture?.sideInset = 44.0 } self.scrollingArea.beginScrolling = { [weak self] in guard let strongSelf = self else { return nil } strongSelf.controller?.requestAttachmentMenuExpansion() strongSelf.isFastScrolling = true return strongSelf.gridNode.scrollView } self.scrollingArea.finishedScrolling = { [weak self] in guard let strongSelf = self else { return } strongSelf.isFastScrolling = false } self.scrollingArea.setContentOffset = { [weak self] offset in guard let strongSelf = self else { return } Queue.concurrentDefaultQueue().async { strongSelf.fastScrollContentOffset.set(offset) } } self.gridNode.visibleItemsUpdated = { [weak self] _ in self?.updateScrollingArea() if let self, let cameraView = self.cameraView { self.isCameraPreviewVisible = self.gridNode.scrollView.bounds.intersects(cameraView.frame) self.updateIsCameraActive() } } self.updateScrollingArea() let throttledContentOffsetSignal = self.fastScrollContentOffset.get() |> mapToThrottled { next -> Signal in return .single(next) |> then(.complete() |> delay(0.05, queue: Queue.concurrentDefaultQueue())) } self.fastScrollDisposable = (throttledContentOffsetSignal |> deliverOnMainQueue).start(next: { [weak self] contentOffset in if let self { self.gridNode.scrollView.setContentOffset(contentOffset, animated: false) } }) if let controller = self.controller, case .assets(nil, .default) = controller.subject { let enableAnimations = self.controller?.context.sharedContext.energyUsageSettings.fullTranslucency ?? true let cameraView = TGAttachmentCameraView(forSelfPortrait: false, videoModeByDefault: controller.bannedSendPhotos != nil && controller.bannedSendVideos == nil)! cameraView.clipsToBounds = true cameraView.removeCorners() cameraView.pressed = { [weak self, weak cameraView] in if let strongSelf = self, !strongSelf.openingMedia { strongSelf.dismissInput() strongSelf.controller?.openCamera?(strongSelf.cameraView) if !enableAnimations { cameraView?.startPreview() } } } self.cameraView = cameraView if enableAnimations { cameraView.startPreview() } self.gridNode.scrollView.addSubview(cameraView) self.gridNode.addSubnode(self.cameraActivateAreaNode) } else { self.containerNode.clipsToBounds = true } } func updateIsCameraActive() { let isCameraActive = !self.isSuspended && !self.hasGallery && self.isCameraPreviewVisible if isCameraActive { self.cameraView?.resumePreview() } else { self.cameraView?.pausePreview() } } func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { if otherGestureRecognizer.view is UIScrollView || otherGestureRecognizer is UIPanGestureRecognizer { return true } else { return false } } fileprivate func dismissInput() { self.view.window?.endEditing(true) } private func scrollerTextForTag(tag: Int32) -> String { let month = Month(packedValue: tag) return stringForMonth(strings: self.presentationData.strings, month: month.month, ofYear: month.year) } private var currentScrollingTag: Int32? private func updateScrollingArea() { guard let (layout, _) = self.validLayout else { return } var tag: Int32? self.gridNode.forEachItemNode { itemNode in if let itemNode = itemNode as? MediaPickerGridItemNode { tag = itemNode.tag } } let dateString = tag.flatMap { self.scrollerTextForTag(tag: $0) } if self.currentScrollingTag != tag { self.currentScrollingTag = tag if self.scrollingArea.isDragging { self.scrollingArea.feedbackTap() } } self.scrollingArea.update( containerSize: layout.size, containerInsets: self.gridNode.gridLayout.insets, contentHeight: self.gridNode.scrollView.contentSize.height, contentOffset: self.gridNode.scrollView.bounds.minY, isScrolling: self.gridNode.scrollView.isDragging || self.gridNode.scrollView.isDecelerating, date: (dateString ?? "", tag ?? 0), theme: self.presentationData.theme, transition: .immediate ) } fileprivate var resetOnUpdate = false private func updateState(_ state: State) { guard let controller = self.controller, let interaction = controller.interaction else { return } let previousState = self.state self.state = state var stableId: Int = 0 var entries: [MediaPickerGridEntry] = [] var updateLayout = false var stories = false var selectable = true if case let .assets(_, mode) = controller.subject, mode != .default { selectable = false if mode == .story { stories = true } } switch state { case let .noAccess(cameraAccess): if case .assets = previousState { updateLayout = true } else if case let .noAccess(previousCameraAccess) = previousState, previousCameraAccess != cameraAccess { updateLayout = true } if case .notDetermined = cameraAccess, !self.requestedCameraAccess { self.requestedCameraAccess = true self.mediaAssetsContext.requestCameraAccess() } case let .assets(fetchResult, preload, drafts, mediaAccess, cameraAccess): if let fetchResult = fetchResult { let totalCount = fetchResult.count let count = preload ? min(13, totalCount) : totalCount var draftIndex = 0 for draft in drafts { entries.append(MediaPickerGridEntry(stableId: stableId, content: .draft(draft, draftIndex), selectable: selectable, stories: stories)) stableId += 1 draftIndex += 1 } for i in 0 ..< count { let index: Int if case let .assets(collection, _) = controller.subject, let _ = collection { index = i } else { index = totalCount - i - 1 } entries.append(MediaPickerGridEntry(stableId: stableId, content: .asset(fetchResult, index), selectable: selectable, stories: stories)) stableId += 1 } if case let .assets(previousFetchResult, _, _, _, previousCameraAccess) = previousState, previousFetchResult == nil || previousCameraAccess != cameraAccess { updateLayout = true } #if DEBUG if case let .assets(collection, _) = controller.subject, collection?.localizedTitle == "BulkTest" { for i in 0 ..< totalCount { let backingAsset = fetchResult.object(at: i) let asset = TGMediaAsset(phAsset: backingAsset) controller.interaction?.selectionState?.setItem(asset, selected: true) } } #endif if !stories, case .notDetermined = cameraAccess, !self.requestedCameraAccess { self.requestedCameraAccess = true self.mediaAssetsContext.requestCameraAccess() } if !controller.didSetupGroups { controller.didSetupGroups = true Queue.concurrentDefaultQueue().after(0.4) { var isCreateSticker = false if case .assets(_, .createSticker) = controller.subject { isCreateSticker = true } controller.groupsPromise.set( combineLatest( self.mediaAssetsContext.fetchAssetsCollections(.album), self.mediaAssetsContext.fetchAssetsCollections(.smartAlbum) ) |> map { albums, smartAlbums -> [MediaGroupItem] in var collections: [PHAssetCollection] = [] smartAlbums.enumerateObjects { collection, _, _ in if [.smartAlbumUserLibrary, .smartAlbumFavorites].contains(collection.assetCollectionSubtype) { collections.append(collection) } } smartAlbums.enumerateObjects { collection, index, _ in var supportedAlbums: [PHAssetCollectionSubtype] = [ .smartAlbumBursts, .smartAlbumPanoramas, .smartAlbumScreenshots, .smartAlbumSelfPortraits, .smartAlbumSlomoVideos, .smartAlbumTimelapses, .smartAlbumVideos, .smartAlbumAllHidden ] if #available(iOS 11, *) { supportedAlbums.append(.smartAlbumAnimated) supportedAlbums.append(.smartAlbumDepthEffect) supportedAlbums.append(.smartAlbumLivePhotos) } if isCreateSticker { supportedAlbums = supportedAlbums.filter { type in if type == .smartAlbumSlomoVideos || type == .smartAlbumTimelapses || type == .smartAlbumVideos { return false } return true } } if supportedAlbums.contains(collection.assetCollectionSubtype) { let result = PHAsset.fetchAssets(in: collection, options: nil) if result.count > 0 { collections.append(collection) } } } albums.enumerateObjects(options: [.reverse]) { collection, _, _ in let result = PHAsset.fetchAssets(in: collection, options: nil) if result.count > 0 { collections.append(collection) } } var items: [MediaGroupItem] = [] for collection in collections { let result = PHAsset.fetchAssets(in: collection, options: nil) let firstItem: PHAsset? if [.smartAlbumUserLibrary, .smartAlbumFavorites].contains(collection.assetCollectionSubtype) { firstItem = result.lastObject } else { firstItem = result.firstObject } items.append( MediaGroupItem( collection: collection, firstItem: firstItem, count: result.count ) ) } return items } ) } } } else if case .notDetermined = mediaAccess, !self.requestedMediaAccess { self.requestedMediaAccess = true self.mediaAssetsContext.requestMediaAccess() } case let .media(media): let count = media.count for i in 0 ..< count { entries.append(MediaPickerGridEntry(stableId: stableId, content: .media(media[i], i), selectable: true, stories: stories)) stableId += 1 } } var previousEntries = self.currentEntries if self.resetOnUpdate { self.enqueueTransaction(MediaPickerGridTransaction(clearList: previousEntries)) self.resetOnUpdate = false previousEntries = [] } self.currentEntries = entries var scrollToItem: GridNodeScrollToItem? if case let .assets(collection, _) = controller.subject, let _ = collection, previousEntries.isEmpty && !entries.isEmpty { scrollToItem = GridNodeScrollToItem(index: entries.count - 1, position: .bottom(0.0), transition: .immediate, directionHint: .down, adjustForSection: false) } let transaction = MediaPickerGridTransaction(previousList: previousEntries, list: entries, context: controller.context, interaction: interaction, theme: self.presentationData.theme, strings: self.presentationData.strings, scrollToItem: scrollToItem) self.enqueueTransaction(transaction) if !self.didSetReady { updateLayout = true } if updateLayout, let (layout, navigationBarHeight) = self.validLayout { self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: previousState == nil ? .immediate : .animated(duration: 0.2, curve: .easeInOut)) } self.updateNavigation(transition: .immediate) } private func resetItems() { } private func updateSelectionState(animated: Bool = false) { self.gridNode.forEachItemNode { itemNode in if let itemNode = itemNode as? MediaPickerGridItemNode { itemNode.updateSelectionState(animated: animated) } } self.selectionNode?.updateSelectionState() let count = Int32(self.controller?.interaction?.selectionState?.count() ?? 0) self.controller?.updateSelectionState(count: count) if let (layout, navigationBarHeight) = self.validLayout { self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.3, curve: .spring)) } } func updatePresentationData(_ presentationData: PresentationData) { self.presentationData = presentationData self.backgroundNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor self.backgroundNode.updateColor(color: self.presentationData.theme.rootController.tabBar.backgroundColor, transition: .immediate) } private var currentDisplayMode: DisplayMode = .all func updateDisplayMode(_ displayMode: DisplayMode, animated: Bool = true) { let updated = self.currentDisplayMode != displayMode self.currentDisplayMode = displayMode self.dismissInput() self.controller?.dismissAllTooltips() if case .selected = displayMode, self.selectionNode == nil, let controller = self.controller { var persistentItems = false if case .media = controller.subject { persistentItems = true } let selectionNode = MediaPickerSelectedListNode(context: controller.context, persistentItems: persistentItems) selectionNode.alpha = animated ? 0.0 : 1.0 selectionNode.layer.allowsGroupOpacity = true selectionNode.isUserInteractionEnabled = false selectionNode.interaction = self.controller?.interaction selectionNode.getTransitionView = { [weak self] identifier in if let strongSelf = self { var node: MediaPickerGridItemNode? strongSelf.gridNode.forEachItemNode { itemNode in if let itemNode = itemNode as? MediaPickerGridItemNode, itemNode.identifier == identifier { node = itemNode } } if let node = node { return (node.view, node.spoilerNode?.dustNode, { [weak node] animateCheckNode in node?.animateFadeIn(animateCheckNode: animateCheckNode, animateSpoilerNode: false) }) } else { return nil } } else { return nil } } self.containerNode.insertSubnode(selectionNode, aboveSubnode: self.gridNode) self.selectionNode = selectionNode if let (layout, navigationBarHeight) = self.validLayout { self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) } } self.gridNode.isUserInteractionEnabled = displayMode == .all self.selectionNode?.isUserInteractionEnabled = displayMode == .selected var completion: () -> Void = {} if updated && displayMode == .all { completion = { self.updateNavigation(transition: .animated(duration: 0.1, curve: .easeInOut)) self.selectionNode?.alpha = 0.0 } } if updated { if animated { switch displayMode { case .selected: self.selectionNode?.animateIn(initiated: { [weak self] in self?.updateNavigation(transition: .immediate) }, completion: completion) case .all: self.selectionNode?.animateOut(completion: completion) } } else { self.updateNavigation(transition: .immediate) } } } private weak var currentGalleryController: TGModernGalleryController? private weak var currentGalleryParentController: ViewController? fileprivate var currentAssetDownloadDisposable = MetaDisposable() fileprivate func closeGalleryController() { if let _ = self.currentGalleryController, let currentGalleryParentController = self.currentGalleryParentController { self.currentGalleryController = nil self.currentGalleryParentController = nil currentGalleryParentController.dismiss(completion: nil) } } fileprivate func cancelAssetDownloads() { guard let downloadManager = self.controller?.downloadManager else { return } self.currentAssetDownloadDisposable.set(nil) downloadManager.cancelAllDownloads() } fileprivate func requestAssetDownload(asset: PHAsset) { guard let downloadManager = self.controller?.downloadManager else { return } downloadManager.download(asset: asset) self.currentAssetDownloadDisposable.set( (downloadManager.downloadProgress(identifier: asset.localIdentifier) |> deliverOnMainQueue).start(next: { [weak self] status in if let self, case .completed = status, let controller = self.controller, let customSelection = self.controller?.customSelection { customSelection(controller, asset) } }) ) } private var openingMedia = false fileprivate func openMedia(fetchResult: PHFetchResult, index: Int, immediateThumbnail: UIImage?) { guard let controller = self.controller, let interaction = controller.interaction, let (layout, _) = self.validLayout, !self.openingMedia else { return } Queue.mainQueue().justDispatch { self.dismissInput() } if let customSelection = controller.customSelection { self.openingMedia = true let asset = fetchResult[index] let _ = (checkIfAssetIsLocal(asset) |> deliverOnMainQueue).start(next: { [weak self] isLocallyAvailable in guard let self else { return } if isLocallyAvailable { self.cancelAssetDownloads() customSelection(controller, asset) } else { self.requestAssetDownload(asset: asset) } }) Queue.mainQueue().after(0.3) { self.openingMedia = false } return } let reversed: Bool if case .assets(nil, _) = controller.subject { reversed = true } else { reversed = false } let index = reversed ? fetchResult.count - index - 1 : index var hasTimer = false if controller.chatLocation?.peerId != controller.context.account.peerId && controller.chatLocation?.peerId?.namespace == Namespaces.Peer.CloudUser { hasTimer = true } let hasSchedule = true self.openingMedia = true self.currentGalleryController = presentLegacyMediaPickerGallery(context: controller.context, peer: controller.peer, threadTitle: controller.threadTitle, chatLocation: controller.chatLocation, isScheduledMessages: controller.isScheduledMessages, presentationData: self.presentationData, source: .fetchResult(fetchResult: fetchResult, index: index, reversed: reversed), immediateThumbnail: immediateThumbnail, selectionContext: interaction.selectionState, editingContext: interaction.editingState, hasSilentPosting: true, hasSchedule: hasSchedule, hasTimer: hasTimer, updateHiddenMedia: { [weak self] id in self?.hiddenMediaId.set(.single(id)) }, initialLayout: layout, transitionHostView: { [weak self] in return self?.gridNode.view }, transitionView: { [weak self] identifier in return self?.transitionView(for: identifier) }, completed: { [weak self] result, silently, scheduleTime, completion in if let strongSelf = self { strongSelf.controller?.interaction?.sendSelected(result, silently, scheduleTime, false, completion) } }, presentSchedulePicker: controller.presentSchedulePicker, presentTimerPicker: controller.presentTimerPicker, getCaptionPanelView: controller.getCaptionPanelView, present: { [weak self] c, a in self?.currentGalleryParentController = c self?.controller?.present(c, in: .window(.root), with: a) }, finishedTransitionIn: { [weak self] in self?.openingMedia = false self?.hasGallery = true self?.updateIsCameraActive() }, willTransitionOut: { [weak self] in self?.hasGallery = false self?.updateIsCameraActive() }, dismissAll: { [weak self] in self?.controller?.dismissAll() }) } fileprivate func openSelectedMedia(item: TGMediaSelectableItem, immediateThumbnail: UIImage?) { guard let controller = self.controller, let interaction = controller.interaction, let (layout, _) = self.validLayout, !self.openingMedia else { return } Queue.mainQueue().justDispatch { self.dismissInput() } var hasTimer = false if controller.chatLocation?.peerId != controller.context.account.peerId && controller.chatLocation?.peerId?.namespace == Namespaces.Peer.CloudUser { hasTimer = true } self.openingMedia = true self.currentGalleryController = presentLegacyMediaPickerGallery(context: controller.context, peer: controller.peer, threadTitle: controller.threadTitle, chatLocation: controller.chatLocation, isScheduledMessages: controller.isScheduledMessages, presentationData: self.presentationData, source: .selection(item: item), immediateThumbnail: immediateThumbnail, selectionContext: interaction.selectionState, editingContext: interaction.editingState, hasSilentPosting: true, hasSchedule: true, hasTimer: hasTimer, updateHiddenMedia: { [weak self] id in self?.hiddenMediaId.set(.single(id)) }, initialLayout: layout, transitionHostView: { [weak self] in return self?.selectionNode?.view }, transitionView: { [weak self] identifier in return self?.transitionView(for: identifier) }, completed: { [weak self] result, silently, scheduleTime, completion in if let strongSelf = self { strongSelf.controller?.interaction?.sendSelected(result, silently, scheduleTime, false, completion) } }, presentSchedulePicker: controller.presentSchedulePicker, presentTimerPicker: controller.presentTimerPicker, getCaptionPanelView: controller.getCaptionPanelView, present: { [weak self] c, a in self?.currentGalleryParentController = c self?.controller?.present(c, in: .window(.root), with: a, blockInteraction: true) }, finishedTransitionIn: { [weak self] in self?.openingMedia = false self?.hasGallery = true self?.updateIsCameraActive() }, willTransitionOut: { [weak self] in self?.hasGallery = false self?.updateIsCameraActive() }, dismissAll: { [weak self] in self?.controller?.dismissAll() }) } fileprivate func openDraft(draft: MediaEditorDraft, immediateThumbnail: UIImage?) { guard let controller = self.controller, !self.openingMedia else { return } Queue.mainQueue().justDispatch { self.dismissInput() } if let customSelection = controller.customSelection { self.openingMedia = true customSelection(controller, draft) Queue.mainQueue().after(0.3) { self.openingMedia = false } } } fileprivate func send(asFile: Bool = false, silently: Bool, scheduleTime: Int32?, animated: Bool, completion: @escaping () -> Void) { guard let controller = self.controller, !controller.completed else { return } controller.dismissAllTooltips() var hasHeic = false let allItems = controller.interaction?.selectionState?.selectedItems() ?? [] for item in allItems { if item is TGCameraCapturedVideo { } else if let asset = item as? TGMediaAsset, asset.uniformTypeIdentifier.contains("heic") { hasHeic = true break } } let proceed: (Bool) -> Void = { convertToJpeg in let signals: [Any]! switch controller.subject { case .assets: signals = TGMediaAssetsController.resultSignals(for: controller.interaction?.selectionState, editingContext: controller.interaction?.editingState, intent: asFile ? TGMediaAssetsControllerSendFileIntent : TGMediaAssetsControllerSendMediaIntent, currentItem: nil, storeAssets: true, convertToJpeg: convertToJpeg, descriptionGenerator: legacyAssetPickerItemGenerator(), saveEditedPhotos: controller.saveEditedPhotos) case .media: signals = TGMediaAssetsController.pasteboardResultSignals(for: controller.interaction?.selectionState, editingContext: controller.interaction?.editingState, intent: asFile ? TGMediaAssetsControllerSendFileIntent : TGMediaAssetsControllerSendMediaIntent, currentItem: nil, descriptionGenerator: legacyAssetPickerItemGenerator()) } guard let signals = signals else { return } controller.completed = true controller.legacyCompletion(signals, silently, scheduleTime, { [weak self] identifier in return !asFile ? self?.getItemSnapshot(identifier) : nil }, { [weak self] in completion() self?.controller?.dismiss(animated: animated) }) } if asFile && hasHeic { controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.MediaPicker_JpegConversionText, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.MediaPicker_KeepHeic, action: { proceed(false) }), TextAlertAction(type: .genericAction, title: self.presentationData.strings.MediaPicker_ConvertToJpeg, action: { proceed(true) })], actionLayout: .vertical), in: .window(.root)) } else { proceed(false) } } private func openLimitedMediaOptions() { let presentationData = self.presentationData let controller = ActionSheetController(presentationData: self.presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } controller.setItemGroups([ ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: presentationData.strings.Media_LimitedAccessSelectMore, color: .accent, action: { [weak self] in dismissAction() if #available(iOS 14.0, *), let strongController = self?.controller { PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: strongController) } }), ActionSheetButtonItem(title: presentationData.strings.Media_LimitedAccessChangeSettings, color: .accent, action: { [weak self] in dismissAction() self?.controller?.context.sharedContext.applicationBindings.openSettings() }) ]), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) ]) self.controller?.present(controller, in: .window(.root)) } private func getItemSnapshot(_ identifier: String) -> UIView? { guard let selectionState = self.controller?.interaction?.selectionState else { return nil } if let galleryController = self.currentGalleryController { if selectionState.count() > 1 { return nil } return galleryController.transitionView() } if selectionState.grouping && selectionState.count() > 1 && (self.selectionNode?.alpha ?? 0.0).isZero { return nil } if let view = self.transitionView(for: identifier, hideSource: true) { return view } else { return nil } } fileprivate func defaultTransitionView() -> UIView? { var transitionNode: MediaPickerGridItemNode? if let itemNode = self.gridNode.itemNodeAtPoint(.zero) { if let itemNode = itemNode as? MediaPickerGridItemNode { transitionNode = itemNode } } let transitionView = transitionNode?.transitionView(snapshot: false) return transitionView } fileprivate func transitionView(for identifier: String, snapshot: Bool = true, hideSource: Bool = false) -> UIView? { if let selectionNode = self.selectionNode, selectionNode.alpha > 0.0 { return selectionNode.transitionView(for: identifier, hideSource: hideSource) } else { var transitionNode: MediaPickerGridItemNode? self.gridNode.forEachItemNode { itemNode in if let itemNode = itemNode as? MediaPickerGridItemNode, itemNode.identifier == identifier { transitionNode = itemNode } } let transitionView = transitionNode?.transitionView(snapshot: snapshot) if hideSource { transitionNode?.isHidden = true } return transitionView } } fileprivate func transitionImage(for identifier: String) -> UIImage? { var transitionNode: MediaPickerGridItemNode? self.gridNode.forEachItemNode { itemNode in if let itemNode = itemNode as? MediaPickerGridItemNode, itemNode.identifier == identifier { transitionNode = itemNode } } return transitionNode?.transitionImage() } private func enqueueTransaction(_ transaction: MediaPickerGridTransaction) { self.enqueuedTransactions.append(transaction) if let _ = self.validLayout { self.dequeueTransaction() } } private var didRestoreContentOffset = false private func dequeueTransaction() { if self.enqueuedTransactions.isEmpty { return } let transaction = self.enqueuedTransactions.removeFirst() self.gridNode.transaction(GridNodeTransaction(deleteItems: transaction.deletions, insertItems: transaction.insertions, updateItems: transaction.updates, scrollToItem: transaction.scrollToItem, updateLayout: nil, itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) if let subject = self.controller?.subject, case .assets(_, .story) = subject, let contentOffset = savedStoriesContentOffset, !self.didRestoreContentOffset { if contentOffset > 64.0 { self.gridNode.scrollView.setContentOffset(CGPoint(x: 0.0, y: contentOffset), animated: false) self.controller?.requestAttachmentMenuExpansion() } self.didRestoreContentOffset = true } } func scrollToTop(animated: Bool = false) { if let selectionNode = self.selectionNode, selectionNode.alpha > 0.0 { selectionNode.scrollToTop(animated: animated) } else { self.gridNode.scrollView.setContentOffset(CGPoint(x: 0.0, y: -self.gridNode.scrollView.contentInset.top), animated: animated) } } private var previousContentOffset: GridNodeVisibleContentOffset? func updateNavigation(delayDisappear: Bool = false, transition: ContainedViewLayoutTransition) { if let selectionNode = self.selectionNode, selectionNode.alpha > 0.0 { self.controller?.navigationBar?.updateBackgroundAlpha(1.0, transition: .immediate) self.controller?.updateTabBarAlpha(1.0, transition) } else if self.placeholderNode != nil { self.controller?.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate) if delayDisappear { Queue.mainQueue().after(0.25) { self.controller?.updateTabBarAlpha(0.0, transition) } } else { self.controller?.updateTabBarAlpha(0.0, transition) } } else { var previousContentOffsetValue: CGFloat? if let previousContentOffset = self.previousContentOffset, case let .known(value) = previousContentOffset { previousContentOffsetValue = value } let offset = self.gridNode.visibleContentOffset() switch offset { case let .known(value): let transition: ContainedViewLayoutTransition if let previousContentOffsetValue = previousContentOffsetValue, value <= 0.0, previousContentOffsetValue > 2.0 { transition = .animated(duration: 0.2, curve: .easeInOut) } else { transition = .immediate } self.controller?.navigationBar?.updateBackgroundAlpha(min(2.0, value) / 2.0, transition: transition) case .unknown, .none: self.controller?.navigationBar?.updateBackgroundAlpha(1.0, transition: .immediate) } self.controller?.updateTabBarAlpha(1.0, transition) } let count = Int32(self.controller?.interaction?.selectionState?.count() ?? 0) if count > 0 { self.controller?.updateTabBarAlpha(1.0, transition) } } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { guard let controller = self.controller else { return } let firstTime = self.validLayout == nil self.validLayout = (layout, navigationBarHeight) var insets = layout.insets(options: []) insets.top += navigationBarHeight let bounds = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: layout.size.height)) let innerBounds = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: layout.size.height)) let itemsPerRow: Int if case .compact = layout.metrics.widthClass { self._ready.set(.single(true)) switch layout.orientation { case .portrait: itemsPerRow = 3 case .landscape: itemsPerRow = 5 } } else { itemsPerRow = 3 } let width = layout.size.width - layout.safeInsets.left - layout.safeInsets.right let itemSpacing: CGFloat = 1.0 let itemWidth = floorToScreenPixels((width - itemSpacing * CGFloat(itemsPerRow - 1)) / CGFloat(itemsPerRow)) var cameraRect: CGRect? = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: 0.0), size: CGSize(width: itemWidth, height: itemWidth * 2.0 + 1.0)) if self.cameraView == nil { cameraRect = nil } var manageHeight: CGFloat = 0.0 if case let .assets(_, _, _, mediaAccess, cameraAccess) = self.state { if cameraAccess == nil { cameraRect = nil } var bannedSendMedia: (Int32, Bool)? if let bannedSendPhotos = controller.bannedSendPhotos, let bannedSendVideos = controller.bannedSendVideos { bannedSendMedia = (max(bannedSendPhotos.0, bannedSendVideos.0), bannedSendPhotos.1 || bannedSendVideos.1) } if let (untilDate, personal) = bannedSendMedia { self.gridNode.isHidden = true self.controller?.titleView.isEnabled = false let banDescription: String if untilDate != 0 && untilDate != Int32.max { banDescription = self.presentationData.strings.Conversation_RestrictedMediaTimed(stringForFullDate(timestamp: untilDate, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat)).string } else if personal { banDescription = self.presentationData.strings.Conversation_RestrictedMedia } else { banDescription = self.presentationData.strings.Conversation_DefaultRestrictedMedia } var placeholderTransition = transition let placeholderNode: MediaPickerPlaceholderNode if let current = self.placeholderNode { placeholderNode = current } else { placeholderNode = MediaPickerPlaceholderNode(content: .bannedSendMedia(text: banDescription, canBoost: controller.canBoostToUnrestrict)) placeholderNode.boostPressed = { [weak controller] in controller?.openBoost() } self.containerNode.insertSubnode(placeholderNode, aboveSubnode: self.gridNode) self.placeholderNode = placeholderNode placeholderTransition = .immediate } placeholderNode.update(layout: layout, theme: self.presentationData.theme, strings: self.presentationData.strings, hasCamera: false, transition: placeholderTransition) placeholderTransition.updateFrame(node: placeholderNode, frame: innerBounds) self.updateNavigation(transition: .immediate) } else if case .notDetermined = mediaAccess { } else { if case .limited = mediaAccess { let manageNode: MediaPickerManageNode if let current = self.manageNode { manageNode = current } else { manageNode = MediaPickerManageNode() manageNode.pressed = { [weak self] in if let strongSelf = self { strongSelf.openLimitedMediaOptions() } } self.manageNode = manageNode self.gridNode.addSubnode(manageNode) } manageHeight = manageNode.update(layout: layout, theme: self.presentationData.theme, strings: self.presentationData.strings, subject: .limitedMedia, transition: transition) transition.updateFrame(node: manageNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -manageHeight), size: CGSize(width: layout.size.width, height: manageHeight))) } else if [.denied, .restricted].contains(cameraAccess) { cameraRect = nil let manageNode: MediaPickerManageNode if let current = self.manageNode { manageNode = current } else { manageNode = MediaPickerManageNode() manageNode.pressed = { [weak self] in self?.controller?.context.sharedContext.applicationBindings.openSettings() } self.manageNode = manageNode self.gridNode.addSubnode(manageNode) } manageHeight = manageNode.update(layout: layout, theme: self.presentationData.theme, strings: self.presentationData.strings, subject: .camera, transition: transition) transition.updateFrame(node: manageNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -manageHeight), size: CGSize(width: layout.size.width, height: manageHeight))) } else if let manageNode = self.manageNode { self.manageNode = nil manageNode.removeFromSupernode() } } } else { cameraRect = nil } let cleanGridInsets = UIEdgeInsets(top: insets.top, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.safeInsets.right) let gridInsets = UIEdgeInsets(top: insets.top + manageHeight, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.safeInsets.right) transition.updateFrame(node: self.gridNode, frame: innerBounds) self.scrollingArea.frame = innerBounds transition.updateFrame(node: self.backgroundNode, frame: innerBounds) self.backgroundNode.update(size: bounds.size, transition: transition) transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: bounds.height))) var itemHeight = itemWidth if case let .assets(_, mode) = controller.subject, case .story = mode { itemHeight = floor(itemWidth * 1.227) } let preloadSize: CGFloat = itemHeight// * 3.0 self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: bounds.size, insets: gridInsets, scrollIndicatorInsets: nil, preloadSize: preloadSize, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemHeight), fillWidth: true, lineSpacing: itemSpacing, itemSpacing: itemSpacing), cutout: cameraRect), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil, updateOpaqueState: nil, synchronousLoads: false), completion: { [weak self] _ in guard let strongSelf = self else { return } if !strongSelf.didSetReady && strongSelf.state != nil { strongSelf.didSetReady = true Queue.mainQueue().justDispatch { strongSelf._ready.set(.single(true) |> delay(0.05, queue: Queue.mainQueue())) Queue.mainQueue().after(0.5, { strongSelf.preloadPromise.set(false) }) } } }) if let selectionNode = self.selectionNode, let controller = self.controller { let selectedItems = controller.interaction?.selectionState?.selectedItems() as? [TGMediaSelectableItem] ?? [] let updateSelectionNode = { selectionNode.updateLayout(size: bounds.size, insets: cleanGridInsets, items: selectedItems, grouped: self.controller?.groupedValue ?? true, theme: self.presentationData.theme, wallpaper: self.presentationData.chatWallpaper, bubbleCorners: self.presentationData.chatBubbleCorners, transition: transition) } if selectedItems.count < 1 && self.currentDisplayMode == .selected { self.updateDisplayMode(.all) Queue.mainQueue().after(0.3, updateSelectionNode) } else { updateSelectionNode() } transition.updateFrame(node: selectionNode, frame: innerBounds) } if let cameraView = self.cameraView { if let cameraRect = cameraRect { transition.updateFrame(view: cameraView, frame: cameraRect) self.cameraActivateAreaNode.frame = cameraRect cameraView.isHidden = false } else { cameraView.isHidden = true } } if firstTime { while !self.enqueuedTransactions.isEmpty { self.dequeueTransaction() } } var bannedSendMedia: (Int32, Bool)? if let bannedSendPhotos = self.controller?.bannedSendPhotos, let bannedSendVideos = self.controller?.bannedSendVideos { bannedSendMedia = (max(bannedSendPhotos.0, bannedSendVideos.0), bannedSendPhotos.1 || bannedSendVideos.1) } if case let .noAccess(cameraAccess) = self.state { var hasCamera = cameraAccess == .authorized var story = false if let subject = self.controller?.subject, case .assets(_, .story) = subject { hasCamera = false story = true self.controller?.navigationItem.rightBarButtonItem = nil } var placeholderTransition = transition let placeholderNode: MediaPickerPlaceholderNode if let current = self.placeholderNode { placeholderNode = current } else { placeholderNode = MediaPickerPlaceholderNode(content: .intro(story: story)) placeholderNode.settingsPressed = { [weak self] in self?.controller?.context.sharedContext.applicationBindings.openSettings() } placeholderNode.cameraPressed = { [weak self] in self?.dismissInput() self?.controller?.openCamera?(nil) } self.containerNode.insertSubnode(placeholderNode, aboveSubnode: self.gridNode) self.placeholderNode = placeholderNode if transition.isAnimated { placeholderNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) } placeholderTransition = .immediate self.updateNavigation(transition: .immediate) } placeholderNode.update(layout: layout, theme: self.presentationData.theme, strings: self.presentationData.strings, hasCamera: hasCamera, transition: placeholderTransition) placeholderTransition.updateFrame(node: placeholderNode, frame: innerBounds) } else if let placeholderNode = self.placeholderNode, bannedSendMedia == nil { self.placeholderNode = nil placeholderNode.removeFromSupernode() } } } private var validLayout: ContainerViewLayout? private var controllerNode: Node { return self.displayNode as! Node } private let _ready = Promise() override public var ready: Promise { return self._ready } private var groupedValue: Bool = true { didSet { self.groupedPromise.set(self.groupedValue) self.interaction?.selectionState?.grouping = self.groupedValue if let layout = self.validLayout { self.containerLayoutUpdated(layout, transition: .immediate) } } } private let groupedPromise = ValuePromise(true) private let downloadManager = AssetDownloadManager() private var isDismissing = false fileprivate let mainButtonState: AttachmentMainButtonState? private let mainButtonAction: (() -> Void)? public init( context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peer: EnginePeer?, threadTitle: String?, chatLocation: ChatLocation?, isScheduledMessages: Bool = false, bannedSendPhotos: (Int32, Bool)? = nil, bannedSendVideos: (Int32, Bool)? = nil, canBoostToUnrestrict: Bool = false, subject: Subject, editingContext: TGMediaEditingContext? = nil, selectionContext: TGMediaSelectionContext? = nil, saveEditedPhotos: Bool = false, mainButtonState: AttachmentMainButtonState? = nil, mainButtonAction: (() -> Void)? = nil ) { self.context = context let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } self.presentationData = presentationData self.updatedPresentationData = updatedPresentationData self.peer = peer self.threadTitle = threadTitle self.chatLocation = chatLocation self.isScheduledMessages = isScheduledMessages self.bannedSendPhotos = bannedSendPhotos self.bannedSendVideos = bannedSendVideos self.canBoostToUnrestrict = canBoostToUnrestrict self.subject = subject self.saveEditedPhotos = saveEditedPhotos self.mainButtonState = mainButtonState self.mainButtonAction = mainButtonAction let selectionContext = selectionContext ?? TGMediaSelectionContext() self.titleView = MediaPickerTitleView(theme: self.presentationData.theme, segments: [self.presentationData.strings.Attachment_AllMedia, self.presentationData.strings.Attachment_SelectedMedia(1)], selectedIndex: 0) if case let .assets(collection, mode) = subject { if let collection = collection { self.titleView.title = collection.localizedTitle ?? presentationData.strings.Attachment_Gallery } else { switch mode { case .default, .createSticker: self.titleView.title = presentationData.strings.MediaPicker_Recents self.titleView.isEnabled = true case .story: self.titleView.title = presentationData.strings.MediaPicker_Recents self.titleView.isEnabled = true case .wallpaper: self.titleView.title = presentationData.strings.Conversation_Theme_ChooseWallpaperTitle case .addImage: self.titleView.title = presentationData.strings.MediaPicker_AddImage } } } else { self.titleView.title = presentationData.strings.Attachment_Gallery } self.moreButtonNode = MoreButtonNode(theme: self.presentationData.theme) self.moreButtonNode.iconNode.enqueueState(.more, animated: false) super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: presentationData)) self.statusBar.statusBarStyle = .Ignore selectionContext.attemptSelectingItem = { [weak self] item in guard let self else { return false } if let _ = item as? TGMediaPickerGalleryPhotoItem { if self.bannedSendPhotos != nil { self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Chat_SendNotAllowedPhoto, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) return false } } else if let _ = item as? TGMediaPickerGalleryVideoItem { if self.bannedSendVideos != nil { self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Chat_SendNotAllowedVideo, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) return false } } else if let asset = item as? TGMediaAsset { if asset.isVideo { if self.bannedSendVideos != nil { self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Chat_SendNotAllowedVideo, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) return false } } else { if self.bannedSendPhotos != nil { self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Chat_SendNotAllowedPhoto, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) return false } } } return true } self.presentationDataDisposable = ((updatedPresentationData?.signal ?? context.sharedContext.presentationData) |> deliverOnMainQueue).start(next: { [weak self] presentationData in if let strongSelf = self { let previousTheme = strongSelf.presentationData.theme let previousStrings = strongSelf.presentationData.strings strongSelf.presentationData = presentationData if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { strongSelf.updateThemeAndStrings() } } }) self.titleView.indexUpdated = { [weak self] index in if let strongSelf = self { strongSelf.controllerNode.updateDisplayMode(index == 0 ? .all : .selected) } } self.titleView.action = { [weak self] in if let self { self.presentGroupsMenu() } } self.navigationItem.titleView = self.titleView if case let .assets(collection, mode) = self.subject, mode != .default { if case .wallpaper = mode { self.navigationItem.leftBarButtonItem = UIBarButtonItem(backButtonAppearanceWithTitle: self.presentationData.strings.Common_Back, target: self, action: #selector(self.backPressed)) } else if collection == nil { self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) if [.createSticker].contains(mode) { self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: self.moreButtonNode) self.navigationItem.rightBarButtonItem?.action = #selector(self.rightButtonPressed) self.navigationItem.rightBarButtonItem?.target = self } } else { self.navigationItem.leftBarButtonItem = UIBarButtonItem(backButtonAppearanceWithTitle: self.presentationData.strings.Common_Back, target: self, action: #selector(self.backPressed)) } } else { if case let .assets(collection, _) = self.subject, collection != nil { self.navigationItem.leftBarButtonItem = UIBarButtonItem(backButtonAppearanceWithTitle: self.presentationData.strings.Common_Back, target: self, action: #selector(self.backPressed)) } else { self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) } if self.bannedSendPhotos != nil && self.bannedSendVideos != nil { } else { self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: self.moreButtonNode) self.navigationItem.rightBarButtonItem?.action = #selector(self.rightButtonPressed) self.navigationItem.rightBarButtonItem?.target = self } } self.moreButtonNode.action = { [weak self] _, gesture in if let strongSelf = self { strongSelf.searchOrMorePressed(node: strongSelf.moreButtonNode.contextSourceNode, gesture: gesture) } } self.scrollToTop = { [weak self] in if let strongSelf = self { if let webSearchController = strongSelf.webSearchController { webSearchController.scrollToTop?() } else { strongSelf.controllerNode.scrollToTop(animated: true) } } } self.scrollToTopWithTabBar = { [weak self] in if let strongSelf = self { if let webSearchController = strongSelf.webSearchController { webSearchController.cancel() } else { strongSelf.scrollToTop?() } } } self.interaction = MediaPickerInteraction(downloadManager: self.downloadManager, openMedia: { [weak self] fetchResult, index, immediateThumbnail in self?.controllerNode.openMedia(fetchResult: fetchResult, index: index, immediateThumbnail: immediateThumbnail) }, openSelectedMedia: { [weak self] item, immediateThumbnail in self?.controllerNode.openSelectedMedia(item: item, immediateThumbnail: immediateThumbnail) }, openDraft: { [weak self] draft, immediateThumbnail in self?.controllerNode.openDraft(draft: draft, immediateThumbnail: immediateThumbnail) }, toggleSelection: { [weak self] item, value, suggestUndo in if let self = self, let selectionState = self.interaction?.selectionState { if let _ = item as? TGMediaPickerGalleryPhotoItem { if self.bannedSendPhotos != nil { self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Chat_SendNotAllowedPhoto, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) return false } } else if let _ = item as? TGMediaPickerGalleryVideoItem { if self.bannedSendVideos != nil { self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Chat_SendNotAllowedVideo, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) return false } } else if let asset = item as? TGMediaAsset { if asset.isVideo { if self.bannedSendVideos != nil { self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Chat_SendNotAllowedVideo, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) return false } } else { if self.bannedSendPhotos != nil { self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Chat_SendNotAllowedPhoto, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) return false } } } var showUndo = false if suggestUndo { if !value { selectionState.saveState() showUndo = true } else { selectionState.clearSavedState() } } let success = selectionState.setItem(item, selected: value) if showUndo { self.showSelectionUndo(item: item) } return success } else { return false } }, sendSelected: { [weak self] currentItem, silently, scheduleTime, animated, completion in if let strongSelf = self, let selectionState = strongSelf.interaction?.selectionState, !strongSelf.isDismissing { strongSelf.isDismissing = true if let currentItem = currentItem { selectionState.setItem(currentItem, selected: true) } strongSelf.controllerNode.send(silently: silently, scheduleTime: scheduleTime, animated: animated, completion: completion) } }, schedule: { [weak self] in if let strongSelf = self { strongSelf.presentSchedulePicker(false, { [weak self] time in self?.interaction?.sendSelected(nil, false, time, true, {}) }) } }, dismissInput: { [weak self] in if let strongSelf = self { strongSelf.controllerNode.dismissInput() } }, selectionState: selectionContext, editingState: editingContext ?? TGMediaEditingContext()) self.interaction?.selectionState?.grouping = true if case let .media(media) = self.subject { for item in media { selectionContext.setItem(item.asset, selected: true) } } self.updateSelectionState(count: Int32(selectionContext.count())) // self.longTapWithTabBar = { [weak self] in // if let self, self.groupsController == nil { // self.presentSearch(activateOnDisplay: false) // } // } } required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.presentationDataDisposable?.dispose() } override public func loadDisplayNode() { self.displayNode = Node(controller: self) self._ready.set(self.controllerNode.ready.get()) if case .media = self.subject { self.controllerNode.updateDisplayMode(.selected, animated: false) } super.displayNodeDidLoad() } public func closeGalleryController() { self.controllerNode.closeGalleryController() } public var groupsPresented: () -> Void = {} private var didSetupGroups = false private let groupsPromise = Promise<[MediaGroupItem]>() public func presentGroupsMenu() { self.groupsPresented() let _ = (self.groupsPromise.get() |> take(1) |> deliverOnMainQueue).start(next: { [weak self] items in guard let self else { return } let items = items.filter { $0.count > 0 || $0.collection.assetCollectionSubtype == .smartAlbumAllHidden } var dismissImpl: (() -> Void)? let content: ContextControllerItemsContent = MediaGroupsContextMenuContent( context: self.context, items: items, selectGroup: { [weak self] collection in guard let self else { return } self.controllerNode.resetOnUpdate = true if collection.assetCollectionSubtype == .smartAlbumUserLibrary { self.selectedCollection.set(.single(nil)) self.titleView.title = self.presentationData.strings.MediaPicker_Recents } else { self.selectedCollection.set(.single(collection)) self.titleView.title = collection.localizedTitle ?? "" } self.scrollToTop?() dismissImpl?() } ) self.titleView.isHighlighted = true let contextController = ContextController( presentationData: self.presentationData, source: .reference(MediaPickerContextReferenceContentSource(controller: self, sourceNode: self.titleView.contextSourceNode)), items: .single(ContextController.Items(content: .custom(content))), gesture: nil ) contextController.dismissed = { [weak self] in self?.titleView.isHighlighted = false } dismissImpl = { [weak contextController] in contextController?.dismiss() } self.presentInGlobalOverlay(contextController) }) } private weak var undoOverlayController: UndoOverlayController? private func showSelectionUndo(item: TGMediaSelectableItem) { let scale = min(2.0, UIScreenScale) let targetSize = CGSize(width: 64.0 * scale, height: 64.0 * scale) let image: Signal if let item = item as? TGMediaAsset { image = assetImage(asset: item.backingAsset, targetSize: targetSize, exact: false) } else if let item = item as? TGCameraCapturedVideo { image = assetImage(asset: item.originalAsset.backingAsset, targetSize: targetSize, exact: false) } else if let item = item as? TGMediaEditableItem { image = Signal { subscriber in let disposable = item.thumbnailImageSignal?().start(next: { next in if let next = next as? UIImage { subscriber.putNext(next) } }, error: { _ in }, completed: { subscriber.putCompletion() }) return ActionDisposable { disposable?.dispose() } } } else { return } let _ = (image |> deliverOnMainQueue).start(next: { [weak self] image in guard let strongSelf = self else { return } var photosCount = 0 var videosCount = 0 strongSelf.interaction?.selectionState?.enumerateDeselectedItems({ item in if let item = item as? TGMediaAsset { if item.isVideo { videosCount += 1 } else { photosCount += 1 } } else if let _ = item as? TGCameraCapturedVideo { videosCount += 1 } else if let _ = item as? UIImage { photosCount += 1 } }) let totalCount = Int32(photosCount + videosCount) let presentationData = strongSelf.presentationData let text: String if photosCount > 0 && videosCount > 0 { text = presentationData.strings.Attachment_DeselectedItems(totalCount) } else if photosCount > 0 { text = presentationData.strings.Attachment_DeselectedPhotos(totalCount) } else if videosCount > 0 { text = presentationData.strings.Attachment_DeselectedVideos(totalCount) } else { text = presentationData.strings.Attachment_DeselectedItems(totalCount) } if let undoOverlayController = strongSelf.undoOverlayController { undoOverlayController.content = .image(image: image ?? UIImage(), title: nil, text: text, round: false, undoText: presentationData.strings.Undo_Undo) } else { var elevatedLayout = true if let layout = strongSelf.validLayout, case .regular = layout.metrics.widthClass { elevatedLayout = false } let undoOverlayController = UndoOverlayController(presentationData: presentationData, content: .image(image: image ?? UIImage(), title: nil, text: text, round: false, undoText: presentationData.strings.Undo_Undo), elevatedLayout: elevatedLayout, action: { [weak self] action in guard let strongSelf = self else { return true } switch action { case .undo: strongSelf.interaction?.selectionState?.restoreState() default: strongSelf.interaction?.selectionState?.clearSavedState() } return true }) if let layout = strongSelf.validLayout, case .regular = layout.metrics.widthClass { strongSelf.present(undoOverlayController, in: .current) } else { strongSelf.present(undoOverlayController, in: .window(.root)) } strongSelf.undoOverlayController = undoOverlayController } }) } private var selectionCount: Int32 = 0 fileprivate func updateSelectionState(count: Int32) { self.selectionCount = count var moreIsVisible = false if case let .assets(_, mode) = self.subject, [.story, .createSticker].contains(mode) { moreIsVisible = true } else if case let .media(media) = self.subject { self.titleView.title = media.count == 1 ? self.presentationData.strings.Attachment_Pasteboard : self.presentationData.strings.Attachment_SelectedMedia(count) self.titleView.segmentsHidden = true moreIsVisible = true // self.moreButtonNode.iconNode.enqueueState(.more, animated: false) } else { if count > 0 { self.titleView.segments = [self.presentationData.strings.Attachment_AllMedia, self.presentationData.strings.Attachment_SelectedMedia(count)] self.titleView.segmentsHidden = false moreIsVisible = true // self.moreButtonNode.iconNode.enqueueState(.more, animated: true) } else { self.titleView.segmentsHidden = true moreIsVisible = false // self.moreButtonNode.iconNode.enqueueState(.search, animated: true) if self.titleView.index != 0 { Queue.mainQueue().after(0.3) { self.titleView.index = 0 } } } } let transition = ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut) transition.updateAlpha(node: self.moreButtonNode.iconNode, alpha: moreIsVisible ? 1.0 : 0.0) transition.updateTransformScale(node: self.moreButtonNode.iconNode, scale: moreIsVisible ? 1.0 : 0.1) } private func updateThemeAndStrings() { self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData)) self.titleView.theme = self.presentationData.theme self.moreButtonNode.theme = self.presentationData.theme self.controllerNode.updatePresentationData(self.presentationData) } @objc private func backPressed() { if let _ = self.navigationController { self.dismiss() } else { self.updateNavigationStack { current in var mediaPickerContext: AttachmentMediaPickerContext? if let first = current.first as? MediaPickerScreen { mediaPickerContext = first.webSearchController?.mediaPickerContext ?? first.mediaPickerContext } return (current.filter { $0 !== self }, mediaPickerContext) } } } func mainButtonPressed() { self.mainButtonAction?() } func dismissAllTooltips() { self.undoOverlayController?.dismissWithCommitAction() } public func requestDismiss(completion: @escaping () -> Void) { if let selectionState = self.interaction?.selectionState, selectionState.count() > 0 { self.isDismissing = true let text: String if case .media = self.subject { text = self.presentationData.strings.Attachment_DiscardPasteboardAlertText } else { text = self.presentationData.strings.Attachment_CancelSelectionAlertText } let controller = textAlertController(context: self.context, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Attachment_CancelSelectionAlertNo, action: { }), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Attachment_CancelSelectionAlertYes, action: { [weak self] in self?.dismissAllTooltips() completion() })]) controller.dismissed = { [weak self] _ in self?.isDismissing = false } self.present(controller, in: .window(.root)) } else { completion() } } public func shouldDismissImmediately() -> Bool { if let selectionState = self.interaction?.selectionState, selectionState.count() > 0 { return false } else { return true } } @objc private func cancelPressed() { self.dismissAllTooltips() self.dismiss() } public override func dismiss(completion: (() -> Void)? = nil) { self.controllerNode.cancelAssetDownloads() if case .assets(_, .story) = self.subject { let contentOffset = self.controllerNode.gridNode.scrollView.contentOffset.y if contentOffset > 100.0 || savedStoriesContentOffset != nil { savedStoriesContentOffset = contentOffset } } super.dismiss(completion: completion) } @objc private func rightButtonPressed() { self.moreButtonNode.buttonPressed() } public func resetForReuse() { if let webSearchController = self.webSearchController { self.webSearchController = nil webSearchController.dismiss() } self.scrollToTop?() self.controllerNode.isSuspended = true self.controllerNode.updateIsCameraActive() } public func prepareForReuse() { self.controllerNode.isSuspended = false self.controllerNode.updateIsCameraActive() self.controllerNode.updateNavigation(delayDisappear: true, transition: .immediate) } private weak var groupsController: MediaGroupsScreen? private func presentSearch(activateOnDisplay: Bool) { guard self.moreButtonNode.iconNode.iconState == .search, case let .assets(_, mode) = self.subject else { return } self.requestAttachmentMenuExpansion() var embedded = true if case .story = mode { embedded = false } var updateNavigationStackImpl: ((AttachmentContainable) -> Void)? let groupsController = MediaGroupsScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, mediaAssetsContext: self.controllerNode.mediaAssetsContext, embedded: embedded, openGroup: { [weak self] collection in if let strongSelf = self { let mediaPicker = MediaPickerScreen(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: strongSelf.peer, threadTitle: strongSelf.threadTitle, chatLocation: strongSelf.chatLocation, isScheduledMessages: strongSelf.isScheduledMessages, bannedSendPhotos: strongSelf.bannedSendPhotos, bannedSendVideos: strongSelf.bannedSendVideos, subject: .assets(collection, mode), editingContext: strongSelf.interaction?.editingState, selectionContext: strongSelf.interaction?.selectionState) mediaPicker.presentSchedulePicker = strongSelf.presentSchedulePicker mediaPicker.presentTimerPicker = strongSelf.presentTimerPicker mediaPicker.getCaptionPanelView = strongSelf.getCaptionPanelView mediaPicker.legacyCompletion = strongSelf.legacyCompletion mediaPicker.customSelection = strongSelf.customSelection mediaPicker.dismissAll = { [weak self] in self?.dismiss(animated: true, completion: nil) } mediaPicker._presentedInModal = true mediaPicker.updateNavigationStack = strongSelf.updateNavigationStack updateNavigationStackImpl?(mediaPicker) } }) groupsController.updateNavigationStack = self.updateNavigationStack if !embedded { groupsController._presentedInModal = true } updateNavigationStackImpl = { [weak self, weak groupsController] c in if let self { if case .story = mode, let groupsController { self.updateNavigationStack({ _ in return ([self, groupsController, c], self.mediaPickerContext)}) } else { self.updateNavigationStack({ _ in return ([self, c], self.mediaPickerContext)}) } } } if case .story = mode { self.updateNavigationStack({ _ in return ([self, groupsController], self.mediaPickerContext)}) } else { self.presentWebSearch(groupsController, activateOnDisplay) } self.groupsController = groupsController } @objc private func searchOrMorePressed(node: ContextReferenceContentNode, gesture: ContextGesture?) { let strings = self.presentationData.strings if case let .assets(_, mode) = self.subject, [.createSticker].contains(mode) { var items: [ContextMenuItem] = [] if mode != .addImage { items.append(.action(ContextMenuActionItem(text: strings.Attachment_Create, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Draw"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.default) self?.createFromScratch() }))) } items.append(.action(ContextMenuActionItem(text: strings.Attachment_SelectFromFiles, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/File"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.default) self?.presentFilePicker() }))) let contextController = ContextController(presentationData: self.presentationData, source: .reference(MediaPickerContextReferenceContentSource(controller: self, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) self.presentInGlobalOverlay(contextController) return } switch self.moreButtonNode.iconNode.iconState { case .search: // self.presentSearch(activateOnDisplay: true) break case .more: let selectionCount = self.selectionCount var isSpoilerAvailable = true if let peer = self.peer, case .secretChat = peer { isSpoilerAvailable = false } var hasSpoilers = false var hasGeneric = false if let selectionContext = self.interaction?.selectionState, let editingContext = self.interaction?.editingState { for case let item as TGMediaEditableItem in selectionContext.selectedItems() { if editingContext.spoiler(for: item) { hasSpoilers = true } else { hasGeneric = true } } } let items: Signal = self.groupedPromise.get() |> deliverOnMainQueue |> map { [weak self] grouped -> ContextController.Items in var items: [ContextMenuItem] = [] if !hasSpoilers { items.append(.action(ContextMenuActionItem(text: selectionCount > 1 ? strings.Attachment_SendAsFiles : strings.Attachment_SendAsFile, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/File"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.default) self?.controllerNode.send(asFile: true, silently: false, scheduleTime: nil, animated: true, completion: {}) }))) } if selectionCount > 1 { if !items.isEmpty { items.append(.separator) } items.append(.action(ContextMenuActionItem(text: strings.Attachment_Grouped, icon: { theme in if !grouped { return nil } return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.default) self?.groupedValue = true }))) items.append(.action(ContextMenuActionItem(text: strings.Attachment_Ungrouped, icon: { theme in if grouped { return nil } return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.default) self?.groupedValue = false }))) } if isSpoilerAvailable { if !items.isEmpty { items.append(.separator) } items.append(.action(ContextMenuActionItem(text: hasGeneric ? strings.Attachment_EnableSpoiler : strings.Attachment_DisableSpoiler, icon: { _ in return nil }, iconAnimation: ContextMenuActionItem.IconAnimation( name: "anim_spoiler", loop: true ), action: { [weak self] _, f in f(.default) guard let strongSelf = self else { return } if let selectionContext = strongSelf.interaction?.selectionState, let editingContext = strongSelf.interaction?.editingState { for case let item as TGMediaEditableItem in selectionContext.selectedItems() { editingContext.setSpoiler(hasGeneric, for: item) } } }))) } return ContextController.Items(content: .list(items)) } let contextController = ContextController(presentationData: self.presentationData, source: .reference(MediaPickerContextReferenceContentSource(controller: self, sourceNode: node)), items: items, gesture: gesture) self.presentInGlobalOverlay(contextController) } } fileprivate func defaultTransitionView() -> UIView? { return self.controllerNode.defaultTransitionView() } public func transitionView(for identifier: String, snapshot: Bool, hideSource: Bool = false) -> UIView? { return self.controllerNode.transitionView(for: identifier, snapshot: snapshot, hideSource: hideSource) } public func transitionImage(for identifier: String) -> UIImage? { return self.controllerNode.transitionImage(for: identifier) } public func updateHiddenMediaId(_ id: String?) { self.controllerNode.hiddenMediaId.set(.single(id)) } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) self.validLayout = layout self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } public var mediaPickerContext: AttachmentMediaPickerContext? { return MediaPickerContext(controller: self) } } final class MediaPickerContext: AttachmentMediaPickerContext { private weak var controller: MediaPickerScreen? var selectionCount: Signal { return Signal { [weak self] subscriber in let disposable = self?.controller?.interaction?.selectionState?.selectionChangedSignal().start(next: { [weak self] value in subscriber.putNext(Int(self?.controller?.interaction?.selectionState?.count() ?? 0)) }, error: { _ in }, completed: { }) return ActionDisposable { disposable?.dispose() } } } var caption: Signal { return Signal { [weak self] subscriber in let disposable = self?.controller?.interaction?.editingState.forcedCaption().start(next: { caption in if let caption = caption as? NSAttributedString { subscriber.putNext(caption) } else { subscriber.putNext(nil) } }, error: { _ in }, completed: { }) return ActionDisposable { disposable?.dispose() } } } public var loadingProgress: Signal { return .single(nil) } public var mainButtonState: Signal { return .single(self.controller?.mainButtonState) } init(controller: MediaPickerScreen) { self.controller = controller } func setCaption(_ caption: NSAttributedString) { self.controller?.interaction?.editingState.setForcedCaption(caption, skipUpdate: true) } func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode) { self.controller?.interaction?.sendSelected(nil, mode == .silently, mode == .whenOnline ? scheduleWhenOnlineTimestamp : nil, true, {}) } func schedule() { self.controller?.interaction?.schedule() } func mainButtonAction() { self.controller?.mainButtonPressed() } } private final class MediaPickerContextReferenceContentSource: ContextReferenceContentSource { private let controller: ViewController private let sourceNode: ContextReferenceContentNode init(controller: ViewController, sourceNode: ContextReferenceContentNode) { self.controller = controller self.sourceNode = sourceNode } func transitionInfo() -> ContextControllerReferenceViewInfo? { return ContextControllerReferenceViewInfo(referenceView: self.sourceNode.view, contentAreaInScreenSpace: UIScreen.main.bounds) } } public class MediaPickerGridSelectionGesture : UIPanGestureRecognizer { public var itemAt: (CGPoint) -> (T, Bool)? = { _ in return nil } public var updateSelection: (T, Bool) -> Void = { _, _ in} public var updateIsScrollEnabled: (Bool) -> Void = { _ in} public var began: () -> Void = {} private var processing = false private var selecting = false private var initialLocation: CGPoint? public var sideInset: CGFloat = 0.0 public init() { super.init(target: nil, action: nil) } public override func touchesBegan(_ touches: Set, with event: UIEvent) { super.touchesBegan(touches, with: event) guard let touch = touches.first, self.numberOfTouches == 1 else { return } let location = touch.location(in: self.view) if location.x > self.sideInset { self.initialLocation = location } else { self.state = .failed } } public override func touchesMoved(_ touches: Set, with event: UIEvent) { super.touchesMoved(touches, with: event) guard let touch = touches.first, let initialLocation = self.initialLocation else { self.state = .failed return } let location = touch.location(in: self.view) let translation = CGPoint(x: location.x - initialLocation.x, y: location.y - initialLocation.y) var additionalLocation: CGPoint? if !self.processing { if abs(translation.y) > 5.0 { self.state = .failed } else if abs(translation.x) > 8.0 { self.processing = true self.updateIsScrollEnabled(false) self.began() if let (_, selected) = self.itemAt(location) { self.selecting = !selected } additionalLocation = self.initialLocation } } if self.processing { if let additionalLocation = additionalLocation { if let (item, selected) = self.itemAt(additionalLocation), selected != self.selecting { self.updateSelection(item, self.selecting) } } if let (item, selected) = self.itemAt(location), selected != self.selecting { self.updateSelection(item, self.selecting) } } } public override func touchesEnded(_ touches: Set, with event: UIEvent) { super.touchesEnded(touches, with: event) self.state = .failed self.reset() } public override func touchesCancelled(_ touches: Set, with event: UIEvent) { super.touchesCancelled(touches, with: event) self.state = .failed self.reset() } public override func reset() { super.reset() self.processing = false self.initialLocation = nil self.updateIsScrollEnabled(true) } } public func wallpaperMediaPickerController( context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peer: EnginePeer, animateAppearance: Bool, completion: @escaping (MediaPickerScreen, Any) -> Void = { _, _ in }, openColors: @escaping () -> Void ) -> ViewController { let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false, makeEntityInputView: { return nil }) controller.animateAppearance = animateAppearance controller.requestController = { [weak controller] _, present in let presentationData = context.sharedContext.currentPresentationData.with { $0 } let mediaPickerController = MediaPickerScreen(context: context, updatedPresentationData: updatedPresentationData, peer: nil, threadTitle: nil, chatLocation: nil, bannedSendPhotos: nil, bannedSendVideos: nil, subject: .assets(nil, .wallpaper), mainButtonState: AttachmentMainButtonState(text: presentationData.strings.Conversation_Theme_SetColorWallpaper, font: .regular, background: .color(.clear), textColor: presentationData.theme.actionSheet.controlAccentColor, isVisible: true, progress: .none, isEnabled: true), mainButtonAction: { controller?.dismiss(animated: true) openColors() }) mediaPickerController.customSelection = completion present(mediaPickerController, mediaPickerController.mediaPickerContext) } controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) return controller } public func mediaPickerController( context: AccountContext, hasSearch: Bool, completion: @escaping (Any) -> Void ) -> ViewController { let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkColorPresentationTheme) let updatedPresentationData: (PresentationData, Signal) = (presentationData, .single(presentationData)) let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false, makeEntityInputView: { return nil }) controller.requestController = { _, present in let mediaPickerController = MediaPickerScreen(context: context, updatedPresentationData: updatedPresentationData, peer: nil, threadTitle: nil, chatLocation: nil, bannedSendPhotos: nil, bannedSendVideos: nil, subject: .assets(nil, .addImage), mainButtonState: nil, mainButtonAction: nil) mediaPickerController.customSelection = { controller, result in completion(result) controller.dismiss(animated: true) } if hasSearch { mediaPickerController.presentWebSearch = { [weak mediaPickerController] groups, activateOnDisplay in let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.SearchBots()) |> deliverOnMainQueue).start(next: { configuration in let webSearchController = WebSearchController( context: context, updatedPresentationData: updatedPresentationData, peer: nil, chatLocation: nil, configuration: configuration, mode: .editor(completion: { [weak mediaPickerController] image in completion(image) mediaPickerController?.dismiss(animated: true) }), activateOnDisplay: activateOnDisplay ) mediaPickerController?.present(webSearchController, in: .current) }) } } present(mediaPickerController, mediaPickerController.mediaPickerContext) } controller.navigationPresentation = .flatModal controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) return controller } public func storyMediaPickerController( context: AccountContext, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void ) -> ViewController { let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkColorPresentationTheme) let updatedPresentationData: (PresentationData, Signal) = (presentationData, .single(presentationData)) let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false, makeEntityInputView: { return nil }) controller.forceSourceRect = true controller.getSourceRect = getSourceRect controller.requestController = { _, present in let mediaPickerController = MediaPickerScreen(context: context, updatedPresentationData: updatedPresentationData, peer: nil, threadTitle: nil, chatLocation: nil, bannedSendPhotos: nil, bannedSendVideos: nil, subject: .assets(nil, .story), mainButtonState: nil, mainButtonAction: nil) mediaPickerController.groupsPresented = groupsPresented mediaPickerController.customSelection = { controller, result in if let result = result as? MediaEditorDraft { controller.updateHiddenMediaId(result.path) if let transitionView = controller.transitionView(for: result.path, snapshot: false) { let transitionOut: (Bool?) -> (UIView, CGRect)? = { isNew in if let isNew { controller.updateHiddenMediaId(result.path) if isNew { if let transitionView = controller.defaultTransitionView() { return (transitionView, transitionView.bounds) } } else { if let transitionView = controller.transitionView(for: result.path, snapshot: false) { return (transitionView, transitionView.bounds) } } } return nil } completion(result, transitionView, transitionView.bounds, controller.transitionImage(for: result.path), transitionOut, { [weak controller] in controller?.updateHiddenMediaId(nil) }) } } else if let result = result as? PHAsset { controller.updateHiddenMediaId(result.localIdentifier) if let transitionView = controller.transitionView(for: result.localIdentifier, snapshot: false) { let transitionOut: (Bool?) -> (UIView, CGRect)? = { isNew in if let isNew { if isNew { controller.updateHiddenMediaId(nil) if let transitionView = controller.defaultTransitionView() { return (transitionView, transitionView.bounds) } } else if let transitionView = controller.transitionView(for: result.localIdentifier, snapshot: false) { return (transitionView, transitionView.bounds) } } return nil } completion(result, transitionView, transitionView.bounds, controller.transitionImage(for: result.localIdentifier), transitionOut, { [weak controller] in controller?.updateHiddenMediaId(nil) }) } } } present(mediaPickerController, mediaPickerController.mediaPickerContext) } controller.willDismiss = { dismissed() } controller.navigationPresentation = .flatModal controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) return controller } public func stickerMediaPickerController( context: AccountContext, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any?, UIView?, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void ) -> ViewController { let presentationData = context.sharedContext.currentPresentationData.with({ $0 }) let updatedPresentationData: (PresentationData, Signal) = (presentationData, .single(presentationData)) let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false, makeEntityInputView: { return nil }) controller.forceSourceRect = true controller.getSourceRect = getSourceRect controller.requestController = { [weak controller] _, present in let mediaPickerController = MediaPickerScreen(context: context, updatedPresentationData: updatedPresentationData, peer: nil, threadTitle: nil, chatLocation: nil, bannedSendPhotos: nil, bannedSendVideos: nil, subject: .assets(nil, .createSticker), mainButtonState: nil, mainButtonAction: nil) mediaPickerController.customSelection = { controller, result in if let result = result as? PHAsset { controller.updateHiddenMediaId(result.localIdentifier) if let transitionView = controller.transitionView(for: result.localIdentifier, snapshot: false) { let transitionOut: (Bool?) -> (UIView, CGRect)? = { isNew in if let isNew { if isNew { controller.updateHiddenMediaId(nil) if let transitionView = controller.defaultTransitionView() { return (transitionView, transitionView.bounds) } } else if let transitionView = controller.transitionView(for: result.localIdentifier, snapshot: false) { return (transitionView, transitionView.bounds) } } return nil } completion(result, transitionView, transitionView.bounds, controller.transitionImage(for: result.localIdentifier), transitionOut, { [weak controller] in controller?.updateHiddenMediaId(nil) }) } } } mediaPickerController.createFromScratch = { [weak controller] in completion(nil, nil, .zero, nil, { _ in return nil }, { [weak controller] in controller?.dismiss(animated: true) }) } mediaPickerController.presentFilePicker = { [weak controller] in controller?.present(legacyICloudFilePicker(theme: presentationData.theme, mode: .import, documentTypes: ["public.image"], forceDarkTheme: false, dismissed: { }, completion: { urls in if let url = urls.first { let isScopedResource = url.startAccessingSecurityScopedResource() Logger.shared.log("MediaPicker", "isScopedResource = \(isScopedResource)") let coordinator = NSFileCoordinator(filePresenter: nil) var error: NSError? coordinator.coordinate(readingItemAt: url, options: .forUploading, error: &error, byAccessor: { sourceUrl in let fileName = "img_\(sourceUrl.lastPathComponent)" let copyPath = NSTemporaryDirectory() + fileName try? FileManager.default.removeItem(atPath: copyPath) do { try FileManager.default.copyItem(at: sourceUrl, to: URL(fileURLWithPath: copyPath)) } catch let e { Logger.shared.log("MediaPicker", "copy file error \(e)") if isScopedResource { url.stopAccessingSecurityScopedResource() } return } if let image = UIImage(contentsOfFile: copyPath) { completion(image, nil, .zero, nil, { _ in return nil }, {}) } }) } }), in: .window(.root)) controller?.dismiss(animated: true) } present(mediaPickerController, mediaPickerController.mediaPickerContext) } controller.willDismiss = { dismissed() } controller.navigationPresentation = .flatModal controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) return controller }