mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
[Temp] Input panel progress
This commit is contained in:
parent
abe701f625
commit
0f1b382265
@ -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";
|
||||
|
@ -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
|
||||
|
@ -593,7 +593,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
|
||||
}, displayVideoUnmuteTip: { _ in
|
||||
}, switchMediaRecordingMode: {
|
||||
}, setupMessageAutoremoveTimeout: {
|
||||
}, sendSticker: { _, _, _, _ in
|
||||
}, sendSticker: { _, _, _, _, _ in
|
||||
return false
|
||||
}, unblockPeer: {
|
||||
}, pinMessage: { _, _ in
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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] = []
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
20
submodules/TelegramUI/Components/MultiVideoRenderer/BUILD
Normal file
20
submodules/TelegramUI/Components/MultiVideoRenderer/BUILD
Normal 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",
|
||||
],
|
||||
)
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
21
submodules/TelegramUI/Components/VideoAnimationCache/BUILD
Normal file
21
submodules/TelegramUI/Components/VideoAnimationCache/BUILD
Normal 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",
|
||||
],
|
||||
)
|
@ -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()
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -105,7 +105,7 @@ final class ChatRecentActionsController: TelegramBaseController {
|
||||
}, displayVideoUnmuteTip: { _ in
|
||||
}, switchMediaRecordingMode: {
|
||||
}, setupMessageAutoremoveTimeout: {
|
||||
}, sendSticker: { _, _, _, _ in
|
||||
}, sendSticker: { _, _, _, _, _ in
|
||||
return false
|
||||
}, unblockPeer: {
|
||||
}, pinMessage: { _, _ in
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -252,7 +252,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
|
||||
}, displayVideoUnmuteTip: { _ in
|
||||
}, switchMediaRecordingMode: {
|
||||
}, setupMessageAutoremoveTimeout: {
|
||||
}, sendSticker: { _, _, _, _ in
|
||||
}, sendSticker: { _, _, _, _, _ in
|
||||
return false
|
||||
}, unblockPeer: {
|
||||
}, pinMessage: { _, _ in
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user