[Temp] Input panel progress

This commit is contained in:
Ali 2022-06-21 21:47:01 +01:00
parent abe701f625
commit 0f1b382265
44 changed files with 1728 additions and 196 deletions

View File

@ -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";

View File

@ -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

View File

@ -593,7 +593,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
}, displayVideoUnmuteTip: { _ in
}, switchMediaRecordingMode: {
}, setupMessageAutoremoveTimeout: {
}, sendSticker: { _, _, _, _ in
}, sendSticker: { _, _, _, _, _ in
return false
}, unblockPeer: {
}, pinMessage: { _, _ in

View File

@ -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

View File

@ -46,19 +46,22 @@ public final class PagerComponentPanelEnvironment: Equatable {
public let contentIcons: [AnyComponentWithIdentity<Empty>]
public let contentAccessoryRightButtons: [AnyComponentWithIdentity<Empty>]
public let activeContentId: AnyHashable?
public let navigateToContentId: (AnyHashable) -> Void
init(
contentOffset: CGFloat,
contentTopPanels: [AnyComponentWithIdentity<Empty>],
contentIcons: [AnyComponentWithIdentity<Empty>],
contentAccessoryRightButtons: [AnyComponentWithIdentity<Empty>],
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<ChildEnvironmentType: Equatable>: 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<ChildEnvironmentType: Equatable>: Component {
contentTopPanels: component.contentTopPanels,
contentIcons: [],
contentAccessoryRightButtons: [],
activeContentId: centralId
activeContentId: centralId,
navigateToContentId: navigateToContentId
)
},
containerSize: availableSize
@ -356,7 +370,8 @@ public final class PagerComponent<ChildEnvironmentType: Equatable>: Component {
contentTopPanels: [],
contentIcons: component.contentIcons,
contentAccessoryRightButtons: component.contentAccessoryRightButtons,
activeContentId: centralId
activeContentId: centralId,
navigateToContentId: navigateToContentId
)
},
containerSize: availableSize
@ -404,15 +419,27 @@ public final class PagerComponent<ChildEnvironmentType: Equatable>: 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<ChildEnvironmentType: Equatable>: 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 {

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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",

View File

@ -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<Empty>] = [:]
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<MediaId>()
var validIds = Set<ItemLayer.Key>()
var validGroupHeaderIds = Set<AnyHashable>()
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

View File

@ -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<Empty>] = []
var contentAccessoryRightButtons: [AnyComponentWithIdentity<Empty>] = []
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

View File

@ -11,9 +11,14 @@ import BundleIconComponent
private final class BottomPanelIconComponent: Component {
let content: AnyComponent<Empty>
let action: () -> Void
init(content: AnyComponent<Empty>) {
init(
content: AnyComponent<Empty>,
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<Empty>
var component: BottomPanelIconComponent?
override init(frame: CGRect) {
self.contentView = ComponentHostView<Empty>()
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<Empty>, 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)

View File

@ -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] = []

View File

@ -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<Int>? {
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<CGPoint>) {
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<MediaId>()
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<EnvironmentType>, 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<EnvironmentType>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -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",
],
)

View File

@ -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<Weak<MultiVideoRenderTarget>>()
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()
}
}
}
}
}
}
}
*/

View File

@ -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",
],
)

View File

@ -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()
}
}

View File

@ -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)

View File

@ -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,

View File

@ -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 {

View File

@ -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 {

View File

@ -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<InputData, NoError> {
static func inputData(context: AccountContext, interfaceInteraction: ChatPanelInterfaceInteraction, controllerInteraction: ChatControllerInteraction) -> Signal<InputData, NoError> {
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<EmojiPagerContentComponent, NoError> = context.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false)
|> map { animatedEmoji -> EmojiPagerContentComponent in
let emojiItems: Signal<EmojiPagerContentComponent, NoError> = 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<EmojiPagerContentComponent, NoError> = context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: orderedItemListCollectionIds, namespaces: namespaces, aroundIndex: nil, count: 10000000)
|> map { view -> EmojiPagerContentComponent in
let stickerItems: Signal<EmojiPagerContentComponent, NoError> = 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<MediaId>()
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<GifPagerContentComponent, NoError> = 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 {

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -105,7 +105,7 @@ final class ChatRecentActionsController: TelegramBaseController {
}, displayVideoUnmuteTip: { _ in
}, switchMediaRecordingMode: {
}, setupMessageAutoremoveTimeout: {
}, sendSticker: { _, _, _, _ in
}, sendSticker: { _, _, _, _, _ in
return false
}, unblockPeer: {
}, pinMessage: { _, _ in

View File

@ -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() {

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -252,7 +252,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
}, displayVideoUnmuteTip: { _ in
}, switchMediaRecordingMode: {
}, setupMessageAutoremoveTimeout: {
}, sendSticker: { _, _, _, _ in
}, sendSticker: { _, _, _, _, _ in
return false
}, unblockPeer: {
}, pinMessage: { _, _ in

View File

@ -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

View File

@ -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)

View File

@ -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
}
}

View File

@ -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
}