diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 77a7b3d94f..6d63268b5b 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -106,6 +106,7 @@ "PUSH_MESSAGE_FILES_TEXT_any" = "sent you %d files"; "PUSH_MESSAGE_THEME" = "%1$@|changed chat theme to %2$@"; "PUSH_MESSAGE_NOTHEME" = "%1$@|disabled chat theme"; +"PUSH_MESSAGE_RECURRING_PAY" = "%1$@|You were charged %2$@"; "PUSH_CHANNEL_MESSAGE_TEXT" = "%1$@|%2$@"; "PUSH_CHANNEL_MESSAGE_NOTEXT" = "%1$@|posted a message"; diff --git a/submodules/AnimatedStickerNode/Sources/VideoStickerFrameSource.swift b/submodules/AnimatedStickerNode/Sources/VideoStickerFrameSource.swift index 166f7cdf80..2038bbed6c 100644 --- a/submodules/AnimatedStickerNode/Sources/VideoStickerFrameSource.swift +++ b/submodules/AnimatedStickerNode/Sources/VideoStickerFrameSource.swift @@ -272,6 +272,10 @@ private final class VideoStickerFrameSourceCache { private let useCache = true +public func makeVideoStickerDirectFrameSource(queue: Queue, path: String, width: Int, height: Int, cachePathPrefix: String?) -> AnimatedStickerFrameSource? { + return VideoStickerDirectFrameSource(queue: queue, path: path, width: width, height: height, cachePathPrefix: cachePathPrefix) +} + final class VideoStickerDirectFrameSource: AnimatedStickerFrameSource { private let queue: Queue private let path: String diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index 011a9cb6af..0cb84a5b20 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -593,7 +593,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { }, displayVideoUnmuteTip: { _ in }, switchMediaRecordingMode: { }, setupMessageAutoremoveTimeout: { - }, sendSticker: { _, _, _, _ in + }, sendSticker: { _, _, _, _, _ in return false }, unblockPeer: { }, pinMessage: { _, _ in diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift index 13e206aff6..cae532aea0 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift @@ -98,7 +98,7 @@ public final class ChatPanelInterfaceInteraction { public let displayVideoUnmuteTip: (CGPoint?) -> Void public let switchMediaRecordingMode: () -> Void public let setupMessageAutoremoveTimeout: () -> Void - public let sendSticker: (FileMediaReference, Bool, UIView, CGRect) -> Bool + public let sendSticker: (FileMediaReference, Bool, UIView, CGRect, CALayer?) -> Bool public let unblockPeer: () -> Void public let pinMessage: (MessageId, ContextControllerProtocol?) -> Void public let unpinMessage: (MessageId, Bool, ContextControllerProtocol?) -> Void @@ -194,7 +194,7 @@ public final class ChatPanelInterfaceInteraction { displayVideoUnmuteTip: @escaping (CGPoint?) -> Void, switchMediaRecordingMode: @escaping () -> Void, setupMessageAutoremoveTimeout: @escaping () -> Void, - sendSticker: @escaping (FileMediaReference, Bool, UIView, CGRect) -> Bool, + sendSticker: @escaping (FileMediaReference, Bool, UIView, CGRect, CALayer?) -> Bool, unblockPeer: @escaping () -> Void, pinMessage: @escaping (MessageId, ContextControllerProtocol?) -> Void, unpinMessage: @escaping (MessageId, Bool, ContextControllerProtocol?) -> Void, @@ -391,7 +391,7 @@ public final class ChatPanelInterfaceInteraction { }, displayVideoUnmuteTip: { _ in }, switchMediaRecordingMode: { }, setupMessageAutoremoveTimeout: { - }, sendSticker: { _, _, _, _ in + }, sendSticker: { _, _, _, _, _ in return false }, unblockPeer: { }, pinMessage: { _, _ in diff --git a/submodules/Components/PagerComponent/Sources/PagerComponent.swift b/submodules/Components/PagerComponent/Sources/PagerComponent.swift index daa4be7312..12b311e547 100644 --- a/submodules/Components/PagerComponent/Sources/PagerComponent.swift +++ b/submodules/Components/PagerComponent/Sources/PagerComponent.swift @@ -46,19 +46,22 @@ public final class PagerComponentPanelEnvironment: Equatable { public let contentIcons: [AnyComponentWithIdentity] public let contentAccessoryRightButtons: [AnyComponentWithIdentity] public let activeContentId: AnyHashable? + public let navigateToContentId: (AnyHashable) -> Void init( contentOffset: CGFloat, contentTopPanels: [AnyComponentWithIdentity], contentIcons: [AnyComponentWithIdentity], contentAccessoryRightButtons: [AnyComponentWithIdentity], - activeContentId: AnyHashable? + activeContentId: AnyHashable?, + navigateToContentId: @escaping (AnyHashable) -> Void ) { self.contentOffset = contentOffset self.contentTopPanels = contentTopPanels self.contentIcons = contentIcons self.contentAccessoryRightButtons = contentAccessoryRightButtons self.activeContentId = activeContentId + self.navigateToContentId = navigateToContentId } public static func ==(lhs: PagerComponentPanelEnvironment, rhs: PagerComponentPanelEnvironment) -> Bool { @@ -253,6 +256,16 @@ public final class PagerComponent: Component { self.component = component self.state = state + let navigateToContentId: (AnyHashable) -> Void = { [weak self] id in + guard let strongSelf = self else { + return + } + if strongSelf.centralId != id { + strongSelf.centralId = id + strongSelf.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + } + } + var centralId: AnyHashable? if let current = self.centralId { if component.contents.contains(where: { $0.id == current }) { @@ -309,7 +322,8 @@ public final class PagerComponent: Component { contentTopPanels: component.contentTopPanels, contentIcons: [], contentAccessoryRightButtons: [], - activeContentId: centralId + activeContentId: centralId, + navigateToContentId: navigateToContentId ) }, containerSize: availableSize @@ -356,7 +370,8 @@ public final class PagerComponent: Component { contentTopPanels: [], contentIcons: component.contentIcons, contentAccessoryRightButtons: component.contentAccessoryRightButtons, - activeContentId: centralId + activeContentId: centralId, + navigateToContentId: navigateToContentId ) }, containerSize: availableSize @@ -404,15 +419,27 @@ public final class PagerComponent: Component { if let centralId = self.centralId, let centralIndex = component.contents.firstIndex(where: { $0.id == centralId }) { let contentSize = CGSize(width: availableSize.width, height: availableSize.height) + var referenceFrames: [AnyHashable: CGRect] = [:] + if case .none = transition.animation { + } else { + for (id, contentView) in self.contentViews { + referenceFrames[id] = contentView.view.frame + } + } + for index in 0 ..< component.contents.count { let indexOffset = index - centralIndex - var contentFrame = CGRect(origin: CGPoint(x: contentSize.width * CGFloat(indexOffset), y: 0.0), size: contentSize) + let clippedIndexOffset = max(-1, min(1, indexOffset)) + var checkingContentFrame = CGRect(origin: CGPoint(x: contentSize.width * CGFloat(indexOffset), y: 0.0), size: contentSize) + var contentFrame = CGRect(origin: CGPoint(x: contentSize.width * CGFloat(clippedIndexOffset), y: 0.0), size: contentSize) + if let paneTransitionGestureState = self.paneTransitionGestureState { + checkingContentFrame.origin.x += paneTransitionGestureState.fraction * availableSize.width contentFrame.origin.x += paneTransitionGestureState.fraction * availableSize.width } let content = component.contents[index] - let isInBounds = CGRect(origin: CGPoint(), size: availableSize).intersects(contentFrame) + let isInBounds = CGRect(origin: CGPoint(), size: availableSize).intersects(checkingContentFrame) var isPartOfTransition = false if case .none = transition.animation { @@ -462,7 +489,35 @@ public final class PagerComponent: Component { ) if wasAdded { - contentView.view.frame = contentFrame + if case .none = transition.animation { + contentView.view.frame = contentFrame + } else { + var referenceDirectionIsRight: Bool? + for (previousId, previousFrame) in referenceFrames { + if let previousIndex = component.contents.firstIndex(where: { $0.id == previousId }) { + if previousFrame.minX == 0.0 { + if previousIndex < index { + referenceDirectionIsRight = true + } else { + referenceDirectionIsRight = false + } + break + } + } + } + if let referenceDirectionIsRight = referenceDirectionIsRight { + contentView.view.frame = contentFrame.offsetBy(dx: referenceDirectionIsRight ? contentFrame.width : (-contentFrame.width), dy: 0.0) + transition.setFrame(view: contentView.view, frame: contentFrame, completion: { [weak self] completed in + if completed && !isInBounds && isPartOfTransition { + DispatchQueue.main.async { + self?.state?.updated(transition: .immediate) + } + } + }) + } else { + contentView.view.frame = contentFrame + } + } } else { transition.setFrame(view: contentView.view, frame: contentFrame, completion: { [weak self] completed in if completed && !isInBounds && isPartOfTransition { diff --git a/submodules/Display/Source/UIKitUtils.swift b/submodules/Display/Source/UIKitUtils.swift index 2b6ee6a68e..3592dc0062 100644 --- a/submodules/Display/Source/UIKitUtils.swift +++ b/submodules/Display/Source/UIKitUtils.swift @@ -483,6 +483,38 @@ private func makeLayerSubtreeSnapshot(layer: CALayer) -> CALayer? { return view } +private func makeLayerSubtreeSnapshotAsView(layer: CALayer) -> UIView? { + if layer is AVSampleBufferDisplayLayer { + return nil + } + let view = UIView() + view.layer.isHidden = layer.isHidden + view.layer.opacity = layer.opacity + view.layer.contents = layer.contents + view.layer.contentsRect = layer.contentsRect + view.layer.contentsScale = layer.contentsScale + view.layer.contentsCenter = layer.contentsCenter + view.layer.contentsGravity = layer.contentsGravity + view.layer.masksToBounds = layer.masksToBounds + view.layer.cornerRadius = layer.cornerRadius + view.layer.backgroundColor = layer.backgroundColor + if let sublayers = layer.sublayers { + for sublayer in sublayers { + let subtree = makeLayerSubtreeSnapshotAsView(layer: sublayer) + if let subtree = subtree { + subtree.layer.transform = sublayer.transform + subtree.layer.frame = sublayer.frame + subtree.layer.bounds = sublayer.bounds + view.addSubview(subtree) + } else { + return nil + } + } + } + return view +} + + public extension UIView { func snapshotContentTree(unhide: Bool = false, keepTransform: Bool = false) -> UIView? { let wasHidden = self.isHidden @@ -523,6 +555,26 @@ public extension CALayer { } } +public extension CALayer { + func snapshotContentTreeAsView(unhide: Bool = false) -> UIView? { + let wasHidden = self.isHidden + if unhide && wasHidden { + self.isHidden = false + } + let snapshot = makeLayerSubtreeSnapshotAsView(layer: self) + if unhide && wasHidden { + self.isHidden = true + } + if let snapshot = snapshot { + snapshot.frame = self.frame + snapshot.bounds = self.bounds + return snapshot + } + + return nil + } +} + public extension CGRect { var topLeft: CGPoint { return self.origin diff --git a/submodules/SoftwareVideo/Sources/SampleBufferPool.swift b/submodules/SoftwareVideo/Sources/SampleBufferPool.swift index 1bc235682e..da15e330a1 100644 --- a/submodules/SoftwareVideo/Sources/SampleBufferPool.swift +++ b/submodules/SoftwareVideo/Sources/SampleBufferPool.swift @@ -18,7 +18,6 @@ public final class SampleBufferLayer { public let layer: AVSampleBufferDisplayLayer private let enqueue: (AVSampleBufferDisplayLayer) -> Void - public var isFreed: Bool = false fileprivate init(layer: AVSampleBufferDisplayLayer, enqueue: @escaping (AVSampleBufferDisplayLayer) -> Void) { self.layer = layer diff --git a/submodules/SoftwareVideo/Sources/SoftwareVideoLayerFrameManager.swift b/submodules/SoftwareVideo/Sources/SoftwareVideoLayerFrameManager.swift index e085159f60..fd3ab89d89 100644 --- a/submodules/SoftwareVideo/Sources/SoftwareVideoLayerFrameManager.swift +++ b/submodules/SoftwareVideo/Sources/SoftwareVideoLayerFrameManager.swift @@ -5,6 +5,7 @@ import TelegramCore import SwiftSignalKit import CoreMedia import UniversalMediaPlayer +import AVFoundation public let softwareVideoApplyQueue = Queue() public let softwareVideoWorkers = ThreadPool(threadCount: 3, threadPriority: 0.2) @@ -25,7 +26,8 @@ public final class SoftwareVideoLayerFrameManager { private let resource: MediaResource private let secondaryResource: MediaResource? private let queue: ThreadPoolQueue - private let layerHolder: SampleBufferLayer + private let layerHolder: SampleBufferLayer? + private weak var layer: AVSampleBufferDisplayLayer? private var rotationAngle: CGFloat = 0.0 private var aspect: CGFloat = 1.0 @@ -33,9 +35,9 @@ public final class SoftwareVideoLayerFrameManager { private var layerRotationAngleAndAspect: (CGFloat, CGFloat)? private var didStart = false - var started: () -> Void = { } + public var started: () -> Void = { } - public init(account: Account, fileReference: FileMediaReference, layerHolder: SampleBufferLayer, hintVP9: Bool = false) { + public init(account: Account, fileReference: FileMediaReference, layerHolder: SampleBufferLayer?, layer: AVSampleBufferDisplayLayer? = nil, hintVP9: Bool = false) { var resource = fileReference.media.resource var secondaryResource: MediaResource? for attribute in fileReference.media.attributes { @@ -54,8 +56,9 @@ public final class SoftwareVideoLayerFrameManager { self.secondaryResource = secondaryResource self.queue = ThreadPoolQueue(threadPool: softwareVideoWorkers) self.layerHolder = layerHolder - layerHolder.layer.videoGravity = .resizeAspectFill - layerHolder.layer.masksToBounds = true + self.layer = layer ?? layerHolder?.layer + self.layer?.videoGravity = .resizeAspectFill + self.layer?.masksToBounds = true self.fetchDisposable = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: fileReference.resourceReference(resource)).start() } @@ -139,8 +142,8 @@ public final class SoftwareVideoLayerFrameManager { for i in (0 ... latestFrameIndex).reversed() { self.frames.remove(at: i) } - if self.layerHolder.layer.status == .failed { - self.layerHolder.layer.flush() + if self.layer?.status == .failed { + self.layer?.flush() } /*if self.layerRotationAngleAndAspect?.0 != self.rotationAngle || self.layerRotationAngleAndAspect?.1 != self.aspect { self.layerRotationAngleAndAspect = (self.rotationAngle, self.aspect) @@ -150,7 +153,7 @@ public final class SoftwareVideoLayerFrameManager { } self.layerHolder.layer.setAffineTransform(transform) }*/ - self.layerHolder.layer.enqueue(frame.sampleBuffer) + self.layer?.enqueue(frame.sampleBuffer) if !self.didStart { self.didStart = true diff --git a/submodules/TelegramUI/Components/EntityKeyboard/BUILD b/submodules/TelegramUI/Components/EntityKeyboard/BUILD index 254b799425..5947ec2a2e 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/BUILD +++ b/submodules/TelegramUI/Components/EntityKeyboard/BUILD @@ -26,8 +26,13 @@ swift_library( "//submodules/AccountContext:AccountContext", "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", "//submodules/TelegramUI/Components/LottieAnimationCache:LottieAnimationCache", + "//submodules/TelegramUI/Components/VideoAnimationCache:VideoAnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", + "//submodules/TelegramUI/Components/MultiVideoRenderer:MultiVideoRenderer", + "//submodules/SoftwareVideo:SoftwareVideo", "//submodules/ShimmerEffect:ShimmerEffect", + "//submodules/PhotoResources:PhotoResources", + "//submodules/StickerResources:StickerResources", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 0c684fa098..f8ed135071 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -10,25 +10,30 @@ import MultiAnimationRenderer import AnimationCache import AccountContext import LottieAnimationCache +import VideoAnimationCache import AnimatedStickerNode import TelegramAnimatedStickerNode import SwiftSignalKit import ShimmerEffect import PagerComponent +import StickerResources public final class EmojiPagerContentComponent: Component { public typealias EnvironmentType = (EntityKeyboardChildEnvironment, PagerComponentChildEnvironment) public final class InputInteraction { - public let performItemAction: (Item, UIView, CGRect) -> Void + public let performItemAction: (Item, UIView, CGRect, CALayer) -> Void public let deleteBackwards: () -> Void + public let openStickerSettings: () -> Void public init( - performItemAction: @escaping (Item, UIView, CGRect) -> Void, - deleteBackwards: @escaping () -> Void + performItemAction: @escaping (Item, UIView, CGRect, CALayer) -> Void, + deleteBackwards: @escaping () -> Void, + openStickerSettings: @escaping () -> Void ) { self.performItemAction = performItemAction self.deleteBackwards = deleteBackwards + self.openStickerSettings = openStickerSettings } } @@ -251,6 +256,11 @@ public final class EmojiPagerContentComponent: Component { } final class ItemLayer: MultiAnimationRenderTarget { + struct Key: Hashable { + var groupId: AnyHashable + var fileId: MediaId + } + let item: Item private let file: TelegramMediaFile @@ -290,38 +300,60 @@ public final class EmojiPagerContentComponent: Component { super.init() - if attemptSynchronousLoad { - if !renderer.loadFirstFrameSynchronously(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize) { - self.displayPlaceholder = true + if file.isAnimatedSticker || file.isVideoSticker { + if attemptSynchronousLoad { + if !renderer.loadFirstFrameSynchronously(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize) { + self.displayPlaceholder = true + + if let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: self.size, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor) { + self.contents = image.cgImage + } + } + } + + self.disposable = renderer.add(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize, fetch: { size, writer in + let source = AnimatedStickerResourceSource(account: context.account, resource: file.resource, fitzModifier: nil, isVideo: false) - if let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: self.size, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor) { - self.contents = image.cgImage + let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in + guard let result = result else { + return + } + + if file.isVideoSticker { + cacheVideoAnimation(path: result, width: Int(size.width), height: Int(size.height), writer: writer) + } else { + guard let data = try? Data(contentsOf: URL(fileURLWithPath: result)) else { + writer.finish() + return + } + cacheLottieAnimation(data: data, width: Int(size.width), height: Int(size.height), writer: writer) + } + }) + + let fetchDisposable = freeMediaFileResourceInteractiveFetched(account: context.account, fileReference: stickerPackFileReference(file), resource: file.resource).start() + + return ActionDisposable { + dataDisposable.dispose() + fetchDisposable.dispose() } - } - } - - self.disposable = renderer.add(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize, fetch: { size, writer in - let source = AnimatedStickerResourceSource(account: context.account, resource: file.resource, fitzModifier: nil, isVideo: false) - - let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in - guard let result = result else { - return - } - - guard let data = try? Data(contentsOf: URL(fileURLWithPath: result)) else { - writer.finish() - return - } - cacheLottieAnimation(data: data, width: Int(size.width), height: Int(size.height), writer: writer) }) - - let fetchDisposable = freeMediaFileInteractiveFetched(account: context.account, fileReference: .standalone(media: file)).start() - - return ActionDisposable { - dataDisposable.dispose() - fetchDisposable.dispose() - } - }) + } else if let dimensions = file.dimensions { + let isSmall: Bool = false + self.disposable = (chatMessageSticker(account: context.account, file: file, small: isSmall, synchronousLoad: attemptSynchronousLoad)).start(next: { [weak self] resultTransform in + let boundingSize = CGSize(width: 93.0, height: 93.0) + let imageSize = dimensions.cgSize.aspectFilled(boundingSize) + + if let image = resultTransform(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets(), resizeMode: .fill(.clear)))?.generateImage() { + Queue.mainQueue().async { + guard let strongSelf = self else { + return + } + + strongSelf.contents = image.cgImage + } + } + }) + } } override public init(layer: Any) { @@ -381,7 +413,7 @@ public final class EmojiPagerContentComponent: Component { private let scrollView: UIScrollView - private var visibleItemLayers: [MediaId: ItemLayer] = [:] + private var visibleItemLayers: [ItemLayer.Key: ItemLayer] = [:] private var visibleGroupHeaders: [AnyHashable: ComponentHostView] = [:] private var ignoreScrolling: Bool = false @@ -402,7 +434,7 @@ public final class EmojiPagerContentComponent: Component { if #available(iOS 13.0, *) { self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false } - self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.showsVerticalScrollIndicator = true self.scrollView.showsHorizontalScrollIndicator = false self.scrollView.delegate = self self.addSubview(self.scrollView) @@ -416,18 +448,18 @@ public final class EmojiPagerContentComponent: Component { @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { - if let component = self.component, let item = self.item(atPoint: recognizer.location(in: self)), let itemView = self.visibleItemLayers[item.file.fileId] { - component.inputInteraction.performItemAction(item, self, self.scrollView.convert(itemView.frame, to: self)) + if let component = self.component, let (item, itemKey) = self.item(atPoint: recognizer.location(in: self)), let itemLayer = self.visibleItemLayers[itemKey] { + component.inputInteraction.performItemAction(item, self, self.scrollView.convert(itemLayer.frame, to: self), itemLayer) } } } - private func item(atPoint point: CGPoint) -> Item? { + private func item(atPoint point: CGPoint) -> (Item, ItemLayer.Key)? { let localPoint = self.convert(point, to: self.scrollView) - for (_, itemLayer) in self.visibleItemLayers { + for (key, itemLayer) in self.visibleItemLayers { if itemLayer.frame.contains(localPoint) { - return itemLayer.item + return (itemLayer.item, key) } } @@ -519,7 +551,7 @@ public final class EmojiPagerContentComponent: Component { return } - var validIds = Set() + var validIds = Set() var validGroupHeaderIds = Set() for groupItems in itemLayout.visibleItems(for: self.scrollView.bounds) { @@ -549,7 +581,7 @@ public final class EmojiPagerContentComponent: Component { for index in groupItems.groupItems.lowerBound ..< groupItems.groupItems.upperBound { let item = itemGroup.items[index] - let itemId = item.file.fileId + let itemId = ItemLayer.Key(groupId: itemGroup.id, fileId: item.file.fileId) validIds.insert(itemId) let itemLayer: ItemLayer @@ -566,7 +598,7 @@ public final class EmojiPagerContentComponent: Component { } } - var removedIds: [MediaId] = [] + var removedIds: [ItemLayer.Key] = [] for (id, itemLayer) in self.visibleItemLayers { if !validIds.contains(id) { removedIds.append(id) @@ -612,6 +644,9 @@ public final class EmojiPagerContentComponent: Component { if self.scrollView.contentSize != itemLayout.contentSize { self.scrollView.contentSize = itemLayout.contentSize } + if self.scrollView.scrollIndicatorInsets != pagerEnvironment.containerInsets { + self.scrollView.scrollIndicatorInsets = pagerEnvironment.containerInsets + } self.previousScrollingOffset = self.scrollView.contentOffset.y self.ignoreScrolling = false diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift index ae928d87ee..3bc7213fdd 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift @@ -30,6 +30,7 @@ public final class EntityKeyboardComponent: Component { public let bottomInset: CGFloat public let emojiContent: EmojiPagerContentComponent public let stickerContent: EmojiPagerContentComponent + public let gifContent: GifPagerContentComponent public let externalTopPanelContainer: UIView? public let topPanelExtensionUpdated: (CGFloat, Transition) -> Void @@ -38,6 +39,7 @@ public final class EntityKeyboardComponent: Component { bottomInset: CGFloat, emojiContent: EmojiPagerContentComponent, stickerContent: EmojiPagerContentComponent, + gifContent: GifPagerContentComponent, externalTopPanelContainer: UIView?, topPanelExtensionUpdated: @escaping (CGFloat, Transition) -> Void ) { @@ -45,6 +47,7 @@ public final class EntityKeyboardComponent: Component { self.bottomInset = bottomInset self.emojiContent = emojiContent self.stickerContent = stickerContent + self.gifContent = gifContent self.externalTopPanelContainer = externalTopPanelContainer self.topPanelExtensionUpdated = topPanelExtensionUpdated } @@ -62,6 +65,9 @@ public final class EntityKeyboardComponent: Component { if lhs.stickerContent != rhs.stickerContent { return false } + if lhs.gifContent != rhs.gifContent { + return false + } if lhs.externalTopPanelContainer != rhs.externalTopPanelContainer { return false } @@ -94,24 +100,78 @@ public final class EntityKeyboardComponent: Component { var contentIcons: [AnyComponentWithIdentity] = [] var contentAccessoryRightButtons: [AnyComponentWithIdentity] = [] - var topStickertems: [EntityKeyboardTopPanelComponent.Item] = [] + contents.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(component.gifContent))) + var topGifItems: [EntityKeyboardTopPanelComponent.Item] = [] + topGifItems.append(EntityKeyboardTopPanelComponent.Item( + id: "recent", + content: AnyComponent(BundleIconComponent( + name: "Chat/Input/Media/RecentTabIcon", + tintColor: component.theme.chat.inputMediaPanel.panelIconColor, + maxSize: CGSize(width: 30.0, height: 30.0)) + ) + )) + topGifItems.append(EntityKeyboardTopPanelComponent.Item( + id: "trending", + content: AnyComponent(BundleIconComponent( + name: "Chat/Input/Media/TrendingGifs", + tintColor: component.theme.chat.inputMediaPanel.panelIconColor, + maxSize: CGSize(width: 30.0, height: 30.0)) + ) + )) + contentTopPanels.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(EntityKeyboardTopPanelComponent( + theme: component.theme, + items: topGifItems + )))) + contentIcons.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(BundleIconComponent( + name: "Chat/Input/Media/EntityInputGifsIcon", + tintColor: component.theme.chat.inputMediaPanel.panelIconColor, + maxSize: nil + )))) + /*contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(Button( + content: AnyComponent(BundleIconComponent( + name: "Chat/Input/Media/EntityInputSettingsIcon", + tintColor: component.theme.chat.inputMediaPanel.panelIconColor, + maxSize: nil + )), + action: { + } + ).minSize(CGSize(width: 38.0, height: 38.0)))))*/ + + var topStickerItems: [EntityKeyboardTopPanelComponent.Item] = [] for itemGroup in component.stickerContent.itemGroups { - if !itemGroup.items.isEmpty { - topStickertems.append(EntityKeyboardTopPanelComponent.Item( - id: AnyHashable(itemGroup.items[0].file.fileId), - content: AnyComponent(EntityKeyboardAnimationTopPanelComponent( - context: component.stickerContent.context, - file: itemGroup.items[0].file, - animationCache: component.stickerContent.animationCache, - animationRenderer: component.stickerContent.animationRenderer + if let id = itemGroup.id.base as? String { + let iconMapping: [String: String] = [ + "recent": "Chat/Input/Media/RecentTabIcon", + "premium": "Chat/Input/Media/PremiumIcon" + ] + if let iconName = iconMapping[id] { + topStickerItems.append(EntityKeyboardTopPanelComponent.Item( + id: id, + content: AnyComponent(BundleIconComponent( + name: iconName, + tintColor: component.theme.chat.inputMediaPanel.panelIconColor, + maxSize: CGSize(width: 30.0, height: 30.0)) + ) )) - )) + } + } else { + if !itemGroup.items.isEmpty { + topStickerItems.append(EntityKeyboardTopPanelComponent.Item( + id: AnyHashable(itemGroup.items[0].file.fileId), + content: AnyComponent(EntityKeyboardAnimationTopPanelComponent( + context: component.stickerContent.context, + file: itemGroup.items[0].file, + animationCache: component.stickerContent.animationCache, + animationRenderer: component.stickerContent.animationRenderer + )) + )) + } } } contents.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(component.stickerContent))) contentTopPanels.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(EntityKeyboardTopPanelComponent( theme: component.theme, - items: topStickertems + items: topStickerItems )))) contentIcons.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(BundleIconComponent( name: "Chat/Input/Media/EntityInputStickersIcon", @@ -125,6 +185,7 @@ public final class EntityKeyboardComponent: Component { maxSize: nil )), action: { + component.stickerContent.inputInteraction.openStickerSettings() } ).minSize(CGSize(width: 38.0, height: 38.0))))) @@ -143,7 +204,6 @@ public final class EntityKeyboardComponent: Component { )) } } - contentTopPanels.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(EntityKeyboardTopPanelComponent( theme: component.theme, items: topEmojiItems diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift index 82a2ec6b70..16c2a40147 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift @@ -11,9 +11,14 @@ import BundleIconComponent private final class BottomPanelIconComponent: Component { let content: AnyComponent + let action: () -> Void - init(content: AnyComponent) { + init( + content: AnyComponent, + action: @escaping () -> Void + ) { self.content = content + self.action = action } static func ==(lhs: BottomPanelIconComponent, rhs: BottomPanelIconComponent) -> Bool { @@ -27,19 +32,31 @@ private final class BottomPanelIconComponent: Component { final class View: UIView { let contentView: ComponentHostView + var component: BottomPanelIconComponent? + override init(frame: CGRect) { self.contentView = ComponentHostView() + self.contentView.isUserInteractionEnabled = false super.init(frame: frame) self.addSubview(self.contentView) + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.component?.action() + } + } + func update(component: BottomPanelIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + let size = CGSize(width: 32.0, height: 32.0) let contentSize = self.contentView.update( @@ -169,12 +186,7 @@ final class EntityKeyboardBottomPanelComponent: Component { let rightAccessoryButtonSize = rightAccessoryButton.view.update( transition: rightAccessoryButtonTransition, - component: AnyComponent(Button( - content: rightAccessoryButtonComponent.component, - action: { [weak self] in - self?.component?.deleteBackwards() - } - ).minSize(CGSize(width: intrinsicHeight, height: intrinsicHeight))), + component: rightAccessoryButtonComponent.component, environment: {}, containerSize: CGSize(width: .greatestFiniteMagnitude, height: intrinsicHeight) ) @@ -208,6 +220,8 @@ final class EntityKeyboardBottomPanelComponent: Component { var iconTotalSize = CGSize() let iconSpacing: CGFloat = 22.0 + let navigateToContentId = panelEnvironment.navigateToContentId + for icon in panelEnvironment.contentIcons { validIconIds.append(icon.id) @@ -225,7 +239,10 @@ final class EntityKeyboardBottomPanelComponent: Component { let iconSize = iconView.update( transition: iconTransition, component: AnyComponent(BottomPanelIconComponent( - content: icon.component + content: icon.component, + action: { + navigateToContentId(icon.id) + } )), environment: {}, containerSize: CGSize(width: 32.0, height: 32.0) diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift index 9e7732f6c4..1a3630a28c 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift @@ -252,14 +252,14 @@ final class EntityKeyboardTopPanelComponent: Component { self.itemViews[item.id] = itemView } - let itemFrame = itemLayout.contentFrame(at: index) - itemView.frame = itemFrame - let _ = itemView.update( + let itemOuterFrame = itemLayout.contentFrame(at: index) + let itemSize = itemView.update( transition: .immediate, component: item.content, environment: {}, - containerSize: itemFrame.size + containerSize: itemOuterFrame.size ) + itemView.frame = CGRect(origin: CGPoint(x: itemOuterFrame.minX + floor((itemOuterFrame.width - itemSize.width) / 2.0), y: itemOuterFrame.minY + floor((itemOuterFrame.height - itemSize.height) / 2.0)), size: itemSize) } } var removedIds: [AnyHashable] = [] diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift new file mode 100644 index 0000000000..3b8a5c5c17 --- /dev/null +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift @@ -0,0 +1,585 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import PagerComponent +import TelegramPresentationData +import TelegramCore +import Postbox +import MultiAnimationRenderer +import AnimationCache +import AccountContext +import LottieAnimationCache +import AnimatedStickerNode +import TelegramAnimatedStickerNode +import SwiftSignalKit +import ShimmerEffect +import PagerComponent +import SoftwareVideo +import AVFoundation +import PhotoResources + +private class GifVideoLayer: AVSampleBufferDisplayLayer { + private let context: AccountContext + private let file: TelegramMediaFile + + private var frameManager: SoftwareVideoLayerFrameManager? + + private var thumbnailDisposable: Disposable? + + private var playbackTimestamp: Double = 0.0 + private var playbackTimer: SwiftSignalKit.Timer? + + var started: (() -> Void)? + + var shouldBeAnimating: Bool = false { + didSet { + if self.shouldBeAnimating == oldValue { + return + } + + if self.shouldBeAnimating { + self.playbackTimer?.invalidate() + self.playbackTimer = SwiftSignalKit.Timer(timeout: 1.0 / 30.0, repeat: true, completion: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.frameManager?.tick(timestamp: strongSelf.playbackTimestamp) + strongSelf.playbackTimestamp += 1.0 / 30.0 + }, queue: .mainQueue()) + self.playbackTimer?.start() + } else { + self.playbackTimer?.invalidate() + self.playbackTimer = nil + } + } + } + + init(context: AccountContext, file: TelegramMediaFile, synchronousLoad: Bool) { + self.context = context + self.file = file + + super.init() + + self.videoGravity = .resizeAspectFill + + if let dimensions = file.dimensions { + self.thumbnailDisposable = (mediaGridMessageVideo(postbox: context.account.postbox, videoReference: .savedGif(media: self.file), synchronousLoad: synchronousLoad, nilForEmptyResult: true) + |> deliverOnMainQueue).start(next: { [weak self] transform in + guard let strongSelf = self else { + return + } + let boundingSize = CGSize(width: 93.0, height: 93.0) + let imageSize = dimensions.cgSize.aspectFilled(boundingSize) + + if let image = transform(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets(), resizeMode: .fill(.clear)))?.generateImage() { + Queue.mainQueue().async { + if let strongSelf = self { + strongSelf.contents = image.cgImage + strongSelf.setupVideo() + strongSelf.started?() + } + } + } else { + strongSelf.setupVideo() + } + }) + } else { + self.setupVideo() + } + } + + override init(layer: Any) { + preconditionFailure() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.thumbnailDisposable?.dispose() + } + + private func setupVideo() { + let frameManager = SoftwareVideoLayerFrameManager(account: self.context.account, fileReference: .savedGif(media: self.file), layerHolder: nil, layer: self) + self.frameManager = frameManager + frameManager.started = { [weak self] in + guard let strongSelf = self else { + return + } + let _ = strongSelf + } + frameManager.start() + } +} + +public final class GifPagerContentComponent: Component { + public typealias EnvironmentType = (EntityKeyboardChildEnvironment, PagerComponentChildEnvironment) + + public final class InputInteraction { + public let performItemAction: (Item, UIView, CGRect) -> Void + + public init( + performItemAction: @escaping (Item, UIView, CGRect) -> Void + ) { + self.performItemAction = performItemAction + } + } + + public final class Item: Equatable { + public let file: TelegramMediaFile + + public init(file: TelegramMediaFile) { + self.file = file + } + + public static func ==(lhs: Item, rhs: Item) -> Bool { + if lhs === rhs { + return true + } + if lhs.file.fileId != rhs.file.fileId { + return false + } + + return true + } + } + + public let context: AccountContext + public let inputInteraction: InputInteraction + public let items: [Item] + + public init( + context: AccountContext, + inputInteraction: InputInteraction, + items: [Item] + ) { + self.context = context + self.inputInteraction = inputInteraction + self.items = items + } + + public static func ==(lhs: GifPagerContentComponent, rhs: GifPagerContentComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.inputInteraction !== rhs.inputInteraction { + return false + } + if lhs.items != rhs.items { + return false + } + + return true + } + + public final class View: UIView, UIScrollViewDelegate { + private struct ItemGroupDescription: Equatable { + let hasTitle: Bool + let itemCount: Int + } + + private struct ItemGroupLayout: Equatable { + let frame: CGRect + let itemTopOffset: CGFloat + let itemCount: Int + } + + private struct ItemLayout: Equatable { + let width: CGFloat + let containerInsets: UIEdgeInsets + let itemCount: Int + let itemSize: CGFloat + let horizontalSpacing: CGFloat + let verticalSpacing: CGFloat + let itemsPerRow: Int + let contentSize: CGSize + + init(width: CGFloat, containerInsets: UIEdgeInsets, itemCount: Int) { + self.width = width + self.containerInsets = containerInsets + self.itemCount = itemCount + self.horizontalSpacing = 1.0 + self.verticalSpacing = 1.0 + + let itemHorizontalSpace = width - self.containerInsets.left - self.containerInsets.right + self.itemSize = floor((width - self.horizontalSpacing * 2.0) / 3.0) + + self.itemsPerRow = Int((itemHorizontalSpace + self.horizontalSpacing) / (self.itemSize + self.horizontalSpacing)) + let numRowsInGroup = (itemCount + (self.itemsPerRow - 1)) / self.itemsPerRow + self.contentSize = CGSize(width: width, height: self.containerInsets.top + self.containerInsets.bottom + CGFloat(numRowsInGroup) * self.itemSize + CGFloat(max(0, numRowsInGroup - 1)) * self.verticalSpacing) + } + + func frame(at index: Int) -> CGRect { + let row = index / self.itemsPerRow + let column = index % self.itemsPerRow + + var rect = CGRect( + origin: CGPoint( + x: self.containerInsets.left + CGFloat(column) * (self.itemSize + self.horizontalSpacing), + y: self.containerInsets.top + CGFloat(row) * (self.itemSize + self.verticalSpacing) + ), + size: CGSize( + width: self.itemSize, + height: self.itemSize + ) + ) + + if column == self.itemsPerRow - 1 { + rect.size.width = self.width - self.containerInsets.right - rect.minX + } + + return rect + } + + func visibleItems(for rect: CGRect) -> Range? { + let offsetRect = rect.offsetBy(dx: -self.containerInsets.left, dy: -containerInsets.top) + var minVisibleRow = Int(floor((offsetRect.minY - self.verticalSpacing) / (self.itemSize + self.verticalSpacing))) + minVisibleRow = max(0, minVisibleRow) + let maxVisibleRow = Int(ceil((offsetRect.maxY - self.verticalSpacing) / (self.itemSize + self.verticalSpacing))) + + let minVisibleIndex = minVisibleRow * self.itemsPerRow + let maxVisibleIndex = min(self.itemCount - 1, (maxVisibleRow + 1) * self.itemsPerRow - 1) + + if maxVisibleIndex >= minVisibleIndex { + return minVisibleIndex ..< (maxVisibleIndex + 1) + } else { + return nil + } + } + } + + fileprivate final class ItemLayer: GifVideoLayer { + let item: Item + + private let file: TelegramMediaFile + private let placeholderColor: UIColor + private var disposable: Disposable? + private var fetchDisposable: Disposable? + + private var isInHierarchyValue: Bool = false + public var isVisibleForAnimations: Bool = false { + didSet { + if self.isVisibleForAnimations != oldValue { + self.updatePlayback() + } + } + } + private var displayPlaceholder: Bool = false + + init( + item: Item, + context: AccountContext, + groupId: String, + attemptSynchronousLoad: Bool, + file: TelegramMediaFile, + placeholderColor: UIColor + ) { + self.item = item + self.file = file + self.placeholderColor = placeholderColor + + super.init(context: context, file: file, synchronousLoad: attemptSynchronousLoad) + + self.updateDisplayPlaceholder(displayPlaceholder: true) + + self.started = { [weak self] in + self?.updateDisplayPlaceholder(displayPlaceholder: false) + } + + /*if attemptSynchronousLoad { + if !renderer.loadFirstFrameSynchronously(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize) { + self.displayPlaceholder = true + + if let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: self.size, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor) { + self.contents = image.cgImage + } + } + } + + self.disposable = renderer.add(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize, fetch: { size, writer in + let source = AnimatedStickerResourceSource(account: context.account, resource: file.resource, fitzModifier: nil, isVideo: false) + + let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in + guard let result = result else { + return + } + + guard let data = try? Data(contentsOf: URL(fileURLWithPath: result)) else { + writer.finish() + return + } + cacheLottieAnimation(data: data, width: Int(size.width), height: Int(size.height), writer: writer) + }) + + let fetchDisposable = freeMediaFileInteractiveFetched(account: context.account, fileReference: .standalone(media: file)).start() + + return ActionDisposable { + dataDisposable.dispose() + fetchDisposable.dispose() + } + })*/ + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.disposable?.dispose() + self.fetchDisposable?.dispose() + } + + override func action(forKey event: String) -> CAAction? { + if event == kCAOnOrderIn { + self.isInHierarchyValue = true + } else if event == kCAOnOrderOut { + self.isInHierarchyValue = false + } + self.updatePlayback() + return nullAction + } + + private func updatePlayback() { + let shouldBePlaying = self.isInHierarchyValue && self.isVisibleForAnimations + + self.shouldBeAnimating = shouldBePlaying + } + + func updateDisplayPlaceholder(displayPlaceholder: Bool) { + if self.displayPlaceholder == displayPlaceholder { + return + } + + self.displayPlaceholder = displayPlaceholder + + if displayPlaceholder { + let placeholderColor = self.placeholderColor + self.backgroundColor = placeholderColor.cgColor + } else { + self.backgroundColor = nil + } + } + } + + private let scrollView: UIScrollView + + private var visibleItemLayers: [MediaId: ItemLayer] = [:] + private var ignoreScrolling: Bool = false + + private var component: GifPagerContentComponent? + private var pagerEnvironment: PagerComponentChildEnvironment? + private var theme: PresentationTheme? + private var itemLayout: ItemLayout? + + override init(frame: CGRect) { + self.scrollView = UIScrollView() + + super.init(frame: frame) + + self.scrollView.delaysContentTouches = false + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + self.scrollView.contentInsetAdjustmentBehavior = .never + } + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.showsVerticalScrollIndicator = true + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.delegate = self + self.addSubview(self.scrollView) + + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + if let component = self.component, let item = self.item(atPoint: recognizer.location(in: self)), let itemView = self.visibleItemLayers[item.file.fileId] { + component.inputInteraction.performItemAction(item, self, self.scrollView.convert(itemView.frame, to: self)) + } + } + } + + private func item(atPoint point: CGPoint) -> Item? { + let localPoint = self.convert(point, to: self.scrollView) + + for (_, itemLayer) in self.visibleItemLayers { + if itemLayer.frame.contains(localPoint) { + return itemLayer.item + } + } + + return nil + } + + private var previousScrollingOffset: CGFloat? + + public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + if let presentation = scrollView.layer.presentation() { + scrollView.bounds = presentation.bounds + scrollView.layer.removeAllAnimations() + } + } + + public func scrollViewDidScroll(_ scrollView: UIScrollView) { + if self.ignoreScrolling { + return + } + + self.updateVisibleItems(attemptSynchronousLoads: false) + + self.updateScrollingOffset(transition: .immediate) + } + + public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + if velocity.y != 0.0 { + targetContentOffset.pointee.y = self.snappedContentOffset(proposedOffset: targetContentOffset.pointee.y) + } + } + + public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + if !decelerate { + self.snapScrollingOffsetToInsets() + } + } + + public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + self.snapScrollingOffsetToInsets() + } + + private func updateScrollingOffset(transition: Transition) { + if let previousScrollingOffsetValue = self.previousScrollingOffset { + let currentBounds = scrollView.bounds + let offsetToTopEdge = max(0.0, currentBounds.minY - 0.0) + let offsetToBottomEdge = max(0.0, scrollView.contentSize.height - currentBounds.maxY) + let offsetToClosestEdge = min(offsetToTopEdge, offsetToBottomEdge) + + let relativeOffset = scrollView.contentOffset.y - previousScrollingOffsetValue + self.pagerEnvironment?.onChildScrollingUpdate(PagerComponentChildEnvironment.ContentScrollingUpdate( + relativeOffset: relativeOffset, + absoluteOffsetToClosestEdge: offsetToClosestEdge, + transition: transition + )) + self.previousScrollingOffset = scrollView.contentOffset.y + } + self.previousScrollingOffset = scrollView.contentOffset.y + } + + private func snappedContentOffset(proposedOffset: CGFloat) -> CGFloat { + guard let pagerEnvironment = self.pagerEnvironment else { + return proposedOffset + } + + var proposedOffset = proposedOffset + let bounds = self.bounds + if proposedOffset + bounds.height > self.scrollView.contentSize.height - pagerEnvironment.containerInsets.bottom { + proposedOffset = self.scrollView.contentSize.height - bounds.height + } + if proposedOffset < pagerEnvironment.containerInsets.top { + proposedOffset = 0.0 + } + + return proposedOffset + } + + private func snapScrollingOffsetToInsets() { + let transition = Transition(animation: .curve(duration: 0.4, curve: .spring)) + + var currentBounds = self.scrollView.bounds + currentBounds.origin.y = self.snappedContentOffset(proposedOffset: currentBounds.minY) + transition.setBounds(view: self.scrollView, bounds: currentBounds) + + self.updateScrollingOffset(transition: transition) + } + + private func updateVisibleItems(attemptSynchronousLoads: Bool) { + guard let component = self.component, let theme = self.theme, let itemLayout = self.itemLayout else { + return + } + + var validIds = Set() + + if let itemRange = itemLayout.visibleItems(for: self.scrollView.bounds) { + for index in itemRange.lowerBound ..< itemRange.upperBound { + let item = component.items[index] + let itemId = item.file.fileId + validIds.insert(itemId) + + let itemLayer: ItemLayer + if let current = self.visibleItemLayers[itemId] { + itemLayer = current + } else { + itemLayer = ItemLayer( + item: item, + context: component.context, + groupId: "savedGif", + attemptSynchronousLoad: attemptSynchronousLoads, + file: item.file, + placeholderColor: theme.chat.inputMediaPanel.stickersBackgroundColor + ) + self.scrollView.layer.addSublayer(itemLayer) + self.visibleItemLayers[itemId] = itemLayer + } + + itemLayer.frame = itemLayout.frame(at: index) + itemLayer.isVisibleForAnimations = true + } + } + + var removedIds: [MediaId] = [] + for (id, itemLayer) in self.visibleItemLayers { + if !validIds.contains(id) { + removedIds.append(id) + itemLayer.removeFromSuperlayer() + } + } + for id in removedIds { + self.visibleItemLayers.removeValue(forKey: id) + } + } + + func update(component: GifPagerContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.theme = environment[EntityKeyboardChildEnvironment.self].value.theme + + let pagerEnvironment = environment[PagerComponentChildEnvironment.self].value + self.pagerEnvironment = pagerEnvironment + + let itemLayout = ItemLayout( + width: availableSize.width, + containerInsets: UIEdgeInsets(top: pagerEnvironment.containerInsets.top, left: pagerEnvironment.containerInsets.left, bottom: pagerEnvironment.containerInsets.bottom, right: pagerEnvironment.containerInsets.right), + itemCount: component.items.count + ) + self.itemLayout = itemLayout + + self.ignoreScrolling = true + transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: availableSize)) + if self.scrollView.contentSize != itemLayout.contentSize { + self.scrollView.contentSize = itemLayout.contentSize + } + if self.scrollView.scrollIndicatorInsets != pagerEnvironment.containerInsets { + self.scrollView.scrollIndicatorInsets = pagerEnvironment.containerInsets + } + self.previousScrollingOffset = self.scrollView.contentOffset.y + self.ignoreScrolling = false + + self.updateVisibleItems(attemptSynchronousLoads: true) + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + diff --git a/submodules/TelegramUI/Components/MultiVideoRenderer/BUILD b/submodules/TelegramUI/Components/MultiVideoRenderer/BUILD new file mode 100644 index 0000000000..e6fa67cd5f --- /dev/null +++ b/submodules/TelegramUI/Components/MultiVideoRenderer/BUILD @@ -0,0 +1,20 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "MultiVideoRenderer", + module_name = "MultiVideoRenderer", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/Display:Display", + "//submodules/SoftwareVideo:SoftwareVideo", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/MultiVideoRenderer/Sources/MultiVideoRenderer.swift b/submodules/TelegramUI/Components/MultiVideoRenderer/Sources/MultiVideoRenderer.swift new file mode 100644 index 0000000000..359224b564 --- /dev/null +++ b/submodules/TelegramUI/Components/MultiVideoRenderer/Sources/MultiVideoRenderer.swift @@ -0,0 +1,399 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Display +import SoftwareVideo + +/*public protocol MultiVideoRenderer: AnyObject { + func add(groupId: String, target: MultiVideoRenderTarget, itemId: String, size: CGSize, source: @escaping (@escaping (String) -> Void) -> Disposable) -> Disposable +} + +open class MultiVideoRenderTarget: SimpleLayer { + fileprivate let deinitCallbacks = Bag<() -> Void>() + fileprivate let updateStateCallbacks = Bag<() -> Void>() + + public final var shouldBeAnimating: Bool = false { + didSet { + if self.shouldBeAnimating != oldValue { + for f in self.updateStateCallbacks.copyItems() { + f() + } + } + } + } + + deinit { + for f in self.deinitCallbacks.copyItems() { + f() + } + } + + open func updateDisplayPlaceholder(displayPlaceholder: Bool) { + } +} + +private final class ItemVideoContext { + static let queue = Queue(name: "ItemVideoContext", qos: .default) + + private let stateUpdated: () -> Void + + private var disposable: Disposable? + private var displayLink: ConstantDisplayLinkAnimator? + private var frameManager: SoftwareVideoLayerFrameManager? + + private(set) var isPlaying: Bool = false { + didSet { + if self.isPlaying != oldValue { + self.stateUpdated() + } + } + } + + let targets = Bag>() + + init(itemId: String, source: @escaping (@escaping (String) -> Void) -> Disposable, stateUpdated: @escaping () -> Void) { + self.stateUpdated = stateUpdated + + self.disposable = source({ [weak self] in + Queue.mainQueue().async { + guard let strongSelf = self else { + return + } + //strongSelf.frameManager = SoftwareVideoLayerFrameManager(account: <#T##Account#>, fileReference: <#T##FileMediaReference#>, layerHolder: <#T##SampleBufferLayer#>) + strongSelf.updateIsPlaying() + + if result.item == nil { + for target in strongSelf.targets.copyItems() { + if let target = target.value { + target.updateDisplayPlaceholder(displayPlaceholder: true) + } + } + } + } + }) + } + + deinit { + self.disposable?.dispose() + self.displayLink?.invalidate() + } + + func updateAddedTarget(target: MultiAnimationRenderTarget) { + if let item = self.item, let currentFrameGroup = self.currentFrameGroup { + let currentFrame = self.frameIndex % item.numFrames + + if let contentsRect = currentFrameGroup.contentsRect(index: currentFrame) { + target.updateDisplayPlaceholder(displayPlaceholder: false) + target.contents = currentFrameGroup.image.cgImage + target.contentsRect = contentsRect + } + } + + self.updateIsPlaying() + } + + func updateIsPlaying() { + var isPlaying = true + if self.item == nil { + isPlaying = false + } + + var shouldBeAnimating = false + for target in self.targets.copyItems() { + if let target = target.value { + if target.shouldBeAnimating { + shouldBeAnimating = true + break + } + } + } + if !shouldBeAnimating { + isPlaying = false + } + + self.isPlaying = isPlaying + } + + func animationTick() -> LoadFrameGroupTask? { + return self.update(advanceFrame: true) + } + + private func update(advanceFrame: Bool) -> LoadFrameGroupTask? { + guard let item = self.item else { + return nil + } + + let currentFrame = self.frameIndex % item.numFrames + + if let currentFrameGroup = self.currentFrameGroup, currentFrameGroup.frameRange.contains(currentFrame) { + } else if !self.isLoadingFrameGroup { + self.currentFrameGroup = nil + self.isLoadingFrameGroup = true + let frameSkip = self.frameSkip + + return LoadFrameGroupTask(task: { [weak self] in + let possibleCounts: [Int] = [10, 12, 14, 16, 18, 20] + let countIndex = Int.random(in: 0 ..< possibleCounts.count) + let currentFrameGroup = FrameGroup(item: item, baseFrameIndex: currentFrame, count: possibleCounts[countIndex], skip: frameSkip) + + return { + guard let strongSelf = self else { + return + } + + strongSelf.isLoadingFrameGroup = false + + if let currentFrameGroup = currentFrameGroup { + strongSelf.currentFrameGroup = currentFrameGroup + for target in strongSelf.targets.copyItems() { + target.value?.contents = currentFrameGroup.image.cgImage + } + + let _ = strongSelf.update(advanceFrame: false) + } + } + }) + } + + if advanceFrame { + self.frameIndex += self.frameSkip + } + + if let currentFrameGroup = self.currentFrameGroup, let contentsRect = currentFrameGroup.contentsRect(index: currentFrame) { + for target in self.targets.copyItems() { + if let target = target.value { + target.updateDisplayPlaceholder(displayPlaceholder: false) + target.contentsRect = contentsRect + } + } + } + + return nil + } +} + +public final class MultiAnimationRendererImpl: MultiAnimationRenderer { + private final class GroupContext { + private var frameSkip: Int + private let stateUpdated: () -> Void + + private var itemContexts: [String: ItemAnimationContext] = [:] + + private(set) var isPlaying: Bool = false { + didSet { + if self.isPlaying != oldValue { + self.stateUpdated() + } + } + } + + init(frameSkip: Int, stateUpdated: @escaping () -> Void) { + self.frameSkip = frameSkip + self.stateUpdated = stateUpdated + } + + func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Disposable { + let itemContext: ItemAnimationContext + if let current = self.itemContexts[itemId] { + itemContext = current + } else { + itemContext = ItemAnimationContext(cache: cache, itemId: itemId, size: size, frameSkip: self.frameSkip, fetch: fetch, stateUpdated: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.updateIsPlaying() + }) + self.itemContexts[itemId] = itemContext + } + + let index = itemContext.targets.add(Weak(target)) + itemContext.updateAddedTarget(target: target) + + let deinitIndex = target.deinitCallbacks.add { [weak self, weak itemContext] in + Queue.mainQueue().async { + guard let strongSelf = self, let itemContext = itemContext, strongSelf.itemContexts[itemId] === itemContext else { + return + } + itemContext.targets.remove(index) + if itemContext.targets.isEmpty { + strongSelf.itemContexts.removeValue(forKey: itemId) + } + } + } + + let updateStateIndex = target.updateStateCallbacks.add { [weak itemContext] in + guard let itemContext = itemContext else { + return + } + itemContext.updateIsPlaying() + } + + return ActionDisposable { [weak self, weak itemContext, weak target] in + guard let strongSelf = self, let itemContext = itemContext, strongSelf.itemContexts[itemId] === itemContext else { + return + } + if let target = target { + target.deinitCallbacks.remove(deinitIndex) + target.updateStateCallbacks.remove(updateStateIndex) + } + itemContext.targets.remove(index) + if itemContext.targets.isEmpty { + strongSelf.itemContexts.removeValue(forKey: itemId) + } + } + } + + func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool { + if let item = cache.getSynchronously(sourceId: itemId, size: size) { + guard let frameGroup = FrameGroup(item: item, baseFrameIndex: 0, count: 1, skip: 1) else { + return false + } + + target.contents = frameGroup.image.cgImage + + return true + } else { + return false + } + } + + private func updateIsPlaying() { + var isPlaying = false + for (_, itemContext) in self.itemContexts { + if itemContext.isPlaying { + isPlaying = true + break + } + } + + self.isPlaying = isPlaying + } + + func animationTick() -> [LoadFrameGroupTask] { + var tasks: [LoadFrameGroupTask] = [] + for (_, itemContext) in self.itemContexts { + if itemContext.isPlaying { + if let task = itemContext.animationTick() { + tasks.append(task) + } + } + } + + return tasks + } + } + + private var groupContexts: [String: GroupContext] = [:] + private var frameSkip: Int + private var displayLink: ConstantDisplayLinkAnimator? + + private(set) var isPlaying: Bool = false { + didSet { + if self.isPlaying != oldValue { + if self.isPlaying { + if self.displayLink == nil { + self.displayLink = ConstantDisplayLinkAnimator { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.animationTick() + } + self.displayLink?.frameInterval = self.frameSkip + self.displayLink?.isPaused = false + } + } else { + if let displayLink = self.displayLink { + self.displayLink = nil + displayLink.invalidate() + } + } + } + } + } + + public init() { + if !ProcessInfo.processInfo.isLowPowerModeEnabled && ProcessInfo.processInfo.activeProcessorCount > 2 { + self.frameSkip = 1 + } else { + self.frameSkip = 2 + } + } + + public func add(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Disposable { + let groupContext: GroupContext + if let current = self.groupContexts[groupId] { + groupContext = current + } else { + groupContext = GroupContext(frameSkip: self.frameSkip, stateUpdated: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.updateIsPlaying() + }) + self.groupContexts[groupId] = groupContext + } + + let disposable = groupContext.add(target: target, cache: cache, itemId: itemId, size: size, fetch: fetch) + + return ActionDisposable { + disposable.dispose() + } + } + + public func loadFirstFrameSynchronously(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool { + let groupContext: GroupContext + if let current = self.groupContexts[groupId] { + groupContext = current + } else { + groupContext = GroupContext(frameSkip: self.frameSkip, stateUpdated: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.updateIsPlaying() + }) + self.groupContexts[groupId] = groupContext + } + + return groupContext.loadFirstFrameSynchronously(target: target, cache: cache, itemId: itemId, size: size) + } + + private func updateIsPlaying() { + var isPlaying = false + for (_, groupContext) in self.groupContexts { + if groupContext.isPlaying { + isPlaying = true + break + } + } + + self.isPlaying = isPlaying + } + + private func animationTick() { + var tasks: [LoadFrameGroupTask] = [] + for (_, groupContext) in self.groupContexts { + if groupContext.isPlaying { + tasks.append(contentsOf: groupContext.animationTick()) + } + } + + if !tasks.isEmpty { + ItemAnimationContext.queue.async { + var completions: [() -> Void] = [] + for task in tasks { + let complete = task.task() + completions.append(complete) + } + + if !completions.isEmpty { + Queue.mainQueue().async { + for completion in completions { + completion() + } + } + } + } + } + } +} +*/ diff --git a/submodules/TelegramUI/Components/VideoAnimationCache/BUILD b/submodules/TelegramUI/Components/VideoAnimationCache/BUILD new file mode 100644 index 0000000000..7eb3cd35c3 --- /dev/null +++ b/submodules/TelegramUI/Components/VideoAnimationCache/BUILD @@ -0,0 +1,21 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "VideoAnimationCache", + module_name = "VideoAnimationCache", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", + "//submodules/Display:Display", + "//submodules/AnimatedStickerNode:AnimatedStickerNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/VideoAnimationCache/Sources/VideoAnimationCache.swift b/submodules/TelegramUI/Components/VideoAnimationCache/Sources/VideoAnimationCache.swift new file mode 100644 index 0000000000..c052c0b77b --- /dev/null +++ b/submodules/TelegramUI/Components/VideoAnimationCache/Sources/VideoAnimationCache.swift @@ -0,0 +1,35 @@ +import Foundation +import UIKit +import AnimationCache +import Display +import AnimatedStickerNode +import SwiftSignalKit + +public func cacheVideoAnimation(path: String, width: Int, height: Int, writer: AnimationCacheItemWriter) { + let queue = Queue() + queue.async { + guard let frameSource = makeVideoStickerDirectFrameSource(queue: queue, path: path, width: width, height: height, cachePathPrefix: nil) else { + return + } + let frameDuration = 1.0 / Double(frameSource.frameRate) + while true { + if let frame = frameSource.takeFrame(draw: true) { + //AnimatedStickerFrame(data: frameData, type: .argb, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1, totalFrames: self.frameCount, multiplyAlpha: true) + if case .argb = frame.type { + let frameWidth = frame.width + let frameHeight = frame.height + let bytesPerRow = frame.bytesPerRow + frame.data.withUnsafeBytes { bytes -> Void in + writer.add(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), length: bytes.count, width: Int(frameWidth), height: Int(frameHeight), bytesPerRow: Int(bytesPerRow), duration: frameDuration) + } + } else { + break + } + } else { + break + } + } + + writer.finish() + } +} diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 9ff570f2e4..e891ae810d 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -816,7 +816,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }, enqueueMessage: { message in self?.sendMessages([message]) }, sendSticker: canSendMessagesToChat(strongSelf.presentationInterfaceState) ? { fileReference, sourceNode, sourceRect in - return self?.controllerInteraction?.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect) ?? false + return self?.controllerInteraction?.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil) ?? false } : nil, setupTemporaryHiddenMedia: { signal, centralIndex, galleryMedia in if let strongSelf = self { strongSelf.temporaryHiddenGalleryMediaDisposable.set((signal |> deliverOnMainQueue).start(next: { entry in @@ -1494,7 +1494,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G attributes.append(TextEntitiesMessageAttribute(entities: entities)) } strongSelf.sendMessages([.message(text: text, attributes: attributes, mediaReference: nil, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil, correlationId: nil)]) - }, sendSticker: { [weak self] fileReference, silentPosting, schedule, query, clearInput, sourceView, sourceRect in + }, sendSticker: { [weak self] fileReference, silentPosting, schedule, query, clearInput, sourceView, sourceRect, sourceLayer in guard let strongSelf = self else { return false } @@ -1568,8 +1568,23 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .inputPanelSearch(itemNode: sourceNode), replyPanel: replyPanel), initiated: {}) } else if let sourceNode = sourceView.asyncdisplaykit_node as? ChatEmptyNodeStickerContentNode { strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .emptyPanel(itemNode: sourceNode), replyPanel: nil), initiated: {}) - } else { - + } else if let sourceLayer = sourceLayer { + strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .universal(sourceContainerView: sourceView, sourceRect: sourceRect, sourceLayer: sourceLayer), replyPanel: replyPanel), initiated: { + guard let strongSelf = self else { + return + } + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { current in + var current = current + current = current.updatedInputMode { current in + if case let .media(mode, maybeExpanded, focused) = current, maybeExpanded != nil { + return .media(mode: mode, expanded: nil, focused: focused) + } + return current + } + + return current + }) + }) } } @@ -1589,10 +1604,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.sendMessages(transformedMessages) } return true - }, sendGif: { [weak self] fileReference, sourceNode, sourceRect, silentPosting, schedule in + }, sendGif: { [weak self] fileReference, sourceView, sourceRect, silentPosting, schedule in if let strongSelf = self { if let _ = strongSelf.presentationInterfaceState.slowmodeState, strongSelf.presentationInterfaceState.subject != .scheduledMessages { - strongSelf.interfaceInteraction?.displaySlowmodeTooltip(sourceNode.view, sourceRect) + strongSelf.interfaceInteraction?.displaySlowmodeTooltip(sourceView, sourceRect) return false } @@ -7583,9 +7598,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } } - }, sendSticker: { [weak self] file, clearInput, sourceView, sourceRect in + }, sendSticker: { [weak self] file, clearInput, sourceView, sourceRect, sourceLayer in if let strongSelf = self, canSendMessagesToChat(strongSelf.presentationInterfaceState) { - return strongSelf.controllerInteraction?.sendSticker(file, false, false, nil, clearInput, sourceView, sourceRect) ?? false + return strongSelf.controllerInteraction?.sendSticker(file, false, false, nil, clearInput, sourceView, sourceRect, sourceLayer) ?? false } else { return false } @@ -14846,7 +14861,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G break } }, sendFile: nil, sendSticker: { [weak self] f, sourceView, sourceRect in - return self?.interfaceInteraction?.sendSticker(f, true, sourceView, sourceRect) ?? false + return self?.interfaceInteraction?.sendSticker(f, true, sourceView, sourceRect, nil) ?? false }, requestMessageActionUrlAuth: { [weak self] subject in if case let .url(url) = subject { self?.controllerInteraction?.requestMessageActionUrlAuth(url, subject) diff --git a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift index 1fac0dc958..6beeb74fd9 100644 --- a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift @@ -73,8 +73,8 @@ public final class ChatControllerInteraction { let toggleMessagesSelection: ([MessageId], Bool) -> Void let sendCurrentMessage: (Bool) -> Void let sendMessage: (String) -> Void - let sendSticker: (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect) -> Bool - let sendGif: (FileMediaReference, ASDisplayNode, CGRect, Bool, Bool) -> Bool + let sendSticker: (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?) -> Bool + let sendGif: (FileMediaReference, UIView, CGRect, Bool, Bool) -> Bool let sendBotContextResultAsGif: (ChatContextResultCollection, ChatContextResult, ASDisplayNode, CGRect, Bool) -> Bool let requestMessageActionCallback: (MessageId, MemoryBuffer?, Bool, Bool) -> Void let requestMessageActionUrlAuth: (String, MessageActionUrlSubject) -> Void @@ -176,8 +176,8 @@ public final class ChatControllerInteraction { toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, - sendSticker: @escaping (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect) -> Bool, - sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect, Bool, Bool) -> Bool, + sendSticker: @escaping (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?) -> Bool, + sendGif: @escaping (FileMediaReference, UIView, CGRect, Bool, Bool) -> Bool, sendBotContextResultAsGif: @escaping (ChatContextResultCollection, ChatContextResult, ASDisplayNode, CGRect, Bool) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageActionUrlSubject) -> Void, diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 81c407e371..873088a728 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -2095,7 +2095,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { if !self.didInitializeInputMediaNodeDataPromise, let interfaceInteraction = self.interfaceInteraction { self.didInitializeInputMediaNodeDataPromise = true - self.inputMediaNodeDataPromise.set(ChatEntityKeyboardInputNode.inputData(context: self.context, interfaceInteraction: interfaceInteraction)) + self.inputMediaNodeDataPromise.set(ChatEntityKeyboardInputNode.inputData(context: self.context, interfaceInteraction: interfaceInteraction, controllerInteraction: self.controllerInteraction)) } if self.inputMediaNode == nil && !"".isEmpty { diff --git a/submodules/TelegramUI/Sources/ChatEmptyNode.swift b/submodules/TelegramUI/Sources/ChatEmptyNode.swift index d14c8a99a1..bf7c3fe2f8 100644 --- a/submodules/TelegramUI/Sources/ChatEmptyNode.swift +++ b/submodules/TelegramUI/Sources/ChatEmptyNode.swift @@ -134,7 +134,7 @@ final class ChatEmptyNodeGreetingChatContent: ASDisplayNode, ChatEmptyNodeSticke guard let stickerItem = self.stickerItem else { return } - let _ = self.interaction?.sendSticker(.standalone(media: stickerItem.stickerItem.file), false, self.view, self.stickerNode.bounds) + let _ = self.interaction?.sendSticker(.standalone(media: stickerItem.stickerItem.file), false, self.view, self.stickerNode.bounds, nil) } func updateLayout(interfaceState: ChatPresentationInterfaceState, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { @@ -303,7 +303,7 @@ final class ChatEmptyNodeNearbyChatContent: ASDisplayNode, ChatEmptyNodeStickerC guard let stickerItem = self.stickerItem else { return } - let _ = self.interaction?.sendSticker(.standalone(media: stickerItem.stickerItem.file), false, self.view, self.stickerNode.bounds) + let _ = self.interaction?.sendSticker(.standalone(media: stickerItem.stickerItem.file), false, self.view, self.stickerNode.bounds, nil) } func updateLayout(interfaceState: ChatPresentationInterfaceState, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { diff --git a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift index 3832a6764c..8c9f9cd9fc 100644 --- a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift @@ -12,24 +12,31 @@ import MultiAnimationRenderer import Postbox import TelegramCore import ComponentDisplayAdapters +import SettingsUI final class ChatEntityKeyboardInputNode: ChatInputNode { struct InputData: Equatable { let emoji: EmojiPagerContentComponent let stickers: EmojiPagerContentComponent + let gifs: GifPagerContentComponent init( emoji: EmojiPagerContentComponent, - stickers: EmojiPagerContentComponent + stickers: EmojiPagerContentComponent, + gifs: GifPagerContentComponent ) { self.emoji = emoji self.stickers = stickers + self.gifs = gifs } } - static func inputData(context: AccountContext, interfaceInteraction: ChatPanelInterfaceInteraction) -> Signal { + static func inputData(context: AccountContext, interfaceInteraction: ChatPanelInterfaceInteraction, controllerInteraction: ChatControllerInteraction) -> Signal { + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) + let isPremiumDisabled = premiumConfiguration.isPremiumDisabled + let emojiInputInteraction = EmojiPagerContentComponent.InputInteraction( - performItemAction: { [weak interfaceInteraction] item, _, _ in + performItemAction: { [weak interfaceInteraction] item, _, _, _ in guard let interfaceInteraction = interfaceInteraction else { return } @@ -40,20 +47,38 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { return } interfaceInteraction.backwardsDeleteText() + }, + openStickerSettings: { } ) let stickerInputInteraction = EmojiPagerContentComponent.InputInteraction( - performItemAction: { [weak interfaceInteraction] item, view, rect in + performItemAction: { [weak interfaceInteraction] item, view, rect, layer in guard let interfaceInteraction = interfaceInteraction else { return } - let _ = interfaceInteraction.sendSticker(.standalone(media: item.file), false, view, rect) + let _ = interfaceInteraction.sendSticker(.standalone(media: item.file), false, view, rect, layer) }, deleteBackwards: { [weak interfaceInteraction] in guard let interfaceInteraction = interfaceInteraction else { return } interfaceInteraction.backwardsDeleteText() + }, + openStickerSettings: { [weak controllerInteraction] in + guard let controllerInteraction = controllerInteraction else { + return + } + let controller = installedStickerPacksController(context: context, mode: .modal) + controller.navigationPresentation = .modal + controllerInteraction.navigationController()?.pushViewController(controller) + } + ) + let gifInputInteraction = GifPagerContentComponent.InputInteraction( + performItemAction: { [weak controllerInteraction] item, view, rect in + guard let controllerInteraction = controllerInteraction else { + return + } + let _ = controllerInteraction.sendGif(.savedGif(media: item.file), view, rect, false, false) } ) @@ -62,10 +87,24 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { }) let animationRenderer = MultiAnimationRendererImpl() - let emojiItems: Signal = context.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false) - |> map { animatedEmoji -> EmojiPagerContentComponent in + let emojiItems: Signal = combineLatest( + context.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false), + context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudSavedStickers)) + ) + |> map { animatedEmoji, savedStickers -> EmojiPagerContentComponent in var emojiItems: [EmojiPagerContentComponent.Item] = [] + for item in savedStickers { + if let item = item.contents.get(SavedStickerItem.self) { + if item.file.isVideoSticker { + emojiItems.append(EmojiPagerContentComponent.Item( + emoji: "", + file: item.file + )) + } + } + } + switch animatedEmoji { case let .result(_, items, _): for item in items { @@ -97,24 +136,146 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { ) } - let orderedItemListCollectionIds: [Int32] = [Namespaces.OrderedItemList.CloudSavedStickers] + let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> map { peer -> Bool in + guard case let .user(user) = peer else { + return false + } + return user.isPremium + } + |> distinctUntilChanged + + let orderedItemListCollectionIds: [Int32] = [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.PremiumStickers, Namespaces.OrderedItemList.CloudPremiumStickers] let namespaces: [ItemCollectionId.Namespace] = [Namespaces.ItemCollection.CloudStickerPacks] - let stickerItems: Signal = context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: orderedItemListCollectionIds, namespaces: namespaces, aroundIndex: nil, count: 10000000) - |> map { view -> EmojiPagerContentComponent in + let stickerItems: Signal = combineLatest( + context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: orderedItemListCollectionIds, namespaces: namespaces, aroundIndex: nil, count: 10000000), + hasPremium + ) + |> map { view, hasPremium -> EmojiPagerContentComponent in struct ItemGroup { - var id: ItemCollectionId + var id: AnyHashable var items: [EmojiPagerContentComponent.Item] } var itemGroups: [ItemGroup] = [] var itemGroupIndexById: [AnyHashable: Int] = [:] + var savedStickers: OrderedItemListView? + var recentStickers: OrderedItemListView? + var premiumStickers: OrderedItemListView? + var cloudPremiumStickers: OrderedItemListView? + for orderedView in view.orderedItemListsViews { + if orderedView.collectionId == Namespaces.OrderedItemList.CloudRecentStickers { + recentStickers = orderedView + } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudSavedStickers { + savedStickers = orderedView + } else if orderedView.collectionId == Namespaces.OrderedItemList.PremiumStickers { + premiumStickers = orderedView + } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudPremiumStickers { + cloudPremiumStickers = orderedView + } + } + + if let savedStickers = savedStickers { + for item in savedStickers.items { + guard let item = item.contents.get(SavedStickerItem.self) else { + continue + } + if isPremiumDisabled && item.file.isPremiumSticker { + continue + } + + let resultItem = EmojiPagerContentComponent.Item( + emoji: "", + file: item.file + ) + + let groupId = "saved" + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + itemGroups.append(ItemGroup(id: groupId, items: [resultItem])) + } + } + } + + if let recentStickers = recentStickers { + var count = 0 + for item in recentStickers.items { + guard let item = item.contents.get(RecentMediaItem.self) else { + continue + } + if isPremiumDisabled && item.media.isPremiumSticker { + continue + } + + let resultItem = EmojiPagerContentComponent.Item( + emoji: "", + file: item.media + ) + + let groupId = "recent" + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + itemGroups.append(ItemGroup(id: groupId, items: [resultItem])) + } + + count += 1 + if count >= 5 { + break + } + } + } + + var hasPremiumStickers = false + if hasPremium { + if let premiumStickers = premiumStickers, !premiumStickers.items.isEmpty { + hasPremiumStickers = true + } else if let cloudPremiumStickers = cloudPremiumStickers, !cloudPremiumStickers.items.isEmpty { + hasPremiumStickers = true + } + } + + if hasPremiumStickers { + var premiumStickers = premiumStickers?.items ?? [] + if let cloudPremiumStickers = cloudPremiumStickers { + premiumStickers.append(contentsOf: cloudPremiumStickers.items) + } + + var processedIds = Set() + for item in premiumStickers { + guard let item = item.contents.get(RecentMediaItem.self) else { + continue + } + if isPremiumDisabled && item.media.isPremiumSticker { + continue + } + if processedIds.contains(item.media.fileId) { + continue + } + processedIds.insert(item.media.fileId) + + let resultItem = EmojiPagerContentComponent.Item( + emoji: "", + file: item.media + ) + + let groupId = "premium" + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + itemGroups.append(ItemGroup(id: groupId, items: [resultItem])) + } + } + } + for entry in view.entries { guard let item = entry.item as? StickerPackItem else { continue } - if !item.file.isAnimatedSticker { - continue - } let resultItem = EmojiPagerContentComponent.Item( emoji: "", file: item.file @@ -135,10 +296,20 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { inputInteraction: stickerInputInteraction, itemGroups: itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in var title: String? - for (id, info, _) in view.collectionInfos { - if id == group.id, let info = info as? StickerPackCollectionInfo { - title = info.title.uppercased() - break + if group.id == AnyHashable("saved") { + title = nil + } else if group.id == AnyHashable("recent") { + //TODO:localize + title = "Recently Used".uppercased() + } else if group.id == AnyHashable("premium") { + //TODO:localize + title = "Premium".uppercased() + } else { + for (id, info, _) in view.collectionInfos { + if AnyHashable(id) == group.id, let info = info as? StickerPackCollectionInfo { + title = info.title.uppercased() + break + } } } @@ -148,14 +319,31 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { ) } + let gifItems: Signal = context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs)) + |> map { savedGifs -> GifPagerContentComponent in + var items: [GifPagerContentComponent.Item] = [] + for gifItem in savedGifs { + items.append(GifPagerContentComponent.Item( + file: gifItem.contents.get(RecentMediaItem.self)!.media + )) + } + return GifPagerContentComponent( + context: context, + inputInteraction: gifInputInteraction, + items: items + ) + } + return combineLatest(queue: .mainQueue(), emojiItems, - stickerItems + stickerItems, + gifItems ) - |> map { emoji, stickers -> InputData in + |> map { emoji, stickers, gifs -> InputData in return InputData( emoji: emoji, - stickers: stickers + stickers: stickers, + gifs: gifs ) } } @@ -196,6 +384,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { bottomInset: bottomInset, emojiContent: self.currentInputData.emoji, stickerContent: self.currentInputData.stickers, + gifContent: self.currentInputData.gifs, externalTopPanelContainer: self.externalTopPanelContainer, topPanelExtensionUpdated: { [weak self] topPanelExtension, transition in guard let strongSelf = self else { diff --git a/submodules/TelegramUI/Sources/ChatMediaInputGifPane.swift b/submodules/TelegramUI/Sources/ChatMediaInputGifPane.swift index 3468153e01..288c9d53ba 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputGifPane.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputGifPane.swift @@ -238,7 +238,7 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { if let (collection, result) = file.contextResult { let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode, sourceRect, false) } else { - let _ = self?.controllerInteraction.sendGif(file.file, sourceNode, sourceRect, false, false) + let _ = self?.controllerInteraction.sendGif(file.file, sourceNode.view, sourceRect, false, false) } } diff --git a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift index 76c63da189..80af486b9a 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift @@ -796,7 +796,7 @@ final class ChatMediaInputNode: ChatInputNode { sendSticker: { fileReference, sourceNode, sourceRect in if let strongSelf = self { - return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect) + return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil) } else { return false } @@ -864,7 +864,7 @@ final class ChatMediaInputNode: ChatInputNode { sendSticker: { fileReference, sourceNode, sourceRect in if let strongSelf = self { - return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect) + return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil) } else { return false } @@ -1096,7 +1096,7 @@ final class ChatMediaInputNode: ChatInputNode { let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash) let controller = StickerPackScreen(context: strongSelf.context, updatedPresentationData: strongSelf.controllerInteraction.updatedPresentationData, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.controllerInteraction.navigationController(), sendSticker: { fileReference, sourceNode, sourceRect in if let strongSelf = self { - return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect) + return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil) } else { return false } @@ -1467,7 +1467,7 @@ final class ChatMediaInputNode: ChatInputNode { }, action: { _, f in f(.default) if isSaved { - let _ = self?.controllerInteraction.sendGif(file.file, sourceNode, sourceRect, false, false) + let _ = self?.controllerInteraction.sendGif(file.file, sourceNode.view, sourceRect, false, false) } else if let (collection, result) = file.contextResult { let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode, sourceRect, false) } @@ -1486,7 +1486,7 @@ final class ChatMediaInputNode: ChatInputNode { }, action: { _, f in f(.default) if isSaved { - let _ = self?.controllerInteraction.sendGif(file.file, sourceNode, sourceRect, true, false) + let _ = self?.controllerInteraction.sendGif(file.file, sourceNode.view, sourceRect, true, false) } else if let (collection, result) = file.contextResult { let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode, sourceRect, true) } @@ -1499,7 +1499,7 @@ final class ChatMediaInputNode: ChatInputNode { }, action: { _, f in f(.default) - let _ = self?.controllerInteraction.sendGif(file.file, sourceNode, sourceRect, false, true) + let _ = self?.controllerInteraction.sendGif(file.file, sourceNode.view, sourceRect, false, true) }))) } } @@ -1608,9 +1608,9 @@ final class ChatMediaInputNode: ChatInputNode { }, action: { _, f in if let strongSelf = self, let peekController = strongSelf.peekController { if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { - let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, false, animationNode.view, animationNode.bounds) + let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, false, animationNode.view, animationNode.bounds, nil) } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { - let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, false, imageNode.view, imageNode.bounds) + let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, false, imageNode.view, imageNode.bounds, nil) } } f(.default) @@ -1622,9 +1622,9 @@ final class ChatMediaInputNode: ChatInputNode { }, action: { _, f in if let strongSelf = self, let peekController = strongSelf.peekController { if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { - let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, false, animationNode.view, animationNode.bounds) + let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, false, animationNode.view, animationNode.bounds, nil) } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { - let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, false, imageNode.view, imageNode.bounds) + let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, false, imageNode.view, imageNode.bounds, nil) } } f(.default) @@ -1678,7 +1678,7 @@ final class ChatMediaInputNode: ChatInputNode { if let packReference = packReference { let controller = StickerPackScreen(context: strongSelf.context, updatedPresentationData: strongSelf.controllerInteraction.updatedPresentationData, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.controllerInteraction.navigationController(), sendSticker: { file, sourceNode, sourceRect in if let strongSelf = self { - return strongSelf.controllerInteraction.sendSticker(file, false, false, nil, false, sourceNode, sourceRect) + return strongSelf.controllerInteraction.sendSticker(file, false, false, nil, false, sourceNode, sourceRect, nil) } else { return false } @@ -1760,9 +1760,9 @@ final class ChatMediaInputNode: ChatInputNode { }, action: { _, f in if let strongSelf = self, let peekController = strongSelf.peekController { if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { - let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, false, animationNode.view, animationNode.bounds) + let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, false, animationNode.view, animationNode.bounds, nil) } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { - let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, false, imageNode.view, imageNode.bounds) + let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, false, imageNode.view, imageNode.bounds, nil) } } f(.default) @@ -1774,9 +1774,9 @@ final class ChatMediaInputNode: ChatInputNode { }, action: { _, f in if let strongSelf = self, let peekController = strongSelf.peekController { if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { - let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, false, animationNode.view, animationNode.bounds) + let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, false, animationNode.view, animationNode.bounds, nil) } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { - let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, false, imageNode.view, imageNode.bounds) + let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, false, imageNode.view, imageNode.bounds, nil) } } f(.default) @@ -1832,7 +1832,7 @@ final class ChatMediaInputNode: ChatInputNode { if let packReference = packReference { let controller = StickerPackScreen(context: strongSelf.context, updatedPresentationData: strongSelf.controllerInteraction.updatedPresentationData, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.controllerInteraction.navigationController(), sendSticker: { file, sourceNode, sourceRect in if let strongSelf = self { - return strongSelf.controllerInteraction.sendSticker(file, false, false, nil, false, sourceNode, sourceRect) + return strongSelf.controllerInteraction.sendSticker(file, false, false, nil, false, sourceNode, sourceRect, nil) } else { return false } diff --git a/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift b/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift index b18d419cf6..8c9fb1451f 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift @@ -412,7 +412,7 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode { if let interfaceInteraction = self.interfaceInteraction, let (_, item, _) = self.currentState, case .ended = recognizer.state { if let isLocked = self.isLocked, isLocked { } else { - let _ = interfaceInteraction.sendSticker(.standalone(media: item.file), false, false, nil, false, self.view, self.bounds) + let _ = interfaceInteraction.sendSticker(.standalone(media: item.file), false, false, nil, false, self.view, self.bounds, nil) self.imageNode.layer.animateAlpha(from: 0.5, to: 1.0, duration: 1.0) } } diff --git a/submodules/TelegramUI/Sources/ChatMediaInputTrendingPane.swift b/submodules/TelegramUI/Sources/ChatMediaInputTrendingPane.swift index 2f102ab512..1b0375f792 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputTrendingPane.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputTrendingPane.swift @@ -326,7 +326,7 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane { let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash) let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.controllerInteraction.navigationController(), sendSticker: { fileReference, sourceNode, sourceRect in if let strongSelf = self { - return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect) + return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil) } else { return false } diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index f519798185..b2cc062ad2 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -2311,18 +2311,13 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { let localSourceContentFrame = CGRect( origin: CGPoint( - x: self.imageNode.frame.minX + self.imageNode.frame.size.width / 2.0 - stickerSource.imageNode.frame.size.width / 2.0, - y: self.imageNode.frame.minY + self.imageNode.frame.size.height / 2.0 - stickerSource.imageNode.frame.size.height / 2.0 + x: self.imageNode.frame.minX + self.imageNode.frame.size.width / 2.0 - stickerSource.sourceFrame.size.width / 2.0, + y: self.imageNode.frame.minY + self.imageNode.frame.size.height / 2.0 - stickerSource.sourceFrame.size.height / 2.0 ), - size: stickerSource.imageNode.frame.size + size: stickerSource.sourceFrame.size ) - var snapshotView: UIView? - if let animationNode = stickerSource.animationNode { - snapshotView = animationNode.view.snapshotContentTree() - } else { - snapshotView = stickerSource.imageNode.view.snapshotContentTree() - } + let snapshotView: UIView? = stickerSource.snapshotContentTree() snapshotView?.frame = localSourceContentFrame if let snapshotView = snapshotView { @@ -2342,7 +2337,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { y: localSourceCenter.y - localSourceContentFrame.height / 2.0 ) - let sourceScale: CGFloat = stickerSource.imageNode.frame.height / self.imageNode.frame.height + let sourceScale: CGFloat = stickerSource.sourceFrame.height / self.imageNode.frame.height let offset = CGPoint( x: sourceCenter.x - self.imageNode.frame.midX, @@ -2385,8 +2380,10 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { animationNode.layer.animateAlpha(from: 0.0, to: animationNode.alpha, duration: 0.4) } - stickerSource.imageNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) - stickerSource.imageNode.layer.animateAlpha(from: 0.0, to: stickerSource.imageNode.alpha, duration: 0.4) + if let sourceLayer = stickerSource.sourceLayer { + sourceLayer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + sourceLayer.animateAlpha(from: 0.0, to: CGFloat(sourceLayer.opacity), duration: 0.4) + } if let placeholderNode = stickerSource.placeholderNode { placeholderNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) diff --git a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift index d7657cb0fd..2f57d3b0a4 100644 --- a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift @@ -1487,17 +1487,17 @@ class ChatMessageStickerItemNode: ChatMessageItemView { let localSourceContentFrame = CGRect( origin: CGPoint( - x: self.imageNode.frame.minX + self.imageNode.frame.size.width / 2.0 - stickerSource.imageNode.frame.size.width / 2.0, - y: self.imageNode.frame.minY + self.imageNode.frame.size.height / 2.0 - stickerSource.imageNode.frame.size.height / 2.0 + x: self.imageNode.frame.minX + self.imageNode.frame.size.width / 2.0 - stickerSource.sourceFrame.size.width / 2.0, + y: self.imageNode.frame.minY + self.imageNode.frame.size.height / 2.0 - stickerSource.sourceFrame.size.height / 2.0 ), - size: stickerSource.imageNode.frame.size + size: stickerSource.sourceFrame.size ) var snapshotView: UIView? if let animationNode = stickerSource.animationNode { snapshotView = animationNode.view.snapshotContentTree() } else { - snapshotView = stickerSource.imageNode.view.snapshotContentTree() + snapshotView = stickerSource.snapshotContentTree() } snapshotView?.frame = localSourceContentFrame @@ -1518,7 +1518,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { y: localSourceCenter.y - localSourceContentFrame.height / 2.0 ) - let sourceScale: CGFloat = stickerSource.imageNode.frame.height / self.imageNode.frame.height + let sourceScale: CGFloat = stickerSource.sourceFrame.height / self.imageNode.frame.height let offset = CGPoint( x: sourceCenter.x - self.imageNode.frame.midX, @@ -1554,8 +1554,10 @@ class ChatMessageStickerItemNode: ChatMessageItemView { animationNode.layer.animateAlpha(from: 0.0, to: animationNode.alpha, duration: 0.4) } - stickerSource.imageNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) - stickerSource.imageNode.layer.animateAlpha(from: 0.0, to: stickerSource.imageNode.alpha, duration: 0.4) + if let sourceLayer = stickerSource.sourceLayer { + sourceLayer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + sourceLayer.animateAlpha(from: 0.0, to: CGFloat(sourceLayer.opacity), duration: 0.4) + } if let placeholderNode = stickerSource.placeholderNode { placeholderNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) diff --git a/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift b/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift index dfa894b6d0..bf079a44e7 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift @@ -111,17 +111,51 @@ public final class ChatMessageTransitionNode: ASDisplayNode { } final class Sticker { - let imageNode: TransformImageNode + let imageNode: TransformImageNode? let animationNode: AnimatedStickerNode? let placeholderNode: ASDisplayNode? + let imageLayer: CALayer? let relativeSourceRect: CGRect + + var sourceFrame: CGRect { + if let imageNode = self.imageNode { + return imageNode.frame + } else if let imageLayer = self.imageLayer { + return imageLayer.bounds + } else { + return CGRect(origin: CGPoint(), size: relativeSourceRect.size) + } + } + + var sourceLayer: CALayer? { + if let imageNode = self.imageNode { + return imageNode.layer + } else if let imageLayer = self.imageLayer { + return imageLayer + } else { + return nil + } + } - init(imageNode: TransformImageNode, animationNode: AnimatedStickerNode?, placeholderNode: ASDisplayNode?, relativeSourceRect: CGRect) { + init(imageNode: TransformImageNode?, animationNode: AnimatedStickerNode?, placeholderNode: ASDisplayNode?, imageLayer: CALayer?, relativeSourceRect: CGRect) { self.imageNode = imageNode self.animationNode = animationNode self.placeholderNode = placeholderNode + self.imageLayer = imageLayer self.relativeSourceRect = relativeSourceRect } + + func snapshotContentTree() -> UIView? { + if let animationNode = self.animationNode { + return animationNode.view.snapshotContentTree() + } else if let imageNode = self.imageNode { + return imageNode.view.snapshotContentTree() + } else if let sourceLayer = self.imageLayer { + return sourceLayer.snapshotContentTreeAsView() + } else { + return nil + } + } } enum Source { @@ -142,6 +176,7 @@ public final class ChatMessageTransitionNode: ASDisplayNode { enum StickerInput { case inputPanel(itemNode: ChatMediaInputStickerGridItemNode) case mediaPanel(itemNode: HorizontalStickerGridItemNode) + case universal(sourceContainerView: UIView, sourceRect: CGRect, sourceLayer: CALayer) case inputPanelSearch(itemNode: StickerPaneSearchStickerItemNode) case emptyPanel(itemNode: ChatEmptyNodeStickerContentNode) } @@ -388,16 +423,19 @@ public final class ChatMessageTransitionNode: ASDisplayNode { let sourceAbsoluteRect: CGRect switch stickerMediaInput { case let .inputPanel(sourceItemNode): - stickerSource = Sticker(imageNode: sourceItemNode.imageNode, animationNode: sourceItemNode.animationNode, placeholderNode: sourceItemNode.placeholderNode, relativeSourceRect: sourceItemNode.imageNode.frame) - sourceAbsoluteRect = sourceItemNode.view.convert(stickerSource.imageNode.frame, to: self.view) + stickerSource = Sticker(imageNode: sourceItemNode.imageNode, animationNode: sourceItemNode.animationNode, placeholderNode: sourceItemNode.placeholderNode, imageLayer: nil, relativeSourceRect: sourceItemNode.imageNode.frame) + sourceAbsoluteRect = sourceItemNode.view.convert(sourceItemNode.imageNode.frame, to: self.view) case let .mediaPanel(sourceItemNode): - stickerSource = Sticker(imageNode: sourceItemNode.imageNode, animationNode: sourceItemNode.animationNode, placeholderNode: sourceItemNode.placeholderNode, relativeSourceRect: sourceItemNode.imageNode.frame) - sourceAbsoluteRect = sourceItemNode.view.convert(stickerSource.imageNode.frame, to: self.view) + stickerSource = Sticker(imageNode: sourceItemNode.imageNode, animationNode: sourceItemNode.animationNode, placeholderNode: sourceItemNode.placeholderNode, imageLayer: nil, relativeSourceRect: sourceItemNode.imageNode.frame) + sourceAbsoluteRect = sourceItemNode.view.convert(sourceItemNode.imageNode.frame, to: self.view) + case let .universal(sourceContainerView, sourceRect, sourceLayer): + stickerSource = Sticker(imageNode: nil, animationNode: nil, placeholderNode: nil, imageLayer: sourceLayer, relativeSourceRect: sourceLayer.frame) + sourceAbsoluteRect = sourceContainerView.convert(sourceRect, to: self.view) case let .inputPanelSearch(sourceItemNode): - stickerSource = Sticker(imageNode: sourceItemNode.imageNode, animationNode: sourceItemNode.animationNode, placeholderNode: nil, relativeSourceRect: sourceItemNode.imageNode.frame) - sourceAbsoluteRect = sourceItemNode.view.convert(stickerSource.imageNode.frame, to: self.view) + stickerSource = Sticker(imageNode: sourceItemNode.imageNode, animationNode: sourceItemNode.animationNode, placeholderNode: nil, imageLayer: nil, relativeSourceRect: sourceItemNode.imageNode.frame) + sourceAbsoluteRect = sourceItemNode.view.convert(sourceItemNode.imageNode.frame, to: self.view) case let .emptyPanel(sourceItemNode): - stickerSource = Sticker(imageNode: sourceItemNode.stickerNode.imageNode, animationNode: sourceItemNode.stickerNode.animationNode, placeholderNode: nil, relativeSourceRect: sourceItemNode.stickerNode.imageNode.frame) + stickerSource = Sticker(imageNode: sourceItemNode.stickerNode.imageNode, animationNode: sourceItemNode.stickerNode.animationNode, placeholderNode: nil, imageLayer: nil, relativeSourceRect: sourceItemNode.stickerNode.imageNode.frame) sourceAbsoluteRect = sourceItemNode.stickerNode.view.convert(sourceItemNode.stickerNode.imageNode.frame, to: self.view) } @@ -442,7 +480,7 @@ public final class ChatMessageTransitionNode: ASDisplayNode { self.containerNode.layer.animatePosition(from: CGPoint(x: sourceAbsoluteRect.midX - targetAbsoluteRect.midX, y: 0.0), to: CGPoint(), duration: horizontalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNode.horizontalAnimationCurve.mediaTimingFunction, additive: true) switch stickerMediaInput { - case .inputPanel: + case .inputPanel, .universal: break case let .mediaPanel(sourceItemNode): sourceItemNode.isHidden = true diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsController.swift b/submodules/TelegramUI/Sources/ChatRecentActionsController.swift index ba675ff969..b700e2524d 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsController.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsController.swift @@ -105,7 +105,7 @@ final class ChatRecentActionsController: TelegramBaseController { }, displayVideoUnmuteTip: { _ in }, switchMediaRecordingMode: { }, setupMessageAutoremoveTimeout: { - }, sendSticker: { _, _, _, _ in + }, sendSticker: { _, _, _, _, _ in return false }, unblockPeer: { }, pinMessage: { _, _ in diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift index fa72a7a018..936cb29704 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift @@ -261,7 +261,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in - }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _ in return false }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { [weak self] url, _, _, _ in + }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _ in return false }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { [weak self] url, _, _, _ in self?.openUrl(url) }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { [weak self] message, associatedData in if let strongSelf = self, let navigationController = strongSelf.getNavigationController() { diff --git a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift b/submodules/TelegramUI/Sources/DrawingStickersScreen.swift index 155e383412..13daf942a7 100644 --- a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift +++ b/submodules/TelegramUI/Sources/DrawingStickersScreen.swift @@ -112,7 +112,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { return false }, openPeer: { _, _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, openMessageReactionContextMenu: { _, _, _, _ in }, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in }, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in - }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { fileReference, _, _, _, _, node, rect in return selectStickerImpl?(fileReference, node, rect) ?? false }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in + }, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { fileReference, _, _, _, _, node, rect, _ in return selectStickerImpl?(fileReference, node, rect) ?? false }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in }, presentControllerInCurrent: { _, _ in }, navigationController: { @@ -236,7 +236,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { sendSticker: { fileReference, sourceNode, sourceRect in if let strongSelf = self { - return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect) + return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil) } else { return false } @@ -349,7 +349,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { sendSticker: { fileReference, sourceNode, sourceRect in if let strongSelf = self { - return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect) + return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil) } else { return false } diff --git a/submodules/TelegramUI/Sources/GifPaneSearchContentNode.swift b/submodules/TelegramUI/Sources/GifPaneSearchContentNode.swift index dc2993be47..6e55f0cbb4 100644 --- a/submodules/TelegramUI/Sources/GifPaneSearchContentNode.swift +++ b/submodules/TelegramUI/Sources/GifPaneSearchContentNode.swift @@ -333,7 +333,7 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode { if let (collection, result) = file.contextResult { let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode, sourceRect, false) } else { - let _ = self?.controllerInteraction.sendGif(file.file, sourceNode, sourceRect, false, false) + let _ = self?.controllerInteraction.sendGif(file.file, sourceNode.view, sourceRect, false, false) } } diff --git a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift index ce97e1b83e..a705670b73 100644 --- a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift @@ -164,7 +164,7 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont if let strongSelf = self { let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.interfaceInteraction?.getNavigationController(), sendSticker: { file, sourceView, sourceRect in if let strongSelf = self { - return strongSelf.interfaceInteraction?.sendSticker(file, false, sourceView, sourceRect) ?? false + return strongSelf.interfaceInteraction?.sendSticker(file, false, sourceView, sourceRect, nil) ?? false } else { return false } diff --git a/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift b/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift index 1d99c75e84..930cdacc64 100755 --- a/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift @@ -72,7 +72,7 @@ private struct StickerEntry: Identifiable, Comparable { return HorizontalStickerGridItem(account: account, file: self.file, theme: theme, isPreviewed: { item in return false//stickersInteraction.previewedStickerItem == item }, sendSticker: { file, node, rect in - let _ = interfaceInteraction.sendSticker(file, true, node, rect) + let _ = interfaceInteraction.sendSticker(file, true, node, rect, nil) }) } } @@ -180,7 +180,7 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode { .action(ContextMenuActionItem(text: strongSelf.strings.StickerPack_Send, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { _, f in f(.default) - let _ = controllerInteraction.sendSticker(.standalone(media: item.file), false, false, nil, true, itemNode.view, itemNode.bounds) + let _ = controllerInteraction.sendSticker(.standalone(media: item.file), false, false, nil, true, itemNode.view, itemNode.bounds, nil) })), .action(ContextMenuActionItem(text: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unfave") : UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.default) @@ -224,7 +224,7 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode { if let packReference = packReference { let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: controllerInteraction.navigationController(), sendSticker: { file, sourceNode, sourceRect in if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { - return controllerInteraction.sendSticker(file, false, false, nil, true, sourceNode, sourceRect) + return controllerInteraction.sendSticker(file, false, false, nil, true, sourceNode, sourceRect, nil) } else { return false } diff --git a/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift b/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift index ef2e3bd426..9a0280d1eb 100644 --- a/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift +++ b/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift @@ -111,9 +111,9 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie }, action: { _, f in if let strongSelf = self, let peekController = strongSelf.peekController { if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { - let _ = controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, true, animationNode.view, animationNode.bounds) + let _ = controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, true, animationNode.view, animationNode.bounds, nil) } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { - let _ = controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, true, imageNode.view, imageNode.bounds) + let _ = controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, true, imageNode.view, imageNode.bounds, nil) } } f(.default) @@ -125,9 +125,9 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie }, action: { _, f in if let strongSelf = self, let peekController = strongSelf.peekController { if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { - let _ = controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, true, animationNode.view, animationNode.bounds) + let _ = controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, true, animationNode.view, animationNode.bounds, nil) } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { - let _ = controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, true, imageNode.view, imageNode.bounds) + let _ = controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, true, imageNode.view, imageNode.bounds, nil) } } f(.default) @@ -179,7 +179,7 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie if let packReference = packReference { let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: controllerInteraction.navigationController(), sendSticker: { file, sourceNode, sourceRect in if let strongSelf = self, let controllerInteraction = strongSelf.getControllerInteraction?() { - return controllerInteraction.sendSticker(file, false, false, nil, true, sourceNode, sourceRect) + return controllerInteraction.sendSticker(file, false, false, nil, true, sourceNode, sourceRect, nil) } else { return false } @@ -576,7 +576,7 @@ final class InlineReactionSearchPanel: ChatInputContextPanelNode { guard let strongSelf = self else { return } - let _ = strongSelf.controllerInteraction?.sendSticker(file, false, false, strongSelf.query, true, node, rect) + let _ = strongSelf.controllerInteraction?.sendSticker(file, false, false, strongSelf.query, true, node, rect, nil) } self.view.disablesInteractiveTransitionGestureRecognizer = true diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 2029e31d4c..5c6dd1e3b0 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -80,7 +80,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in - }, sendSticker: { _, _, _, _, _, _, _ in + }, sendSticker: { _, _, _, _, _, _, _, _ in return false }, sendGif: { _, _, _, _, _ in return false diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 20756b514b..a89a74796a 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -310,7 +310,7 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode { }, displayVideoUnmuteTip: { _ in }, switchMediaRecordingMode: { }, setupMessageAutoremoveTimeout: { - }, sendSticker: { _, _, _, _ in + }, sendSticker: { _, _, _, _, _ in return false }, unblockPeer: { }, pinMessage: { _, _ in @@ -2210,7 +2210,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate strongSelf.paneContainerNode.updateSelectedMessageIds(strongSelf.state.selectedMessageIds, animated: true) }, sendCurrentMessage: { _ in }, sendMessage: { _ in - }, sendSticker: { _, _, _, _, _, _, _ in + }, sendSticker: { _, _, _, _, _, _, _, _ in return false }, sendGif: { _, _, _, _, _ in return false diff --git a/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift index b88389adb5..6d71ab7ba3 100644 --- a/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift @@ -252,7 +252,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { }, displayVideoUnmuteTip: { _ in }, switchMediaRecordingMode: { }, setupMessageAutoremoveTimeout: { - }, sendSticker: { _, _, _, _ in + }, sendSticker: { _, _, _, _, _ in return false }, unblockPeer: { }, pinMessage: { _, _ in diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 0771a98da8..aa47eec6c7 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1280,7 +1280,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { tapMessage?(message) }, clickThroughMessage: { clickThroughMessage?() - }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _ in return false }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _ in + }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _ in return false }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in }, presentController: { _, _ in diff --git a/submodules/TelegramUI/Sources/StickerPaneSearchContentNode.swift b/submodules/TelegramUI/Sources/StickerPaneSearchContentNode.swift index 581ac2be30..92ea1c0b39 100644 --- a/submodules/TelegramUI/Sources/StickerPaneSearchContentNode.swift +++ b/submodules/TelegramUI/Sources/StickerPaneSearchContentNode.swift @@ -226,7 +226,7 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash) let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.controllerInteraction.navigationController(), sendSticker: { [weak self] fileReference, sourceNode, sourceRect in if let strongSelf = self { - return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect) + return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil) } else { return false } @@ -321,7 +321,7 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { } }, sendSticker: { [weak self] file, sourceView, sourceRect in if let strongSelf = self { - let _ = strongSelf.controllerInteraction.sendSticker(file, false, false, nil, false, sourceView, sourceRect) + let _ = strongSelf.controllerInteraction.sendSticker(file, false, false, nil, false, sourceView, sourceRect, nil) } }, getItemIsPreviewed: { item in return inputNodeInteraction.previewedStickerPackItem == .pack(item) diff --git a/submodules/TelegramUI/Sources/StickersChatInputContextPanelItem.swift b/submodules/TelegramUI/Sources/StickersChatInputContextPanelItem.swift index e474a5c6ee..d313b5cb28 100644 --- a/submodules/TelegramUI/Sources/StickersChatInputContextPanelItem.swift +++ b/submodules/TelegramUI/Sources/StickersChatInputContextPanelItem.swift @@ -126,7 +126,7 @@ final class StickersChatInputContextPanelItemNode: ListViewItemNode { for i in 0 ..< self.nodes.count { if self.nodes[i].frame.contains(location) { let file = item.files[i] - let _ = item.interfaceInteraction.sendSticker(.standalone(media: file), true, self.nodes[i].view, self.nodes[i].bounds) + let _ = item.interfaceInteraction.sendSticker(.standalone(media: file), true, self.nodes[i].view, self.nodes[i].bounds, nil) break } } diff --git a/submodules/TelegramUI/Sources/StickersChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/StickersChatInputContextPanelNode.swift index 8f23e7f235..f72a055c94 100644 --- a/submodules/TelegramUI/Sources/StickersChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/StickersChatInputContextPanelNode.swift @@ -136,7 +136,7 @@ final class StickersChatInputContextPanelNode: ChatInputContextPanelNode { .action(ContextMenuActionItem(text: strongSelf.strings.StickerPack_Send, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { _, f in f(.default) - let _ = controllerInteraction.sendSticker(.standalone(media: item.file), false, false, nil, true, itemNode.view, itemNode.bounds) + let _ = controllerInteraction.sendSticker(.standalone(media: item.file), false, false, nil, true, itemNode.view, itemNode.bounds, nil) })), .action(ContextMenuActionItem(text: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unfave") : UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.default) @@ -180,7 +180,7 @@ final class StickersChatInputContextPanelNode: ChatInputContextPanelNode { if let packReference = packReference { let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: controllerInteraction.navigationController(), sendSticker: { file, sourceNode, sourceRect in if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction { - return controllerInteraction.sendSticker(file, false, false, nil, true, sourceNode, sourceRect) + return controllerInteraction.sendSticker(file, false, false, nil, true, sourceNode, sourceRect, nil) } else { return false }