diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 597c58abda..98ce855b3d 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -9796,3 +9796,5 @@ Sorry for the inconvenience."; "Premium.MaxStoriesMonthlyText" = "You can post **%@** stories in a month. Upgrade to **Telegram Premium** to increase this limit to **%@**."; "Premium.MaxStoriesMonthlyNoPremiumText" = "You have reached the limit of **%@** stories per month."; "Premium.MaxStoriesMonthlyFinalText" = "You have reached the limit of **%@** stories per month."; + +"MediaPicker.Recents" = "Recents"; diff --git a/submodules/ContextUI/Sources/ContextActionNode.swift b/submodules/ContextUI/Sources/ContextActionNode.swift index af50961aee..83918597a5 100644 --- a/submodules/ContextUI/Sources/ContextActionNode.swift +++ b/submodules/ContextUI/Sources/ContextActionNode.swift @@ -131,7 +131,11 @@ public final class ContextActionNode: ASDisplayNode, ContextActionNodeProtocol { self.iconNode.displaysAsynchronously = false self.iconNode.displayWithoutProcessing = true self.iconNode.isUserInteractionEnabled = false - if action.iconSource == nil { + if let iconSource = action.iconSource { + self.iconNode.clipsToBounds = true + self.iconNode.contentMode = iconSource.contentMode + self.iconNode.cornerRadius = iconSource.cornerRadius + } else { self.iconNode.image = action.icon(presentationData.theme) } diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index f13352629f..f1e00f7752 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -60,10 +60,14 @@ public enum ContextMenuActionItemFont { public struct ContextMenuActionItemIconSource { public let size: CGSize + public let contentMode: UIView.ContentMode + public let cornerRadius: CGFloat public let signal: Signal - public init(size: CGSize, signal: Signal) { + public init(size: CGSize, contentMode: UIView.ContentMode = .scaleToFill, cornerRadius: CGFloat = 0.0, signal: Signal) { self.size = size + self.contentMode = contentMode + self.cornerRadius = cornerRadius self.signal = signal } } diff --git a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift index 90b14f7ad4..413322b028 100644 --- a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift @@ -287,6 +287,9 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin let iconSize: CGSize? if let iconSource = self.item.iconSource { iconSize = iconSource.size + self.iconNode.cornerRadius = iconSource.cornerRadius + self.iconNode.contentMode = iconSource.contentMode + self.iconNode.clipsToBounds = true if self.iconDisposable == nil { self.iconDisposable = (iconSource.signal |> deliverOnMainQueue).start(next: { [weak self] image in guard let strongSelf = self else { diff --git a/submodules/MediaPickerUI/Sources/MediaGroupsContextMenuContent.swift b/submodules/MediaPickerUI/Sources/MediaGroupsContextMenuContent.swift new file mode 100644 index 0000000000..25838debc9 --- /dev/null +++ b/submodules/MediaPickerUI/Sources/MediaGroupsContextMenuContent.swift @@ -0,0 +1,438 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import ContextUI +import AccountContext +import TelegramPresentationData +import Photos + +struct MediaGroupItem { + let collection: PHAssetCollection + let firstItem: PHAsset? + let count: Int +} + +final class MediaGroupsContextMenuContent: ContextControllerItemsContent { + private final class GroupsListNode: ASDisplayNode, UIScrollViewDelegate { + private final class ItemNode: HighlightTrackingButtonNode { + let context: AccountContext + let highlightBackgroundNode: ASDisplayNode + let titleLabelNode: ImmediateTextNode + let subtitleLabelNode: ImmediateTextNode + let iconNode: ImageNode + let separatorNode: ASDisplayNode + + let action: () -> Void + + private var item: MediaGroupItem? + + init(context: AccountContext, action: @escaping () -> Void) { + self.action = action + self.context = context + + self.highlightBackgroundNode = ASDisplayNode() + self.highlightBackgroundNode.isAccessibilityElement = false + self.highlightBackgroundNode.alpha = 0.0 + + self.titleLabelNode = ImmediateTextNode() + self.titleLabelNode.isAccessibilityElement = false + self.titleLabelNode.maximumNumberOfLines = 1 + self.titleLabelNode.isUserInteractionEnabled = false + + self.subtitleLabelNode = ImmediateTextNode() + self.subtitleLabelNode.isAccessibilityElement = false + self.subtitleLabelNode.maximumNumberOfLines = 1 + self.subtitleLabelNode.isUserInteractionEnabled = false + + self.iconNode = ImageNode() + self.iconNode.clipsToBounds = true + self.iconNode.contentMode = .scaleAspectFill + self.iconNode.cornerRadius = 6.0 + + self.separatorNode = ASDisplayNode() + self.separatorNode.isAccessibilityElement = false + + super.init() + + self.isAccessibilityElement = true + + self.addSubnode(self.separatorNode) + self.addSubnode(self.highlightBackgroundNode) + self.addSubnode(self.titleLabelNode) + self.addSubnode(self.subtitleLabelNode) + self.addSubnode(self.iconNode) + + self.highligthedChanged = { [weak self] highlighted in + guard let strongSelf = self else { + return + } + if highlighted { + strongSelf.highlightBackgroundNode.alpha = 1.0 + } else { + let previousAlpha = strongSelf.highlightBackgroundNode.alpha + strongSelf.highlightBackgroundNode.alpha = 0.0 + strongSelf.highlightBackgroundNode.layer.animateAlpha(from: previousAlpha, to: 0.0, duration: 0.2) + } + } + + self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) + } + + @objc private func pressed() { + self.action() + } + + func update(size: CGSize, presentationData: PresentationData, item: MediaGroupItem, isLast: Bool, syncronousLoad: Bool) { + let leftInset: CGFloat = 16.0 + let rightInset: CGFloat = 48.0 + + if self.item?.collection.localIdentifier != item.collection.localIdentifier { + self.item = item + + self.accessibilityLabel = item.collection.localizedTitle + + if let asset = item.firstItem { + self.iconNode.setSignal(assetImage(asset: asset, targetSize: CGSize(width: 24.0, height: 24.0), exact: false)) + } + } + + self.highlightBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor + + self.highlightBackgroundNode.frame = CGRect(origin: CGPoint(), size: size) + + self.titleLabelNode.attributedText = NSAttributedString(string: item.collection.localizedTitle ?? "", font: Font.regular(17.0), textColor: presentationData.theme.contextMenu.primaryColor) + + self.subtitleLabelNode.attributedText = NSAttributedString(string: "\(item.count)", font: Font.regular(15.0), textColor: presentationData.theme.contextMenu.secondaryColor) + let maxTextWidth: CGFloat = size.width - leftInset - rightInset + + let titleSize = self.titleLabelNode.updateLayout(CGSize(width: maxTextWidth, height: 100.0)) + let subtitleSize = self.subtitleLabelNode.updateLayout(CGSize(width: maxTextWidth, height: 100.0)) + + let spacing: CGFloat = 2.0 + let contentHeight = titleSize.height + spacing + subtitleSize.height + + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((size.height - contentHeight) / 2.0)), size: titleSize) + self.titleLabelNode.frame = titleFrame + + let subtitleFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + spacing), size: titleSize) + self.subtitleLabelNode.frame = subtitleFrame + + let iconSize = CGSize(width: 24.0, height: 24.0) + let iconFrame = CGRect(origin: CGPoint(x: size.width - leftInset - iconSize.width, y: floor((size.height - iconSize.height) / 2.0)), size: iconSize) + self.iconNode.frame = iconFrame + + self.separatorNode.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor + self.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: size.height), size: CGSize(width: size.width, height: UIScreenPixel)) + self.separatorNode.isHidden = isLast + } + } + + private let context: AccountContext + private let items: [MediaGroupItem] + private let requestUpdate: (GroupsListNode, ContainedViewLayoutTransition) -> Void + private let requestUpdateApparentHeight: (GroupsListNode, ContainedViewLayoutTransition) -> Void + private let selectGroup: (PHAssetCollection) -> Void + + private let scrollNode: ASScrollNode + private var ignoreScrolling: Bool = false + private var animateIn: Bool = false + private var bottomScrollInset: CGFloat = 0.0 + + private var presentationData: PresentationData? + private var currentSize: CGSize? + private var apparentHeight: CGFloat = 0.0 + + private var itemNodes: [Int: ItemNode] = [:] + + init( + context: AccountContext, + items: [MediaGroupItem], + requestUpdate: @escaping (GroupsListNode, ContainedViewLayoutTransition) -> Void, + requestUpdateApparentHeight: @escaping (GroupsListNode, ContainedViewLayoutTransition) -> Void, + selectGroup: @escaping (PHAssetCollection) -> Void + ) { + self.context = context + self.items = items + self.requestUpdate = requestUpdate + self.requestUpdateApparentHeight = requestUpdateApparentHeight + self.selectGroup = selectGroup + + self.scrollNode = ASScrollNode() + self.scrollNode.canCancelAllTouchesInViews = true + self.scrollNode.view.delaysContentTouches = false + self.scrollNode.view.showsVerticalScrollIndicator = false + if #available(iOS 11.0, *) { + self.scrollNode.view.contentInsetAdjustmentBehavior = .never + } + self.scrollNode.clipsToBounds = false + + super.init() + + self.addSubnode(self.scrollNode) + self.scrollNode.view.delegate = self + + self.clipsToBounds = true + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if self.ignoreScrolling { + return + } + self.updateVisibleItems(animated: false, syncronousLoad: false) + + if let size = self.currentSize { + var apparentHeight = -self.scrollNode.view.contentOffset.y + self.scrollNode.view.contentSize.height + apparentHeight = max(apparentHeight, 44.0) + apparentHeight = min(apparentHeight, size.height) + if self.apparentHeight != apparentHeight { + self.apparentHeight = apparentHeight + + self.requestUpdateApparentHeight(self, .immediate) + } + } + } + + private func updateVisibleItems(animated: Bool, syncronousLoad: Bool) { + guard let size = self.currentSize else { + return + } + guard let presentationData = self.presentationData else { + return + } + let itemHeight: CGFloat = 54.0 + let visibleBounds = self.scrollNode.bounds.insetBy(dx: 0.0, dy: -180.0) + + var validIds = Set() + + let minVisibleIndex = max(0, Int(floor(visibleBounds.minY / itemHeight))) + let maxVisibleIndex = Int(ceil(visibleBounds.maxY / itemHeight)) + + if minVisibleIndex <= maxVisibleIndex { + for index in minVisibleIndex ... maxVisibleIndex { + if index < self.items.count { + let height = itemHeight + let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: CGFloat(index) * itemHeight), size: CGSize(width: size.width, height: height)) + + let item = self.items[index] + validIds.insert(index) + + let itemNode: ItemNode + if let current = self.itemNodes[index] { + itemNode = current + } else { + let selectGroup = self.selectGroup + itemNode = ItemNode(context: self.context, action: { + selectGroup(item.collection) + }) + self.itemNodes[index] = itemNode + self.scrollNode.addSubnode(itemNode) + } + + itemNode.update(size: itemFrame.size, presentationData: presentationData, item: item, isLast: index == self.items.count - 1, syncronousLoad: syncronousLoad) + itemNode.frame = itemFrame + } + } + } + + var removeIds: [Int] = [] + for (id, itemNode) in self.itemNodes { + if !validIds.contains(id) { + removeIds.append(id) + itemNode.removeFromSupernode() + } + } + for id in removeIds { + self.itemNodes.removeValue(forKey: id) + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + var extendedScrollNodeFrame = self.scrollNode.frame + extendedScrollNodeFrame.size.height += self.bottomScrollInset + + if extendedScrollNodeFrame.contains(point) { + return self.scrollNode.view.hitTest(self.view.convert(point, to: self.scrollNode.view), with: event) + } + + return super.hitTest(point, with: event) + } + + func update(presentationData: PresentationData, constrainedSize: CGSize, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> (height: CGFloat, apparentHeight: CGFloat) { + let itemHeight: CGFloat = 54.0 + + self.presentationData = presentationData + + let contentHeight = CGFloat(self.items.count) * itemHeight + let size = CGSize(width: constrainedSize.width, height: contentHeight) + + let containerSize = CGSize(width: size.width, height: min(constrainedSize.height, size.height)) + self.currentSize = containerSize + + self.ignoreScrolling = true + + if self.scrollNode.frame != CGRect(origin: CGPoint(), size: containerSize) { + self.scrollNode.frame = CGRect(origin: CGPoint(), size: containerSize) + } + if self.scrollNode.view.contentInset.bottom != bottomInset { + self.scrollNode.view.contentInset.bottom = bottomInset + } + self.bottomScrollInset = bottomInset + let scrollContentSize = CGSize(width: size.width, height: size.height) + if self.scrollNode.view.contentSize != scrollContentSize { + self.scrollNode.view.contentSize = scrollContentSize + } + self.ignoreScrolling = false + + self.updateVisibleItems(animated: transition.isAnimated, syncronousLoad: !transition.isAnimated) + + self.animateIn = false + + var apparentHeight = -self.scrollNode.view.contentOffset.y + self.scrollNode.view.contentSize.height + apparentHeight = max(apparentHeight, 44.0) + apparentHeight = min(apparentHeight, containerSize.height) + self.apparentHeight = apparentHeight + + return (containerSize.height, apparentHeight) + } + } + + final class ItemsNode: ASDisplayNode, ContextControllerItemsNode { + private let context: AccountContext + private let items: [MediaGroupItem] + private let requestUpdate: (ContainedViewLayoutTransition) -> Void + private let requestUpdateApparentHeight: (ContainedViewLayoutTransition) -> Void + + private var presentationData: PresentationData + + private let currentTabIndex: Int = 0 + private var visibleTabNodes: [Int: GroupsListNode] = [:] + + private let selectGroup: (PHAssetCollection) -> Void + + private(set) var apparentHeight: CGFloat = 0.0 + + init( + context: AccountContext, + items: [MediaGroupItem], + requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void, + requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void, + selectGroup: @escaping (PHAssetCollection) -> Void + ) { + self.context = context + self.items = items + self.selectGroup = selectGroup + self.presentationData = context.sharedContext.currentPresentationData.with({ $0 }) + + self.requestUpdate = requestUpdate + self.requestUpdateApparentHeight = requestUpdateApparentHeight + + super.init() + } + + func update(presentationData: PresentationData, constrainedWidth: CGFloat, maxHeight: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> (cleanSize: CGSize, apparentHeight: CGFloat) { + let constrainedSize = CGSize(width: min(190.0, constrainedWidth), height: min(295.0, maxHeight)) + + let topContentHeight: CGFloat = 0.0 + + var tabLayouts: [Int: (height: CGFloat, apparentHeight: CGFloat)] = [:] + + var visibleIndices: [Int] = [] + visibleIndices.append(self.currentTabIndex) + + let previousVisibleTabFrames: [(Int, CGRect)] = self.visibleTabNodes.map { key, value -> (Int, CGRect) in + return (key, value.frame) + } + + for index in visibleIndices { + var tabTransition = transition + let tabNode: GroupsListNode + var initialReferenceFrame: CGRect? + if let current = self.visibleTabNodes[index] { + tabNode = current + } else { + for (previousIndex, previousFrame) in previousVisibleTabFrames { + if index > previousIndex { + initialReferenceFrame = previousFrame.offsetBy(dx: constrainedSize.width, dy: 0.0) + } else { + initialReferenceFrame = previousFrame.offsetBy(dx: -constrainedSize.width, dy: 0.0) + } + break + } + + tabNode = GroupsListNode( + context: self.context, + items: self.items, + requestUpdate: { [weak self] tab, transition in + guard let strongSelf = self else { + return + } + if strongSelf.visibleTabNodes.contains(where: { $0.value === tab }) { + strongSelf.requestUpdate(transition) + } + }, + requestUpdateApparentHeight: { [weak self] tab, transition in + guard let strongSelf = self else { + return + } + if strongSelf.visibleTabNodes.contains(where: { $0.value === tab }) { + strongSelf.requestUpdateApparentHeight(transition) + } + }, + selectGroup: self.selectGroup + ) + self.addSubnode(tabNode) + self.visibleTabNodes[index] = tabNode + tabTransition = .immediate + } + + let tabLayout = tabNode.update(presentationData: presentationData, constrainedSize: CGSize(width: constrainedSize.width, height: constrainedSize.height - topContentHeight), bottomInset: bottomInset, transition: tabTransition) + tabLayouts[index] = tabLayout + let currentFractionalTabIndex = CGFloat(self.currentTabIndex) + let xOffset: CGFloat = (CGFloat(index) - currentFractionalTabIndex) * constrainedSize.width + let tabFrame = CGRect(origin: CGPoint(x: xOffset, y: topContentHeight), size: CGSize(width: constrainedSize.width, height: tabLayout.height)) + tabTransition.updateFrame(node: tabNode, frame: tabFrame) + if let initialReferenceFrame = initialReferenceFrame { + transition.animatePositionAdditive(node: tabNode, offset: CGPoint(x: initialReferenceFrame.minX - tabFrame.minX, y: 0.0)) + } + } + + var contentSize = CGSize(width: constrainedSize.width, height: topContentHeight) + var apparentHeight = topContentHeight + + if let tabLayout = tabLayouts[self.currentTabIndex] { + contentSize.height += tabLayout.height + apparentHeight += tabLayout.apparentHeight + } + + return (contentSize, apparentHeight) + } + } + + let context: AccountContext + let items: [MediaGroupItem] + let selectGroup: (PHAssetCollection) -> Void + + public init( + context: AccountContext, + items: [MediaGroupItem], + selectGroup: @escaping (PHAssetCollection) -> Void + ) { + self.context = context + self.items = items + self.selectGroup = selectGroup + } + + func node( + requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void, + requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void + ) -> ContextControllerItemsNode { + return ItemsNode( + context: self.context, + items: self.items, + requestUpdate: requestUpdate, + requestUpdateApparentHeight: requestUpdateApparentHeight, + selectGroup: self.selectGroup + ) + } +} diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 635916808d..174d9a67c6 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -186,6 +186,8 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { public var isContainerPanning: () -> Bool = { return false } public var isContainerExpanded: () -> Bool = { return false } + private let selectedCollection = Promise(nil) + var dismissAll: () -> Void = { } private class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate { @@ -237,7 +239,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { private var fastScrollContentOffset = ValuePromise(ignoreRepeated: true) private var fastScrollDisposable: Disposable? - + private var didSetReady = false private let _ready = Promise() var ready: Promise { @@ -284,6 +286,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { 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 { @@ -301,15 +304,19 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } else if [.restricted, .denied].contains(mediaAccess) { return .single(.noAccess(cameraAccess: cameraAccess)) } else { - if let collection = collection { - return combineLatest(mediaAssetsContext.fetchAssets(collection), preloadPromise.get()) - |> map { fetchResult, preload in - return .assets(fetchResult: fetchResult, preload: preload, drafts: [], mediaAccess: mediaAccess, cameraAccess: 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) + 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) + } } } } @@ -1455,8 +1462,12 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self.titleView.title = collection.localizedTitle ?? presentationData.strings.Attachment_Gallery } else { switch mode { - case .default, .story: - self.titleView.title = presentationData.strings.Attachment_Gallery + case .default: + 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: @@ -1527,6 +1538,12 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { strongSelf.controllerNode.updateDisplayMode(index == 0 ? .all : .selected) } } + + self.titleView.action = { [weak self] in + if let self { + self.openGroupsMenu() + } + } self.navigationItem.titleView = self.titleView @@ -1703,6 +1720,114 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self.controllerNode.closeGalleryController() } + public func openGroupsMenu() { + let updatedState = combineLatest( + queue: Queue.mainQueue(), + self.controllerNode.mediaAssetsContext.fetchAssetsCollections(.album), + self.controllerNode.mediaAssetsContext.fetchAssetsCollections(.smartAlbum) + ) + let _ = (updatedState + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] albums, smartAlbums in + guard let self else { + return + } + + 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 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 + 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 + } +// let iconSource: ContextMenuActionItemIconSource? +// if let firstItem { +// let targetSize = CGSize(width: 24.0, height: 24.0) +// iconSource = ContextMenuActionItemIconSource(size: CGSize(width: 24.0, height: 24.0), contentMode: .scaleAspectFill, cornerRadius: 6.0, signal: assetImage(asset: firstItem, targetSize: targetSize, exact: false)) +// } else { +// iconSource = nil +// } + items.append( + MediaGroupItem( + collection: collection, + firstItem: firstItem, + count: result.count + ) + ) + } + + var dismissImpl: (() -> Void)? + let content: ContextControllerItemsContent = MediaGroupsContextMenuContent( + context: self.context, + items: items, + selectGroup: { [weak self] collection in + guard let self else { + return + } + 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 ?? "" + } + dismissImpl?() + } + ) + + self.titleView.isHighlighted = true + let contextController = ContextController( + account: self.context.account, + 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) diff --git a/submodules/MediaPickerUI/Sources/MediaPickerTitleView.swift b/submodules/MediaPickerUI/Sources/MediaPickerTitleView.swift index 7fe0210eeb..de02ec5ed3 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerTitleView.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerTitleView.swift @@ -4,9 +4,13 @@ import AsyncDisplayKit import Display import TelegramPresentationData import SegmentedControlNode +import ContextUI final class MediaPickerTitleView: UIView { + let contextSourceNode: ContextReferenceContentNode + private let buttonNode: HighlightTrackingButtonNode private let titleNode: ImmediateTextNode + private let arrowNode: ASImageNode private let segmentedControlNode: SegmentedControlNode public var theme: PresentationTheme { @@ -25,6 +29,19 @@ final class MediaPickerTitleView: UIView { } } + public var isEnabled: Bool = false { + didSet { + self.buttonNode.isUserInteractionEnabled = self.isEnabled + self.arrowNode.isHidden = !self.isEnabled + } + } + + public var isHighlighted: Bool = false { + didSet { + self.alpha = self.isHighlighted ? 0.5 : 1.0 + } + } + public var segmentsHidden = true { didSet { if self.segmentsHidden != oldValue { @@ -55,14 +72,23 @@ final class MediaPickerTitleView: UIView { } public var indexUpdated: ((Int) -> Void)? + public var action: () -> Void = {} public init(theme: PresentationTheme, segments: [String], selectedIndex: Int) { self.theme = theme self.segments = segments + self.contextSourceNode = ContextReferenceContentNode() + self.buttonNode = HighlightTrackingButtonNode() + self.titleNode = ImmediateTextNode() self.titleNode.displaysAsynchronously = false + self.arrowNode = ASImageNode() + self.arrowNode.displaysAsynchronously = false + self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Stories/SelectorArrowDown"), color: theme.rootController.navigationBar.secondaryTextColor) + self.arrowNode.isHidden = true + self.segmentedControlNode = SegmentedControlNode(theme: SegmentedControlTheme(theme: theme), items: segments.map { SegmentedControlItem(title: $0) }, selectedIndex: selectedIndex) self.segmentedControlNode.alpha = 0.0 self.segmentedControlNode.isUserInteractionEnabled = false @@ -73,8 +99,26 @@ final class MediaPickerTitleView: UIView { self?.indexUpdated?(index) } + self.buttonNode.highligthedChanged = { [weak self] highlighted in + guard let self else { + return + } + if highlighted { + self.arrowNode.alpha = 0.5 + self.titleNode.alpha = 0.5 + } else { + self.arrowNode.alpha = 1.0 + self.titleNode.alpha = 1.0 + } + } + + self.addSubnode(self.contextSourceNode) self.addSubnode(self.titleNode) + self.addSubnode(self.arrowNode) + self.addSubnode(self.buttonNode) self.addSubnode(self.segmentedControlNode) + + self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) } required public init?(coder aDecoder: NSCoder) { @@ -85,10 +129,21 @@ final class MediaPickerTitleView: UIView { super.layoutSubviews() let size = self.bounds.size + self.contextSourceNode.frame = self.bounds.insetBy(dx: 0.0, dy: 14.0) + let controlSize = self.segmentedControlNode.updateLayout(.stretchToFill(width: min(300.0, size.width - 36.0)), transition: .immediate) self.segmentedControlNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - controlSize.width) / 2.0), y: floorToScreenPixels((size.height - controlSize.height) / 2.0)), size: controlSize) let titleSize = self.titleNode.updateLayout(CGSize(width: 210.0, height: 44.0)) self.titleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize) + + if let arrowSize = self.arrowNode.image?.size { + self.arrowNode.frame = CGRect(origin: CGPoint(x: self.titleNode.frame.maxX + 5.0, y: floorToScreenPixels((size.height - arrowSize.height) / 2.0) + 1.0 - UIScreenPixel), size: arrowSize) + } + self.buttonNode.frame = CGRect(origin: .zero, size: size) + } + + @objc private func buttonPressed() { + self.action() } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index 77fded820a..8084b7b4f8 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -570,7 +570,10 @@ public final class MediaEditor { } else if case .forceRendering = mode { self.forceRendering = true } - self.values = f(self.values) + let updatedValues = f(self.values) + if self.values != updatedValues { + self.values = updatedValues + } if case .skipRendering = mode { self.skipRendering = false } else if case .forceRendering = mode {