Swiftgram/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift
2022-02-22 23:49:26 +03:00

1003 lines
49 KiB
Swift

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
final class MediaPickerInteraction {
let openMedia: (PHFetchResult<PHAsset>, Int, UIImage?) -> Void
let openSelectedMedia: (TGMediaSelectableItem, UIImage?) -> Void
let toggleSelection: (TGMediaSelectableItem, Bool) -> Void
let sendSelected: (TGMediaSelectableItem?, Bool, Int32?, Bool) -> Void
let schedule: () -> Void
let dismissInput: () -> Void
let selectionState: TGMediaSelectionContext?
let editingState: TGMediaEditingContext
var hiddenMediaId: String?
init(openMedia: @escaping (PHFetchResult<PHAsset>, Int, UIImage?) -> Void, openSelectedMedia: @escaping (TGMediaSelectableItem, UIImage?) -> Void, toggleSelection: @escaping (TGMediaSelectableItem, Bool) -> Void, sendSelected: @escaping (TGMediaSelectableItem?, Bool, Int32?, Bool) -> Void, schedule: @escaping () -> Void, dismissInput: @escaping () -> Void, selectionState: TGMediaSelectionContext?, editingState: TGMediaEditingContext) {
self.openMedia = openMedia
self.openSelectedMedia = openSelectedMedia
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
static func <(lhs: MediaPickerGridEntry, rhs: MediaPickerGridEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(account: Account, interaction: MediaPickerInteraction, theme: PresentationTheme) -> MediaPickerGridItem {
return MediaPickerGridItem(content: self.content, interaction: interaction, theme: theme)
}
}
private struct MediaPickerGridTransaction {
let deletions: [Int]
let insertions: [GridNodeInsertItem]
let updates: [GridNodeUpdateItem]
let scrollToItem: GridNodeScrollToItem?
init(previousList: [MediaPickerGridEntry], list: [MediaPickerGridEntry], account: Account, interaction: MediaPickerInteraction, theme: PresentationTheme, 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(account: account, interaction: interaction, theme: theme), previousIndex: $0.2) }
self.updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interaction: interaction, theme: theme)) }
self.scrollToItem = scrollToItem
}
}
public final class MediaPickerScreen: ViewController, AttachmentContainable {
private let context: AccountContext
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
fileprivate var interaction: MediaPickerInteraction?
private let peer: EnginePeer?
private let chatLocation: ChatLocation?
private let bannedSendMedia: (Int32, Bool)?
private let titleView: MediaPickerTitleView
private let moreButtonNode: MediaPickerMoreButtonNode
public weak var webSearchController: WebSearchController?
public var openCamera: ((TGAttachmentCameraView?) -> Void)?
public var presentStickers: ((@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?)?
public var presentSchedulePicker: (Bool, @escaping (Int32) -> Void) -> Void = { _, _ in }
public var presentTimerPicker: (@escaping (Int32) -> Void) -> Void = { _ in }
public var presentWebSearch: () -> Void = {}
public var getCaptionPanelView: () -> TGCaptionPanelView? = { return nil }
public var legacyCompletion: (_ signals: [Any], _ silently: Bool, _ scheduleTime: Int32?) -> Void = { _, _, _ in }
public var requestAttachmentMenuExpansion: () -> Void = {}
private class Node: ViewControllerTracingNode {
enum DisplayMode {
case all
case selected
}
enum State {
case noAccess(cameraAccess: AVAuthorizationStatus?)
case assets(fetchResult: PHFetchResult<PHAsset>?, preload: Bool, mediaAccess: PHAuthorizationStatus, cameraAccess: AVAuthorizationStatus?)
}
private weak var controller: MediaPickerScreen?
private var presentationData: PresentationData
private let mediaAssetsContext: MediaAssetsContext
private let backgroundNode: NavigationBackgroundNode
private let gridNode: GridNode
fileprivate var cameraView: TGAttachmentCameraView?
private var placeholderNode: MediaPickerPlaceholderNode?
private var manageNode: MediaPickerManageNode?
private var bannedTextNode: ImmediateTextNode?
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<Bool>(true)
private var itemsDisposable: Disposable?
private var selectionChangedDisposable: Disposable?
private var itemsDimensionsUpdatedDisposable: Disposable?
private var hiddenMediaDisposable: Disposable?
private let hiddenMediaId = Promise<String?>(nil)
private var didSetReady = false
private let _ready = Promise<Bool>(true)
var ready: Promise<Bool> {
return self._ready
}
private var validLayout: (ContainerViewLayout, CGFloat)?
init(controller: MediaPickerScreen) {
self.controller = controller
self.presentationData = controller.presentationData
let mediaAssetsContext = MediaAssetsContext()
self.mediaAssetsContext = mediaAssetsContext
self.backgroundNode = NavigationBackgroundNode(color: self.presentationData.theme.rootController.tabBar.backgroundColor)
self.gridNode = GridNode()
super.init()
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.addSubnode(self.backgroundNode)
self.addSubnode(self.gridNode)
let preloadPromise = self.preloadPromise
let updatedState = combineLatest(mediaAssetsContext.mediaAccess(), mediaAssetsContext.cameraAccess())
|> mapToSignal { mediaAccess, cameraAccess -> Signal<State, NoError> in
if case .notDetermined = mediaAccess {
return .single(.assets(fetchResult: nil, preload: false, mediaAccess: mediaAccess, cameraAccess: cameraAccess))
} else if [.restricted, .denied].contains(mediaAccess) {
return .single(.noAccess(cameraAccess: cameraAccess))
} else {
return combineLatest(mediaAssetsContext.recentAssets(), preloadPromise.get())
|> map { fetchResult, preload in
return .assets(fetchResult: fetchResult, preload: preload, mediaAccess: mediaAccess, cameraAccess: cameraAccess)
}
}
}
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.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<Void, NoError> {
return Signal { subscriber in
let disposable = selectionState.selectionChangedSignal()?.start(next: { next in
subscriber.putNext(Void())
}, completed: {})
return ActionDisposable {
disposable?.dispose()
}
}
}
self.selectionChangedDisposable = (selectionChangedSignal(selectionState: selectionState)
|> deliverOnMainQueue).start(next: { [weak self] _ in
if let strongSelf = self {
strongSelf.updateSelectionState()
}
})
}
if let editingState = self.controller?.interaction?.editingState {
func itemsDimensionsUpdatedSignal(editingState: TGMediaEditingContext) -> Signal<Void, NoError> {
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()
}
override func didLoad() {
super.didLoad()
self.gridNode.scrollView.alwaysBounceVertical = true
self.gridNode.scrollView.showsVerticalScrollIndicator = false
let cameraView = TGAttachmentCameraView(forSelfPortrait: false)!
cameraView.clipsToBounds = true
cameraView.removeCorners()
cameraView.pressed = { [weak self] in
if let strongSelf = self {
strongSelf.controller?.openCamera?(strongSelf.cameraView)
}
}
self.cameraView = cameraView
cameraView.startPreview()
self.gridNode.scrollView.addSubview(cameraView)
// self.controller?.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate)
}
fileprivate func dismissInput() {
self.view.window?.endEditing(true)
}
private var requestedMediaAccess = false
private var requestedCameraAccess = 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
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, mediaAccess, cameraAccess):
if let fetchResult = fetchResult {
let totalCount = fetchResult.count
let count = preload ? min(10, totalCount) : totalCount
for i in 0 ..< count {
entries.append(MediaPickerGridEntry(stableId: stableId, content: .asset(fetchResult, totalCount - i - 1)))
stableId += 1
}
if case let .assets(previousFetchResult, _, _, previousCameraAccess) = previousState, previousFetchResult == nil || previousCameraAccess != cameraAccess {
updateLayout = true
}
if case .notDetermined = cameraAccess, !self.requestedCameraAccess {
self.requestedCameraAccess = true
self.mediaAssetsContext.requestCameraAccess()
}
} else if case .notDetermined = mediaAccess, !self.requestedMediaAccess {
self.requestedMediaAccess = true
self.mediaAssetsContext.requestMediaAccess()
}
}
let previousEntries = self.currentEntries
self.currentEntries = entries
let transaction = MediaPickerGridTransaction(previousList: previousEntries, list: entries, account: controller.context.account, interaction: interaction, theme: self.presentationData.theme, scrollToItem: nil)
self.enqueueTransaction(transaction)
if updateLayout, let (layout, navigationBarHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: previousState == nil ? .immediate : .animated(duration: 0.2, curve: .easeInOut))
}
}
private func updateSelectionState() {
self.gridNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? MediaPickerGridItemNode {
itemNode.updateSelectionState()
}
}
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.backgroundColor = presentationData.theme.list.plainBackgroundColor
}
private var currentDisplayMode: DisplayMode = .all
func updateMode(_ displayMode: DisplayMode) {
self.currentDisplayMode = displayMode
if case .selected = displayMode, self.selectionNode == nil, let controller = self.controller {
let selectionNode = MediaPickerSelectedListNode(context: controller.context)
selectionNode.alpha = 0.0
selectionNode.isUserInteractionEnabled = false
selectionNode.interaction = self.controller?.interaction
self.insertSubnode(selectionNode, aboveSubnode: self.gridNode)
self.selectionNode = selectionNode
if let (layout, navigationBarHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
}
}
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
self.gridNode.isUserInteractionEnabled = displayMode == .all
if let selectionNode = self.selectionNode {
transition.updateAlpha(node: selectionNode, alpha: displayMode == .selected ? 1.0 : 0.0)
selectionNode.isUserInteractionEnabled = displayMode == .selected
}
}
fileprivate func openMedia(fetchResult: PHFetchResult<PHAsset>, index: Int, immediateThumbnail: UIImage?) {
guard let controller = self.controller, let interaction = controller.interaction, let (layout, _) = self.validLayout else {
return
}
Queue.mainQueue().justDispatch {
self.dismissInput()
}
let index = fetchResult.count - index - 1
presentLegacyMediaPickerGallery(context: controller.context, peer: controller.peer, chatLocation: controller.chatLocation, presentationData: self.presentationData, source: .fetchResult(fetchResult: fetchResult, index: index), immediateThumbnail: immediateThumbnail, selectionContext: interaction.selectionState, editingContext: interaction.editingState, hasSilentPosting: true, hasSchedule: true, hasTimer: true, 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 in
if let strongSelf = self {
strongSelf.controller?.interaction?.sendSelected(result, silently, scheduleTime, false)
}
}, presentStickers: controller.presentStickers, presentSchedulePicker: controller.presentSchedulePicker, presentTimerPicker: controller.presentTimerPicker, getCaptionPanelView: controller.getCaptionPanelView, present: { [weak self] c, a in
self?.controller?.present(c, in: .window(.root), with: a)
})
}
fileprivate func openSelectedMedia(item: TGMediaSelectableItem, immediateThumbnail: UIImage?) {
guard let controller = self.controller, let interaction = controller.interaction, let (layout, _) = self.validLayout else {
return
}
Queue.mainQueue().justDispatch {
self.dismissInput()
}
presentLegacyMediaPickerGallery(context: controller.context, peer: controller.peer, chatLocation: controller.chatLocation, presentationData: self.presentationData, source: .selection(item: item), immediateThumbnail: immediateThumbnail, selectionContext: interaction.selectionState, editingContext: interaction.editingState, hasSilentPosting: true, hasSchedule: true, hasTimer: true, 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 in
if let strongSelf = self {
strongSelf.controller?.interaction?.sendSelected(result, silently, scheduleTime, false)
}
}, presentStickers: controller.presentStickers, presentSchedulePicker: controller.presentSchedulePicker, presentTimerPicker: controller.presentTimerPicker, getCaptionPanelView: controller.getCaptionPanelView, present: { [weak self] c, a in
self?.controller?.present(c, in: .window(.root), with: a)
})
}
fileprivate func send(asFile: Bool = false, silently: Bool, scheduleTime: Int32?, animated: Bool) {
guard let signals = TGMediaAssetsController.resultSignals(for: self.controller?.interaction?.selectionState, editingContext: self.controller?.interaction?.editingState, intent: asFile ? TGMediaAssetsControllerSendFileIntent : TGMediaAssetsControllerSendMediaIntent, currentItem: nil, storeAssets: true, convertToJpeg: false, descriptionGenerator: legacyAssetPickerItemGenerator(), saveEditedPhotos: true) else {
return
}
self.controller?.legacyCompletion(signals, silently, scheduleTime)
self.controller?.dismiss(animated: animated)
}
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 transitionView(for identifier: String) -> UIView? {
if let selectionNode = self.selectionNode, selectionNode.alpha > 0.0 {
return selectionNode.transitionView(for: identifier)
} else {
var transitionNode: MediaPickerGridItemNode?
self.gridNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? MediaPickerGridItemNode, itemNode.identifier == identifier {
transitionNode = itemNode
}
}
return transitionNode?.transitionView()
}
}
private func enqueueTransaction(_ transaction: MediaPickerGridTransaction) {
self.enqueuedTransactions.append(transaction)
if let _ = self.validLayout {
self.dequeueTransaction()
}
}
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 })
}
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)
}
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
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 itemsPerRow: Int
if case .compact = layout.metrics.widthClass {
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))
var manageHeight: CGFloat = 0.0
if case let .assets(_, _, mediaAccess, cameraAccess) = self.state {
if cameraAccess == nil {
cameraRect = nil
}
if let (untilDate, personal) = self.controller?.bannedSendMedia {
self.gridNode.alpha = 0.3
self.gridNode.isUserInteractionEnabled = 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
}
let bannedTextNode: ImmediateTextNode
if let current = self.bannedTextNode {
bannedTextNode = current
} else {
bannedTextNode = ImmediateTextNode()
bannedTextNode.maximumNumberOfLines = 0
bannedTextNode.textAlignment = .center
self.bannedTextNode = bannedTextNode
self.addSubnode(bannedTextNode)
}
bannedTextNode.attributedText = NSAttributedString(string: banDescription, font: Font.regular(15.0), textColor: self.presentationData.theme.list.freeTextColor, paragraphAlignment: .center)
let bannedTextSize = bannedTextNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - 16.0, height: layout.size.height))
manageHeight = max(44.0, bannedTextSize.height + 20.0)
transition.updateFrame(node: bannedTextNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - bannedTextSize.width) / 2.0), y: insets.top + floorToScreenPixels((manageHeight - bannedTextSize.height) / 2.0) - 4.0), size: bannedTextSize))
} 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: bounds)
transition.updateFrame(node: self.backgroundNode, frame: bounds)
self.backgroundNode.update(size: bounds.size, transition: transition)
self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: bounds.size, insets: gridInsets, scrollIndicatorInsets: nil, preloadSize: itemWidth, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemWidth), 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))
Queue.mainQueue().after(0.5, {
strongSelf.preloadPromise.set(false)
})
}
}
})
if let selectionNode = self.selectionNode {
let selectedItems = self.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.updateMode(.all)
Queue.mainQueue().after(0.3, updateSelectionNode)
} else {
updateSelectionNode()
}
transition.updateFrame(node: selectionNode, frame: bounds)
}
if let cameraView = self.cameraView {
if let cameraRect = cameraRect {
transition.updateFrame(view: cameraView, frame: cameraRect)
cameraView.isHidden = false
} else {
cameraView.isHidden = true
}
}
if firstTime {
while !self.enqueuedTransactions.isEmpty {
self.dequeueTransaction()
}
}
if case let .noAccess(cameraAccess) = self.state {
var placeholderTransition = transition
let placeholderNode: MediaPickerPlaceholderNode
if let current = self.placeholderNode {
placeholderNode = current
} else {
placeholderNode = MediaPickerPlaceholderNode()
placeholderNode.settingsPressed = { [weak self] in
self?.controller?.context.sharedContext.applicationBindings.openSettings()
}
placeholderNode.cameraPressed = { [weak self] in
self?.controller?.openCamera?(nil)
}
self.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
}
placeholderNode.update(layout: layout, theme: self.presentationData.theme, strings: self.presentationData.strings, hasCamera: cameraAccess == .authorized, transition: placeholderTransition)
placeholderTransition.updateFrame(node: placeholderNode, frame: bounds)
} else if let placeholderNode = self.placeholderNode {
self.placeholderNode = nil
placeholderNode.removeFromSupernode()
}
}
}
private var validLayout: ContainerViewLayout?
private var controllerNode: Node {
return self.displayNode as! Node
}
private let _ready = Promise<Bool>()
override public var ready: Promise<Bool> {
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<Bool>(true)
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peer: EnginePeer?, chatLocation: ChatLocation?, bannedSendMedia: (Int32, Bool)?) {
self.context = context
self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
self.peer = peer
self.chatLocation = chatLocation
self.bannedSendMedia = bannedSendMedia
self.titleView = MediaPickerTitleView(theme: self.presentationData.theme, segments: [self.presentationData.strings.Attachment_AllMedia, self.presentationData.strings.Attachment_SelectedMedia(1)], selectedIndex: 0)
self.titleView.title = self.presentationData.strings.Attachment_Gallery
self.moreButtonNode = MediaPickerMoreButtonNode(theme: self.presentationData.theme)
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: presentationData, hideBackground: false, hideBadge: false, hideSeparator: true))
self.statusBar.statusBarStyle = .Ignore
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.updateMode(index == 0 ? .all : .selected)
}
}
self.navigationItem.titleView = self.titleView
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: self.moreButtonNode)
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(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)
}, toggleSelection: { [weak self] item, value in
if let strongSelf = self {
strongSelf.interaction?.selectionState?.setItem(item, selected: value)
}
}, sendSelected: { [weak self] currentItem, silently, scheduleTime, animated in
if let strongSelf = self, let selectionState = strongSelf.interaction?.selectionState {
if let currentItem = currentItem {
selectionState.setItem(currentItem, selected: true)
}
strongSelf.controllerNode.send(silently: silently, scheduleTime: scheduleTime, animated: animated)
}
}, 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: TGMediaSelectionContext(), editingState: TGMediaEditingContext())
self.interaction?.selectionState?.grouping = true
}
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())
super.displayNodeDidLoad()
}
private var selectionCount: Int32 = 0
fileprivate func updateSelectionState(count: Int32) {
self.selectionCount = count
if count > 0 {
self.titleView.segments = [self.presentationData.strings.Attachment_AllMedia, self.presentationData.strings.Attachment_SelectedMedia(count)]
self.titleView.segmentsHidden = false
self.moreButtonNode.iconNode.enqueueState(.more, animated: true)
} else {
self.titleView.segmentsHidden = true
self.moreButtonNode.iconNode.enqueueState(.search, animated: true)
if self.titleView.index != 0 {
Queue.mainQueue().after(0.3) {
self.titleView.index = 0
}
}
}
}
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 cancelPressed() {
self.dismiss()
}
public func resetForReuse() {
if let webSearchController = self.webSearchController {
self.webSearchController = nil
webSearchController.dismiss()
}
self.scrollToTop?()
self.controllerNode.cameraView?.pausePreview()
}
public func prepareForReuse() {
self.controllerNode.cameraView?.resumePreview()
}
@objc private func searchOrMorePressed(node: ContextReferenceContentNode, gesture: ContextGesture?) {
switch self.moreButtonNode.iconNode.iconState {
case .search:
self.requestAttachmentMenuExpansion()
self.presentWebSearch()
case .more:
let strings = self.presentationData.strings
let selectionCount = self.selectionCount
let items: Signal<ContextController.Items, NoError> = self.groupedPromise.get()
|> deliverOnMainQueue
|> map { [weak self] grouped -> ContextController.Items in
var items: [ContextMenuItem] = []
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)
})))
if selectionCount > 1 {
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
})))
}
return ContextController.Items(content: .list(items))
}
let contextController = ContextController(account: self.context.account, presentationData: self.presentationData, source: .reference(MediaPickerContextReferenceContentSource(controller: self, sourceNode: node)), items: items, gesture: gesture)
self.presentInGlobalOverlay(contextController)
}
}
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? {
if let interaction = self.interaction {
return MediaPickerContext(interaction: interaction)
} else {
return nil
}
}
}
final class MediaPickerContext: AttachmentMediaPickerContext {
private weak var interaction: MediaPickerInteraction?
var selectionCount: Signal<Int, NoError> {
return Signal { [weak self] subscriber in
let disposable = self?.interaction?.selectionState?.selectionChangedSignal().start(next: { [weak self] value in
subscriber.putNext(Int(self?.interaction?.selectionState?.count() ?? 0))
}, error: { _ in }, completed: { })
return ActionDisposable {
disposable?.dispose()
}
}
}
var caption: Signal<NSAttributedString?, NoError> {
return Signal { [weak self] subscriber in
let disposable = self?.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()
}
}
}
init(interaction: MediaPickerInteraction) {
self.interaction = interaction
}
func setCaption(_ caption: NSAttributedString) {
self.interaction?.editingState.setForcedCaption(caption, skipUpdate: true)
}
func send(silently: Bool, mode: AttachmentMediaPickerSendMode) {
self.interaction?.sendSelected(nil, silently, nil, true)
}
func schedule() {
self.interaction?.schedule()
}
}
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(referenceNode: self.sourceNode, contentAreaInScreenSpace: UIScreen.main.bounds)
}
}