Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "MediaPickerUI",
module_name = "MediaPickerUI",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
"//submodules/AccountContext:AccountContext",
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
"//submodules/AppBundle:AppBundle",
"//submodules/CheckNode:CheckNode",
"//submodules/MergeLists:MergeLists",
"//submodules/LegacyComponents:LegacyComponents",
"//submodules/LegacyUI:LegacyUI",
"//submodules/LegacyMediaPickerUI:LegacyMediaPickerUI",
"//submodules/AttachmentUI:AttachmentUI",
"//submodules/SegmentedControlNode:SegmentedControlNode",
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
"//submodules/PhotoResources:PhotoResources",
"//submodules/ContextUI:ContextUI",
"//submodules/MosaicLayout:MosaicLayout",
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/WallpaperBackgroundNode:WallpaperBackgroundNode",
"//submodules/WebSearchUI:WebSearchUI",
"//submodules/ChatMessageBackground:ChatMessageBackground",
"//submodules/SparseItemGrid:SparseItemGrid",
"//submodules/UndoUI:UndoUI",
"//submodules/MoreButtonNode:MoreButtonNode",
"//submodules/InvisibleInkDustNode:InvisibleInkDustNode",
"//submodules/TelegramUI/Components/MediaEditor",
"//submodules/RadialStatusNode",
"//submodules/Camera",
"//submodules/TelegramUI/Components/MediaEditor/ImageObjectSeparation",
"//submodules/ChatSendMessageActionUI",
"//submodules/ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/AnimatedCountLabelNode",
"//submodules/TelegramUI/Components/MediaAssetsContext",
"//submodules/TelegramUI/Components/AvatarBackground",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
"//submodules/TelegramUI/Components/EdgeEffect",
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
"//submodules/TelegramUI/Components/LottieComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,191 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import Postbox
import AvatarBackground
import AccountContext
import EmojiTextAttachmentView
import TextFormat
import ComponentFlow
import MultilineTextComponent
final class AvatarEditorPreviewView: UIView {
private let context: AccountContext
private var disposable: Disposable?
private var files: [TelegramMediaFile] = []
private var currentIndex = 0
private var currentBackgroundIndex = 0
private var switchingToNext = false
private let backgroundView = UIImageView()
private let label = ComponentView<Empty>()
private var animationLayer: InlineStickerItemLayer?
private var preloadDisposableSet = DisposableSet()
private var timer: SwiftSignalKit.Timer?
private var currentSize: CGSize?
var tapped: () -> Void = {}
init(context: AccountContext) {
self.context = context
super.init(frame: .zero)
self.addSubview(self.backgroundView)
let stickersKey: PostboxViewKey = .orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedProfilePhotoEmoji)
self.disposable = (context.account.postbox.combinedView(keys: [stickersKey])
|> runOn(Queue.concurrentDefaultQueue())
|> deliverOnMainQueue).start(next: { [weak self] views in
guard let self else {
return
}
if let view = views.views[stickersKey] as? OrderedItemListView {
var files: [TelegramMediaFile] = []
for item in view.items.prefix(8) {
if let mediaItem = item.contents.get(RecentMediaItem.self) {
let file = mediaItem.media._parse()
files.append(file)
self.preloadDisposableSet.add(freeMediaFileResourceInteractiveFetched(account: context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start())
}
}
self.files = files
if let size = self.currentSize {
self.updateLayout(size: size)
}
}
})
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap))
self.addGestureRecognizer(tapRecognizer)
}
required init?(coder: NSCoder) {
preconditionFailure()
}
deinit {
self.disposable?.dispose()
self.preloadDisposableSet.dispose()
self.timer?.invalidate()
}
@objc private func handleTap() {
self.tapped()
}
func updateLayout(size: CGSize) {
self.currentSize = size
self.backgroundView.frame = CGRect(origin: .zero, size: size)
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let labelSize = self.label.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(
string: presentationData.strings.MediaPicker_UseAnEmoji,
font: Font.semibold(12.0),
textColor: .white
)),
textShadowColor: UIColor(white: 0.0, alpha: 0.3),
textShadowBlur: 3.0
)
),
environment: {},
containerSize: size
)
if let view = self.label.view {
if view.superview == nil {
self.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: floor((size.width - labelSize.width) / 2.0), y: size.height - labelSize.height - 22.0), size: labelSize)
}
guard !self.files.isEmpty else {
if self.backgroundView.image == nil {
self.backgroundView.image = AvatarBackground.defaultBackgrounds[self.currentBackgroundIndex].generateImage(size: size)
}
return
}
if self.timer == nil {
self.timer = SwiftSignalKit.Timer(timeout: 2.0, repeat: true, completion: { [weak self] in
guard let self else {
return
}
self.switchingToNext = true
if let size = self.currentSize {
self.updateLayout(size: size)
}
}, queue: Queue.mainQueue())
self.timer?.start()
}
let iconSize = CGSize(width: 64.0, height: 64.0)
let animationLayer: InlineStickerItemLayer
var disappearingAnimationLayer: InlineStickerItemLayer?
if let current = self.animationLayer, !self.switchingToNext {
animationLayer = current
} else {
if self.switchingToNext {
self.currentIndex = (self.currentIndex + 1) % self.files.count
self.currentBackgroundIndex = (self.currentBackgroundIndex + 1) % AvatarBackground.defaultBackgrounds.count
disappearingAnimationLayer = self.animationLayer
self.switchingToNext = false
}
if let image = self.backgroundView.image {
let snapshotView = UIImageView(image: image)
self.insertSubview(snapshotView, aboveSubview: self.backgroundView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
snapshotView.removeFromSuperview()
})
}
self.backgroundView.image = AvatarBackground.defaultBackgrounds[self.currentBackgroundIndex].generateImage(size: size)
let file = self.files[self.currentIndex]
let emoji = ChatTextInputTextCustomEmojiAttribute(
interactivelySelectedFromPackId: nil,
fileId: file.fileId.id,
file: file
)
animationLayer = InlineStickerItemLayer(
context: .account(self.context),
userLocation: .other,
attemptSynchronousLoad: false,
emoji: emoji,
file: file,
cache: self.context.animationCache,
renderer: self.context.animationRenderer,
unique: true,
placeholderColor: UIColor(white: 1.0, alpha: 0.1),
pointSize: iconSize,
loopCount: 1
)
animationLayer.isVisibleForAnimations = true
self.layer.addSublayer(animationLayer)
self.animationLayer = animationLayer
animationLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
animationLayer.animatePosition(from: CGPoint(x: 0.0, y: 10.0), to: .zero, duration: 0.2, additive: true)
animationLayer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
}
animationLayer.frame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0) - 10.0), size: iconSize)
if let disappearingAnimationLayer {
disappearingAnimationLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
disappearingAnimationLayer.removeFromSuperlayer()
})
disappearingAnimationLayer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: -10.0), duration: 0.2, removeOnCompletion: false, additive: true)
disappearingAnimationLayer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
}
}
}
@@ -0,0 +1,378 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import Postbox
import SSignalKit
import TelegramPresentationData
import AccountContext
import LegacyComponents
import LegacyUI
import LegacyMediaPickerUI
import Photos
import MediaAssetsContext
private func galleryFetchResultItems(fetchResult: PHFetchResult<PHAsset>, index: Int, reversed: Bool, selectionContext: TGMediaSelectionContext?, editingContext: TGMediaEditingContext, stickersContext: TGPhotoPaintStickersContext, immediateThumbnail: UIImage?) -> ([TGModernGalleryItem], TGModernGalleryItem?) {
var focusItem: TGModernGalleryItem?
var galleryItems: [TGModernGalleryItem] = []
let legacyFetchResult = TGMediaAssetFetchResult(phFetchResult: fetchResult as? PHFetchResult<AnyObject>, reversed: reversed)
for i in 0 ..< fetchResult.count {
if let galleryItem = TGMediaPickerGalleryFetchResultItem(fetchResult: legacyFetchResult, index: UInt(i)) {
galleryItem.selectionContext = selectionContext
galleryItem.editingContext = editingContext
galleryItem.stickersContext = stickersContext
galleryItems.append(galleryItem)
if i == index {
galleryItem.immediateThumbnailImage = immediateThumbnail
focusItem = galleryItem
}
}
}
return (galleryItems, focusItem)
}
private func gallerySelectionItems(item: TGMediaSelectableItem, selectionContext: TGMediaSelectionContext?, editingContext: TGMediaEditingContext, stickersContext: TGPhotoPaintStickersContext, immediateThumbnail: UIImage?) -> ([TGModernGalleryItem], TGModernGalleryItem?) {
var focusItem: TGModernGalleryItem?
var galleryItems: [TGModernGalleryItem] = []
if let selectionContext = selectionContext {
for case let selectedItem as TGMediaSelectableItem in selectionContext.selectedItems() {
if let asset = selectedItem as? TGMediaAsset {
let galleryItem: (TGModernGallerySelectableItem & TGModernGalleryEditableItem)
switch asset.type {
case TGMediaAssetVideoType:
galleryItem = TGMediaPickerGalleryVideoItem(asset: asset)
case TGMediaAssetGifType:
let convertedAsset = TGCameraCapturedVideo(asset: asset, livePhoto: false)
galleryItem = TGMediaPickerGalleryVideoItem(asset: convertedAsset)
default:
galleryItem = TGMediaPickerGalleryPhotoItem(asset: asset)
}
galleryItem.selectionContext = selectionContext
galleryItem.editingContext = editingContext
galleryItem.stickersContext = stickersContext
galleryItems.append(galleryItem)
if selectedItem.uniqueIdentifier == item.uniqueIdentifier {
if let galleryItem = galleryItem as? TGMediaPickerGalleryItem {
galleryItem.immediateThumbnailImage = immediateThumbnail
}
focusItem = galleryItem
}
} else if let asset = selectedItem as? UIImage {
let galleryItem: (TGModernGallerySelectableItem & TGModernGalleryEditableItem) = TGMediaPickerGalleryPhotoItem(asset: asset)
galleryItem.selectionContext = selectionContext
galleryItem.editingContext = editingContext
galleryItem.stickersContext = stickersContext
galleryItems.append(galleryItem)
if selectedItem.uniqueIdentifier == item.uniqueIdentifier {
if let galleryItem = galleryItem as? TGMediaPickerGalleryItem {
galleryItem.immediateThumbnailImage = immediateThumbnail
}
focusItem = galleryItem
}
} else if let asset = selectedItem as? TGCameraCapturedVideo {
let galleryItem: (TGModernGallerySelectableItem & TGModernGalleryEditableItem) = TGMediaPickerGalleryVideoItem(asset: asset)
galleryItem.selectionContext = selectionContext
galleryItem.editingContext = editingContext
galleryItem.stickersContext = stickersContext
galleryItems.append(galleryItem)
if selectedItem.uniqueIdentifier == item.uniqueIdentifier {
if let galleryItem = galleryItem as? TGMediaPickerGalleryItem {
galleryItem.immediateThumbnailImage = immediateThumbnail
}
focusItem = galleryItem
}
}
}
}
return (galleryItems, focusItem)
}
enum LegacyMediaPickerGallerySource {
case fetchResult(fetchResult: PHFetchResult<PHAsset>, index: Int, reversed: Bool)
case selection(item: TGMediaSelectableItem)
}
func presentLegacyMediaPickerGallery(context: AccountContext, peer: EnginePeer?, threadTitle: String?, chatLocation: ChatLocation?, isScheduledMessages: Bool, presentationData: PresentationData, source: LegacyMediaPickerGallerySource, immediateThumbnail: UIImage?, selectionContext: TGMediaSelectionContext?, editingContext: TGMediaEditingContext, hasSilentPosting: Bool, hasSchedule: Bool, hasTimer: Bool, updateHiddenMedia: @escaping (String?) -> Void, initialLayout: ContainerViewLayout?, transitionHostView: @escaping () -> UIView?, transitionView: @escaping (String) -> UIView?, completed: @escaping (TGMediaSelectableItem & TGMediaEditableItem, Bool, Int32?, @escaping () -> Void) -> Void, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, present: @escaping (ViewController, Any?) -> Void, finishedTransitionIn: @escaping () -> Void, willTransitionOut: @escaping () -> Void, dismissAll: @escaping () -> Void, editCover: @escaping (CGSize, @escaping (UIImage) -> Void) -> Void = { _, _ in }) -> TGModernGalleryController {
let reminder = peer?.id == context.account.peerId
let hasSilentPosting = hasSilentPosting && peer?.id != context.account.peerId
var hasCoverButton = false
if case let .channel(channel) = peer, case .broadcast = channel.info {
hasCoverButton = true
} else if peer?.id == context.account.peerId {
hasCoverButton = true
}
let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme, initialLayout: nil)
legacyController.statusBar.statusBarStyle = presentationData.theme.rootController.statusBarStyle.style
let paintStickersContext = LegacyPaintStickersContext(context: context)
paintStickersContext.captionPanelView = {
return getCaptionPanelView()
}
paintStickersContext.editCover = { dimensions, completion in
editCover(dimensions, completion)
}
let controller = TGModernGalleryController(context: legacyController.context)!
controller.asyncTransitionIn = true
legacyController.bind(controller: controller)
let (items, focusItem): ([TGModernGalleryItem], TGModernGalleryItem?)
switch source {
case let .fetchResult(fetchResult, index, reversed):
(items, focusItem) = galleryFetchResultItems(fetchResult: fetchResult, index: index, reversed: reversed, selectionContext: selectionContext, editingContext: editingContext, stickersContext: paintStickersContext, immediateThumbnail: immediateThumbnail)
case let .selection(item):
(items, focusItem) = gallerySelectionItems(item: item, selectionContext: selectionContext, editingContext: editingContext, stickersContext: paintStickersContext, immediateThumbnail: immediateThumbnail)
}
let recipientName: String?
if let threadTitle {
recipientName = threadTitle
} else {
if peer?.id == context.account.peerId {
recipientName = presentationData.strings.DialogList_SavedMessages
} else {
recipientName = peer?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
}
}
let model = TGMediaPickerGalleryModel(context: legacyController.context, items: items, focus: focusItem, selectionContext: selectionContext, editingContext: editingContext, hasCaptions: true, allowCaptionEntities: true, hasTimer: hasTimer, onlyCrop: false, inhibitDocumentCaptions: false, hasSelectionPanel: true, hasCamera: false, recipientName: recipientName, isScheduledMessages: isScheduledMessages, hasCoverButton: hasCoverButton)!
model.stickersContext = paintStickersContext
controller.model = model
model.controller = controller
model.willFinishEditingItem = { item, adjustments, representation, hasChanges in
if hasChanges {
editingContext.setAdjustments(adjustments, for: item)
editingContext.setTemporaryRep(representation, for: item)
}
if let selectionContext = selectionContext, adjustments != nil, let item = item as? TGMediaSelectableItem {
selectionContext.setItem(item, selected: true)
}
}
model.didFinishEditingItem = { item, adjustments, result, thumbnail in
editingContext.setImage(result, thumbnailImage: thumbnail, for: item, synchronous: false)
}
model.saveItemCaption = { item, caption in
editingContext.setCaption(caption, for: item)
if let selectionContext = selectionContext, let caption = caption, caption.length > 0, let item = item as? TGMediaSelectableItem {
selectionContext.setItem(item, selected: true)
}
}
model.didFinishRenderingFullSizeImage = { item, result in
editingContext.setFullSizeImage(result, for: item)
}
model.requestAdjustments = { item in
return editingContext.adjustments(for: item)
}
if let selectionContext = selectionContext {
model.interfaceView.updateSelectionInterface(selectionContext.count(), counterVisible: selectionContext.count() > 0, animated: false)
}
controller.transitionHost = {
return transitionHostView()
}
var transitionedIn = false
controller.itemFocused = { item in
if let item = item as? TGMediaPickerGalleryItem, transitionedIn {
updateHiddenMedia(item.asset.uniqueIdentifier)
}
}
controller.beginTransitionIn = { item, itemView in
if let item = item as? TGMediaPickerGalleryItem {
if let itemView = itemView as? TGMediaPickerGalleryVideoItemView {
itemView.setIsCurrent(true)
}
return transitionView(item.asset.uniqueIdentifier)
} else {
return nil
}
}
controller.startedTransitionIn = {
transitionedIn = true
if let focusItem = focusItem as? TGModernGallerySelectableItem {
updateHiddenMedia(focusItem.selectableMediaItem().uniqueIdentifier)
}
}
controller.beginTransitionOut = { item, itemView in
willTransitionOut()
if let item = item as? TGMediaPickerGalleryItem {
if let itemView = itemView as? TGMediaPickerGalleryVideoItemView {
itemView.stop()
}
return transitionView(item.asset.uniqueIdentifier)
} else {
return nil
}
}
controller.finishedTransitionIn = { [weak model] _, _ in
model?.interfaceView.setSelectedItemsModel(model?.selectedItemsModel)
finishedTransitionIn()
}
controller.completedTransitionOut = { [weak legacyController] in
updateHiddenMedia(nil)
legacyController?.dismiss()
}
model.interfaceView.donePressed = { [weak controller] item in
if let item = item as? TGMediaPickerGalleryItem {
completed(item.asset, false, nil, {
controller?.dismissWhenReady(animated: true)
dismissAll()
})
}
}
if !isScheduledMessages && peer != nil {
model.interfaceView.doneLongPressed = { [weak selectionContext, weak editingContext, weak legacyController, weak model] item in
if let legacyController = legacyController, let item = item as? TGMediaPickerGalleryItem, let model = model, let selectionContext = selectionContext {
var effectiveHasSchedule = hasSchedule
if let editingContext = editingContext {
if let timer = editingContext.timer(for: item.asset)?.intValue, timer > 0 {
effectiveHasSchedule = false
}
for item in selectionContext.selectedItems() {
if let editableItem = item as? TGMediaEditableItem, let timer = editingContext.timer(for: editableItem)?.intValue, timer > 0 {
effectiveHasSchedule = false
break
}
}
}
let sendWhenOnlineAvailable: Signal<Bool, NoError>
if let peer {
if case .secretChat = peer {
effectiveHasSchedule = false
}
sendWhenOnlineAvailable = context.account.viewTracker.peerView(peer.id)
|> take(1)
|> map { peerView -> Bool in
guard let peer = peerViewMainPeer(peerView) else {
return false
}
var sendWhenOnlineAvailable = false
if let presence = peerView.peerPresences[peer.id] as? TelegramUserPresence, case let .present(until) = presence.status {
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
if currentTime > until {
sendWhenOnlineAvailable = true
}
}
if peer.id.namespace == Namespaces.Peer.CloudUser && peer.id.id._internalGetInt64Value() == 777000 {
sendWhenOnlineAvailable = false
}
return sendWhenOnlineAvailable
}
} else {
sendWhenOnlineAvailable = .single(false)
}
let _ = (sendWhenOnlineAvailable
|> take(1)
|> deliverOnMainQueue).start(next: { sendWhenOnlineAvailable in
let legacySheetController = LegacyController(presentation: .custom, theme: presentationData.theme, initialLayout: nil)
let sheetController = TGMediaPickerSendActionSheetController(context: legacyController.context, isDark: true, sendButtonFrame: model.interfaceView.doneButtonFrame, canSendSilently: hasSilentPosting, canSendWhenOnline: sendWhenOnlineAvailable && effectiveHasSchedule, canSchedule: effectiveHasSchedule, reminder: reminder, hasTimer: hasTimer)
let dismissImpl = { [weak model] in
model?.dismiss(true, false)
dismissAll()
}
sheetController.send = {
completed(item.asset, false, nil, {
dismissImpl()
})
}
sheetController.sendSilently = { [weak model] in
model?.interfaceView.onDismiss()
completed(item.asset, true, nil, {
dismissImpl()
})
}
sheetController.sendWhenOnline = {
completed(item.asset, false, scheduleWhenOnlineTimestamp, {
dismissImpl()
})
}
sheetController.schedule = {
presentSchedulePicker(true, { time in
completed(item.asset, false, time, {
dismissImpl()
})
})
}
sheetController.sendWithTimer = {
presentTimerPicker { time in
var items = selectionContext.selectedItems() ?? []
items.append(item.asset as Any)
for case let item as TGMediaEditableItem in items {
editingContext?.setTimer(time as NSNumber, for: item)
}
completed(item.asset, false, nil, {
dismissImpl()
})
}
}
sheetController.customDismissBlock = { [weak legacySheetController] in
legacySheetController?.dismiss()
}
legacySheetController.bind(controller: sheetController)
present(legacySheetController, nil)
let hapticFeedback = HapticFeedback()
hapticFeedback.impact()
})
}
}
}
model.interfaceView.setThumbnailSignalForItem { item in
let imageSignal = SSignal(generator: { subscriber in
var asset: PHAsset?
if let item = item as? TGCameraCapturedVideo, item.originalAsset != nil {
asset = item.originalAsset.backingAsset
} else if let item = item as? TGMediaAsset {
asset = item.backingAsset
}
var disposable: Disposable?
if let asset = asset {
let scale = min(2.0, UIScreenScale)
disposable = assetImage(asset: asset, targetSize: CGSize(width: 128.0 * scale, height: 128.0 * scale), exact: false).start(next: { image in
subscriber.putNext(image)
}, completed: {
subscriber.putCompletion()
})
} else {
subscriber.putCompletion()
}
return SBlockDisposable(block: {
disposable?.dispose()
})
})
if let item = item as? TGMediaEditableItem {
return editingContext.thumbnailImageSignal(for: item).map(toSignal: { result in
if let result = result {
return SSignal.single(result)
} else {
return imageSignal
}
})
} else {
return imageSignal
}
}
present(legacyController, nil)
return controller
}
@@ -0,0 +1,377 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
import MergeLists
import Photos
import MediaAssetsContext
private struct MediaGroupsGridAlbumEntry: Comparable, Identifiable {
let theme: PresentationTheme
let index: Int
let collection: PHAssetCollection
let firstItem: PHAsset?
let count: String
var stableId: String {
return self.collection.localIdentifier
}
static func ==(lhs: MediaGroupsGridAlbumEntry, rhs: MediaGroupsGridAlbumEntry) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.index != rhs.index {
return false
}
if lhs.collection != rhs.collection {
return false
}
if lhs.firstItem != rhs.firstItem {
return false
}
if lhs.count != rhs.count {
return false
}
return true
}
static func <(lhs: MediaGroupsGridAlbumEntry, rhs: MediaGroupsGridAlbumEntry) -> Bool {
return lhs.index < rhs.index
}
func item(action: @escaping (PHAssetCollection) -> Void) -> ListViewItem {
return MediaGroupsGridAlbumItem(theme: theme, collection: self.collection, firstItem: self.firstItem, count: self.count, action: action)
}
}
private class MediaGroupsGridAlbumItem: ListViewItem {
let theme: PresentationTheme
let collection: PHAssetCollection
let firstItem: PHAsset?
let count: String
let action: (PHAssetCollection) -> Void
public init(theme: PresentationTheme, collection: PHAssetCollection, firstItem: PHAsset?, count: String, action: @escaping (PHAssetCollection) -> Void) {
self.theme = theme
self.collection = collection
self.firstItem = firstItem
self.count = count
self.action = action
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = MediaGroupsGridAlbumItemNode()
let (nodeLayout, apply) = node.asyncLayout()(self, params)
node.insets = nodeLayout.insets
node.contentSize = nodeLayout.contentSize
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in
apply(false)
})
})
}
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
assert(node() is MediaGroupsGridAlbumItemNode)
if let nodeValue = node() as? MediaGroupsGridAlbumItemNode {
let layout = nodeValue.asyncLayout()
async {
let (nodeLayout, apply) = layout(self, params)
Queue.mainQueue().async {
completion(nodeLayout, { _ in
apply(animation.isAnimated)
})
}
}
}
}
}
public var selectable = true
public func selected(listView: ListView) {
self.action(self.collection)
}
}
private let textFont = Font.regular(15.0)
private final class MediaGroupsGridAlbumItemNode : ListViewItemNode {
private let containerNode: ASDisplayNode
private let imageNode: ImageNode
private let titleNode: TextNode
private let countNode: TextNode
var item: MediaGroupsGridAlbumItem?
init() {
self.containerNode = ASDisplayNode()
self.imageNode = ImageNode()
self.imageNode.clipsToBounds = true
self.imageNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 62.0, height: 62.0))
self.imageNode.contentMode = .scaleAspectFill
self.imageNode.animateFirstTransition = false
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.countNode = TextNode()
self.countNode.isUserInteractionEnabled = false
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.imageNode)
self.containerNode.addSubnode(self.titleNode)
self.containerNode.addSubnode(self.countNode)
}
override func didLoad() {
super.didLoad()
self.imageNode.cornerRadius = 5.0
if #available(iOS 13.0, *) {
self.imageNode.layer.cornerCurve = .continuous
}
self.containerNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
}
func asyncLayout() -> (MediaGroupsGridAlbumItem, ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (Bool) -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeCountLayout = TextNode.asyncLayout(self.countNode)
return { [weak self] item, params in
let title = NSAttributedString(string: item.collection.localizedTitle ?? "", font: textFont, textColor: item.theme.list.itemPrimaryTextColor)
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: title, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 170.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let count = NSAttributedString(string: item.count, font: textFont, textColor: item.theme.list.itemSecondaryTextColor)
let (countLayout, countApply) = makeCountLayout(TextNodeLayoutArguments(attributedString: count, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 170.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let itemLayout = ListViewItemNodeLayout(contentSize: CGSize(width: 220.0, height: 182.0), insets: UIEdgeInsets())
return (itemLayout, { animated in
if let strongSelf = self {
strongSelf.item = item
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 220.0, height: 220.0))
if let firstItem = item.firstItem {
let scale = min(2.0, UIScreenScale)
let targetSize = CGSize(width: 160.0 * scale, height: 160.0 * scale)
strongSelf.imageNode.setSignal(assetImage(asset: firstItem, targetSize: targetSize, exact: false))
}
strongSelf.imageNode.frame = CGRect(origin: CGPoint(x: 6.0, y: 0.0), size: CGSize(width: 170.0, height: 170.0))
let _ = titleApply()
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: 6.0, y: 176.0), size: titleLayout.size)
let _ = countApply()
strongSelf.countNode.frame = CGRect(origin: CGPoint(x: 6.0, y: 196.0), size: countLayout.size)
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
super.animateInsertion(currentTimestamp, duration: duration, options: options)
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
super.animateRemoved(currentTimestamp, duration: duration)
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
}
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
super.animateAdded(currentTimestamp, duration: duration)
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
private struct MediaGroupsAlbumGridItemNodeTransition {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
let entries: [MediaGroupsGridAlbumEntry]
}
private func preparedTransition(action: @escaping (PHAssetCollection) -> Void, from fromEntries: [MediaGroupsGridAlbumEntry], to toEntries: [MediaGroupsGridAlbumEntry]) -> MediaGroupsAlbumGridItemNodeTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(action: action), directionHint: .Down) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(action: action), directionHint: nil) }
return MediaGroupsAlbumGridItemNodeTransition(deletions: deletions, insertions: insertions, updates: updates, entries: toEntries)
}
final class MediaGroupsAlbumGridItem: ListViewItem {
let presentationData: PresentationData
let collections: [PHAssetCollection]
let action: (PHAssetCollection) -> Void
public init(presentationData: PresentationData, collections: [PHAssetCollection], action: @escaping (PHAssetCollection) -> Void) {
self.presentationData = presentationData
self.collections = collections
self.action = action
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
Queue.mainQueue().async {
let node = MediaGroupsAlbumGridItemNode()
let makeLayout = node.asyncLayout()
async {
let (nodeLayout, nodeApply) = makeLayout(self, params)
node.contentSize = nodeLayout.contentSize
node.insets = nodeLayout.insets
Queue.mainQueue().async {
completion(node, nodeApply)
}
}
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? MediaGroupsAlbumGridItemNode {
let layout = nodeValue.asyncLayout()
async {
let (nodeLayout, apply) = layout(self, params)
Queue.mainQueue().async {
completion(nodeLayout, { info in
apply().1(info)
})
}
}
}
}
}
public var selectable: Bool {
return false
}
}
private let titleFont = Font.bold(20.0)
private class MediaGroupsAlbumGridItemNode: ListViewItemNode {
private var item: MediaGroupsAlbumGridItem?
private var layoutParams: ListViewItemLayoutParams?
private let listNode: ListView
private var entries: [MediaGroupsGridAlbumEntry]?
private var enqueuedTransitions: [MediaGroupsAlbumGridItemNodeTransition] = []
init() {
self.listNode = ListView()
self.listNode.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.addSubnode(self.listNode)
}
private func enqueueTransition(_ transition: MediaGroupsAlbumGridItemNodeTransition) {
self.enqueuedTransitions.append(transition)
if let _ = self.item {
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
}
}
private func dequeueTransition() {
guard let _ = self.item, let transition = self.enqueuedTransitions.first else {
return
}
self.enqueuedTransitions.remove(at: 0)
var options = ListViewDeleteAndInsertOptions()
options.insert(.Synchronous)
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, scrollToItem: nil, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in
})
}
override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
if let item = self.item {
let makeLayout = self.asyncLayout()
let (nodeLayout, nodeApply) = makeLayout(item, params)
self.contentSize = nodeLayout.contentSize
self.insets = nodeLayout.insets
let _ = nodeApply()
}
}
func asyncLayout() -> (_ item: MediaGroupsAlbumGridItem, _ params: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) {
return { [weak self] item, params in
let contentSize = CGSize(width: params.width, height: 220.0)
let nodeLayout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets())
return (nodeLayout, { [weak self] in
return (nil, { _ in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
let listInsets = UIEdgeInsets(top: 10.0, left: 0.0, bottom: 10.0, right: 0.0)
strongSelf.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: contentSize.height, height: contentSize.width - params.leftInset - params.rightInset)
strongSelf.listNode.position = CGPoint(x: contentSize.width / 2.0, y: contentSize.height / 2.0)
strongSelf.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: CGSize(width: contentSize.height, height: contentSize.width - params.leftInset - params.rightInset), insets: listInsets, duration: 0.0, curve: .Default(duration: nil)), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
var entries: [MediaGroupsGridAlbumEntry] = []
var index: Int = 0
for collection in item.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
}
if let firstItem = firstItem {
let count = presentationStringsFormattedNumber(Int32(result.count), item.presentationData.dateTimeFormat.groupingSeparator)
entries.append(MediaGroupsGridAlbumEntry(theme: item.presentationData.theme, index: index, collection: collection, firstItem: firstItem, count: count))
index += 1
}
}
let previousEntries = strongSelf.entries ?? []
let transition = preparedTransition(action: { [weak item] collection in
item?.action(collection)
}, from: previousEntries, to: entries)
strongSelf.enqueueTransition(transition)
strongSelf.entries = entries
}
})
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false)
}
}
@@ -0,0 +1,330 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AppBundle
class MediaGroupsAlbumItem: ListViewItem, ListViewItemWithHeader {
enum Icon {
case bursts
case panoramas
case screenshots
case selfPortraits
case slomoVideos
case timelapses
case videos
case animated
case depthEffect
case livePhotos
case hidden
var image: UIImage? {
switch self {
case .bursts:
return UIImage(bundleImageName: "Chat/Attach Menu/Burst")
case .panoramas:
return UIImage(bundleImageName: "Chat/Attach Menu/Panorama")
case .screenshots:
return UIImage(bundleImageName: "Chat/Attach Menu/Screenshot")
case .selfPortraits:
return UIImage(bundleImageName: "Chat/Attach Menu/Selfie")
case .slomoVideos:
return UIImage(bundleImageName: "Chat/Attach Menu/SloMo")
case .timelapses:
return UIImage(bundleImageName: "Chat/Attach Menu/Timelapse")
case .videos:
return UIImage(bundleImageName: "Chat/Attach Menu/Video")
case .animated:
return UIImage(bundleImageName: "Chat/Attach Menu/Animated")
case .depthEffect:
return UIImage(bundleImageName: "Chat/Attach Menu/Portrait")
case .livePhotos:
return UIImage(bundleImageName: "Chat/Attach Menu/LivePhoto")
case .hidden:
return UIImage(bundleImageName: "Chat/Attach Menu/Hidden")
}
}
}
let presentationData: ItemListPresentationData
let title: String
let count: String
let icon: Icon?
let action: () -> Void
let header: ListViewItemHeader? = nil
init(presentationData: ItemListPresentationData, title: String, count: String, icon: Icon?, action: @escaping () -> Void) {
self.presentationData = presentationData
self.title = title
self.count = count
self.icon = icon
self.action = action
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = MediaGroupsAlbumItemNode()
let (first, last) = MediaGroupsAlbumItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem)
let (layout, apply) = node.asyncLayout()(self, params, first, last)
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? MediaGroupsAlbumItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (first, last) = MediaGroupsAlbumItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem)
let (layout, apply) = makeLayout(self, params, first, last)
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
var selectable: Bool = true
public func selected(listView: ListView){
self.action()
listView.clearHighlightAnimated(true)
}
static func mergeType(item: MediaGroupsAlbumItem, previousItem: ListViewItem?, nextItem: ListViewItem?) -> (first: Bool, last: Bool) {
var first = false
var last = false
if let previousItem = previousItem, !(previousItem is MediaGroupsAlbumItem) {
first = true
}
if nextItem == nil {
last = true
}
return (first, last)
}
}
class MediaGroupsAlbumItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let iconNode: ASImageNode
private let titleNode: TextNode
private let countNode: TextNode
private let arrowNode: ASImageNode
private let activateArea: AccessibilityAreaNode
private var item: MediaGroupsAlbumItem?
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.iconNode = ASImageNode()
self.iconNode.isLayerBacked = true
self.iconNode.displayWithoutProcessing = true
self.iconNode.displaysAsynchronously = false
self.countNode = TextNode()
self.countNode.isUserInteractionEnabled = false
self.countNode.contentMode = .left
self.countNode.contentsScale = UIScreen.main.scale
self.arrowNode = ASImageNode()
self.arrowNode.isLayerBacked = true
self.arrowNode.displayWithoutProcessing = true
self.arrowNode.displaysAsynchronously = false
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
self.activateArea = AccessibilityAreaNode()
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.iconNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.countNode)
self.addSubnode(self.arrowNode)
self.addSubnode(self.activateArea)
self.activateArea.activate = { [weak self] in
self?.item?.action()
return true
}
}
func asyncLayout() -> (_ item: MediaGroupsAlbumItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeCountLayout = TextNode.asyncLayout(self.countNode)
let currentItem = self.item
return { item, params, first, last in
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
let titleFont = Font.regular(21.0)
let countFont = Font.regular(17.0)
let leftInset: CGFloat = 60.0 + params.leftInset
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemAccentColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - 10.0 - leftInset - params.rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (countLayout, countApply) = makeCountLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.count, font: countFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - 10.0 - leftInset - params.rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let contentHeight: CGFloat = 48.0
let contentSize = CGSize(width: params.width, height: contentHeight)
let insets = UIEdgeInsets()
let separatorHeight = UIScreenPixel
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.activateArea.accessibilityLabel = item.title
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: layout.contentSize.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
strongSelf.iconNode.image = generateTintedImage(image: item.icon?.image, color: item.presentationData.theme.list.itemAccentColor)
strongSelf.arrowNode.image = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme)
}
strongSelf.addSubnode(strongSelf.activateArea)
let _ = titleApply()
let _ = countApply()
let titleOffset = leftInset
let hideBottomStripe: Bool = last
if let image = strongSelf.iconNode.image {
strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 14.0, y: floorToScreenPixels((contentSize.height - image.size.height) / 2.0)), size: image.size)
}
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.topStripeNode.isHidden = true
strongSelf.bottomStripeNode.isHidden = hideBottomStripe
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: titleOffset, y: floorToScreenPixels((contentSize.height - titleLayout.size.height) / 2.0) + 1.0), size: titleLayout.size)
if let arrowSize = strongSelf.arrowNode.image?.size {
strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - arrowSize.width - 12.0, y: floorToScreenPixels((contentSize.height - arrowSize.height) / 2.0)), size: arrowSize)
strongSelf.countNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - countLayout.size.width - arrowSize.width - 12.0 - 2.0, y: floorToScreenPixels((contentSize.height - countLayout.size.height) / 2.0) + 1.0), size: countLayout.size)
}
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: contentSize.height + UIScreenPixel + UIScreenPixel))
}
})
}
}
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
if highlighted {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
var anchorNode: ASDisplayNode?
if self.bottomStripeNode.supernode != nil {
anchorNode = self.bottomStripeNode
} else if self.topStripeNode.supernode != nil {
anchorNode = self.topStripeNode
} else if self.backgroundNode.supernode != nil {
anchorNode = self.backgroundNode
}
if let anchorNode = anchorNode {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
} else {
self.addSubnode(self.highlightedBackgroundNode)
}
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if animated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
override public func headers() -> [ListViewItemHeader]? {
if let item = self.item {
return item.header.flatMap { [$0] }
} else {
return nil
}
}
}
@@ -0,0 +1,439 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ContextUI
import AccountContext
import TelegramPresentationData
import Photos
import MediaAssetsContext
struct MediaGroupItem {
let collection: PHAssetCollection
let firstItem: PHAsset?
let count: Int
}
final class MediaGroupsContextMenuContent: ContextControllerItemsContent {
private final class GroupsListNode: ASDisplayNode, ASScrollViewDelegate {
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.wrappedScrollViewDelegate
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<Int>()
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
)
}
}
@@ -0,0 +1,110 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
final class MediaGroupsHeaderItem: ListViewItem {
let presentationData: ItemListPresentationData
let title: String
public init(presentationData: ItemListPresentationData, title: String) {
self.presentationData = presentationData
self.title = title
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = MediaGroupsHeaderItemNode()
let makeLayout = node.asyncLayout()
let (nodeLayout, nodeApply) = makeLayout(self, params)
node.contentSize = nodeLayout.contentSize
node.insets = nodeLayout.insets
completion(node, nodeApply)
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? MediaGroupsHeaderItemNode {
let layout = nodeValue.asyncLayout()
async {
let (nodeLayout, apply) = layout(self, params)
Queue.mainQueue().async {
completion(nodeLayout, { info in
apply().1(info)
})
}
}
}
}
}
public var selectable: Bool {
return false
}
}
private let titleFont = Font.bold(22.0)
private class MediaGroupsHeaderItemNode: ListViewItemNode {
private let titleNode: TextNode
private var item: MediaGroupsHeaderItem?
private var layoutParams: ListViewItemLayoutParams?
init() {
self.titleNode = TextNode()
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.addSubnode(self.titleNode)
}
override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
if let item = self.item {
let makeLayout = self.asyncLayout()
let (nodeLayout, nodeApply) = makeLayout(item, params)
self.contentSize = nodeLayout.contentSize
self.insets = nodeLayout.insets
let _ = nodeApply()
}
}
func asyncLayout() -> (_ item: MediaGroupsHeaderItem, _ params: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
return { [weak self] item, params in
let titleString = NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor)
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 16.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let contentSize = CGSize(width: params.width, height: 45.0)
let nodeLayout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets())
return (nodeLayout, { [weak self] in
return (nil, { _ in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
let _ = titleApply()
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 17.0, y: floor((nodeLayout.contentSize.height - titleLayout.size.height) / 2.0) + 2.0), size: titleLayout.size)
}
})
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false)
}
}
@@ -0,0 +1,487 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import AccountContext
import TelegramPresentationData
import TelegramUIPreferences
import MergeLists
import Photos
import LegacyComponents
import AttachmentUI
import ItemListUI
import MediaAssetsContext
private enum MediaGroupsEntry: Comparable, Identifiable {
enum StableId: Hashable {
case albumsHeader
case albums
case smartAlbumsHeader
case smartAlbum(String)
}
case albumsHeader(PresentationTheme, String)
case albums(PresentationTheme, [PHAssetCollection])
case smartAlbumsHeader(PresentationTheme, String)
case smartAlbum(PresentationTheme, Int, PHAssetCollection, Int)
var stableId: StableId {
switch self {
case .albumsHeader:
return .albumsHeader
case .albums:
return .albums
case .smartAlbumsHeader:
return .smartAlbumsHeader
case let .smartAlbum(_, _, album, _):
return .smartAlbum(album.localIdentifier)
}
}
static func ==(lhs: MediaGroupsEntry, rhs: MediaGroupsEntry) -> Bool {
switch lhs {
case let .albumsHeader(lhsTheme, lhsText):
if case let .albumsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .albums(lhsTheme, lhsAssetCollections):
if case let .albums(rhsTheme, rhsAssetCollections) = rhs, lhsTheme === rhsTheme, lhsAssetCollections == rhsAssetCollections {
return true
} else {
return false
}
case let .smartAlbumsHeader(lhsTheme, lhsText):
if case let .smartAlbumsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .smartAlbum(lhsTheme, lhsIndex, lhsAssetCollection, lhsCount):
if case let .smartAlbum(rhsTheme, rhsIndex, rhsAssetCollection, rhsCount) = rhs, lhsTheme === rhsTheme, lhsIndex == rhsIndex, lhsAssetCollection == rhsAssetCollection, lhsCount == rhsCount {
return true
} else {
return false
}
}
}
private var sortId: Int {
switch self {
case .albumsHeader:
return 0
case .albums:
return 1
case .smartAlbumsHeader:
return 2
case let .smartAlbum(_, index, _, _):
return 3 + index
}
}
static func <(lhs: MediaGroupsEntry, rhs: MediaGroupsEntry) -> Bool {
return lhs.sortId < rhs.sortId
}
func item(presentationData: PresentationData, openGroup: @escaping (PHAssetCollection) -> Void) -> ListViewItem {
switch self {
case let .albumsHeader(_, text), let .smartAlbumsHeader(_, text):
return MediaGroupsHeaderItem(presentationData: ItemListPresentationData(presentationData), title: text)
case let .albums(_, collections):
return MediaGroupsAlbumGridItem(presentationData: presentationData, collections: collections, action: { collection in
openGroup(collection)
})
case let .smartAlbum(_, _, collection, count):
let title = collection.localizedTitle ?? ""
let count = presentationStringsFormattedNumber(Int32(count), presentationData.dateTimeFormat.groupingSeparator)
var icon: MediaGroupsAlbumItem.Icon?
switch collection.assetCollectionSubtype {
case .smartAlbumAnimated:
icon = .animated
case .smartAlbumBursts:
icon = .bursts
case .smartAlbumDepthEffect:
icon = .depthEffect
case .smartAlbumLivePhotos:
icon = .livePhotos
case .smartAlbumPanoramas:
icon = .panoramas
case .smartAlbumScreenshots:
icon = .screenshots
case .smartAlbumSelfPortraits:
icon = .selfPortraits
case .smartAlbumSlomoVideos:
icon = .slomoVideos
case .smartAlbumTimelapses:
icon = .timelapses
case .smartAlbumVideos:
icon = .videos
case .smartAlbumAllHidden:
icon = .hidden
default:
icon = nil
}
return MediaGroupsAlbumItem(presentationData: ItemListPresentationData(presentationData), title: title, count: count, icon: icon, action: {
openGroup(collection)
})
}
}
}
private struct MediaGroupsTransition {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
}
private func preparedTransition(from fromEntries: [MediaGroupsEntry], to toEntries: [MediaGroupsEntry], presentationData: PresentationData, openGroup: @escaping (PHAssetCollection) -> Void) -> MediaGroupsTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, openGroup: openGroup), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, openGroup: openGroup), directionHint: nil) }
return MediaGroupsTransition(deletions: deletions, insertions: insertions, updates: updates)
}
public final class MediaGroupsScreen: ViewController, AttachmentContainable {
public var requestAttachmentMenuExpansion: () -> Void = {}
public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in }
public var parentController: () -> ViewController? = {
return nil
}
public var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in }
public var updateTabBarVisibility: (Bool, ContainedViewLayoutTransition) -> Void = { _, _ in }
public var cancelPanGesture: () -> Void = { }
public var isContainerPanning: () -> Bool = { return false }
public var isContainerExpanded: () -> Bool = { return false }
public var isMinimized: Bool = false
public var mediaPickerContext: AttachmentMediaPickerContext? {
return nil
}
private let context: AccountContext
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private let mediaAssetsContext: MediaAssetsContext
private let embedded: Bool
private let openGroup: (PHAssetCollection) -> Void
private class Node: ViewControllerTracingNode {
struct State {
let albums: PHFetchResult<PHAssetCollection>
let smartAlbums: PHFetchResult<PHAssetCollection>
}
private weak var controller: MediaGroupsScreen?
private var presentationData: PresentationData
private let containerNode: ASDisplayNode
private let backgroundNode: NavigationBackgroundNode
private let listNode: ListView
private var nextStableId: Int = 1
private var currentEntries: [MediaGroupsEntry] = []
private var enqueuedTransactions: [MediaGroupsTransition] = []
private var state: State?
private var itemsDisposable: Disposable?
private var didSetReady = false
private let _ready = Promise<Bool>()
var ready: Promise<Bool> {
return self._ready
}
private var validLayout: (ContainerViewLayout, CGFloat)?
init(controller: MediaGroupsScreen) {
self.controller = controller
self.presentationData = controller.presentationData
self.containerNode = ASDisplayNode()
self.backgroundNode = NavigationBackgroundNode(color: self.presentationData.theme.rootController.tabBar.backgroundColor)
self.listNode = ListView()
super.init()
if !controller.embedded {
self.addSubnode(self.backgroundNode)
} else {
self.containerNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
}
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.listNode)
let updatedState = combineLatest(queue: Queue.mainQueue(), controller.mediaAssetsContext.fetchAssetsCollections(.album), controller.mediaAssetsContext.fetchAssetsCollections(.smartAlbum))
self.itemsDisposable = (updatedState
|> deliverOnMainQueue).start(next: { [weak self] albums, smartAlbums in
guard let strongSelf = self else {
return
}
strongSelf.updateState(State(albums: albums, smartAlbums: smartAlbums))
})
self.listNode.beganInteractiveDragging = { [weak self] _ in
self?.view.window?.endEditing(true)
}
self.listNode.visibleContentOffsetChanged = { [weak self] _ in
self?.updateNavigation(transition: .immediate)
}
}
deinit {
self.itemsDisposable?.dispose()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
if result == self.view {
return nil
}
return result
}
private func updateState(_ state: State) {
self.state = state
var entries: [MediaGroupsEntry] = []
var albums: [PHAssetCollection] = []
entries.append(.albumsHeader(self.presentationData.theme, self.presentationData.strings.Attachment_MyAlbums))
state.smartAlbums.enumerateObjects { collection, _, _ in
if [.smartAlbumUserLibrary, .smartAlbumFavorites].contains(collection.assetCollectionSubtype) {
albums.append(collection)
}
}
state.albums.enumerateObjects(options: [.reverse]) { collection, _, _ in
albums.append(collection)
}
entries.append(.albums(self.presentationData.theme, albums))
let smartAlbumsHeaderIndex = entries.count
var addedSmartAlbum = false
state.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 {
addedSmartAlbum = true
entries.append(.smartAlbum(self.presentationData.theme, index, collection, result.count))
}
}
}
if addedSmartAlbum {
entries.insert(.smartAlbumsHeader(self.presentationData.theme, self.presentationData.strings.Attachment_MediaTypes), at: smartAlbumsHeaderIndex)
}
let previousEntries = self.currentEntries
self.currentEntries = entries
let transaction = preparedTransition(from: previousEntries, to: entries, presentationData: self.presentationData, openGroup: { [weak self] collection in
self?.view.window?.endEditing(true)
self?.controller?.openGroup(collection)
})
self.enqueueTransaction(transaction)
}
func updatePresentationData(_ presentationData: PresentationData) {
self.presentationData = presentationData
if self.controller?.embedded == true {
self.containerNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
}
self.backgroundNode.updateColor(color: self.presentationData.theme.rootController.tabBar.backgroundColor, transition: .immediate)
}
private func enqueueTransaction(_ transaction: MediaGroupsTransition) {
self.enqueuedTransactions.append(transaction)
if let _ = self.validLayout {
while !self.enqueuedTransactions.isEmpty {
self.dequeueTransaction()
}
}
}
private func dequeueTransaction() {
if self.enqueuedTransactions.isEmpty {
return
}
let transaction = self.enqueuedTransactions.removeFirst()
let options = ListViewDeleteAndInsertOptions()
self.listNode.transaction(deleteIndices: transaction.deletions, insertIndicesAndItems: transaction.insertions, updateIndicesAndItems: transaction.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in
if let strongSelf = self {
if !strongSelf.didSetReady {
strongSelf.didSetReady = true
strongSelf._ready.set(.single(true))
}
}
})
}
func scrollToTop() {
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
guard let controller = self.controller else {
return
}
let firstTime = self.validLayout == nil
self.validLayout = (layout, navigationBarHeight)
let topInset: CGFloat = controller.embedded ? 12.0 : 0.0
let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight + topInset), size: CGSize(width: layout.size.width, height: layout.size.height - navigationBarHeight - topInset))
transition.updateFrame(node: self.containerNode, frame: containerFrame)
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: .zero, size: layout.size))
self.backgroundNode.update(size: layout.size, transition: transition)
let size = layout.size
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: 0.0, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom + 56.0, right: layout.safeInsets.right), headerInsets: UIEdgeInsets(), scrollIndicatorInsets: UIEdgeInsets(), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size))
if firstTime {
self.dequeueTransaction()
}
}
private var previousContentOffset: GridNodeVisibleContentOffset?
func updateNavigation(delayDisappear: Bool = false, transition: ContainedViewLayoutTransition) {
var previousContentOffsetValue: CGFloat?
if let previousContentOffset = self.previousContentOffset, case let .known(value) = previousContentOffset {
previousContentOffsetValue = value
}
let offset = self.listNode.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)
}
}
}
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
}
init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, mediaAssetsContext: MediaAssetsContext, embedded: Bool = false, openGroup: @escaping (PHAssetCollection) -> Void) {
self.context = context
self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
self.mediaAssetsContext = mediaAssetsContext
self.embedded = embedded
self.openGroup = openGroup
super.init(navigationBarPresentationData: !embedded ? NavigationBarPresentationData(presentationData: presentationData) : nil)
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.controllerNode.updatePresentationData(presentationData)
}
}
})
self.scrollToTop = { [weak self] in
if let strongSelf = self {
strongSelf.controllerNode.scrollToTop()
}
}
if !embedded {
self.title = "Albums"
self.navigationItem.leftBarButtonItem = UIBarButtonItem(backButtonAppearanceWithTitle: self.presentationData.strings.Common_Back, target: self, action: #selector(self.backPressed))
}
}
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()
}
@objc private func backPressed() {
if let _ = self.navigationController {
self.dismiss()
} else {
self.updateNavigationStack { current in
var mediaPickerContext: AttachmentMediaPickerContext?
if let first = current.first as? MediaPickerScreenImpl {
mediaPickerContext = first.webSearchController?.mediaPickerContext ?? first.mediaPickerContext
}
return (current.filter { $0 !== self }, mediaPickerContext)
}
}
}
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)
}
}
@@ -0,0 +1,875 @@
import Foundation
import UIKit
import Display
import TelegramCore
import SwiftSignalKit
import AsyncDisplayKit
import Postbox
import AccountContext
import TelegramPresentationData
import TelegramStringFormatting
import Photos
import CheckNode
import LegacyComponents
import PhotoResources
import InvisibleInkDustNode
import ImageBlur
import FastBlur
import MediaEditor
import RadialStatusNode
import MediaAssetsContext
private let leftShadowImage: UIImage = {
let baseImage = UIImage(bundleImageName: "Peer Info/MediaGridShadow")!
let image = generateImage(baseImage.size, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: -1.0, y: 1.0)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
UIGraphicsPushContext(context)
baseImage.draw(in: CGRect(origin: CGPoint(), size: size))
UIGraphicsPopContext()
})
return image!
}()
private let rightShadowImage: UIImage = {
let baseImage = UIImage(bundleImageName: "Peer Info/MediaGridShadow")!
let image = generateImage(baseImage.size, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
baseImage.draw(in: CGRect(origin: CGPoint(), size: size))
UIGraphicsPopContext()
})
return image!
}()
enum MediaPickerGridItemContent: Equatable {
case asset(PHFetchResult<PHAsset>, Int)
case media(MediaPickerScreenImpl.Subject.Media, Int)
case draft(MediaEditorDraft, Int)
}
final class MediaPickerGridItem: GridItem {
let content: MediaPickerGridItemContent
let interaction: MediaPickerInteraction
let theme: PresentationTheme
let strings: PresentationStrings
let selectable: Bool
let enableAnimations: Bool
let stories: Bool
let section: GridSection? = nil
init(content: MediaPickerGridItemContent, interaction: MediaPickerInteraction, theme: PresentationTheme, strings: PresentationStrings, selectable: Bool, enableAnimations: Bool, stories: Bool) {
self.content = content
self.interaction = interaction
self.strings = strings
self.theme = theme
self.selectable = selectable
self.enableAnimations = enableAnimations
self.stories = stories
}
func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
switch self.content {
case let .asset(fetchResult, index):
let node = MediaPickerGridItemNode()
node.setup(interaction: self.interaction, fetchResult: fetchResult, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories)
return node
case let .media(media, index):
let node = MediaPickerGridItemNode()
node.setup(interaction: self.interaction, media: media, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories)
return node
case let .draft(draft, index):
let node = MediaPickerGridItemNode()
node.setup(interaction: self.interaction, draft: draft, index: index, theme: self.theme, strings: self.strings, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories)
return node
}
}
func update(node: GridItemNode) {
guard let node = node as? MediaPickerGridItemNode else {
assertionFailure()
return
}
switch self.content {
case let .asset(fetchResult, index):
node.setup(interaction: self.interaction, fetchResult: fetchResult, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories)
case let .media(media, index):
node.setup(interaction: self.interaction, media: media, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories)
case let .draft(draft, index):
node.setup(interaction: self.interaction, draft: draft, index: index, theme: self.theme, strings: self.strings, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories)
}
}
}
final class MediaPickerGridItemNode: GridItemNode {
var currentMediaState: (TGMediaSelectableItem, Int)?
var currentAssetState: (PHFetchResult<PHAsset>, Int)?
var currentAsset: PHAsset?
var currentDraftState: (MediaEditorDraft, Int)?
var enableAnimations: Bool = true
var stories: Bool = false
private var selectable: Bool = false
private let backgroundNode: ASImageNode
private let imageNode: ImageNode
private var checkNode: InteractiveCheckNode?
private let leftShadowNode: ASImageNode
private let rightShadowNode: ASImageNode
private let typeIconNode: ASImageNode
private let durationNode: ImmediateTextNode
private let draftNode: ImmediateTextNode
private var statusNode: RadialStatusNode?
private let activateAreaNode: AccessibilityAreaNode
private var interaction: MediaPickerInteraction?
private var theme: PresentationTheme?
private struct SelectionState: Equatable {
let selected: Bool
let index: Int?
let count: Int
}
private let selectionPromise = ValuePromise<SelectionState>(SelectionState(selected: false, index: nil, count: 0))
private let spoilerDisposable = MetaDisposable()
var spoilerNode: SpoilerOverlayNode?
var priceNode: PriceNode?
private let progressDisposable = MetaDisposable()
private var currentIsPreviewing = false
var selected: (() -> Void)?
override init() {
self.backgroundNode = ASImageNode()
self.backgroundNode.contentMode = .scaleToFill
self.backgroundNode.isLayerBacked = true
self.imageNode = ImageNode()
self.imageNode.clipsToBounds = true
self.imageNode.contentMode = .scaleAspectFill
self.imageNode.isLayerBacked = true
self.imageNode.animateFirstTransition = false
self.leftShadowNode = ASImageNode()
self.leftShadowNode.displaysAsynchronously = false
self.leftShadowNode.displayWithoutProcessing = true
self.leftShadowNode.image = leftShadowImage
self.leftShadowNode.isLayerBacked = true
self.rightShadowNode = ASImageNode()
self.rightShadowNode.displaysAsynchronously = false
self.rightShadowNode.displayWithoutProcessing = true
self.rightShadowNode.image = rightShadowImage
self.rightShadowNode.isLayerBacked = true
self.typeIconNode = ASImageNode()
self.typeIconNode.displaysAsynchronously = false
self.typeIconNode.displayWithoutProcessing = true
self.typeIconNode.isLayerBacked = true
self.durationNode = ImmediateTextNode()
self.durationNode.isLayerBacked = true
self.durationNode.textShadowColor = UIColor(white: 0.0, alpha: 0.4)
self.durationNode.textShadowBlur = 4.0
self.draftNode = ImmediateTextNode()
self.activateAreaNode = AccessibilityAreaNode()
self.activateAreaNode.accessibilityTraits = [.image]
super.init()
self.clipsToBounds = true
self.addSubnode(self.imageNode)
self.addSubnode(self.activateAreaNode)
self.imageNode.contentUpdated = { [weak self] image in
self?.spoilerNode?.setImage(image)
}
}
deinit {
self.spoilerDisposable.dispose()
}
var identifier: String {
if let (draft, _) = self.currentDraftState {
return draft.path
} else {
return self.selectableItem?.uniqueIdentifier ?? ""
}
}
var selectableItem: TGMediaSelectableItem? {
if let (media, _) = self.currentMediaState {
return media
} else if let (fetchResult, index) = self.currentAssetState {
return TGMediaAsset(phAsset: fetchResult[index])
} else {
return nil
}
}
var _cachedTag: Int32?
var tag: Int32? {
if let tag = self._cachedTag {
return tag
} else if let (fetchResult, index) = self.currentAssetState {
let asset = fetchResult.object(at: index)
if let localTimestamp = asset.creationDate?.timeIntervalSince1970 {
let tag = Month(localTimestamp: Int32(exactly: floor(localTimestamp)) ?? 0).packedValue
self._cachedTag = tag
return tag
} else {
return nil
}
} else if let (draft, _) = self.currentDraftState {
let tag = Month(localTimestamp: draft.timestamp).packedValue
self._cachedTag = tag
return tag
} else {
return nil
}
}
func updateSelectionState(isFirstTime: Bool = false, animated: Bool = false) {
if self.checkNode == nil, let _ = self.interaction?.selectionState, self.selectable, let theme = self.theme {
let checkNode = InteractiveCheckNode(theme: CheckNodeTheme(theme: theme, style: .overlay))
checkNode.valueChanged = { [weak self] value in
if let strongSelf = self, let interaction = strongSelf.interaction, let selectableItem = strongSelf.selectableItem {
if !interaction.toggleSelection(selectableItem, value, false) {
strongSelf.checkNode?.setSelected(false, animated: false)
}
}
}
self.addSubnode(checkNode)
self.checkNode = checkNode
self.setNeedsLayout()
if !isFirstTime {
checkNode.layer.animateScale(from: 0.2, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
checkNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
}
}
if let interaction = self.interaction, let selectionState = interaction.selectionState {
let selected = selectionState.isIdentifierSelected(self.identifier)
var selectionIndex: Int?
if let selectableItem = self.selectableItem {
let index = selectionState.index(of: selectableItem)
if index != NSNotFound {
self.checkNode?.content = .counter(Int(index))
selectionIndex = Int(index)
}
}
self.checkNode?.setSelected(selected, animated: animated)
self.selectionPromise.set(SelectionState(selected: selected, index: selectionIndex, count: selectionState.selectedItems().count))
}
}
private var innerIsHidden = false
func updateHiddenMedia() {
let wasHidden = self.innerIsHidden
if self.identifier == self.interaction?.hiddenMediaId {
self.isHidden = true
self.innerIsHidden = true
} else {
self.isHidden = false
self.innerIsHidden = false
if wasHidden {
self.animateFadeIn(animateCheckNode: true, animateSpoilerNode: true)
}
}
}
func animateFadeIn(animateCheckNode: Bool, animateSpoilerNode: Bool) {
if animateCheckNode {
self.checkNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
self.leftShadowNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.rightShadowNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.typeIconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.durationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.draftNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.priceNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
if animateSpoilerNode || self.priceNode != nil {
self.spoilerNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
}
override func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:))))
}
func updateProgress(_ value: Float?, animated: Bool) {
if let value {
let statusNode: RadialStatusNode
if let current = self.statusNode {
statusNode = current
} else {
statusNode = RadialStatusNode(backgroundNodeColor: UIColor(rgb: 0x000000, alpha: 0.6))
statusNode.isUserInteractionEnabled = false
self.addSubnode(statusNode)
self.statusNode = statusNode
}
let adjustedProgress = max(0.027, CGFloat(value))
let state: RadialStatusNodeState = .progress(color: .white, lineWidth: nil, value: adjustedProgress, cancelEnabled: true, animateRotation: true)
statusNode.transitionToState(state)
} else if let statusNode = self.statusNode {
self.statusNode = nil
if animated {
statusNode.transitionToState(.none, animated: true, completion: { [weak statusNode] in
statusNode?.removeFromSupernode()
})
} else {
statusNode.removeFromSupernode()
}
}
}
func setup(interaction: MediaPickerInteraction, draft: MediaEditorDraft, index: Int, theme: PresentationTheme, strings: PresentationStrings, selectable: Bool, enableAnimations: Bool, stories: Bool) {
self.interaction = interaction
self.theme = theme
self.selectable = selectable
self.enableAnimations = enableAnimations
self.backgroundColor = theme.list.mediaPlaceholderColor
if self.currentDraftState == nil || self.currentDraftState?.0.path != draft.path || self.currentDraftState!.1 != index || self.currentAssetState != nil {
let imageSignal: Signal<UIImage?, NoError> = .single(draft.thumbnail)
self.imageNode.setSignal(imageSignal)
self.currentDraftState = (draft, index)
if self.currentAssetState != nil {
self.currentAsset = nil
self.currentAssetState = nil
self.typeIconNode.removeFromSupernode()
self.progressDisposable.set(nil)
self.updateProgress(nil, animated: false)
self.backgroundNode.image = nil
self.imageNode.contentMode = .scaleAspectFill
}
if self.draftNode.supernode == nil {
self.draftNode.attributedText = NSAttributedString(string: strings.MediaEditor_Draft, font: Font.semibold(12.0), textColor: .white)
self.addSubnode(self.draftNode)
}
if draft.isVideo {
self.typeIconNode.image = UIImage(bundleImageName: "Media Editor/MediaVideo")
self.durationNode.attributedText = NSAttributedString(string: stringForDuration(Int32(draft.duration ?? 0.0)), font: Font.semibold(11.0), textColor: .white)
if self.typeIconNode.supernode == nil {
self.addSubnode(self.rightShadowNode)
self.addSubnode(self.typeIconNode)
self.addSubnode(self.durationNode)
self.setNeedsLayout()
}
} else {
if self.typeIconNode.supernode != nil {
self.typeIconNode.removeFromSupernode()
}
if self.durationNode.supernode != nil {
self.durationNode.removeFromSupernode()
}
if self.leftShadowNode.supernode != nil {
self.leftShadowNode.removeFromSupernode()
}
if self.rightShadowNode.supernode != nil {
self.rightShadowNode.removeFromSupernode()
}
}
self.setNeedsLayout()
}
self.updateSelectionState()
self.updateHiddenMedia()
}
func setup(interaction: MediaPickerInteraction, media: MediaPickerScreenImpl.Subject.Media, index: Int, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool, stories: Bool) {
self.interaction = interaction
self.theme = theme
self.selectable = selectable
self.enableAnimations = enableAnimations
self.stories = stories
self.backgroundColor = theme.list.mediaPlaceholderColor
if stories {
if self.backgroundNode.supernode == nil {
self.insertSubnode(self.backgroundNode, at: 0)
}
}
if self.draftNode.supernode != nil {
self.draftNode.removeFromSupernode()
}
if self.currentMediaState == nil || self.currentMediaState!.0.uniqueIdentifier != media.identifier || self.currentMediaState!.1 != index {
self.currentMediaState = (media.asset, index)
if self.draftNode.supernode != nil {
self.draftNode.removeFromSupernode()
}
self.setNeedsLayout()
}
self.updateSelectionState()
self.updateHiddenMedia()
}
func setup(interaction: MediaPickerInteraction, fetchResult: PHFetchResult<PHAsset>, index: Int, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool, stories: Bool) {
let isFirstTime = self.currentAssetState == nil
self.interaction = interaction
self.theme = theme
self.selectable = selectable
self.enableAnimations = enableAnimations
self.stories = stories
self.backgroundColor = theme.list.mediaPlaceholderColor
if stories {
if self.backgroundNode.supernode == nil {
self.insertSubnode(self.backgroundNode, at: 0)
}
}
if self.draftNode.supernode != nil {
self.draftNode.removeFromSupernode()
}
if self.currentAssetState == nil || self.currentAssetState!.0 !== fetchResult || self.currentAssetState!.1 != index || self.currentDraftState != nil {
let editingContext = interaction.editingState
let asset = fetchResult.object(at: index)
if asset.localIdentifier == self.currentAsset?.localIdentifier {
return
}
self.backgroundNode.image = nil
self.progressDisposable.set(
(interaction.downloadManager.downloadProgress(identifier: asset.localIdentifier)
|> deliverOnMainQueue).start(next: { [weak self] status in
if let self {
switch status {
case .none, .completed:
self.updateProgress(nil, animated: true)
case let .progress(progress):
self.updateProgress(progress, animated: true)
}
}
})
)
self.backgroundNode.image = nil
if #available(iOS 15.0, *) {
self.activateAreaNode.accessibilityLabel = "Photo \(asset.creationDate?.formatted(date: .abbreviated, time: .standard) ?? "")"
}
let editedSignal = Signal<UIImage?, NoError> { subscriber in
if let signal = editingContext.thumbnailImageSignal(forIdentifier: asset.localIdentifier) {
let disposable = signal.start(next: { next in
if let image = next as? UIImage {
subscriber.putNext(image)
} else {
subscriber.putNext(nil)
}
}, error: { _ in
}, completed: nil)!
return ActionDisposable {
disposable.dispose()
}
} else {
return EmptyDisposable
}
}
let scale = min(2.0, UIScreenScale)
let targetSize: CGSize
if stories {
targetSize = CGSize(width: 128.0 * UIScreenScale, height: 128.0 * UIScreenScale)
} else {
targetSize = CGSize(width: 128.0 * scale, height: 128.0 * scale)
}
let assetImageSignal = assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false, deliveryMode: .opportunistic, synchronous: false)
// |> then(
// assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false, deliveryMode: .highQualityFormat, synchronous: false)
// |> delay(0.03, queue: Queue.concurrentDefaultQueue())
// )
if stories {
self.imageNode.contentUpdated = { [weak self] image in
if let self {
if self.backgroundNode.image == nil {
if let image, image.size.width > image.size.height {
self.imageNode.contentMode = .scaleAspectFit
Queue.concurrentDefaultQueue().async {
let colors = mediaEditorGetGradientColors(from: image)
let gradientImage = mediaEditorGenerateGradientImage(size: CGSize(width: 3.0, height: 128.0), colors: colors.array)
Queue.mainQueue().async {
self.backgroundNode.image = gradientImage
}
}
} else {
self.imageNode.contentMode = .scaleAspectFill
}
}
}
}
}
let originalSignal = assetImageSignal
let imageSignal: Signal<UIImage?, NoError> = editedSignal
|> mapToSignal { result in
if let result = result {
return .single(result)
} else {
return originalSignal
}
}
self.imageNode.setSignal(imageSignal)
let spoilerSignal = Signal<Bool, NoError> { subscriber in
if let signal = editingContext.spoilerSignal(forIdentifier: asset.localIdentifier) {
let disposable = signal.start(next: { next in
if let next = next as? Bool {
subscriber.putNext(next)
}
}, error: { _ in
}, completed: nil)!
return ActionDisposable {
disposable.dispose()
}
} else {
return EmptyDisposable
}
}
let priceSignal = Signal<Int64?, NoError> { subscriber in
if let signal = editingContext.priceSignal(forIdentifier: asset.localIdentifier) {
let disposable = signal.start(next: { next in
subscriber.putNext(next as? Int64)
}, error: { _ in
}, completed: nil)!
return ActionDisposable {
disposable.dispose()
}
} else {
return EmptyDisposable
}
}
self.spoilerDisposable.set((combineLatest(spoilerSignal, priceSignal, self.selectionPromise.get())
|> deliverOnMainQueue).start(next: { [weak self] hasSpoiler, price, selectionState in
guard let strongSelf = self else {
return
}
strongSelf.updateHasSpoiler(hasSpoiler, price: selectionState.selected ? price : nil, isSingle: selectionState.count == 1 || selectionState.index == 1)
}))
if self.currentDraftState != nil {
self.currentDraftState = nil
}
var typeIcon: UIImage?
var duration: String?
if asset.mediaType == .video {
if asset.mediaSubtypes.contains(.videoHighFrameRate) {
typeIcon = UIImage(bundleImageName: "Media Editor/MediaSlomo")
} else if asset.mediaSubtypes.contains(.videoTimelapse) {
typeIcon = UIImage(bundleImageName: "Media Editor/MediaTimelapse")
} else {
typeIcon = UIImage(bundleImageName: "Media Editor/MediaVideo")
}
duration = stringForDuration(Int32(asset.duration))
}
if asset.isFavorite {
typeIcon = generateTintedImage(image: UIImage(bundleImageName: "Media Grid/Favorite"), color: .white)
}
if typeIcon != nil {
if self.leftShadowNode.supernode == nil {
self.addSubnode(self.leftShadowNode)
}
} else if self.leftShadowNode.supernode != nil {
self.leftShadowNode.removeFromSupernode()
}
if duration != nil {
if self.rightShadowNode.supernode == nil {
self.addSubnode(self.rightShadowNode)
}
} else if self.rightShadowNode.supernode != nil {
self.rightShadowNode.removeFromSupernode()
}
if let typeIcon {
self.typeIconNode.image = typeIcon
if self.typeIconNode.supernode == nil {
self.addSubnode(self.typeIconNode)
}
} else if self.typeIconNode.supernode != nil {
self.typeIconNode.removeFromSupernode()
}
if let duration {
self.durationNode.attributedText = NSAttributedString(string: duration, font: Font.semibold(11.0), textColor: .white)
if self.durationNode.supernode == nil {
self.addSubnode(self.durationNode)
}
} else if self.durationNode.supernode != nil {
self.durationNode.removeFromSupernode()
}
self.currentAssetState = (fetchResult, index)
self.currentAsset = asset
self.setNeedsLayout()
}
self.updateSelectionState(isFirstTime: isFirstTime)
self.updateHiddenMedia()
}
private var currentPrice: Int64?
private var didSetupSpoiler = false
private func updateHasSpoiler(_ hasSpoiler: Bool, price: Int64?, isSingle: Bool) {
var animated = true
if !self.didSetupSpoiler {
animated = false
self.didSetupSpoiler = true
}
self.currentPrice = isSingle ? price : nil
if hasSpoiler || price != nil {
if self.spoilerNode == nil {
let spoilerNode = SpoilerOverlayNode(enableAnimations: self.enableAnimations)
self.insertSubnode(spoilerNode, aboveSubnode: self.imageNode)
self.spoilerNode = spoilerNode
spoilerNode.setImage(self.imageNode.image)
if animated {
spoilerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
let bounds = self.bounds
self.spoilerNode?.update(size: bounds.size, transition: .immediate)
self.spoilerNode?.frame = CGRect(origin: .zero, size: bounds.size)
if let price {
let priceNode: PriceNode
if let currentPriceNode = self.priceNode {
priceNode = currentPriceNode
} else {
priceNode = PriceNode()
if let spoilerNode = self.spoilerNode {
self.insertSubnode(priceNode, aboveSubnode: spoilerNode)
}
self.priceNode = priceNode
if animated {
priceNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
self.priceNode?.update(size: bounds.size, price: isSingle ? price : nil, small: true, transition: .immediate)
}
} else if let spoilerNode = self.spoilerNode {
self.spoilerNode = nil
spoilerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak spoilerNode] _ in
spoilerNode?.removeFromSupernode()
})
if let priceNode = self.priceNode {
self.priceNode = nil
priceNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak priceNode] _ in
priceNode?.removeFromSupernode()
})
}
}
}
override func layout() {
super.layout()
let backgroundSize = CGSize(width: self.bounds.width, height: floorToScreenPixels(self.bounds.height / 9.0 * 16.0))
self.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((self.bounds.height - backgroundSize.height) / 2.0)), size: backgroundSize)
self.imageNode.frame = self.bounds
self.leftShadowNode.frame = CGRect(x: 0.0, y: self.bounds.height - leftShadowImage.size.height, width: min(leftShadowImage.size.width, self.bounds.width), height: leftShadowImage.size.height)
self.rightShadowNode.frame = CGRect(x: self.bounds.width - min(rightShadowImage.size.width, self.bounds.width), y: self.bounds.height - rightShadowImage.size.height, width: min(rightShadowImage.size.width, self.bounds.width), height: rightShadowImage.size.height)
self.typeIconNode.frame = CGRect(x: 0.0, y: self.bounds.height - 20.0, width: 19.0, height: 19.0)
self.activateAreaNode.frame = self.bounds
if self.durationNode.supernode != nil {
let durationSize = self.durationNode.updateLayout(self.bounds.size)
self.durationNode.frame = CGRect(origin: CGPoint(x: self.bounds.size.width - durationSize.width - 6.0, y: self.bounds.height - durationSize.height - 6.0), size: durationSize)
}
if self.draftNode.supernode != nil {
let draftSize = self.draftNode.updateLayout(self.bounds.size)
self.draftNode.frame = CGRect(origin: CGPoint(x: 7.0, y: 5.0), size: draftSize)
}
let checkSize = CGSize(width: 29.0, height: 29.0)
self.checkNode?.frame = CGRect(origin: CGPoint(x: self.bounds.width - checkSize.width - 3.0, y: 3.0), size: checkSize)
if let spoilerNode = self.spoilerNode, self.bounds.width > 0.0 {
spoilerNode.frame = self.bounds
spoilerNode.update(size: self.bounds.size, transition: .immediate)
}
if let priceNode = self.priceNode, self.bounds.width > 0.0 {
priceNode.frame = self.bounds
priceNode.update(size: self.bounds.size, price: self.currentPrice, small: true, transition: .immediate)
}
let statusSize = CGSize(width: 40.0, height: 40.0)
if let statusNode = self.statusNode {
statusNode.view.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((self.bounds.width - statusSize.width) / 2.0), y: floorToScreenPixels((self.bounds.height - statusSize.height) / 2.0)), size: statusSize)
}
}
func transitionView(snapshot: Bool) -> UIView {
if snapshot {
let view = self.imageNode.layer.snapshotContentTreeAsView(unhide: true)!
view.frame = self.convert(self.bounds, to: nil)
return view
} else {
return self.view
}
}
func transitionImage() -> UIImage? {
if let backgroundImage = self.backgroundNode.image {
let size = CGSize(width: self.bounds.width, height: self.bounds.height / 9.0 * 16.0)
return generateImage(size, contextGenerator: { size, context in
if let cgImage = backgroundImage.cgImage {
context.draw(cgImage, in: CGRect(origin: .zero, size: size))
if let image = self.imageNode.image, let cgImage = image.cgImage {
let fittedSize = image.size.fitted(size)
let fittedFrame = CGRect(origin: CGPoint(x: (size.width - fittedSize.width) / 2.0, y: (size.height - fittedSize.height) / 2.0), size: fittedSize)
context.draw(cgImage, in: fittedFrame)
}
}
})
} else {
return self.imageNode.image
}
}
@objc func imageNodeTap(_ recognizer: UITapGestureRecognizer) {
if let (draft, _) = self.currentDraftState {
self.interaction?.openDraft(draft, self.imageNode.image)
return
}
guard let (fetchResult, index) = self.currentAssetState else {
return
}
if self.statusNode != nil {
if let asset = self.currentAsset {
self.interaction?.downloadManager.cancel(identifier: asset.localIdentifier)
}
} else {
self.interaction?.openMedia(fetchResult, index, self.imageNode.image)
}
}
}
class SpoilerOverlayNode: ASDisplayNode {
private let blurNode: ASImageNode
let dustNode: MediaDustNode
private var maskView: UIView?
private var maskLayer: CAShapeLayer?
init(enableAnimations: Bool) {
self.blurNode = ASImageNode()
self.blurNode.displaysAsynchronously = false
self.blurNode.contentMode = .scaleAspectFill
self.dustNode = MediaDustNode(enableAnimations: enableAnimations)
super.init()
self.clipsToBounds = true
self.isUserInteractionEnabled = false
self.addSubnode(self.blurNode)
self.addSubnode(self.dustNode)
}
override func didLoad() {
super.didLoad()
let maskView = UIView()
self.maskView = maskView
// self.dustNode.view.mask = maskView
let maskLayer = CAShapeLayer()
maskLayer.fillRule = .evenOdd
maskLayer.fillColor = UIColor.white.cgColor
maskView.layer.addSublayer(maskLayer)
self.maskLayer = maskLayer
}
func setImage(_ image: UIImage?) {
self.blurNode.image = image.flatMap { blurredImage($0) }
}
func update(size: CGSize, transition: ContainedViewLayoutTransition) {
transition.updateFrame(node: self.blurNode, frame: CGRect(origin: .zero, size: size))
transition.updateFrame(node: self.dustNode, frame: CGRect(origin: .zero, size: size))
self.dustNode.update(size: size, color: .white, transition: transition)
}
}
private func blurredImage(_ image: UIImage) -> UIImage? {
guard let image = image.cgImage else {
return nil
}
let thumbnailSize = CGSize(width: image.width, height: image.height)
let thumbnailContextSize = thumbnailSize.aspectFilled(CGSize(width: 20.0, height: 20.0))
if let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) {
thumbnailContext.withFlippedContext { c in
c.interpolationQuality = .none
c.draw(image, in: CGRect(origin: CGPoint(), size: thumbnailContextSize))
}
imageFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes)
let thumbnailContext2Size = thumbnailSize.aspectFitted(CGSize(width: 100.0, height: 100.0))
if let thumbnailContext2 = DrawingContext(size: thumbnailContext2Size, scale: 1.0) {
thumbnailContext2.withFlippedContext { c in
c.interpolationQuality = .none
if let image = thumbnailContext.generateImage()?.cgImage {
c.draw(image, in: CGRect(origin: CGPoint(), size: thumbnailContext2Size))
}
}
imageFastBlur(Int32(thumbnailContext2Size.width), Int32(thumbnailContext2Size.height), Int32(thumbnailContext2.bytesPerRow), thumbnailContext2.bytes)
adjustSaturationInContext(context: thumbnailContext2, saturation: 1.7)
return thumbnailContext2.generateImage()
}
}
return nil
}
@@ -0,0 +1,74 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import SolidRoundedButtonNode
final class MediaPickerManageNode: ASDisplayNode {
enum Subject {
case limitedMedia
case camera
}
private let textNode: ImmediateTextNode
private let measureButtonNode: ImmediateTextNode
private let buttonNode: SolidRoundedButtonNode
var pressed: () -> Void = {}
override init() {
self.textNode = ImmediateTextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.textAlignment = .left
self.textNode.maximumNumberOfLines = 0
self.measureButtonNode = ImmediateTextNode()
self.buttonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(backgroundColor: .black, foregroundColor: .white), fontSize: 15.0, height: 28.0, cornerRadius: 14.0)
super.init()
self.addSubnode(self.textNode)
self.addSubnode(self.buttonNode)
self.buttonNode.pressed = { [weak self] in
self?.pressed()
}
}
private var theme: PresentationTheme?
func update(layout: ContainerViewLayout, theme: PresentationTheme, strings: PresentationStrings, subject: Subject, transition: ContainedViewLayoutTransition) -> CGFloat {
let themeUpdated = self.theme != theme
self.theme = theme
let text: String
switch subject {
case .limitedMedia:
text = strings.Attachment_LimitedMediaAccessText
case .camera:
text = strings.Attachment_CameraAccessText
}
let title = strings.Attachment_Manage.uppercased()
self.measureButtonNode.attributedText = NSAttributedString(string: title, font: Font.semibold(15.0), textColor: .white, paragraphAlignment: .center)
let measureButtonSize = self.measureButtonNode.updateLayout(layout.size)
let buttonWidth = measureButtonSize.width + 26.0
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: theme.list.freeTextColor, paragraphAlignment: .left)
let textSize = self.textNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - 16.0 - buttonWidth - 26.0, height: layout.size.height))
let panelHeight = max(64.0, textSize.height + 24.0)
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + 16.0, y: floorToScreenPixels((panelHeight - textSize.height) / 2.0) - 5.0), size: textSize))
if themeUpdated {
self.buttonNode.updateTheme(SolidRoundedButtonTheme(theme: theme))
}
self.buttonNode.title = title
let buttonHeight = self.buttonNode.updateLayout(width: buttonWidth, transition: transition)
transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.right - buttonWidth - 10.0, y: floorToScreenPixels((panelHeight - buttonHeight) / 2.0) - 5.0), size: CGSize(width: buttonWidth, height: buttonHeight)))
return panelHeight
}
}
@@ -0,0 +1,203 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import SolidRoundedButtonNode
import PresentationDataUtils
final class MediaPickerPlaceholderNode: ASDisplayNode {
enum Content {
case intro(story: Bool)
case bannedSendMedia(text: String, canBoost: Bool)
}
private let content: Content
private var animationNode: AnimatedStickerNode
private let titleNode: ImmediateTextNode
private let textNode: ImmediateTextNode
private let buttonNode: SolidRoundedButtonNode
private var validLayout: ContainerViewLayout?
private var cameraButtonNode: HighlightTrackingButtonNode
private var cameraTextNode: ImmediateTextNode
private var cameraIconNode: ASImageNode
var boostPressed: () -> Void = {}
var settingsPressed: () -> Void = {}
var cameraPressed: () -> Void = {}
init(content: Content) {
self.content = content
let name: String
let playbackMode: AnimatedStickerPlaybackMode
switch content {
case .intro:
name = "Photos"
playbackMode = .loop
case .bannedSendMedia:
name = "Banned"
playbackMode = .once
}
self.animationNode = DefaultAnimatedStickerNodeImpl()
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: name), width: 320, height: 320, playbackMode: playbackMode, mode: .direct(cachePathPrefix: nil))
self.animationNode.visibility = true
self.titleNode = ImmediateTextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.textAlignment = .center
self.titleNode.maximumNumberOfLines = 1
self.textNode = ImmediateTextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.lineSpacing = 0.1
self.textNode.textAlignment = .center
self.textNode.maximumNumberOfLines = 0
self.buttonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(backgroundColor: .black, foregroundColor: .white), height: 50.0, cornerRadius: 11.0, isShimmering: true)
self.cameraButtonNode = HighlightTrackingButtonNode()
self.cameraButtonNode.alpha = 0.0
self.cameraButtonNode.isUserInteractionEnabled = false
self.cameraTextNode = ImmediateTextNode()
self.cameraTextNode.isUserInteractionEnabled = false
self.cameraIconNode = ASImageNode()
self.cameraIconNode.displaysAsynchronously = false
self.cameraIconNode.isUserInteractionEnabled = false
super.init()
self.addSubnode(self.animationNode)
self.addSubnode(self.textNode)
switch self.content {
case .bannedSendMedia(_, true):
self.addSubnode(self.buttonNode)
self.buttonNode.pressed = { [weak self] in
self?.boostPressed()
}
case .intro:
self.addSubnode(self.titleNode)
self.addSubnode(self.buttonNode)
self.addSubnode(self.cameraButtonNode)
self.cameraButtonNode.addSubnode(self.cameraTextNode)
self.cameraButtonNode.addSubnode(self.cameraIconNode)
self.cameraButtonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.cameraTextNode.layer.removeAnimation(forKey: "opacity")
strongSelf.cameraTextNode.alpha = 0.4
strongSelf.cameraIconNode.layer.removeAnimation(forKey: "opacity")
strongSelf.cameraIconNode.alpha = 0.4
} else {
strongSelf.cameraTextNode.alpha = 1.0
strongSelf.cameraTextNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.cameraIconNode.alpha = 1.0
strongSelf.cameraIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.cameraButtonNode.addTarget(self, action: #selector(self.cameraButtonPressed), forControlEvents: .touchUpInside)
self.buttonNode.pressed = { [weak self] in
self?.settingsPressed()
}
default:
break
}
}
@objc private func cameraButtonPressed() {
self.cameraPressed()
}
private var theme: PresentationTheme?
func update(layout: ContainerViewLayout, theme: PresentationTheme, strings: PresentationStrings, hasCamera: Bool, transition: ContainedViewLayoutTransition) {
self.validLayout = layout
let themeUpdated = self.theme != theme
self.theme = theme
var imageSize = CGSize(width: 144.0, height: 144.0)
var insets = layout.insets(options: [])
if layout.size.width == 320.0 {
insets.top += -60.0
imageSize = CGSize(width: 112.0, height: 112.0)
} else {
insets.top += -160.0
}
let imageSpacing: CGFloat = 12.0
let textSpacing: CGFloat = 12.0
let buttonSpacing: CGFloat = 20.0
let cameraSpacing: CGFloat = 13.0
let imageHeight = layout.size.width < layout.size.height ? imageSize.height + imageSpacing : 0.0
if themeUpdated {
self.buttonNode.updateTheme(SolidRoundedButtonTheme(theme: theme))
self.cameraIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Attach Menu/OpenCamera"), color: theme.list.itemAccentColor)
}
switch self.content {
case .intro:
self.buttonNode.title = strings.Attachment_OpenSettings
case .bannedSendMedia:
self.buttonNode.title = strings.Attachment_BoostToUnlock
}
let buttonWidth: CGFloat = 248.0
let buttonHeight = self.buttonNode.updateLayout(width: buttonWidth, transition: transition)
let title: String
let text: String
switch self.content {
case let .intro(story):
title = strings.Attachment_MediaAccessTitle
text = story ? strings.Attachment_MediaAccessStoryText : strings.Attachment_MediaAccessText
case let .bannedSendMedia(banDescription, _):
title = ""
text = banDescription
}
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(17.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .center)
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: theme.list.freeTextColor, paragraphAlignment: .center)
self.cameraTextNode.attributedText = NSAttributedString(string: strings.Attachment_OpenCamera, font: Font.regular(17.0), textColor: theme.list.itemAccentColor, paragraphAlignment: .center)
let titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - 40.0, height: max(1.0, layout.size.height - insets.top - insets.bottom)))
let textSize = self.textNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - 40.0, height: max(1.0, layout.size.height - insets.top - insets.bottom)))
let cameraSize = self.cameraTextNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - 70.0, height: max(1.0, layout.size.height - insets.top - insets.bottom)))
let totalHeight = imageHeight + titleSize.height + textSpacing + textSize.height + buttonSpacing + buttonHeight + cameraSpacing + cameraSize.height
let topOffset = insets.top + floor((layout.size.height - insets.top - insets.bottom - totalHeight) / 2.0)
transition.updateAlpha(node: self.animationNode, alpha: imageHeight > 0.0 ? 1.0 : 0.0)
transition.updateFrame(node: self.animationNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - imageSize.width) / 2.0), y: topOffset), size: imageSize))
self.animationNode.updateLayout(size: imageSize)
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + floor((layout.size.width - titleSize.width - layout.safeInsets.left - layout.safeInsets.right) / 2.0), y: topOffset + imageHeight), size: titleSize))
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + floor((layout.size.width - textSize.width - layout.safeInsets.left - layout.safeInsets.right) / 2.0), y: self.titleNode.frame.maxY + textSpacing), size: textSize))
transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + floor((layout.size.width - buttonWidth - layout.safeInsets.left - layout.safeInsets.right) / 2.0), y: self.textNode.frame.maxY + buttonSpacing), size: CGSize(width: buttonWidth, height: buttonHeight)))
if let image = self.cameraIconNode.image {
let cameraTotalSize = CGSize(width: cameraSize.width + image.size.width + 10.0, height: 44.0)
transition.updateFrame(node: self.cameraIconNode, frame: CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((cameraTotalSize.height - image.size.height) / 2.0)), size: image.size))
transition.updateFrame(node: self.cameraTextNode, frame: CGRect(origin: CGPoint(x: cameraTotalSize.width - cameraSize.width, y: floorToScreenPixels((cameraTotalSize.height - cameraSize.height) / 2.0)), size: cameraSize))
transition.updateFrame(node: self.cameraButtonNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + floor((layout.size.width - cameraTotalSize.width - layout.safeInsets.left - layout.safeInsets.right) / 2.0), y: self.buttonNode.frame.maxY + cameraSpacing), size: cameraTotalSize))
}
self.cameraButtonNode.isUserInteractionEnabled = hasCamera
transition.updateAlpha(node: self.cameraButtonNode, alpha: hasCamera ? 1.0 : 0.0)
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,283 @@
import Foundation
import UIKit
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 subtitleNode: ImmediateTextNode
private let arrowNode: ASImageNode
private let segmentedControlNode: SegmentedControlNode
private let glass: Bool
public var theme: PresentationTheme {
didSet {
self.titleNode.attributedText = NSAttributedString(string: self.title, font: NavigationBar.titleFont, textColor: self.isDark ? .white : self.theme.rootController.navigationBar.primaryTextColor)
self.subtitleNode.attributedText = NSAttributedString(string: self.subtitle, font: Font.regular(12.0), textColor: self.isDark ? .white.withAlphaComponent(0.5) : self.theme.rootController.navigationBar.secondaryTextColor)
self.segmentedControlNode.updateTheme(SegmentedControlTheme(theme: self.theme))
if self.glass {
self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Navigation/TitleExpand"), color: self.isDark ? UIColor.white.withAlphaComponent(0.5) : self.theme.rootController.navigationBar.primaryTextColor.withAlphaComponent(0.4))
}
self.setNeedsLayout()
}
}
public var isDark: Bool = false {
didSet {
if self.isDark != oldValue {
self.titleNode.attributedText = NSAttributedString(string: self.title, font: NavigationBar.titleFont, textColor: self.isDark ? .white : self.theme.rootController.navigationBar.primaryTextColor)
self.subtitleNode.attributedText = NSAttributedString(string: self.subtitle, font: Font.regular(12.0), textColor: self.isDark ? .white.withAlphaComponent(0.5) : self.theme.rootController.navigationBar.secondaryTextColor)
if self.glass {
self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Navigation/TitleExpand"), color: self.isDark ? UIColor.white.withAlphaComponent(0.5) : self.theme.rootController.navigationBar.primaryTextColor.withAlphaComponent(0.4))
}
self.setNeedsLayout()
}
}
}
public var title: String = "" {
didSet {
if self.title != oldValue {
self.titleNode.attributedText = NSAttributedString(string: self.title, font: NavigationBar.titleFont, textColor: self.isDark ? .white : self.theme.rootController.navigationBar.primaryTextColor)
self.setNeedsLayout()
}
}
}
public var subtitle: String = "" {
didSet {
if self.subtitle != oldValue {
self.subtitleNode.attributedText = NSAttributedString(string: self.subtitle, font: Font.regular(12.0), textColor: self.isDark ? .white.withAlphaComponent(0.5) : self.theme.rootController.navigationBar.secondaryTextColor)
self.setNeedsLayout()
}
}
}
public var isEnabled: Bool = false {
didSet {
self.buttonNode.isUserInteractionEnabled = self.isEnabled
self.arrowNode.isHidden = !self.isEnabled
}
}
public func updateIsDark(isDark: Bool, animated: Bool) {
if animated {
if self.isDark != isDark {
if let snapshotView = self.titleNode.view.snapshotContentTree() {
snapshotView.frame = self.titleNode.frame
self.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.5, removeOnCompletion: false, completion: { _ in
snapshotView.removeFromSuperview()
})
self.titleNode.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.5)
}
if let snapshotView = self.subtitleNode.view.snapshotContentTree() {
snapshotView.frame = self.subtitleNode.frame
self.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.5, removeOnCompletion: false, completion: { _ in
snapshotView.removeFromSuperview()
})
self.subtitleNode.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.5)
}
if let snapshotView = self.arrowNode.view.snapshotContentTree() {
snapshotView.frame = self.arrowNode.frame
self.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.5, removeOnCompletion: false, completion: { _ in
snapshotView.removeFromSuperview()
})
self.arrowNode.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.5)
}
}
}
self.isDark = isDark
}
public func updateTitle(title: String, subtitle: String = "", isEnabled: Bool, animated: Bool) {
if animated {
if self.title != title {
if let snapshotView = self.titleNode.view.snapshotContentTree() {
snapshotView.frame = self.titleNode.frame
self.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
snapshotView.removeFromSuperview()
})
self.titleNode.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
if self.subtitle != subtitle {
if let snapshotView = self.subtitleNode.view.snapshotContentTree() {
snapshotView.frame = self.subtitleNode.frame
self.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
snapshotView.removeFromSuperview()
})
self.subtitleNode.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
if self.isEnabled != isEnabled {
if let snapshotView = self.arrowNode.view.snapshotContentTree() {
snapshotView.frame = self.arrowNode.frame
self.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
snapshotView.removeFromSuperview()
})
self.arrowNode.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
}
self.title = title
self.subtitle = subtitle
self.isEnabled = isEnabled
}
public var isHighlighted: Bool = false {
didSet {
self.alpha = self.isHighlighted ? 0.5 : 1.0
}
}
public var segmentsHidden = true {
didSet {
if self.segmentsHidden != oldValue {
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
transition.updateAlpha(node: self.titleNode, alpha: self.segmentsHidden ? 1.0 : 0.0)
transition.updateAlpha(node: self.subtitleNode, alpha: self.segmentsHidden ? 1.0 : 0.0)
transition.updateAlpha(node: self.arrowNode, alpha: self.segmentsHidden ? 1.0 : 0.0)
transition.updateAlpha(node: self.segmentedControlNode, alpha: self.segmentsHidden ? 0.0 : 1.0)
self.segmentedControlNode.isUserInteractionEnabled = !self.segmentsHidden
self.buttonNode.isUserInteractionEnabled = self.isEnabled && self.segmentsHidden
}
}
}
public var segments: [String] {
didSet {
if self.segments != oldValue {
self.segmentedControlNode.items = self.segments.map { SegmentedControlItem(title: $0) }
self.setNeedsLayout()
}
}
}
public var index: Int {
get {
return self.segmentedControlNode.selectedIndex
}
set {
self.segmentedControlNode.selectedIndex = newValue
}
}
public var indexUpdated: ((Int) -> Void)?
public var action: () -> Void = {}
public init(theme: PresentationTheme, glass: Bool, segments: [String], selectedIndex: Int) {
self.theme = theme
self.glass = glass
self.segments = segments
self.contextSourceNode = ContextReferenceContentNode()
self.buttonNode = HighlightTrackingButtonNode()
self.titleNode = ImmediateTextNode()
self.titleNode.displaysAsynchronously = false
self.subtitleNode = ImmediateTextNode()
self.subtitleNode.displaysAsynchronously = false
self.arrowNode = ASImageNode()
self.arrowNode.displaysAsynchronously = false
if glass {
self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Navigation/TitleExpand"), color: theme.rootController.navigationBar.primaryTextColor.withAlphaComponent(0.4))
} else {
self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/DownArrow"), 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
super.init(frame: CGRect())
self.segmentedControlNode.selectedIndexChanged = { [weak self] index in
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.subtitleNode)
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) {
fatalError("init(coder:) has not been implemented")
}
override public func layoutSubviews() {
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))
let subtitleSize = self.subtitleNode.updateLayout(CGSize(width: 210.0, height: 44.0))
var totalHeight: CGFloat = titleSize.height
if subtitleSize.height > 0.0 {
totalHeight += subtitleSize.height
}
let verticalOffset: CGFloat = self.glass ? 3.0 : 0.0
let arrowOffset: CGFloat = self.glass ? 1.0 : 5.0
var totalWidth = titleSize.width
if let arrowSize = self.arrowNode.image?.size, !self.arrowNode.isHidden {
totalWidth += arrowOffset + arrowSize.width
}
self.titleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - totalWidth) / 2.0), y: floorToScreenPixels((size.height - totalHeight) / 2.0) + verticalOffset), size: titleSize)
self.subtitleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - subtitleSize.width) / 2.0), y: floorToScreenPixels((size.height - totalHeight) / 2.0) + subtitleSize.height + 7.0 + verticalOffset), size: subtitleSize)
if let arrowSize = self.arrowNode.image?.size {
self.arrowNode.frame = CGRect(origin: CGPoint(x: self.titleNode.frame.maxX + arrowOffset, y: floorToScreenPixels((size.height - totalHeight) / 2.0) + titleSize.height / 2.0 - arrowSize.height / 2.0 + 1.0 - UIScreenPixel + verticalOffset), size: arrowSize)
}
self.buttonNode.frame = CGRect(origin: .zero, size: size)
}
@objc private func buttonPressed() {
self.action()
}
}