mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
GIFs in media editor
Image search in media editor
This commit is contained in:
parent
9ddb8fa2d6
commit
b71304a3d2
@ -900,7 +900,7 @@ public protocol SharedAccountContext: AnyObject {
|
||||
|
||||
func makeStickerPackScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], loadedStickerPacks: [LoadedStickerPack], parentNavigationController: NavigationController?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?) -> ViewController
|
||||
|
||||
func makeMediaPickerScreen(context: AccountContext, completion: @escaping (Any) -> Void) -> ViewController
|
||||
func makeMediaPickerScreen(context: AccountContext, hasSearch: Bool, completion: @escaping (Any) -> Void) -> ViewController
|
||||
|
||||
func makeStoryMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController
|
||||
|
||||
|
@ -98,6 +98,9 @@ swift_library(
|
||||
"//submodules/StickerPackPreviewUI:StickerPackPreviewUI",
|
||||
"//submodules/TelegramUI/Components/LottieComponent",
|
||||
"//submodules/ImageTransparency",
|
||||
"//submodules/GalleryUI",
|
||||
"//submodules/MediaPlayer:UniversalMediaPlayer",
|
||||
"//submodules/TelegramUniversalVideoContent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -21,30 +21,13 @@ public final class DrawingMediaEntityView: DrawingEntityView, DrawingEntityMedia
|
||||
if let previewView = self.previewView {
|
||||
previewView.isUserInteractionEnabled = false
|
||||
previewView.layer.allowsEdgeAntialiasing = true
|
||||
if self.additionalView == nil {
|
||||
self.addSubview(previewView)
|
||||
}
|
||||
self.addSubview(previewView)
|
||||
} else {
|
||||
oldValue?.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var additionalView: DrawingStickerEntityView.VideoView? {
|
||||
didSet {
|
||||
if let additionalView = self.additionalView {
|
||||
self.addSubview(additionalView)
|
||||
} else {
|
||||
if let previous = oldValue, previous.superview === self {
|
||||
previous.removeFromSuperview()
|
||||
}
|
||||
if let previewView = self.previewView {
|
||||
self.addSubview(previewView)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private let snapTool = DrawingEntitySnapTool()
|
||||
|
||||
init(context: AccountContext, entity: DrawingMediaEntity) {
|
||||
@ -113,14 +96,8 @@ public final class DrawingMediaEntityView: DrawingEntityView, DrawingEntityMedia
|
||||
if self.previewView?.superview === self {
|
||||
self.previewView?.frame = CGRect(origin: .zero, size: size)
|
||||
}
|
||||
if let additionalView = self.additionalView, additionalView.superview === self {
|
||||
additionalView.frame = CGRect(origin: .zero, size: size)
|
||||
}
|
||||
self.update(animated: false)
|
||||
}
|
||||
if let additionalView = self.additionalView, additionalView.superview === self {
|
||||
self.additionalView?.frame = self.bounds
|
||||
}
|
||||
}
|
||||
|
||||
public var updated: (() -> Void)?
|
||||
|
@ -759,7 +759,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
emojiItems,
|
||||
stickerItems
|
||||
) |> map { emoji, stickers -> StickerPickerInputData in
|
||||
return StickerPickerInputData(emoji: emoji, stickers: stickers, masks: nil)
|
||||
return StickerPickerInputData(emoji: emoji, stickers: stickers, gifs: nil)
|
||||
}
|
||||
|
||||
stickerPickerInputData.set(signal)
|
||||
@ -3057,9 +3057,7 @@ public final class DrawingToolsInteraction {
|
||||
|
||||
var isVideo = false
|
||||
if let entity = entityView.entity as? DrawingStickerEntity {
|
||||
if case .video = entity.content {
|
||||
isVideo = true
|
||||
} else if case .dualVideoReference = entity.content {
|
||||
if case .dualVideoReference = entity.content {
|
||||
isVideo = true
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import AVFoundation
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
@ -9,31 +10,10 @@ import TelegramAnimatedStickerNode
|
||||
import StickerResources
|
||||
import AccountContext
|
||||
import MediaEditor
|
||||
import UniversalMediaPlayer
|
||||
import TelegramUniversalVideoContent
|
||||
|
||||
public final class DrawingStickerEntityView: DrawingEntityView {
|
||||
public class VideoView: UIView {
|
||||
init(player: AVPlayer) {
|
||||
super.init(frame: .zero)
|
||||
|
||||
self.videoLayer.player = player
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
var videoLayer: AVPlayerLayer {
|
||||
guard let layer = self.layer as? AVPlayerLayer else {
|
||||
fatalError()
|
||||
}
|
||||
return layer
|
||||
}
|
||||
|
||||
public override class var layerClass: AnyClass {
|
||||
return AVPlayerLayer.self
|
||||
}
|
||||
}
|
||||
|
||||
public final class DrawingStickerEntityView: DrawingEntityView {
|
||||
private var stickerEntity: DrawingStickerEntity {
|
||||
return self.entity as! DrawingStickerEntity
|
||||
}
|
||||
@ -46,27 +26,8 @@ public final class DrawingStickerEntityView: DrawingEntityView {
|
||||
|
||||
private let imageNode: TransformImageNode
|
||||
private var animationNode: AnimatedStickerNode?
|
||||
|
||||
private var videoContainerView: UIView?
|
||||
private var videoPlayer: AVPlayer?
|
||||
public var videoView: VideoView?
|
||||
private var videoImageView: UIImageView?
|
||||
|
||||
public var mainView: MediaEditorPreviewView? {
|
||||
didSet {
|
||||
if let mainView = self.mainView {
|
||||
self.videoContainerView?.addSubview(mainView)
|
||||
} else {
|
||||
if let previous = oldValue, previous.superview === self {
|
||||
previous.removeFromSuperview()
|
||||
}
|
||||
if let videoView = self.videoView {
|
||||
self.videoContainerView?.addSubview(videoView)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var videoNode: UniversalVideoNode?
|
||||
|
||||
private var didSetUpAnimationNode = false
|
||||
private let stickerFetchedDisposable = MetaDisposable()
|
||||
private let cachedDisposable = MetaDisposable()
|
||||
@ -109,9 +70,9 @@ public final class DrawingStickerEntityView: DrawingEntityView {
|
||||
}
|
||||
}
|
||||
|
||||
private var video: String? {
|
||||
if case let .video(path, _, _) = self.stickerEntity.content {
|
||||
return path
|
||||
private var video: TelegramMediaFile? {
|
||||
if case let .video(file) = self.stickerEntity.content {
|
||||
return file
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
@ -123,13 +84,8 @@ public final class DrawingStickerEntityView: DrawingEntityView {
|
||||
return file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0)
|
||||
case let .image(image, _):
|
||||
return image.size
|
||||
case let .video(_, image, _):
|
||||
if let image {
|
||||
let minSide = min(image.size.width, image.size.height)
|
||||
return CGSize(width: minSide, height: minSide)
|
||||
} else {
|
||||
return CGSize(width: 512.0, height: 512.0)
|
||||
}
|
||||
case let .video(file):
|
||||
return file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0)
|
||||
case .dualVideoReference:
|
||||
return CGSize(width: 512.0, height: 512.0)
|
||||
}
|
||||
@ -220,30 +176,46 @@ public final class DrawingStickerEntityView: DrawingEntityView {
|
||||
return context
|
||||
}), attemptSynchronously: synchronous)
|
||||
self.setNeedsLayout()
|
||||
} else if case let .video(videoPath, image, _) = self.stickerEntity.content {
|
||||
let url = URL(fileURLWithPath: videoPath)
|
||||
let asset = AVURLAsset(url: url)
|
||||
let playerItem = AVPlayerItem(asset: asset)
|
||||
let player = AVPlayer(playerItem: playerItem)
|
||||
player.automaticallyWaitsToMinimizeStalling = false
|
||||
|
||||
let videoContainerView = UIView()
|
||||
videoContainerView.clipsToBounds = true
|
||||
|
||||
let videoView = VideoView(player: player)
|
||||
videoContainerView.addSubview(videoView)
|
||||
|
||||
self.addSubview(videoContainerView)
|
||||
|
||||
self.videoPlayer = player
|
||||
self.videoContainerView = videoContainerView
|
||||
self.videoView = videoView
|
||||
|
||||
let imageView = UIImageView(image: image)
|
||||
imageView.clipsToBounds = true
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
videoContainerView.addSubview(imageView)
|
||||
self.videoImageView = imageView
|
||||
} else if case let .video(file) = self.stickerEntity.content {
|
||||
let videoNode = UniversalVideoNode(
|
||||
postbox: self.context.account.postbox,
|
||||
audioSession: self.context.sharedContext.mediaManager.audioSession,
|
||||
manager: self.context.sharedContext.mediaManager.universalVideoManager,
|
||||
decoration: StickerVideoDecoration(),
|
||||
content: NativeVideoContent(
|
||||
id: .contextResult(0, "\(UInt64.random(in: 0 ... UInt64.max))"),
|
||||
userLocation: .other,
|
||||
fileReference: .standalone(media: file),
|
||||
imageReference: nil,
|
||||
streamVideo: .story,
|
||||
loopVideo: true,
|
||||
enableSound: false,
|
||||
soundMuted: true,
|
||||
beginWithAmbientSound: false,
|
||||
mixWithOthers: true,
|
||||
useLargeThumbnail: false,
|
||||
autoFetchFullSizeThumbnail: false,
|
||||
tempFilePath: nil,
|
||||
captureProtected: false,
|
||||
hintDimensions: file.dimensions?.cgSize,
|
||||
storeAfterDownload: nil,
|
||||
displayImage: false,
|
||||
hasSentFramesToDisplay: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.videoNode?.isHidden = false
|
||||
}
|
||||
),
|
||||
priority: .gallery
|
||||
)
|
||||
videoNode.canAttachContent = true
|
||||
videoNode.isUserInteractionEnabled = false
|
||||
videoNode.cornerRadius = floor(CGFloat(file.dimensions?.width ?? 512) * 0.03)
|
||||
videoNode.clipsToBounds = true
|
||||
self.addSubnode(videoNode)
|
||||
self.videoNode = videoNode
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
@ -251,27 +223,14 @@ public final class DrawingStickerEntityView: DrawingEntityView {
|
||||
self.isVisible = true
|
||||
self.applyVisibility()
|
||||
|
||||
if let player = self.videoPlayer {
|
||||
player.play()
|
||||
|
||||
if let videoImageView = self.videoImageView {
|
||||
self.videoImageView = nil
|
||||
Queue.mainQueue().after(0.1) {
|
||||
videoImageView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false, completion: { [weak videoImageView] _ in
|
||||
videoImageView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
self.videoNode?.play()
|
||||
}
|
||||
|
||||
public override func pause() {
|
||||
self.isVisible = false
|
||||
self.applyVisibility()
|
||||
|
||||
if let player = self.videoPlayer {
|
||||
player.pause()
|
||||
}
|
||||
self.videoNode?.pause()
|
||||
}
|
||||
|
||||
public override func seek(to timestamp: Double) {
|
||||
@ -279,9 +238,7 @@ public final class DrawingStickerEntityView: DrawingEntityView {
|
||||
self.isPlaying = false
|
||||
self.animationNode?.seekTo(.timestamp(timestamp))
|
||||
|
||||
if let player = self.videoPlayer {
|
||||
player.seek(to: CMTime(seconds: timestamp, preferredTimescale: CMTimeScale(60.0)), toleranceBefore: .zero, toleranceAfter: .zero, completionHandler: { _ in })
|
||||
}
|
||||
self.videoNode?.seek(timestamp)
|
||||
}
|
||||
|
||||
override func resetToStart() {
|
||||
@ -343,16 +300,10 @@ public final class DrawingStickerEntityView: DrawingEntityView {
|
||||
}
|
||||
}
|
||||
|
||||
if let videoView = self.videoView {
|
||||
let videoSize = CGSize(width: imageFrame.width, height: imageFrame.width / 9.0 * 16.0)
|
||||
videoView.frame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((imageFrame.height - videoSize.height) / 2.0)), size: videoSize)
|
||||
}
|
||||
if let videoContainerView = self.videoContainerView {
|
||||
videoContainerView.layer.cornerRadius = imageFrame.width / 2.0
|
||||
videoContainerView.frame = imageFrame
|
||||
}
|
||||
if let videoImageView = self.videoImageView {
|
||||
videoImageView.frame = CGRect(origin: .zero, size: imageFrame.size)
|
||||
if let videoNode = self.videoNode {
|
||||
let videoSize = self.dimensions.aspectFitted(boundingSize)
|
||||
videoNode.frame = CGRect(origin: CGPoint(x: floor((size.width - videoSize.width) * 0.5), y: floor((size.height - videoSize.height) * 0.5)), size: videoSize)
|
||||
videoNode.updateLayout(size: videoSize, transition: .immediate)
|
||||
}
|
||||
|
||||
self.update(animated: false)
|
||||
@ -386,18 +337,18 @@ public final class DrawingStickerEntityView: DrawingEntityView {
|
||||
UIView.animate(withDuration: 0.25, animations: {
|
||||
self.imageNode.transform = animationTargetTransform
|
||||
self.animationNode?.transform = animationTargetTransform
|
||||
self.videoContainerView?.layer.transform = animationTargetTransform
|
||||
self.videoNode?.transform = animationTargetTransform
|
||||
}, completion: { finished in
|
||||
self.imageNode.transform = staticTransform
|
||||
self.animationNode?.transform = staticTransform
|
||||
self.videoContainerView?.layer.transform = staticTransform
|
||||
self.videoNode?.transform = staticTransform
|
||||
})
|
||||
} else {
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
self.imageNode.transform = staticTransform
|
||||
self.animationNode?.transform = staticTransform
|
||||
self.videoContainerView?.layer.transform = staticTransform
|
||||
self.videoNode?.transform = staticTransform
|
||||
CATransaction.commit()
|
||||
}
|
||||
|
||||
@ -713,3 +664,117 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView {
|
||||
self.rightHandle.position = CGPoint(x: self.bounds.maxX - actualInset, y: self.bounds.midY)
|
||||
}
|
||||
}
|
||||
|
||||
private final class StickerVideoDecoration: UniversalVideoDecoration {
|
||||
public let backgroundNode: ASDisplayNode? = nil
|
||||
public let contentContainerNode: ASDisplayNode
|
||||
public let foregroundNode: ASDisplayNode? = nil
|
||||
|
||||
private var contentNode: (ASDisplayNode & UniversalVideoContentNode)?
|
||||
|
||||
private var validLayoutSize: CGSize?
|
||||
|
||||
public init() {
|
||||
self.contentContainerNode = ASDisplayNode()
|
||||
}
|
||||
|
||||
public func updateContentNode(_ contentNode: (UniversalVideoContentNode & ASDisplayNode)?) {
|
||||
if self.contentNode !== contentNode {
|
||||
let previous = self.contentNode
|
||||
self.contentNode = contentNode
|
||||
|
||||
if let previous = previous {
|
||||
if previous.supernode === self.contentContainerNode {
|
||||
previous.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
|
||||
if let contentNode = contentNode {
|
||||
if contentNode.supernode !== self.contentContainerNode {
|
||||
self.contentContainerNode.addSubnode(contentNode)
|
||||
if let validLayoutSize = self.validLayoutSize {
|
||||
contentNode.frame = CGRect(origin: CGPoint(), size: validLayoutSize)
|
||||
contentNode.updateLayout(size: validLayoutSize, transition: .immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func updateCorners(_ corners: ImageCorners) {
|
||||
self.contentContainerNode.clipsToBounds = true
|
||||
if isRoundEqualCorners(corners) {
|
||||
self.contentContainerNode.cornerRadius = corners.topLeft.radius
|
||||
} else {
|
||||
let boundingSize: CGSize = CGSize(width: max(corners.topLeft.radius, corners.bottomLeft.radius) + max(corners.topRight.radius, corners.bottomRight.radius), height: max(corners.topLeft.radius, corners.topRight.radius) + max(corners.bottomLeft.radius, corners.bottomRight.radius))
|
||||
let size: CGSize = CGSize(width: boundingSize.width + corners.extendedEdges.left + corners.extendedEdges.right, height: boundingSize.height + corners.extendedEdges.top + corners.extendedEdges.bottom)
|
||||
let arguments = TransformImageArguments(corners: corners, imageSize: size, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets())
|
||||
guard let context = DrawingContext(size: size, clear: true) else {
|
||||
return
|
||||
}
|
||||
context.withContext { ctx in
|
||||
ctx.setFillColor(UIColor.black.cgColor)
|
||||
ctx.fill(arguments.drawingRect)
|
||||
}
|
||||
addCorners(context, arguments: arguments)
|
||||
|
||||
if let maskImage = context.generateImage() {
|
||||
let mask = CALayer()
|
||||
mask.contents = maskImage.cgImage
|
||||
mask.contentsScale = maskImage.scale
|
||||
mask.contentsCenter = CGRect(x: max(corners.topLeft.radius, corners.bottomLeft.radius) / maskImage.size.width, y: max(corners.topLeft.radius, corners.topRight.radius) / maskImage.size.height, width: (maskImage.size.width - max(corners.topLeft.radius, corners.bottomLeft.radius) - max(corners.topRight.radius, corners.bottomRight.radius)) / maskImage.size.width, height: (maskImage.size.height - max(corners.topLeft.radius, corners.topRight.radius) - max(corners.bottomLeft.radius, corners.bottomRight.radius)) / maskImage.size.height)
|
||||
|
||||
self.contentContainerNode.layer.mask = mask
|
||||
self.contentContainerNode.layer.mask?.frame = self.contentContainerNode.bounds
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func updateClippingFrame(_ frame: CGRect, completion: (() -> Void)?) {
|
||||
self.contentContainerNode.layer.animate(from: NSValue(cgRect: self.contentContainerNode.bounds), to: NSValue(cgRect: frame), keyPath: "bounds", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
})
|
||||
|
||||
if let maskLayer = self.contentContainerNode.layer.mask {
|
||||
maskLayer.animate(from: NSValue(cgRect: self.contentContainerNode.bounds), to: NSValue(cgRect: frame), keyPath: "bounds", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
})
|
||||
|
||||
maskLayer.animate(from: NSValue(cgPoint: maskLayer.position), to: NSValue(cgPoint: frame.center), keyPath: "position", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
})
|
||||
}
|
||||
|
||||
if let contentNode = self.contentNode {
|
||||
contentNode.layer.animate(from: NSValue(cgPoint: contentNode.layer.position), to: NSValue(cgPoint: frame.center), keyPath: "position", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
completion?()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public func updateContentNodeSnapshot(_ snapshot: UIView?) {
|
||||
}
|
||||
|
||||
public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayoutSize = size
|
||||
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
if let backgroundNode = self.backgroundNode {
|
||||
transition.updateFrame(node: backgroundNode, frame: bounds)
|
||||
}
|
||||
if let foregroundNode = self.foregroundNode {
|
||||
transition.updateFrame(node: foregroundNode, frame: bounds)
|
||||
}
|
||||
transition.updateFrame(node: self.contentContainerNode, frame: bounds)
|
||||
if let maskLayer = self.contentContainerNode.layer.mask {
|
||||
transition.updateFrame(layer: maskLayer, frame: bounds)
|
||||
}
|
||||
if let contentNode = self.contentNode {
|
||||
transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size))
|
||||
contentNode.updateLayout(size: size, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
public func setStatus(_ status: Signal<MediaPlayerStatus?, NoError>) {
|
||||
}
|
||||
|
||||
public func tap() {
|
||||
}
|
||||
}
|
||||
|
@ -18,20 +18,23 @@ import ContextUI
|
||||
import ChatPresentationInterfaceState
|
||||
import MediaEditor
|
||||
import StickerPackPreviewUI
|
||||
import EntityKeyboardGifContent
|
||||
import GalleryUI
|
||||
import UndoUI
|
||||
|
||||
public struct StickerPickerInputData: Equatable {
|
||||
var emoji: EmojiPagerContentComponent
|
||||
var stickers: EmojiPagerContentComponent?
|
||||
var masks: EmojiPagerContentComponent?
|
||||
var gifs: GifPagerContentComponent?
|
||||
|
||||
public init(
|
||||
emoji: EmojiPagerContentComponent,
|
||||
stickers: EmojiPagerContentComponent?,
|
||||
masks: EmojiPagerContentComponent?
|
||||
gifs: GifPagerContentComponent?
|
||||
) {
|
||||
self.emoji = emoji
|
||||
self.stickers = stickers
|
||||
self.masks = masks
|
||||
self.gifs = gifs
|
||||
}
|
||||
}
|
||||
|
||||
@ -95,9 +98,18 @@ private final class StickerSelectionComponent: Component {
|
||||
return true
|
||||
}
|
||||
|
||||
final class KeyboardClippingView: UIView {
|
||||
var hitEdgeInsets: UIEdgeInsets = .zero
|
||||
|
||||
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||
let bounds = self.bounds.inset(by: self.hitEdgeInsets)
|
||||
return bounds.contains(point)
|
||||
}
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
fileprivate let keyboardView: ComponentView<Empty>
|
||||
private let keyboardClippingView: UIView
|
||||
private let keyboardClippingView: KeyboardClippingView
|
||||
private let panelHostView: PagerExternalTopPanelContainer
|
||||
private let panelBackgroundView: BlurredBackgroundView
|
||||
private let panelSeparatorView: UIView
|
||||
@ -107,18 +119,20 @@ private final class StickerSelectionComponent: Component {
|
||||
|
||||
private var interaction: ChatEntityKeyboardInputNode.Interaction?
|
||||
private var inputNodeInteraction: ChatMediaInputNodeInteraction?
|
||||
private let trendingGifsPromise = Promise<ChatMediaInputGifPaneTrendingState?>(nil)
|
||||
|
||||
private var searchVisible = false
|
||||
private var forceUpdate = false
|
||||
|
||||
private var ignoreNextZeroScrollingOffset = false
|
||||
private var topPanelScrollingOffset: CGFloat = 0.0
|
||||
private var keyboardContentId: AnyHashable?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.keyboardView = ComponentView<Empty>()
|
||||
self.keyboardClippingView = UIView()
|
||||
self.keyboardClippingView = KeyboardClippingView()
|
||||
self.panelHostView = PagerExternalTopPanelContainer()
|
||||
self.panelBackgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
|
||||
self.panelBackgroundView.isUserInteractionEnabled = false
|
||||
self.panelSeparatorView = UIView()
|
||||
|
||||
super.init(frame: frame)
|
||||
@ -210,6 +224,12 @@ private final class StickerSelectionComponent: Component {
|
||||
deinit {
|
||||
}
|
||||
|
||||
func scrolledToItemGroup() {
|
||||
self.topPanelScrollingOffset = 30.0
|
||||
self.ignoreNextZeroScrollingOffset = true
|
||||
self.state?.updated(transition: .easeInOut(duration: 0.2))
|
||||
}
|
||||
|
||||
func update(component: StickerSelectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
self.backgroundColor = component.backgroundColor
|
||||
let panelBackgroundColor = component.backgroundColor.withMultipliedAlpha(0.85)
|
||||
@ -236,7 +256,6 @@ private final class StickerSelectionComponent: Component {
|
||||
}
|
||||
)
|
||||
|
||||
let trendingGifsPromise = self.trendingGifsPromise
|
||||
let keyboardSize = self.keyboardView.update(
|
||||
transition: transition.withUserData(EmojiPagerContentComponent.SynchronousLoadBehavior(isDisabled: true)),
|
||||
component: AnyComponent(EntityKeyboardComponent(
|
||||
@ -247,9 +266,9 @@ private final class StickerSelectionComponent: Component {
|
||||
topPanelInsets: UIEdgeInsets(top: 0.0, left: 4.0, bottom: 0.0, right: 4.0),
|
||||
emojiContent: component.content.emoji,
|
||||
stickerContent: component.content.stickers,
|
||||
maskContent: component.content.masks,
|
||||
gifContent: nil,
|
||||
hasRecentGifs: false,
|
||||
maskContent: nil,
|
||||
gifContent: component.content.gifs,
|
||||
hasRecentGifs: true,
|
||||
availableGifSearchEmojies: [],
|
||||
defaultToEmojiTab: defaultToEmoji,
|
||||
externalTopPanelContainer: self.panelHostView,
|
||||
@ -259,7 +278,11 @@ private final class StickerSelectionComponent: Component {
|
||||
},
|
||||
topPanelScrollingOffset: { [weak self] offset, transition in
|
||||
if let self {
|
||||
self.topPanelScrollingOffset = offset
|
||||
if self.ignoreNextZeroScrollingOffset && offset == 0.0 {
|
||||
self.ignoreNextZeroScrollingOffset = false
|
||||
} else {
|
||||
self.topPanelScrollingOffset = offset
|
||||
}
|
||||
}
|
||||
},
|
||||
hideInputUpdated: { [weak self] _, searchVisible, transition in
|
||||
@ -297,14 +320,19 @@ private final class StickerSelectionComponent: Component {
|
||||
inputNodeInteraction: inputNodeInteraction,
|
||||
mode: mappedMode,
|
||||
stickerActionTitle: presentationData.strings.StickerPack_AddSticker,
|
||||
trendingGifsPromise: trendingGifsPromise,
|
||||
trendingGifsPromise: Promise(nil),
|
||||
cancel: {
|
||||
},
|
||||
peekBehavior: stickerPeekBehavior
|
||||
)
|
||||
return searchContainerNode
|
||||
},
|
||||
contentIdUpdated: { _ in },
|
||||
contentIdUpdated: { [weak self] id in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.keyboardContentId = id
|
||||
},
|
||||
deviceMetrics: component.deviceMetrics,
|
||||
hiddenInputHeight: 0.0,
|
||||
inputHeight: 0.0,
|
||||
@ -330,6 +358,7 @@ private final class StickerSelectionComponent: Component {
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.keyboardClippingView, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight), size: CGSize(width: availableSize.width, height: availableSize.height - topPanelHeight)))
|
||||
self.keyboardClippingView.hitEdgeInsets = UIEdgeInsets(top: -topPanelHeight, left: 0.0, bottom: 0.0, right: 0.0)
|
||||
|
||||
transition.setFrame(view: keyboardComponentView, frame: CGRect(origin: CGPoint(x: 0.0, y: -topPanelHeight), size: keyboardSize))
|
||||
transition.setFrame(view: self.panelHostView, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight - 34.0), size: CGSize(width: keyboardSize.width, height: 0.0)))
|
||||
@ -338,7 +367,7 @@ private final class StickerSelectionComponent: Component {
|
||||
self.panelBackgroundView.update(size: self.panelBackgroundView.bounds.size, transition: transition.containedViewLayoutTransition)
|
||||
|
||||
let topPanelAlpha: CGFloat
|
||||
if self.searchVisible {
|
||||
if self.searchVisible || self.keyboardContentId == AnyHashable("gifs") {
|
||||
topPanelAlpha = 0.0
|
||||
} else {
|
||||
topPanelAlpha = max(0.0, min(1.0, (self.topPanelScrollingOffset / 20.0)))
|
||||
@ -386,6 +415,8 @@ public class StickerPickerScreen: ViewController {
|
||||
|
||||
private var content: StickerPickerInputData?
|
||||
private let contentDisposable = MetaDisposable()
|
||||
private var hasRecentGifsDisposable: Disposable?
|
||||
private let trendingGifsPromise = Promise<ChatMediaInputGifPaneTrendingState?>(nil)
|
||||
private var scheduledEmojiContentAnimationHint: EmojiPagerContentComponent.ContentAnimation?
|
||||
|
||||
private(set) var isExpanded = false
|
||||
@ -397,6 +428,24 @@ public class StickerPickerScreen: ViewController {
|
||||
|
||||
fileprivate var temporaryDismiss = false
|
||||
|
||||
private var gifMode: GifPagerContentComponent.Subject? {
|
||||
didSet {
|
||||
if let gifMode = self.gifMode, gifMode != oldValue {
|
||||
self.reloadGifContext()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var gifContext: GifContext? {
|
||||
didSet {
|
||||
if let gifContext = self.gifContext {
|
||||
self.gifComponent.set(gifContext.component)
|
||||
}
|
||||
}
|
||||
}
|
||||
private let gifComponent = Promise<EntityKeyboardGifContent>()
|
||||
private var gifInputInteraction: GifPagerContentComponent.InputInteraction?
|
||||
|
||||
private struct EmojiSearchResult {
|
||||
var groups: [EmojiPagerContentComponent.ItemGroup]
|
||||
var id: AnyHashable
|
||||
@ -431,7 +480,7 @@ public class StickerPickerScreen: ViewController {
|
||||
}
|
||||
|
||||
private var storyStickersContentView: StoryStickersContentView?
|
||||
|
||||
|
||||
init(context: AccountContext, controller: StickerPickerScreen, theme: PresentationTheme) {
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.controller = controller
|
||||
@ -464,15 +513,18 @@ public class StickerPickerScreen: ViewController {
|
||||
let data = combineLatest(
|
||||
queue: Queue.mainQueue(),
|
||||
controller.inputData,
|
||||
.single(nil) |> then(self.gifComponent.get() |> map(Optional.init)),
|
||||
self.stickerSearchState.get(),
|
||||
self.emojiSearchState.get()
|
||||
)
|
||||
|
||||
self.contentDisposable.set(data.start(next: { [weak self] inputData, stickerSearchState, emojiSearchState in
|
||||
self.contentDisposable.set(data.start(next: { [weak self] inputData, gifData, stickerSearchState, emojiSearchState in
|
||||
if let strongSelf = self {
|
||||
let presentationData = strongSelf.presentationData
|
||||
var inputData = inputData
|
||||
|
||||
inputData.gifs = gifData?.component
|
||||
|
||||
let emoji = inputData.emoji
|
||||
if let emojiSearchResult = emojiSearchState.result {
|
||||
var emptySearchResults: EmojiPagerContentComponent.EmptySearchResults?
|
||||
@ -509,12 +561,183 @@ public class StickerPickerScreen: ViewController {
|
||||
strongSelf.updateContent(inputData)
|
||||
}
|
||||
}))
|
||||
|
||||
if controller.hasGifs {
|
||||
let hasRecentGifs = context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs))
|
||||
|> map { savedGifs -> Bool in
|
||||
return !savedGifs.isEmpty
|
||||
}
|
||||
|
||||
self.hasRecentGifsDisposable = (hasRecentGifs
|
||||
|> deliverOnMainQueue).start(next: { [weak self] hasRecentGifs in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
if let gifMode = strongSelf.gifMode {
|
||||
if !hasRecentGifs, case .recent = gifMode {
|
||||
strongSelf.gifMode = .trending
|
||||
}
|
||||
} else {
|
||||
strongSelf.gifMode = hasRecentGifs ? .recent : .trending
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
self.gifInputInteraction = GifPagerContentComponent.InputInteraction(
|
||||
performItemAction: { [weak self] item, view, rect in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.controller?.completion(.video(item.file.media))
|
||||
self.controller?.dismiss(animated: true)
|
||||
},
|
||||
openGifContextMenu: { [weak self] item, sourceView, sourceRect, gesture, isSaved in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.openGifContextMenu(file: item.file, contextResult: item.contextResult, sourceView: sourceView, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved)
|
||||
},
|
||||
loadMore: { [weak self] token in
|
||||
guard let strongSelf = self, let gifContext = strongSelf.gifContext else {
|
||||
return
|
||||
}
|
||||
gifContext.loadMore(token: token)
|
||||
},
|
||||
openSearch: { [weak self] in
|
||||
if let self, let componentView = self.hostView.componentView as? StickerSelectionComponent.View {
|
||||
if let pagerView = componentView.keyboardView.view as? EntityKeyboardComponent.View {
|
||||
pagerView.openSearch()
|
||||
}
|
||||
self.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring))
|
||||
}
|
||||
},
|
||||
updateSearchQuery: { [weak self] query in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let query {
|
||||
self.gifMode = .emojiSearch(query)
|
||||
} else {
|
||||
self.gifMode = .recent
|
||||
}
|
||||
},
|
||||
hideBackground: true,
|
||||
hasSearch: true
|
||||
)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.contentDisposable.dispose()
|
||||
self.emojiSearchDisposable.dispose()
|
||||
self.stickerSearchDisposable.dispose()
|
||||
self.hasRecentGifsDisposable?.dispose()
|
||||
}
|
||||
|
||||
private func reloadGifContext() {
|
||||
if let gifInputInteraction = self.gifInputInteraction, let gifMode = self.gifMode, let context = self.controller?.context {
|
||||
self.gifContext = GifContext(context: context, subject: gifMode, gifInputInteraction: gifInputInteraction, trendingGifs: self.trendingGifsPromise.get())
|
||||
}
|
||||
}
|
||||
|
||||
private func openGifContextMenu(file: FileMediaReference, contextResult: (ChatContextResultCollection, ChatContextResult)?, sourceView: UIView, sourceRect: CGRect, gesture: ContextGesture, isSaved: Bool) {
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
let context = controller.context
|
||||
|
||||
let canSaveGif: Bool
|
||||
if file.media.fileId.namespace == Namespaces.Media.CloudFile {
|
||||
canSaveGif = true
|
||||
} else {
|
||||
canSaveGif = false
|
||||
}
|
||||
|
||||
let _ = (context.engine.stickers.isGifSaved(id: file.media.fileId)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] isGifSaved in
|
||||
var isGifSaved = isGifSaved
|
||||
if !canSaveGif {
|
||||
isGifSaved = false
|
||||
}
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: PeerId(0), namespace: Namespaces.Message.Local, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [file.media], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
|
||||
|
||||
let gallery = GalleryController(context: context, source: .standaloneMessage(message), streamSingleVideo: true, replaceRootController: { _, _ in
|
||||
}, baseNavigationController: nil)
|
||||
gallery.setHintWillBePresentedInPreviewingContext(true)
|
||||
|
||||
var items: [ContextMenuItem] = []
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaPicker_Send, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { [weak self] _, f in
|
||||
f(.default)
|
||||
if let self {
|
||||
if isSaved {
|
||||
self.controller?.completion(.video(file.media))
|
||||
self.controller?.dismiss(animated: true)
|
||||
} else {
|
||||
|
||||
}
|
||||
// if isSaved {
|
||||
// let _ = self.interaction?.sendGif(file, sourceView, sourceRect, false, false)
|
||||
// } else if let (collection, result) = contextResult {
|
||||
// let _ = self.interaction?.sendBotContextResultAsGif(collection, result, sourceView, sourceRect, false, false)
|
||||
// }
|
||||
}
|
||||
})))
|
||||
|
||||
if isSaved || isGifSaved {
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ContextMenuDelete, textColor: .destructive, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor)
|
||||
}, action: { _, f in
|
||||
f(.dismissWithoutContent)
|
||||
|
||||
let _ = removeSavedGif(postbox: context.account.postbox, mediaId: file.media.fileId).start()
|
||||
})))
|
||||
} else if canSaveGif && !isGifSaved {
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Preview_SaveGif, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { [weak self] _, f in
|
||||
f(.dismissWithoutContent)
|
||||
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let _ = (toggleGifSaved(account: context.account, fileReference: file, saved: true)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
guard let controller = self?.controller else {
|
||||
return
|
||||
}
|
||||
switch result {
|
||||
case .generic:
|
||||
controller.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: nil, text: presentationData.strings.Gallery_GifSaved, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
|
||||
case let .limitExceeded(limit, premiumLimit):
|
||||
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
|
||||
let text: String
|
||||
if limit == premiumLimit || premiumConfiguration.isPremiumDisabled {
|
||||
text = presentationData.strings.Premium_MaxSavedGifsFinalText
|
||||
} else {
|
||||
text = presentationData.strings.Premium_MaxSavedGifsText("\(premiumLimit)").string
|
||||
}
|
||||
controller.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: presentationData.strings.Premium_MaxSavedGifsTitle("\(limit)").string, text: text, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { [weak controller] action in
|
||||
if case .info = action {
|
||||
let premiumController = context.sharedContext.makePremiumIntroController(context: context, source: .savedGifs, forceDark: true)
|
||||
controller?.push(premiumController)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}), in: .window(.root))
|
||||
}
|
||||
})
|
||||
})))
|
||||
}
|
||||
|
||||
let contextController = ContextController(account: context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceView: sourceView, sourceRect: sourceRect)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
|
||||
controller.presentInGlobalOverlay(contextController)
|
||||
})
|
||||
}
|
||||
|
||||
func updateContent(_ content: StickerPickerInputData) {
|
||||
@ -922,6 +1145,9 @@ public class StickerPickerScreen: ViewController {
|
||||
}
|
||||
},
|
||||
updateScrollingToItemGroup: { [weak self] in
|
||||
if let self, let componentView = self.hostView.componentView as? StickerSelectionComponent.View {
|
||||
componentView.scrolledToItemGroup()
|
||||
}
|
||||
self?.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring))
|
||||
},
|
||||
onScroll: {},
|
||||
@ -1187,6 +1413,9 @@ public class StickerPickerScreen: ViewController {
|
||||
}
|
||||
},
|
||||
updateScrollingToItemGroup: { [weak self] in
|
||||
if let self, let componentView = self.hostView.componentView as? StickerSelectionComponent.View {
|
||||
componentView.scrolledToItemGroup()
|
||||
}
|
||||
self?.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring))
|
||||
},
|
||||
onScroll: {},
|
||||
@ -1653,6 +1882,7 @@ public class StickerPickerScreen: ViewController {
|
||||
private let theme: PresentationTheme
|
||||
private let inputData: Signal<StickerPickerInputData, NoError>
|
||||
fileprivate let defaultToEmoji: Bool
|
||||
let hasGifs: Bool
|
||||
|
||||
private var currentLayout: ContainerViewLayout?
|
||||
|
||||
@ -1664,11 +1894,12 @@ public class StickerPickerScreen: ViewController {
|
||||
public var presentGallery: () -> Void = { }
|
||||
public var presentLocationPicker: () -> Void = { }
|
||||
|
||||
public init(context: AccountContext, inputData: Signal<StickerPickerInputData, NoError>, defaultToEmoji: Bool = false) {
|
||||
public init(context: AccountContext, inputData: Signal<StickerPickerInputData, NoError>, defaultToEmoji: Bool = false, hasGifs: Bool = false) {
|
||||
self.context = context
|
||||
self.theme = defaultDarkColorPresentationTheme
|
||||
self.inputData = inputData
|
||||
self.defaultToEmoji = defaultToEmoji
|
||||
self.hasGifs = hasGifs
|
||||
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
||||
@ -1809,3 +2040,37 @@ final class StoryStickersContentView: UIView, EmojiCustomContentView {
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
private final class ContextControllerContentSourceImpl: ContextControllerContentSource {
|
||||
let controller: ViewController
|
||||
weak var sourceView: UIView?
|
||||
let sourceRect: CGRect
|
||||
|
||||
let navigationController: NavigationController? = nil
|
||||
|
||||
let passthroughTouches: Bool = false
|
||||
|
||||
init(controller: ViewController, sourceView: UIView?, sourceRect: CGRect) {
|
||||
self.controller = controller
|
||||
self.sourceView = sourceView
|
||||
self.sourceRect = sourceRect
|
||||
}
|
||||
|
||||
func transitionInfo() -> ContextControllerTakeControllerInfo? {
|
||||
let sourceView = self.sourceView
|
||||
let sourceRect = self.sourceRect
|
||||
return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceView] in
|
||||
if let sourceView = sourceView {
|
||||
return (sourceView, sourceRect)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func animatedIn() {
|
||||
if let controller = self.controller as? GalleryController {
|
||||
controller.viewDidAppear(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1534,7 +1534,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
if collection == nil {
|
||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
|
||||
|
||||
if mode == .story {
|
||||
if mode == .story || mode == .addImage {
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: self.moreButtonNode)
|
||||
self.navigationItem.rightBarButtonItem?.action = #selector(self.rightButtonPressed)
|
||||
self.navigationItem.rightBarButtonItem?.target = self
|
||||
@ -2289,6 +2289,7 @@ public func wallpaperMediaPickerController(
|
||||
|
||||
public func mediaPickerController(
|
||||
context: AccountContext,
|
||||
hasSearch: Bool,
|
||||
completion: @escaping (Any) -> Void
|
||||
) -> ViewController {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkColorPresentationTheme)
|
||||
@ -2302,6 +2303,26 @@ public func mediaPickerController(
|
||||
completion(result)
|
||||
controller.dismiss(animated: true)
|
||||
}
|
||||
if hasSearch {
|
||||
mediaPickerController.presentWebSearch = { [weak mediaPickerController] groups, activateOnDisplay in
|
||||
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.SearchBots())
|
||||
|> deliverOnMainQueue).start(next: { configuration in
|
||||
let webSearchController = WebSearchController(
|
||||
context: context,
|
||||
updatedPresentationData: updatedPresentationData,
|
||||
peer: nil,
|
||||
chatLocation: nil,
|
||||
configuration: configuration,
|
||||
mode: .editor(completion: { [weak mediaPickerController] image in
|
||||
completion(image)
|
||||
mediaPickerController?.dismiss(animated: true)
|
||||
}),
|
||||
activateOnDisplay: activateOnDisplay
|
||||
)
|
||||
mediaPickerController?.present(webSearchController, in: .current)
|
||||
})
|
||||
}
|
||||
}
|
||||
present(mediaPickerController, mediaPickerController.mediaPickerContext)
|
||||
}
|
||||
controller.navigationPresentation = .flatModal
|
||||
|
@ -39,6 +39,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/ChatControllerInteraction:ChatControllerInteraction",
|
||||
"//submodules/FeaturedStickersScreen:FeaturedStickersScreen",
|
||||
"//submodules/StickerPackPreviewUI",
|
||||
"//submodules/TelegramUI/Components/EntityKeyboardGifContent:EntityKeyboardGifContent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -31,6 +31,7 @@ import ChatControllerInteraction
|
||||
import FeaturedStickersScreen
|
||||
import Pasteboard
|
||||
import StickerPackPreviewUI
|
||||
import EntityKeyboardGifContent
|
||||
|
||||
public final class EmptyInputView: UIView, UIInputViewAudioFeedback {
|
||||
public var enableInputClicksWhenVisible: Bool {
|
||||
@ -43,36 +44,6 @@ public struct ChatMediaInputPaneScrollState {
|
||||
let relativeChange: CGFloat
|
||||
}
|
||||
|
||||
public final class ChatMediaInputGifPaneTrendingState {
|
||||
public let files: [MultiplexedVideoNodeFile]
|
||||
public let nextOffset: String?
|
||||
|
||||
public init(files: [MultiplexedVideoNodeFile], nextOffset: String?) {
|
||||
self.files = files
|
||||
self.nextOffset = nextOffset
|
||||
}
|
||||
}
|
||||
|
||||
public final class EntityKeyboardGifContent: Equatable {
|
||||
public let hasRecentGifs: Bool
|
||||
public let component: GifPagerContentComponent
|
||||
|
||||
public init(hasRecentGifs: Bool, component: GifPagerContentComponent) {
|
||||
self.hasRecentGifs = hasRecentGifs
|
||||
self.component = component
|
||||
}
|
||||
|
||||
public static func ==(lhs: EntityKeyboardGifContent, rhs: EntityKeyboardGifContent) -> Bool {
|
||||
if lhs.hasRecentGifs != rhs.hasRecentGifs {
|
||||
return false
|
||||
}
|
||||
if lhs.component != rhs.component {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
public final class Interaction {
|
||||
let sendSticker: (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?, [ItemCollectionId]) -> Bool
|
||||
@ -407,244 +378,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
}
|
||||
|
||||
public var useExternalSearchContainer: Bool = false
|
||||
|
||||
private final class GifContext {
|
||||
private var componentValue: EntityKeyboardGifContent? {
|
||||
didSet {
|
||||
if let componentValue = self.componentValue {
|
||||
self.componentResult.set(.single(componentValue))
|
||||
}
|
||||
}
|
||||
}
|
||||
private let componentPromise = Promise<EntityKeyboardGifContent>()
|
||||
|
||||
private let componentResult = Promise<EntityKeyboardGifContent>()
|
||||
var component: Signal<EntityKeyboardGifContent, NoError> {
|
||||
return self.componentResult.get()
|
||||
}
|
||||
private var componentDisposable: Disposable?
|
||||
|
||||
private let context: AccountContext
|
||||
private let subject: GifPagerContentComponent.Subject
|
||||
private let gifInputInteraction: GifPagerContentComponent.InputInteraction
|
||||
|
||||
private var loadingMoreToken: String?
|
||||
|
||||
init(context: AccountContext, subject: GifPagerContentComponent.Subject, gifInputInteraction: GifPagerContentComponent.InputInteraction, trendingGifs: Signal<ChatMediaInputGifPaneTrendingState?, NoError>) {
|
||||
self.context = context
|
||||
self.subject = subject
|
||||
self.gifInputInteraction = gifInputInteraction
|
||||
|
||||
let hideBackground = gifInputInteraction.hideBackground
|
||||
|
||||
let hasRecentGifs = context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs))
|
||||
|> map { savedGifs -> Bool in
|
||||
return !savedGifs.isEmpty
|
||||
}
|
||||
|
||||
let searchCategories: Signal<EmojiSearchCategories?, NoError> = context.engine.stickers.emojiSearchCategories(kind: .emoji)
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let gifItems: Signal<EntityKeyboardGifContent, NoError>
|
||||
switch subject {
|
||||
case .recent:
|
||||
gifItems = combineLatest(
|
||||
context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs)),
|
||||
searchCategories
|
||||
)
|
||||
|> map { savedGifs, searchCategories -> EntityKeyboardGifContent in
|
||||
var items: [GifPagerContentComponent.Item] = []
|
||||
for gifItem in savedGifs {
|
||||
items.append(GifPagerContentComponent.Item(
|
||||
file: .savedGif(media: gifItem.contents.get(RecentMediaItem.self)!.media),
|
||||
contextResult: nil
|
||||
))
|
||||
}
|
||||
return EntityKeyboardGifContent(
|
||||
hasRecentGifs: true,
|
||||
component: GifPagerContentComponent(
|
||||
context: context,
|
||||
inputInteraction: gifInputInteraction,
|
||||
subject: subject,
|
||||
items: items,
|
||||
isLoading: false,
|
||||
loadMoreToken: nil,
|
||||
displaySearchWithPlaceholder: gifInputInteraction.hasSearch ? presentationData.strings.Common_Search : nil,
|
||||
searchCategories: searchCategories,
|
||||
searchInitiallyHidden: true,
|
||||
searchState: .empty(hasResults: false),
|
||||
hideBackground: hideBackground
|
||||
)
|
||||
)
|
||||
}
|
||||
case .trending:
|
||||
gifItems = combineLatest(hasRecentGifs, trendingGifs, searchCategories)
|
||||
|> map { hasRecentGifs, trendingGifs, searchCategories -> EntityKeyboardGifContent in
|
||||
var items: [GifPagerContentComponent.Item] = []
|
||||
|
||||
var isLoading = false
|
||||
if let trendingGifs = trendingGifs {
|
||||
for file in trendingGifs.files {
|
||||
items.append(GifPagerContentComponent.Item(
|
||||
file: file.file,
|
||||
contextResult: file.contextResult
|
||||
))
|
||||
}
|
||||
} else {
|
||||
isLoading = true
|
||||
}
|
||||
|
||||
return EntityKeyboardGifContent(
|
||||
hasRecentGifs: hasRecentGifs,
|
||||
component: GifPagerContentComponent(
|
||||
context: context,
|
||||
inputInteraction: gifInputInteraction,
|
||||
subject: subject,
|
||||
items: items,
|
||||
isLoading: isLoading,
|
||||
loadMoreToken: nil,
|
||||
displaySearchWithPlaceholder: gifInputInteraction.hasSearch ? presentationData.strings.Common_Search : nil,
|
||||
searchCategories: searchCategories,
|
||||
searchInitiallyHidden: true,
|
||||
searchState: .empty(hasResults: false),
|
||||
hideBackground: hideBackground
|
||||
)
|
||||
)
|
||||
}
|
||||
case let .emojiSearch(query):
|
||||
gifItems = combineLatest(
|
||||
hasRecentGifs,
|
||||
paneGifSearchForQuery(context: context, query: query.joined(separator: ""), offset: nil, incompleteResults: true, staleCachedResults: true, delayRequest: false, updateActivity: nil),
|
||||
searchCategories
|
||||
)
|
||||
|> map { hasRecentGifs, result, searchCategories -> EntityKeyboardGifContent in
|
||||
var items: [GifPagerContentComponent.Item] = []
|
||||
|
||||
var loadMoreToken: String?
|
||||
var isLoading = false
|
||||
if let result = result {
|
||||
for file in result.files {
|
||||
items.append(GifPagerContentComponent.Item(
|
||||
file: file.file,
|
||||
contextResult: file.contextResult
|
||||
))
|
||||
}
|
||||
loadMoreToken = result.nextOffset
|
||||
} else {
|
||||
isLoading = true
|
||||
}
|
||||
|
||||
return EntityKeyboardGifContent(
|
||||
hasRecentGifs: hasRecentGifs,
|
||||
component: GifPagerContentComponent(
|
||||
context: context,
|
||||
inputInteraction: gifInputInteraction,
|
||||
subject: subject,
|
||||
items: items,
|
||||
isLoading: isLoading,
|
||||
loadMoreToken: loadMoreToken,
|
||||
displaySearchWithPlaceholder: gifInputInteraction.hasSearch ? presentationData.strings.Common_Search : nil,
|
||||
searchCategories: searchCategories,
|
||||
searchInitiallyHidden: true,
|
||||
searchState: .active,
|
||||
hideBackground: gifInputInteraction.hideBackground
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
self.componentPromise.set(gifItems)
|
||||
self.componentDisposable = (self.componentPromise.get()
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.componentValue = result
|
||||
})
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.componentDisposable?.dispose()
|
||||
}
|
||||
|
||||
func loadMore(token: String) {
|
||||
if self.loadingMoreToken == token {
|
||||
return
|
||||
}
|
||||
self.loadingMoreToken = token
|
||||
|
||||
guard let componentValue = self.componentValue else {
|
||||
return
|
||||
}
|
||||
|
||||
let context = self.context
|
||||
let subject = self.subject
|
||||
let gifInputInteraction = self.gifInputInteraction
|
||||
|
||||
switch self.subject {
|
||||
case let .emojiSearch(query):
|
||||
let hasRecentGifs = context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs))
|
||||
|> map { savedGifs -> Bool in
|
||||
return !savedGifs.isEmpty
|
||||
}
|
||||
|
||||
let searchCategories: Signal<EmojiSearchCategories?, NoError> = context.engine.stickers.emojiSearchCategories(kind: .emoji)
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let gifItems: Signal<EntityKeyboardGifContent, NoError>
|
||||
gifItems = combineLatest(hasRecentGifs, paneGifSearchForQuery(context: context, query: query.joined(separator: ""), offset: token, incompleteResults: true, staleCachedResults: true, delayRequest: false, updateActivity: nil), searchCategories)
|
||||
|> map { hasRecentGifs, result, searchCategories -> EntityKeyboardGifContent in
|
||||
var items: [GifPagerContentComponent.Item] = []
|
||||
var existingIds = Set<MediaId>()
|
||||
for item in componentValue.component.items {
|
||||
items.append(item)
|
||||
existingIds.insert(item.file.media.fileId)
|
||||
}
|
||||
|
||||
var loadMoreToken: String?
|
||||
var isLoading = false
|
||||
if let result = result {
|
||||
for file in result.files {
|
||||
if existingIds.contains(file.file.media.fileId) {
|
||||
continue
|
||||
}
|
||||
existingIds.insert(file.file.media.fileId)
|
||||
items.append(GifPagerContentComponent.Item(
|
||||
file: file.file,
|
||||
contextResult: file.contextResult
|
||||
))
|
||||
}
|
||||
if !result.isComplete {
|
||||
loadMoreToken = result.nextOffset
|
||||
}
|
||||
} else {
|
||||
isLoading = true
|
||||
}
|
||||
|
||||
return EntityKeyboardGifContent(
|
||||
hasRecentGifs: hasRecentGifs,
|
||||
component: GifPagerContentComponent(
|
||||
context: context,
|
||||
inputInteraction: gifInputInteraction,
|
||||
subject: subject,
|
||||
items: items,
|
||||
isLoading: isLoading,
|
||||
loadMoreToken: loadMoreToken,
|
||||
displaySearchWithPlaceholder: gifInputInteraction.hasSearch ? presentationData.strings.Common_Search : nil,
|
||||
searchCategories: searchCategories,
|
||||
searchInitiallyHidden: true,
|
||||
searchState: .active,
|
||||
hideBackground: gifInputInteraction.hideBackground
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
self.componentPromise.set(gifItems)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
private var gifContext: GifContext? {
|
||||
didSet {
|
||||
if let gifContext = self.gifContext {
|
||||
@ -2975,112 +2709,3 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class PaneGifSearchForQueryResult {
|
||||
public let files: [MultiplexedVideoNodeFile]
|
||||
public let nextOffset: String?
|
||||
public let isComplete: Bool
|
||||
public let isStale: Bool
|
||||
|
||||
public init(files: [MultiplexedVideoNodeFile], nextOffset: String?, isComplete: Bool, isStale: Bool) {
|
||||
self.files = files
|
||||
self.nextOffset = nextOffset
|
||||
self.isComplete = isComplete
|
||||
self.isStale = isStale
|
||||
}
|
||||
}
|
||||
|
||||
public func paneGifSearchForQuery(context: AccountContext, query: String, offset: String?, incompleteResults: Bool = false, staleCachedResults: Bool = false, delayRequest: Bool = true, updateActivity: ((Bool) -> Void)?) -> Signal<PaneGifSearchForQueryResult?, NoError> {
|
||||
let contextBot = context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.SearchBots())
|
||||
|> mapToSignal { searchBots -> Signal<EnginePeer?, NoError> in
|
||||
let botName = searchBots.gifBotUsername ?? "gif"
|
||||
return context.engine.peers.resolvePeerByName(name: botName)
|
||||
}
|
||||
|> mapToSignal { peer -> Signal<(ChatPresentationInputQueryResult?, Bool, Bool), NoError> in
|
||||
if case let .user(user) = peer, let botInfo = user.botInfo, let _ = botInfo.inlinePlaceholder {
|
||||
let results = requestContextResults(engine: context.engine, botId: user.id, query: query, peerId: context.account.peerId, offset: offset ?? "", incompleteResults: incompleteResults, staleCachedResults: staleCachedResults, limit: 1)
|
||||
|> map { results -> (ChatPresentationInputQueryResult?, Bool, Bool) in
|
||||
return (.contextRequestResult(.user(user), results?.results), results != nil, results?.isStale ?? false)
|
||||
}
|
||||
|
||||
let maybeDelayedContextResults: Signal<(ChatPresentationInputQueryResult?, Bool, Bool), NoError>
|
||||
if delayRequest {
|
||||
maybeDelayedContextResults = results |> delay(0.4, queue: Queue.concurrentDefaultQueue())
|
||||
} else {
|
||||
maybeDelayedContextResults = results
|
||||
}
|
||||
|
||||
return maybeDelayedContextResults
|
||||
} else {
|
||||
return .single((nil, true, false))
|
||||
}
|
||||
}
|
||||
return contextBot
|
||||
|> mapToSignal { result -> Signal<PaneGifSearchForQueryResult?, NoError> in
|
||||
if let r = result.0, case let .contextRequestResult(_, maybeCollection) = r, let collection = maybeCollection {
|
||||
let results = collection.results
|
||||
var references: [MultiplexedVideoNodeFile] = []
|
||||
for result in results {
|
||||
switch result {
|
||||
case let .externalReference(externalReference):
|
||||
var imageResource: TelegramMediaResource?
|
||||
var thumbnailResource: TelegramMediaResource?
|
||||
var thumbnailIsVideo: Bool = false
|
||||
var uniqueId: Int64?
|
||||
if let content = externalReference.content {
|
||||
imageResource = content.resource
|
||||
if let resource = content.resource as? WebFileReferenceMediaResource {
|
||||
uniqueId = Int64(HashFunctions.murMurHash32(resource.url))
|
||||
}
|
||||
}
|
||||
if let thumbnail = externalReference.thumbnail {
|
||||
thumbnailResource = thumbnail.resource
|
||||
if thumbnail.mimeType.hasPrefix("video/") {
|
||||
thumbnailIsVideo = true
|
||||
}
|
||||
}
|
||||
|
||||
if externalReference.type == "gif", let resource = imageResource, let content = externalReference.content, let dimensions = content.dimensions {
|
||||
var previews: [TelegramMediaImageRepresentation] = []
|
||||
var videoThumbnails: [TelegramMediaFile.VideoThumbnail] = []
|
||||
if let thumbnailResource = thumbnailResource {
|
||||
if thumbnailIsVideo {
|
||||
videoThumbnails.append(TelegramMediaFile.VideoThumbnail(
|
||||
dimensions: dimensions,
|
||||
resource: thumbnailResource
|
||||
))
|
||||
} else {
|
||||
previews.append(TelegramMediaImageRepresentation(
|
||||
dimensions: dimensions,
|
||||
resource: thumbnailResource,
|
||||
progressiveSizes: [],
|
||||
immediateThumbnailData: nil,
|
||||
hasVideo: false,
|
||||
isPersonal: false
|
||||
))
|
||||
}
|
||||
}
|
||||
let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: uniqueId ?? 0), partialReference: nil, resource: resource, previewRepresentations: previews, videoThumbnails: videoThumbnails, immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [], preloadSize: nil)])
|
||||
references.append(MultiplexedVideoNodeFile(file: FileMediaReference.standalone(media: file), contextResult: (collection, result)))
|
||||
}
|
||||
case let .internalReference(internalReference):
|
||||
if let file = internalReference.file {
|
||||
references.append(MultiplexedVideoNodeFile(file: FileMediaReference.standalone(media: file), contextResult: (collection, result)))
|
||||
}
|
||||
}
|
||||
}
|
||||
return .single(PaneGifSearchForQueryResult(files: references, nextOffset: collection.nextOffset, isComplete: result.1, isStale: result.2))
|
||||
} else if incompleteResults {
|
||||
return .single(nil)
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
|> deliverOnMainQueue
|
||||
|> beforeStarted {
|
||||
updateActivity?(true)
|
||||
}
|
||||
|> afterCompleted {
|
||||
updateActivity?(false)
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import AppBundle
|
||||
import ChatControllerInteraction
|
||||
import MultiplexedVideoNode
|
||||
import ChatPresentationInterfaceState
|
||||
import EntityKeyboardGifContent
|
||||
|
||||
final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
|
||||
private let context: AccountContext
|
||||
|
@ -13,6 +13,7 @@ import ChatControllerInteraction
|
||||
import MultiplexedVideoNode
|
||||
import FeaturedStickersScreen
|
||||
import StickerPeekUI
|
||||
import EntityKeyboardGifContent
|
||||
|
||||
private let searchBarHeight: CGFloat = 52.0
|
||||
|
||||
|
@ -18,7 +18,6 @@ import PagerComponent
|
||||
import SoftwareVideo
|
||||
import AVFoundation
|
||||
import PhotoResources
|
||||
//import ContextUI
|
||||
import ShimmerEffect
|
||||
|
||||
private class GifVideoLayer: AVSampleBufferDisplayLayer {
|
||||
@ -269,6 +268,7 @@ public final class GifPagerContentComponent: Component {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
public final class View: ContextControllerSourceView, PagerContentViewWithBackground, UIScrollViewDelegate {
|
||||
private struct ItemGroupDescription: Equatable {
|
||||
let hasTitle: Bool
|
||||
@ -994,7 +994,13 @@ public final class GifPagerContentComponent: Component {
|
||||
vibrancyEffectView.contentView.addSubview(self.mirrorSearchHeaderContainer)
|
||||
}
|
||||
}
|
||||
self.backgroundView.updateColor(color: theme.chat.inputMediaPanel.backgroundColor, enableBlur: true, forceKeepBlur: false, transition: transition.containedViewLayoutTransition)
|
||||
|
||||
let hideBackground = self.component?.hideBackground ?? false
|
||||
var backgroundColor = theme.chat.inputMediaPanel.backgroundColor
|
||||
if hideBackground {
|
||||
backgroundColor = backgroundColor.withAlphaComponent(0.01)
|
||||
}
|
||||
self.backgroundView.updateColor(color: backgroundColor, enableBlur: true, forceKeepBlur: false, transition: transition.containedViewLayoutTransition)
|
||||
transition.setFrame(view: self.backgroundView, frame: backgroundFrame)
|
||||
self.backgroundView.update(size: backgroundFrame.size, transition: transition.containedViewLayoutTransition)
|
||||
|
||||
@ -1115,8 +1121,6 @@ public final class GifPagerContentComponent: Component {
|
||||
transition.setPosition(view: self.scrollClippingView, position: clippingFrame.center)
|
||||
transition.setBounds(view: self.scrollClippingView, bounds: clippingFrame)
|
||||
|
||||
self.backgroundView.isHidden = component.hideBackground
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,43 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "EntityKeyboardGifContent",
|
||||
module_name = "EntityKeyboardGifContent",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/ComponentFlow:ComponentFlow",
|
||||
"//submodules/Components/PagerComponent:PagerComponent",
|
||||
"//submodules/Components/BlurredBackgroundComponent:BlurredBackgroundComponent",
|
||||
"//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters",
|
||||
"//submodules/Components/BundleIconComponent:BundleIconComponent",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//submodules/Postbox:Postbox",
|
||||
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
|
||||
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
|
||||
"//submodules/YuvConversion:YuvConversion",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/SoftwareVideo:SoftwareVideo",
|
||||
"//submodules/ShimmerEffect:ShimmerEffect",
|
||||
"//submodules/PhotoResources:PhotoResources",
|
||||
"//submodules/StickerResources:StickerResources",
|
||||
"//submodules/AppBundle:AppBundle",
|
||||
"//submodules/Components/MultilineTextComponent:MultilineTextComponent",
|
||||
"//submodules/Components/SolidRoundedButtonComponent:SolidRoundedButtonComponent",
|
||||
"//submodules/Components/LottieAnimationComponent:LottieAnimationComponent",
|
||||
"//submodules/LocalizedPeerData:LocalizedPeerData",
|
||||
"//submodules/TelegramNotices:TelegramNotices",
|
||||
"//submodules/TelegramUI/Components/MultiplexedVideoNode:MultiplexedVideoNode",
|
||||
"//submodules/TelegramUI/Components/EntityKeyboard:EntityKeyboard",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -0,0 +1,385 @@
|
||||
import Foundation
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import AccountContext
|
||||
import MultiplexedVideoNode
|
||||
import EntityKeyboard
|
||||
|
||||
public final class ChatMediaInputGifPaneTrendingState {
|
||||
public let files: [MultiplexedVideoNodeFile]
|
||||
public let nextOffset: String?
|
||||
|
||||
public init(files: [MultiplexedVideoNodeFile], nextOffset: String?) {
|
||||
self.files = files
|
||||
self.nextOffset = nextOffset
|
||||
}
|
||||
}
|
||||
|
||||
public final class EntityKeyboardGifContent: Equatable {
|
||||
public let hasRecentGifs: Bool
|
||||
public let component: GifPagerContentComponent
|
||||
|
||||
public init(hasRecentGifs: Bool, component: GifPagerContentComponent) {
|
||||
self.hasRecentGifs = hasRecentGifs
|
||||
self.component = component
|
||||
}
|
||||
|
||||
public static func ==(lhs: EntityKeyboardGifContent, rhs: EntityKeyboardGifContent) -> Bool {
|
||||
if lhs.hasRecentGifs != rhs.hasRecentGifs {
|
||||
return false
|
||||
}
|
||||
if lhs.component != rhs.component {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public class PaneGifSearchForQueryResult {
|
||||
public let files: [MultiplexedVideoNodeFile]
|
||||
public let nextOffset: String?
|
||||
public let isComplete: Bool
|
||||
public let isStale: Bool
|
||||
|
||||
public init(files: [MultiplexedVideoNodeFile], nextOffset: String?, isComplete: Bool, isStale: Bool) {
|
||||
self.files = files
|
||||
self.nextOffset = nextOffset
|
||||
self.isComplete = isComplete
|
||||
self.isStale = isStale
|
||||
}
|
||||
}
|
||||
|
||||
public func paneGifSearchForQuery(context: AccountContext, query: String, offset: String?, incompleteResults: Bool = false, staleCachedResults: Bool = false, delayRequest: Bool = true, updateActivity: ((Bool) -> Void)?) -> Signal<PaneGifSearchForQueryResult?, NoError> {
|
||||
let contextBot = context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.SearchBots())
|
||||
|> mapToSignal { searchBots -> Signal<EnginePeer?, NoError> in
|
||||
let botName = searchBots.gifBotUsername ?? "gif"
|
||||
return context.engine.peers.resolvePeerByName(name: botName)
|
||||
}
|
||||
|> mapToSignal { peer -> Signal<(ChatPresentationInputQueryResult?, Bool, Bool), NoError> in
|
||||
if case let .user(user) = peer, let botInfo = user.botInfo, let _ = botInfo.inlinePlaceholder {
|
||||
let results = requestContextResults(engine: context.engine, botId: user.id, query: query, peerId: context.account.peerId, offset: offset ?? "", incompleteResults: incompleteResults, staleCachedResults: staleCachedResults, limit: 1)
|
||||
|> map { results -> (ChatPresentationInputQueryResult?, Bool, Bool) in
|
||||
return (.contextRequestResult(.user(user), results?.results), results != nil, results?.isStale ?? false)
|
||||
}
|
||||
|
||||
let maybeDelayedContextResults: Signal<(ChatPresentationInputQueryResult?, Bool, Bool), NoError>
|
||||
if delayRequest {
|
||||
maybeDelayedContextResults = results |> delay(0.4, queue: Queue.concurrentDefaultQueue())
|
||||
} else {
|
||||
maybeDelayedContextResults = results
|
||||
}
|
||||
|
||||
return maybeDelayedContextResults
|
||||
} else {
|
||||
return .single((nil, true, false))
|
||||
}
|
||||
}
|
||||
return contextBot
|
||||
|> mapToSignal { result -> Signal<PaneGifSearchForQueryResult?, NoError> in
|
||||
if let r = result.0, case let .contextRequestResult(_, maybeCollection) = r, let collection = maybeCollection {
|
||||
let results = collection.results
|
||||
var references: [MultiplexedVideoNodeFile] = []
|
||||
for result in results {
|
||||
switch result {
|
||||
case let .externalReference(externalReference):
|
||||
var imageResource: TelegramMediaResource?
|
||||
var thumbnailResource: TelegramMediaResource?
|
||||
var thumbnailIsVideo: Bool = false
|
||||
var uniqueId: Int64?
|
||||
if let content = externalReference.content {
|
||||
imageResource = content.resource
|
||||
if let resource = content.resource as? WebFileReferenceMediaResource {
|
||||
uniqueId = Int64(HashFunctions.murMurHash32(resource.url))
|
||||
}
|
||||
}
|
||||
if let thumbnail = externalReference.thumbnail {
|
||||
thumbnailResource = thumbnail.resource
|
||||
if thumbnail.mimeType.hasPrefix("video/") {
|
||||
thumbnailIsVideo = true
|
||||
}
|
||||
}
|
||||
|
||||
if externalReference.type == "gif", let resource = imageResource, let content = externalReference.content, let dimensions = content.dimensions {
|
||||
var previews: [TelegramMediaImageRepresentation] = []
|
||||
var videoThumbnails: [TelegramMediaFile.VideoThumbnail] = []
|
||||
if let thumbnailResource = thumbnailResource {
|
||||
if thumbnailIsVideo {
|
||||
videoThumbnails.append(TelegramMediaFile.VideoThumbnail(
|
||||
dimensions: dimensions,
|
||||
resource: thumbnailResource
|
||||
))
|
||||
} else {
|
||||
previews.append(TelegramMediaImageRepresentation(
|
||||
dimensions: dimensions,
|
||||
resource: thumbnailResource,
|
||||
progressiveSizes: [],
|
||||
immediateThumbnailData: nil,
|
||||
hasVideo: false,
|
||||
isPersonal: false
|
||||
))
|
||||
}
|
||||
}
|
||||
let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: uniqueId ?? 0), partialReference: nil, resource: resource, previewRepresentations: previews, videoThumbnails: videoThumbnails, immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [], preloadSize: nil)])
|
||||
references.append(MultiplexedVideoNodeFile(file: FileMediaReference.standalone(media: file), contextResult: (collection, result)))
|
||||
}
|
||||
case let .internalReference(internalReference):
|
||||
if let file = internalReference.file {
|
||||
references.append(MultiplexedVideoNodeFile(file: FileMediaReference.standalone(media: file), contextResult: (collection, result)))
|
||||
}
|
||||
}
|
||||
}
|
||||
return .single(PaneGifSearchForQueryResult(files: references, nextOffset: collection.nextOffset, isComplete: result.1, isStale: result.2))
|
||||
} else if incompleteResults {
|
||||
return .single(nil)
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
|> deliverOnMainQueue
|
||||
|> beforeStarted {
|
||||
updateActivity?(true)
|
||||
}
|
||||
|> afterCompleted {
|
||||
updateActivity?(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public final class GifContext {
|
||||
private var componentValue: EntityKeyboardGifContent? {
|
||||
didSet {
|
||||
if let componentValue = self.componentValue {
|
||||
self.componentResult.set(.single(componentValue))
|
||||
}
|
||||
}
|
||||
}
|
||||
private let componentPromise = Promise<EntityKeyboardGifContent>()
|
||||
|
||||
private let componentResult = Promise<EntityKeyboardGifContent>()
|
||||
public var component: Signal<EntityKeyboardGifContent, NoError> {
|
||||
return self.componentResult.get()
|
||||
}
|
||||
private var componentDisposable: Disposable?
|
||||
|
||||
private let context: AccountContext
|
||||
private let subject: GifPagerContentComponent.Subject
|
||||
private let gifInputInteraction: GifPagerContentComponent.InputInteraction
|
||||
|
||||
private var loadingMoreToken: String?
|
||||
|
||||
public init(context: AccountContext, subject: GifPagerContentComponent.Subject, gifInputInteraction: GifPagerContentComponent.InputInteraction, trendingGifs: Signal<ChatMediaInputGifPaneTrendingState?, NoError>) {
|
||||
self.context = context
|
||||
self.subject = subject
|
||||
self.gifInputInteraction = gifInputInteraction
|
||||
|
||||
let hideBackground = gifInputInteraction.hideBackground
|
||||
|
||||
let hasRecentGifs = context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs))
|
||||
|> map { savedGifs -> Bool in
|
||||
return !savedGifs.isEmpty
|
||||
}
|
||||
|
||||
let searchCategories: Signal<EmojiSearchCategories?, NoError> = context.engine.stickers.emojiSearchCategories(kind: .emoji)
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let gifItems: Signal<EntityKeyboardGifContent, NoError>
|
||||
switch subject {
|
||||
case .recent:
|
||||
gifItems = combineLatest(
|
||||
context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs)),
|
||||
searchCategories
|
||||
)
|
||||
|> map { savedGifs, searchCategories -> EntityKeyboardGifContent in
|
||||
var items: [GifPagerContentComponent.Item] = []
|
||||
for gifItem in savedGifs {
|
||||
items.append(GifPagerContentComponent.Item(
|
||||
file: .savedGif(media: gifItem.contents.get(RecentMediaItem.self)!.media),
|
||||
contextResult: nil
|
||||
))
|
||||
}
|
||||
return EntityKeyboardGifContent(
|
||||
hasRecentGifs: true,
|
||||
component: GifPagerContentComponent(
|
||||
context: context,
|
||||
inputInteraction: gifInputInteraction,
|
||||
subject: subject,
|
||||
items: items,
|
||||
isLoading: false,
|
||||
loadMoreToken: nil,
|
||||
displaySearchWithPlaceholder: gifInputInteraction.hasSearch ? presentationData.strings.Common_Search : nil,
|
||||
searchCategories: searchCategories,
|
||||
searchInitiallyHidden: true,
|
||||
searchState: .empty(hasResults: false),
|
||||
hideBackground: hideBackground
|
||||
)
|
||||
)
|
||||
}
|
||||
case .trending:
|
||||
gifItems = combineLatest(hasRecentGifs, trendingGifs, searchCategories)
|
||||
|> map { hasRecentGifs, trendingGifs, searchCategories -> EntityKeyboardGifContent in
|
||||
var items: [GifPagerContentComponent.Item] = []
|
||||
|
||||
var isLoading = false
|
||||
if let trendingGifs = trendingGifs {
|
||||
for file in trendingGifs.files {
|
||||
items.append(GifPagerContentComponent.Item(
|
||||
file: file.file,
|
||||
contextResult: file.contextResult
|
||||
))
|
||||
}
|
||||
} else {
|
||||
isLoading = true
|
||||
}
|
||||
|
||||
return EntityKeyboardGifContent(
|
||||
hasRecentGifs: hasRecentGifs,
|
||||
component: GifPagerContentComponent(
|
||||
context: context,
|
||||
inputInteraction: gifInputInteraction,
|
||||
subject: subject,
|
||||
items: items,
|
||||
isLoading: isLoading,
|
||||
loadMoreToken: nil,
|
||||
displaySearchWithPlaceholder: gifInputInteraction.hasSearch ? presentationData.strings.Common_Search : nil,
|
||||
searchCategories: searchCategories,
|
||||
searchInitiallyHidden: true,
|
||||
searchState: .empty(hasResults: false),
|
||||
hideBackground: hideBackground
|
||||
)
|
||||
)
|
||||
}
|
||||
case let .emojiSearch(query):
|
||||
gifItems = combineLatest(
|
||||
hasRecentGifs,
|
||||
paneGifSearchForQuery(context: context, query: query.joined(separator: ""), offset: nil, incompleteResults: true, staleCachedResults: true, delayRequest: false, updateActivity: nil),
|
||||
searchCategories
|
||||
)
|
||||
|> map { hasRecentGifs, result, searchCategories -> EntityKeyboardGifContent in
|
||||
var items: [GifPagerContentComponent.Item] = []
|
||||
|
||||
var loadMoreToken: String?
|
||||
var isLoading = false
|
||||
if let result = result {
|
||||
for file in result.files {
|
||||
items.append(GifPagerContentComponent.Item(
|
||||
file: file.file,
|
||||
contextResult: file.contextResult
|
||||
))
|
||||
}
|
||||
loadMoreToken = result.nextOffset
|
||||
} else {
|
||||
isLoading = true
|
||||
}
|
||||
|
||||
return EntityKeyboardGifContent(
|
||||
hasRecentGifs: hasRecentGifs,
|
||||
component: GifPagerContentComponent(
|
||||
context: context,
|
||||
inputInteraction: gifInputInteraction,
|
||||
subject: subject,
|
||||
items: items,
|
||||
isLoading: isLoading,
|
||||
loadMoreToken: loadMoreToken,
|
||||
displaySearchWithPlaceholder: gifInputInteraction.hasSearch ? presentationData.strings.Common_Search : nil,
|
||||
searchCategories: searchCategories,
|
||||
searchInitiallyHidden: true,
|
||||
searchState: .active,
|
||||
hideBackground: gifInputInteraction.hideBackground
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
self.componentPromise.set(gifItems)
|
||||
self.componentDisposable = (self.componentPromise.get()
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.componentValue = result
|
||||
})
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.componentDisposable?.dispose()
|
||||
}
|
||||
|
||||
public func loadMore(token: String) {
|
||||
if self.loadingMoreToken == token {
|
||||
return
|
||||
}
|
||||
self.loadingMoreToken = token
|
||||
|
||||
guard let componentValue = self.componentValue else {
|
||||
return
|
||||
}
|
||||
|
||||
let context = self.context
|
||||
let subject = self.subject
|
||||
let gifInputInteraction = self.gifInputInteraction
|
||||
|
||||
switch self.subject {
|
||||
case let .emojiSearch(query):
|
||||
let hasRecentGifs = context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs))
|
||||
|> map { savedGifs -> Bool in
|
||||
return !savedGifs.isEmpty
|
||||
}
|
||||
|
||||
let searchCategories: Signal<EmojiSearchCategories?, NoError> = context.engine.stickers.emojiSearchCategories(kind: .emoji)
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let gifItems: Signal<EntityKeyboardGifContent, NoError>
|
||||
gifItems = combineLatest(hasRecentGifs, paneGifSearchForQuery(context: context, query: query.joined(separator: ""), offset: token, incompleteResults: true, staleCachedResults: true, delayRequest: false, updateActivity: nil), searchCategories)
|
||||
|> map { hasRecentGifs, result, searchCategories -> EntityKeyboardGifContent in
|
||||
var items: [GifPagerContentComponent.Item] = []
|
||||
var existingIds = Set<MediaId>()
|
||||
for item in componentValue.component.items {
|
||||
items.append(item)
|
||||
existingIds.insert(item.file.media.fileId)
|
||||
}
|
||||
|
||||
var loadMoreToken: String?
|
||||
var isLoading = false
|
||||
if let result = result {
|
||||
for file in result.files {
|
||||
if existingIds.contains(file.file.media.fileId) {
|
||||
continue
|
||||
}
|
||||
existingIds.insert(file.file.media.fileId)
|
||||
items.append(GifPagerContentComponent.Item(
|
||||
file: file.file,
|
||||
contextResult: file.contextResult
|
||||
))
|
||||
}
|
||||
if !result.isComplete {
|
||||
loadMoreToken = result.nextOffset
|
||||
}
|
||||
} else {
|
||||
isLoading = true
|
||||
}
|
||||
|
||||
return EntityKeyboardGifContent(
|
||||
hasRecentGifs: hasRecentGifs,
|
||||
component: GifPagerContentComponent(
|
||||
context: context,
|
||||
inputInteraction: gifInputInteraction,
|
||||
subject: subject,
|
||||
items: items,
|
||||
isLoading: isLoading,
|
||||
loadMoreToken: loadMoreToken,
|
||||
displaySearchWithPlaceholder: gifInputInteraction.hasSearch ? presentationData.strings.Common_Search : nil,
|
||||
searchCategories: searchCategories,
|
||||
searchInitiallyHidden: true,
|
||||
searchState: .active,
|
||||
hideBackground: gifInputInteraction.hideBackground
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
self.componentPromise.set(gifItems)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
@ -21,7 +21,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
||||
}
|
||||
case file(TelegramMediaFile)
|
||||
case image(UIImage, ImageType)
|
||||
case video(String, UIImage?, Bool)
|
||||
case video(TelegramMediaFile)
|
||||
case dualVideoReference
|
||||
|
||||
public static func == (lhs: Content, rhs: Content) -> Bool {
|
||||
@ -38,9 +38,9 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .video(lhsPath, _, lhsInternalMirrored):
|
||||
if case let .video(rhsPath, _, rhsInternalMirrored) = rhs {
|
||||
return lhsPath == rhsPath && lhsInternalMirrored == rhsInternalMirrored
|
||||
case let .video(lhsFile):
|
||||
if case let .video(rhsFile) = rhs {
|
||||
return lhsFile.fileId == rhsFile.fileId
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
@ -57,9 +57,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
||||
case uuid
|
||||
case file
|
||||
case imagePath
|
||||
case videoPath
|
||||
case videoImagePath
|
||||
case videoMirrored
|
||||
case videoFile
|
||||
case isRectangle
|
||||
case isDualPhoto
|
||||
case dualVideo
|
||||
@ -98,8 +96,8 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
||||
dimensions = image.size
|
||||
case let .file(file):
|
||||
dimensions = file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0)
|
||||
case let .video(_, image, _):
|
||||
dimensions = image?.size ?? CGSize(width: 512.0, height: 512.0)
|
||||
case let .video(file):
|
||||
dimensions = file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0)
|
||||
case .dualVideoReference:
|
||||
dimensions = CGSize(width: 512.0, height: 512.0)
|
||||
}
|
||||
@ -129,6 +127,8 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
||||
switch self.content {
|
||||
case let .image(_, imageType):
|
||||
return imageType == .rectangle
|
||||
case .video:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
@ -173,13 +173,8 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
||||
imageType = .sticker
|
||||
}
|
||||
self.content = .image(image, imageType)
|
||||
} else if let videoPath = try container.decodeIfPresent(String.self, forKey: .videoPath) {
|
||||
var imageValue: UIImage?
|
||||
if let imagePath = try container.decodeIfPresent(String.self, forKey: .videoImagePath), let image = UIImage(contentsOfFile: fullEntityMediaPath(imagePath)) {
|
||||
imageValue = image
|
||||
}
|
||||
let videoMirrored = try container.decodeIfPresent(Bool.self, forKey: .videoMirrored) ?? false
|
||||
self.content = .video(videoPath, imageValue, videoMirrored)
|
||||
} else if let file = try container.decodeIfPresent(TelegramMediaFile.self, forKey: .videoFile) {
|
||||
self.content = .video(file)
|
||||
} else {
|
||||
fatalError()
|
||||
}
|
||||
@ -213,16 +208,8 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
||||
default:
|
||||
break
|
||||
}
|
||||
case let .video(path, image, videoMirrored):
|
||||
try container.encode(path, forKey: .videoPath)
|
||||
let imagePath = "\(self.uuid).jpg"
|
||||
let fullImagePath = fullEntityMediaPath(imagePath)
|
||||
if let imageData = image?.jpegData(compressionQuality: 0.87) {
|
||||
try? FileManager.default.createDirectory(atPath: entitiesPath(), withIntermediateDirectories: true)
|
||||
try? imageData.write(to: URL(fileURLWithPath: fullImagePath))
|
||||
try container.encodeIfPresent(imagePath, forKey: .videoImagePath)
|
||||
}
|
||||
try container.encode(videoMirrored, forKey: .videoMirrored)
|
||||
case let .video(file):
|
||||
try container.encode(file, forKey: .videoFile)
|
||||
case .dualVideoReference:
|
||||
try container.encode(true, forKey: .dualVideo)
|
||||
}
|
||||
|
@ -68,8 +68,8 @@ func composerEntitiesForDrawingEntity(account: Account, textScale: CGFloat, enti
|
||||
content = .file(file)
|
||||
case let .image(image, _):
|
||||
content = .image(image)
|
||||
case let .video(path, _, _):
|
||||
content = .video(path)
|
||||
case let .video(file):
|
||||
content = .video(file)
|
||||
case .dualVideoReference:
|
||||
return []
|
||||
}
|
||||
@ -135,8 +135,8 @@ private class MediaEditorComposerStaticEntity: MediaEditorComposerEntity {
|
||||
private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
|
||||
public enum Content {
|
||||
case file(TelegramMediaFile)
|
||||
case video(TelegramMediaFile)
|
||||
case image(UIImage)
|
||||
case video(String)
|
||||
|
||||
var file: TelegramMediaFile? {
|
||||
if case let .file(file) = self {
|
||||
@ -146,6 +146,7 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
|
||||
}
|
||||
}
|
||||
|
||||
let account: Account
|
||||
let content: Content
|
||||
let position: CGPoint
|
||||
let scale: CGFloat
|
||||
@ -181,6 +182,7 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
|
||||
let imagePromise = Promise<UIImage>()
|
||||
|
||||
init(account: Account, content: Content, position: CGPoint, scale: CGFloat, rotation: CGFloat, baseSize: CGSize, mirrored: Bool, colorSpace: CGColorSpace, tintColor: UIColor?, isStatic: Bool) {
|
||||
self.account = account
|
||||
self.content = content
|
||||
self.position = position
|
||||
self.scale = scale
|
||||
@ -263,27 +265,8 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
|
||||
case let .image(image):
|
||||
self.isAnimated = false
|
||||
self.imagePromise.set(.single(image))
|
||||
case let .video(videoPath):
|
||||
case .video:
|
||||
self.isAnimated = true
|
||||
|
||||
let url = URL(fileURLWithPath: videoPath)
|
||||
let asset = AVURLAsset(url: url)
|
||||
|
||||
if let assetReader = try? AVAssetReader(asset: asset), let videoTrack = asset.tracks(withMediaType: .video).first {
|
||||
let outputSettings: [String: Any] = [
|
||||
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
|
||||
kCVPixelBufferMetalCompatibilityKey as String: true
|
||||
]
|
||||
let videoOutput = AVAssetReaderTrackOutput(track: videoTrack, outputSettings: outputSettings)
|
||||
videoOutput.alwaysCopiesSampleData = true
|
||||
if assetReader.canAdd(videoOutput) {
|
||||
assetReader.add(videoOutput)
|
||||
}
|
||||
|
||||
assetReader.startReading()
|
||||
self.assetReader = assetReader
|
||||
self.videoOutput = videoOutput
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -291,51 +274,109 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
|
||||
self.disposables.dispose()
|
||||
}
|
||||
|
||||
private var circleMaskFilter: CIFilter?
|
||||
private func setupVideoOutput() {
|
||||
if case let .video(file) = self.content {
|
||||
if let path = self.account.postbox.mediaBox.completedResourcePath(file.resource, pathExtension: "mp4") {
|
||||
let url = URL(fileURLWithPath: path)
|
||||
let asset = AVURLAsset(url: url)
|
||||
|
||||
if let assetReader = try? AVAssetReader(asset: asset), let videoTrack = asset.tracks(withMediaType: .video).first {
|
||||
self.videoFrameRate = videoTrack.nominalFrameRate
|
||||
let outputSettings: [String: Any] = [
|
||||
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
|
||||
kCVPixelBufferMetalCompatibilityKey as String: true
|
||||
]
|
||||
let videoOutput = AVAssetReaderTrackOutput(track: videoTrack, outputSettings: outputSettings)
|
||||
videoOutput.alwaysCopiesSampleData = true
|
||||
if assetReader.canAdd(videoOutput) {
|
||||
assetReader.add(videoOutput)
|
||||
}
|
||||
|
||||
assetReader.startReading()
|
||||
self.assetReader = assetReader
|
||||
self.videoOutput = videoOutput
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var videoFrameRate: Float?
|
||||
private var maskFilter: CIFilter?
|
||||
func image(for time: CMTime, frameRate: Float, context: CIContext, completion: @escaping (CIImage?) -> Void) {
|
||||
let currentTime = CMTimeGetSeconds(time)
|
||||
|
||||
if case .video = self.content {
|
||||
if self.videoOutput == nil {
|
||||
self.setupVideoOutput()
|
||||
}
|
||||
if let videoOutput = self.videoOutput {
|
||||
if let sampleBuffer = videoOutput.copyNextSampleBuffer(), let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) {
|
||||
var ciImage = CIImage(cvPixelBuffer: imageBuffer)
|
||||
ciImage = ciImage.oriented(forExifOrientation: UIImage.Orientation.right.exifOrientation)
|
||||
let minSide = min(ciImage.extent.size.width, ciImage.extent.size.height)
|
||||
let cropRect = CGRect(origin: CGPoint(x: floor((ciImage.extent.size.width - minSide) / 2.0), y: floor((ciImage.extent.size.height - minSide) / 2.0)), size: CGSize(width: minSide, height: minSide))
|
||||
ciImage = ciImage.cropped(to: cropRect).samplingLinear()
|
||||
ciImage = ciImage.transformed(by: CGAffineTransform(translationX: 0.0, y: -420.0))
|
||||
var frameAdvancement: Int = 0
|
||||
if let frameRate = self.videoFrameRate, frameRate > 0 {
|
||||
let frameTime = 1.0 / Double(frameRate)
|
||||
let frameIndex = Int(floor(currentTime / frameTime))
|
||||
|
||||
var circleMaskFilter: CIFilter?
|
||||
if let current = self.circleMaskFilter {
|
||||
circleMaskFilter = current
|
||||
} else {
|
||||
let circleImage = generateImage(CGSize(width: minSide, height: minSide), scale: 1.0, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: .zero, size: size))
|
||||
context.setFillColor(UIColor.white.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: .zero, size: size))
|
||||
})!
|
||||
let circleMask = CIImage(image: circleImage)
|
||||
if let filter = CIFilter(name: "CIBlendWithAlphaMask") {
|
||||
filter.setValue(circleMask, forKey: kCIInputMaskImageKey)
|
||||
self.circleMaskFilter = filter
|
||||
circleMaskFilter = filter
|
||||
}
|
||||
}
|
||||
|
||||
let _ = circleMaskFilter
|
||||
if let circleMaskFilter {
|
||||
circleMaskFilter.setValue(ciImage, forKey: kCIInputImageKey)
|
||||
if let output = circleMaskFilter.outputImage {
|
||||
ciImage = output
|
||||
let currentFrameIndex = self.currentFrameIndex
|
||||
if currentFrameIndex != frameIndex {
|
||||
let previousFrameIndex = currentFrameIndex
|
||||
self.currentFrameIndex = frameIndex
|
||||
|
||||
var delta = 1
|
||||
if let previousFrameIndex = previousFrameIndex {
|
||||
delta = max(1, frameIndex - previousFrameIndex)
|
||||
}
|
||||
frameAdvancement = delta
|
||||
}
|
||||
}
|
||||
|
||||
if frameAdvancement == 0, let image = self.image {
|
||||
completion(image)
|
||||
return
|
||||
} else {
|
||||
var sampleBuffer = videoOutput.copyNextSampleBuffer()
|
||||
if sampleBuffer == nil && self.assetReader?.status == .completed {
|
||||
self.setupVideoOutput()
|
||||
sampleBuffer = self.videoOutput?.copyNextSampleBuffer()
|
||||
}
|
||||
|
||||
completion(ciImage)
|
||||
if let sampleBuffer, let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) {
|
||||
var ciImage = CIImage(cvPixelBuffer: imageBuffer)
|
||||
|
||||
var circleMaskFilter: CIFilter?
|
||||
if let current = self.maskFilter {
|
||||
circleMaskFilter = current
|
||||
} else {
|
||||
let circleImage = generateImage(ciImage.extent.size, scale: 1.0, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: .zero, size: size))
|
||||
context.setFillColor(UIColor.white.cgColor)
|
||||
|
||||
let path = UIBezierPath(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: floor(size.width * 0.03))
|
||||
context.addPath(path.cgPath)
|
||||
context.fillPath()
|
||||
})!
|
||||
let circleMask = CIImage(image: circleImage)
|
||||
if let filter = CIFilter(name: "CIBlendWithAlphaMask") {
|
||||
filter.setValue(circleMask, forKey: kCIInputMaskImageKey)
|
||||
self.maskFilter = filter
|
||||
circleMaskFilter = filter
|
||||
}
|
||||
}
|
||||
|
||||
let _ = circleMaskFilter
|
||||
if let circleMaskFilter {
|
||||
circleMaskFilter.setValue(ciImage, forKey: kCIInputImageKey)
|
||||
if let output = circleMaskFilter.outputImage {
|
||||
ciImage = output
|
||||
}
|
||||
}
|
||||
|
||||
self.image = ciImage
|
||||
completion(ciImage)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
completion(nil)
|
||||
}
|
||||
} else if self.isAnimated {
|
||||
let currentTime = CMTimeGetSeconds(time)
|
||||
|
||||
var tintColor: UIColor?
|
||||
if let file = self.content.file, file.isCustomTemplateEmoji {
|
||||
tintColor = self.tintColor ?? UIColor(rgb: 0xffffff)
|
||||
@ -389,8 +430,8 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
|
||||
}
|
||||
}
|
||||
|
||||
if frameAdvancement == 0 && strongSelf.image != nil {
|
||||
completion(strongSelf.image)
|
||||
if frameAdvancement == 0, let image = strongSelf.image {
|
||||
completion(image)
|
||||
} else {
|
||||
if let frame = takeFrame(max(1, frameAdvancement)) {
|
||||
var imagePixelBuffer: CVPixelBuffer?
|
||||
|
@ -1,6 +1,7 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SwiftSignalKit
|
||||
import CoreLocation
|
||||
import TelegramCore
|
||||
import TelegramUIPreferences
|
||||
import PersistentStringHash
|
||||
@ -67,6 +68,8 @@ public final class MediaEditorDraft: Codable, Equatable {
|
||||
case caption
|
||||
case privacy
|
||||
case timestamp
|
||||
case locationLatitude
|
||||
case locationLongitude
|
||||
}
|
||||
|
||||
public let path: String
|
||||
@ -78,8 +81,9 @@ public final class MediaEditorDraft: Codable, Equatable {
|
||||
public let caption: NSAttributedString
|
||||
public let privacy: MediaEditorResultPrivacy?
|
||||
public let timestamp: Int32
|
||||
public let location: CLLocationCoordinate2D?
|
||||
|
||||
public init(path: String, isVideo: Bool, thumbnail: UIImage, dimensions: PixelDimensions, duration: Double?, values: MediaEditorValues, caption: NSAttributedString, privacy: MediaEditorResultPrivacy?, timestamp: Int32) {
|
||||
public init(path: String, isVideo: Bool, thumbnail: UIImage, dimensions: PixelDimensions, duration: Double?, values: MediaEditorValues, caption: NSAttributedString, privacy: MediaEditorResultPrivacy?, timestamp: Int32, location: CLLocationCoordinate2D?) {
|
||||
self.path = path
|
||||
self.isVideo = isVideo
|
||||
self.thumbnail = thumbnail
|
||||
@ -89,6 +93,7 @@ public final class MediaEditorDraft: Codable, Equatable {
|
||||
self.caption = caption
|
||||
self.privacy = privacy
|
||||
self.timestamp = timestamp
|
||||
self.location = location
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
@ -122,6 +127,12 @@ public final class MediaEditorDraft: Codable, Equatable {
|
||||
}
|
||||
|
||||
self.timestamp = try container.decodeIfPresent(Int32.self, forKey: .timestamp) ?? 1688909663
|
||||
|
||||
if let latitude = try container.decodeIfPresent(Double.self, forKey: .locationLatitude), let longitude = try container.decodeIfPresent(Double.self, forKey: .locationLongitude) {
|
||||
self.location = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
|
||||
} else {
|
||||
self.location = nil
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
@ -141,7 +152,7 @@ public final class MediaEditorDraft: Codable, Equatable {
|
||||
let chatInputText = ChatTextInputStateText(attributedText: self.caption)
|
||||
try container.encode(chatInputText, forKey: .caption)
|
||||
|
||||
if let privacy = self .privacy {
|
||||
if let privacy = self.privacy {
|
||||
if let data = try? JSONEncoder().encode(privacy) {
|
||||
try container.encode(data, forKey: .privacy)
|
||||
} else {
|
||||
@ -151,6 +162,14 @@ public final class MediaEditorDraft: Codable, Equatable {
|
||||
try container.encodeNil(forKey: .privacy)
|
||||
}
|
||||
try container.encode(self.timestamp, forKey: .timestamp)
|
||||
|
||||
if let location = self.location {
|
||||
try container.encode(location.latitude, forKey: .locationLatitude)
|
||||
try container.encode(location.longitude, forKey: .locationLongitude)
|
||||
} else {
|
||||
try container.encodeNil(forKey: .locationLatitude)
|
||||
try container.encodeNil(forKey: .locationLongitude)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -807,7 +807,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
containerSize: CGSize(width: 40.0, height: 40.0)
|
||||
)
|
||||
let textButtonFrame = CGRect(
|
||||
origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 5.0 * 2.0 - textButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + controlsBottomInset + 2.0),
|
||||
origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 5.0 * 2.0 - textButtonSize.width / 2.0 - 1.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + controlsBottomInset + 2.0),
|
||||
size: textButtonSize
|
||||
)
|
||||
if let textButtonView = self.textButton.view {
|
||||
@ -836,7 +836,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
containerSize: CGSize(width: 40.0, height: 40.0)
|
||||
)
|
||||
let stickerButtonFrame = CGRect(
|
||||
origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 5.0 * 3.0 - stickerButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + controlsBottomInset + 2.0),
|
||||
origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 5.0 * 3.0 - stickerButtonSize.width / 2.0 + 1.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + controlsBottomInset + 2.0),
|
||||
size: stickerButtonSize
|
||||
)
|
||||
if let stickerButtonView = self.stickerButton.view {
|
||||
@ -1746,7 +1746,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
emojiItems,
|
||||
stickerItems
|
||||
) |> map { emoji, stickers -> StickerPickerInputData in
|
||||
return StickerPickerInputData(emoji: emoji, stickers: stickers, masks: nil)
|
||||
return StickerPickerInputData(emoji: emoji, stickers: stickers, gifs: nil)
|
||||
}
|
||||
|
||||
stickerPickerInputData.set(signal)
|
||||
@ -2655,7 +2655,9 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
let tooltipController = SaveProgressScreen(context: self.context, content: .progress(text, 0.0))
|
||||
tooltipController.cancelled = { [weak self] in
|
||||
if let self, let controller = self.controller {
|
||||
controller.isSavingAvailable = true
|
||||
controller.cancelVideoExport()
|
||||
controller.requestLayout(transition: .animated(duration: 0.25, curve: .easeInOut))
|
||||
}
|
||||
}
|
||||
controller.present(tooltipController, in: .current)
|
||||
@ -2667,37 +2669,46 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
let galleryController = self.context.sharedContext.makeMediaPickerScreen(context: self.context, completion: { [weak self] result in
|
||||
guard let self, let asset = result as? PHAsset else {
|
||||
let galleryController = self.context.sharedContext.makeMediaPickerScreen(context: self.context, hasSearch: true, completion: { [weak self] result in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
let options = PHImageRequestOptions()
|
||||
options.deliveryMode = .highQualityFormat
|
||||
PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { [weak self] image, _ in
|
||||
if let self, let image {
|
||||
Queue.mainQueue().async {
|
||||
func roundedImageWithTransparentCorners(image: UIImage, cornerRadius: CGFloat) -> UIImage? {
|
||||
let rect = CGRect(origin: .zero, size: image.size)
|
||||
UIGraphicsBeginImageContextWithOptions(image.size, false, image.scale)
|
||||
let context = UIGraphicsGetCurrentContext()
|
||||
|
||||
if let context = context {
|
||||
let path = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius)
|
||||
context.addPath(path.cgPath)
|
||||
context.clip()
|
||||
image.draw(in: rect)
|
||||
}
|
||||
|
||||
let newImage = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
|
||||
return newImage
|
||||
func roundedImageWithTransparentCorners(image: UIImage, cornerRadius: CGFloat) -> UIImage? {
|
||||
let rect = CGRect(origin: .zero, size: image.size)
|
||||
UIGraphicsBeginImageContextWithOptions(image.size, false, image.scale)
|
||||
let context = UIGraphicsGetCurrentContext()
|
||||
|
||||
if let context = context {
|
||||
let path = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius)
|
||||
context.addPath(path.cgPath)
|
||||
context.clip()
|
||||
image.draw(in: rect)
|
||||
}
|
||||
|
||||
let newImage = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
|
||||
return newImage
|
||||
}
|
||||
|
||||
let completeWithImage: (UIImage) -> Void = { [weak self] image in
|
||||
let updatedImage = roundedImageWithTransparentCorners(image: image, cornerRadius: floor(image.size.width * 0.03))!
|
||||
self?.interaction?.insertEntity(DrawingStickerEntity(content: .image(updatedImage, .rectangle)), scale: 2.5)
|
||||
}
|
||||
|
||||
if let asset = result as? PHAsset {
|
||||
let options = PHImageRequestOptions()
|
||||
options.deliveryMode = .highQualityFormat
|
||||
PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { image, _ in
|
||||
if let image {
|
||||
Queue.mainQueue().async {
|
||||
completeWithImage(image)
|
||||
}
|
||||
let updatedImage = roundedImageWithTransparentCorners(image: image, cornerRadius: floor(image.size.width * 0.03))!
|
||||
self.interaction?.insertEntity(DrawingStickerEntity(content: .image(updatedImage, .rectangle)), scale: 2.5)
|
||||
}
|
||||
}
|
||||
} else if let image = result as? UIImage {
|
||||
completeWithImage(image)
|
||||
}
|
||||
})
|
||||
galleryController.customModalStyleOverlayTransitionFactorUpdated = { [weak self, weak galleryController] transition in
|
||||
@ -2945,7 +2956,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
switch mode {
|
||||
case .sticker:
|
||||
self.mediaEditor?.stop()
|
||||
let controller = StickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData.get(), defaultToEmoji: self.defaultToEmoji)
|
||||
let controller = StickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData.get(), defaultToEmoji: self.defaultToEmoji, hasGifs: true)
|
||||
controller.completion = { [weak self] content in
|
||||
if let self {
|
||||
if let content {
|
||||
@ -3767,10 +3778,15 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
let duration = mediaEditor.duration ?? 0.0
|
||||
|
||||
var timestamp: Int32
|
||||
var location: CLLocationCoordinate2D?
|
||||
if case let .draft(draft, _) = subject {
|
||||
timestamp = draft.timestamp
|
||||
location = draft.location
|
||||
} else {
|
||||
timestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
|
||||
if case let .asset(asset) = subject {
|
||||
location = asset.location?.coordinate
|
||||
}
|
||||
}
|
||||
|
||||
if let resultImage = mediaEditor.resultImage {
|
||||
@ -3786,7 +3802,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
if let thumbnailImage = generateScaledImage(image: resultImage, size: fittedSize) {
|
||||
let path = "\(Int64.random(in: .min ... .max)).jpg"
|
||||
if let data = image.jpegData(compressionQuality: 0.87) {
|
||||
let draft = MediaEditorDraft(path: path, isVideo: false, thumbnail: thumbnailImage, dimensions: dimensions, duration: nil, values: values, caption: caption, privacy: privacy, timestamp: timestamp)
|
||||
let draft = MediaEditorDraft(path: path, isVideo: false, thumbnail: thumbnailImage, dimensions: dimensions, duration: nil, values: values, caption: caption, privacy: privacy, timestamp: timestamp, location: location)
|
||||
try? data.write(to: URL(fileURLWithPath: draft.fullPath()))
|
||||
if let id {
|
||||
saveStorySource(engine: context.engine, item: draft, peerId: context.account.peerId, id: id)
|
||||
@ -3800,7 +3816,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
let saveVideoDraft: (String, PixelDimensions, Double) -> Void = { videoPath, dimensions, duration in
|
||||
if let thumbnailImage = generateScaledImage(image: resultImage, size: fittedSize) {
|
||||
let path = "\(Int64.random(in: .min ... .max)).mp4"
|
||||
let draft = MediaEditorDraft(path: path, isVideo: true, thumbnail: thumbnailImage, dimensions: dimensions, duration: duration, values: values, caption: caption, privacy: privacy, timestamp: timestamp)
|
||||
let draft = MediaEditorDraft(path: path, isVideo: true, thumbnail: thumbnailImage, dimensions: dimensions, duration: duration, values: values, caption: caption, privacy: privacy, timestamp: timestamp, location: location)
|
||||
try? FileManager.default.moveItem(atPath: videoPath, toPath: draft.fullPath())
|
||||
if let id {
|
||||
saveStorySource(engine: context.engine, item: draft, peerId: context.account.peerId, id: id)
|
||||
|
@ -300,6 +300,7 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
private let categoryTemplateItem = ComponentView<Empty>()
|
||||
private let peerTemplateItem = ComponentView<Empty>()
|
||||
private let optionTemplateItem = ComponentView<Empty>()
|
||||
private let footerTemplateItem = ComponentView<Empty>()
|
||||
|
||||
private let itemContainerView: UIView
|
||||
private var visibleSectionHeaders: [Int: ComponentView<Empty>] = [:]
|
||||
@ -1033,7 +1034,7 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
}
|
||||
)),
|
||||
maximumNumberOfLines: 0,
|
||||
lineSpacing: 0.2,
|
||||
lineSpacing: 0.1,
|
||||
highlightColor: UIColor(rgb: 0x007aff, alpha: 0.2),
|
||||
highlightAction: { attributes in
|
||||
if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] {
|
||||
@ -1166,7 +1167,7 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
itemTransition.setFrame(view: itemView, frame: itemFrame)
|
||||
}
|
||||
}
|
||||
} else if section.id == 2 {
|
||||
} else if section.id == 2 && section.itemCount > 0 {
|
||||
for i in 0 ..< component.optionItems.count {
|
||||
let itemFrame = CGRect(origin: CGPoint(x: itemLayout.sideInset, y: sectionOffset + section.insets.top + CGFloat(i) * section.itemHeight), size: CGSize(width: itemLayout.containerSize.width, height: section.itemHeight))
|
||||
if !visibleBounds.intersects(itemFrame) {
|
||||
@ -1645,7 +1646,6 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
|
||||
transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
|
||||
|
||||
let categoryItemSize = self.categoryTemplateItem.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(CategoryListItemComponent(
|
||||
@ -1697,6 +1697,76 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
containerSize: CGSize(width: itemsContainerWidth, height: 1000.0)
|
||||
)
|
||||
|
||||
var footersTotalHeight: CGFloat = 0.0
|
||||
if case let .stories(editing) = component.stateContext.subject {
|
||||
let body = MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.freeTextColor)
|
||||
let bold = MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.freeTextColor)
|
||||
let link = MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemAccentColor)
|
||||
|
||||
let firstFooterText: String
|
||||
if let grayListPeers = component.stateContext.stateValue?.grayListPeers, !grayListPeers.isEmpty {
|
||||
let footerValue = environment.strings.Story_Privacy_GrayListPeople(Int32(grayListPeers.count))
|
||||
firstFooterText = environment.strings.Story_Privacy_GrayListSelected(footerValue).string
|
||||
} else {
|
||||
firstFooterText = environment.strings.Story_Privacy_GrayListSelect
|
||||
}
|
||||
|
||||
let footerInset: CGFloat = 7.0
|
||||
let firstFooterSize = self.footerTemplateItem.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .markdown(text: firstFooterText, attributes: MarkdownAttributes(
|
||||
body: body,
|
||||
bold: bold,
|
||||
link: link,
|
||||
linkAttribute: { url in
|
||||
return ("URL", url)
|
||||
}
|
||||
)),
|
||||
maximumNumberOfLines: 0,
|
||||
lineSpacing: 0.1,
|
||||
highlightColor: .clear,
|
||||
highlightAction: { _ in
|
||||
return nil
|
||||
},
|
||||
tapAction: { _, _ in
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: itemsContainerWidth - 16.0 * 2.0, height: 1000.0)
|
||||
)
|
||||
footersTotalHeight += firstFooterSize.height + footerInset
|
||||
|
||||
if !editing {
|
||||
let footerValue = environment.strings.Story_Privacy_KeepOnMyPageHours(Int32(component.timeout / 3600))
|
||||
let secondFooterText = environment.strings.Story_Privacy_KeepOnMyPageInfo(footerValue).string
|
||||
let secondFooterSize = self.footerTemplateItem.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .markdown(text: secondFooterText, attributes: MarkdownAttributes(
|
||||
body: body,
|
||||
bold: bold,
|
||||
link: link,
|
||||
linkAttribute: { url in
|
||||
return ("URL", url)
|
||||
}
|
||||
)),
|
||||
maximumNumberOfLines: 0,
|
||||
lineSpacing: 0.1,
|
||||
highlightColor: .clear,
|
||||
highlightAction: { _ in
|
||||
return nil
|
||||
},
|
||||
tapAction: { _, _ in
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: itemsContainerWidth - 16.0 * 2.0, height: 1000.0)
|
||||
)
|
||||
footersTotalHeight += secondFooterSize.height + footerInset
|
||||
}
|
||||
}
|
||||
|
||||
var sections: [ItemLayout.Section] = []
|
||||
if let stateValue = self.effectiveStateValue {
|
||||
if case .stories = component.stateContext.subject {
|
||||
@ -1815,13 +1885,14 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
if environment.inputHeight != 0.0 || !self.navigationTextFieldState.text.isEmpty {
|
||||
topInset = 0.0
|
||||
} else {
|
||||
let inset: CGFloat
|
||||
var inset: CGFloat
|
||||
if case let .stories(editing) = component.stateContext.subject {
|
||||
if editing {
|
||||
inset = 478.0
|
||||
inset = 351.0
|
||||
} else {
|
||||
inset = 630.0
|
||||
inset = 464.0
|
||||
}
|
||||
inset += 10.0 + environment.safeInsets.bottom + 50.0 + footersTotalHeight
|
||||
} else {
|
||||
inset = 600.0
|
||||
}
|
||||
@ -2024,7 +2095,7 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
transition.setFrame(layer: self.bottomSeparatorLayer, frame: CGRect(origin: CGPoint(x: containerSideInset + sideInset, y: availableSize.height - bottomPanelHeight - 8.0 - UIScreenPixel), size: CGSize(width: containerWidth, height: UIScreenPixel)))
|
||||
|
||||
let itemContainerSize = CGSize(width: itemsContainerWidth, height: availableSize.height)
|
||||
let itemLayout = ItemLayout(style: itemLayoutStyle, containerSize: itemContainerSize, containerInset: containerInset, bottomInset: bottomPanelHeight, topInset: topInset, sideInset: sideInset, navigationHeight: navigationHeight, sections: sections)
|
||||
let itemLayout = ItemLayout(style: itemLayoutStyle, containerSize: itemContainerSize, containerInset: containerInset, bottomInset: footersTotalHeight, topInset: topInset, sideInset: sideInset, navigationHeight: navigationHeight, sections: sections)
|
||||
let previousItemLayout = self.itemLayout
|
||||
self.itemLayout = itemLayout
|
||||
|
||||
|
@ -84,6 +84,8 @@ swift_library(
|
||||
"//submodules/Speak",
|
||||
"//submodules/TranslateUI",
|
||||
"//submodules/TelegramNotices",
|
||||
"//submodules/MediaPlayer:UniversalMediaPlayer",
|
||||
"//submodules/TelegramUniversalVideoContent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -758,6 +758,9 @@ final class StoryItemContentComponent: Component {
|
||||
if maskView.subviews.isEmpty {
|
||||
let referenceSize = availableSize
|
||||
for mediaArea in component.item.mediaAreas {
|
||||
guard case .venue = mediaArea else {
|
||||
continue
|
||||
}
|
||||
let size = CGSize(width: mediaArea.coordinates.width / 100.0 * referenceSize.width, height: mediaArea.coordinates.height / 100.0 * referenceSize.height)
|
||||
let position = CGPoint(x: mediaArea.coordinates.x / 100.0 * referenceSize.width, y: mediaArea.coordinates.y / 100.0 * referenceSize.height)
|
||||
|
||||
|
@ -1847,8 +1847,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
return StickerPackScreen(context: context, updatedPresentationData: updatedPresentationData, mainStickerPack: mainStickerPack, stickerPacks: stickerPacks, loadedStickerPacks: loadedStickerPacks, parentNavigationController: parentNavigationController, sendSticker: sendSticker)
|
||||
}
|
||||
|
||||
public func makeMediaPickerScreen(context: AccountContext, completion: @escaping (Any) -> Void) -> ViewController {
|
||||
return mediaPickerController(context: context, completion: completion)
|
||||
public func makeMediaPickerScreen(context: AccountContext, hasSearch: Bool, completion: @escaping (Any) -> Void) -> ViewController {
|
||||
return mediaPickerController(context: context, hasSearch: hasSearch, completion: completion)
|
||||
}
|
||||
|
||||
public func makeStoryMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController {
|
||||
|
@ -17,14 +17,15 @@ public enum WebSearchMode {
|
||||
|
||||
public enum WebSearchControllerMode {
|
||||
case media(attachment: Bool, completion: (ChatContextResultCollection, TGMediaSelectionContext, TGMediaEditingContext, Bool) -> Void)
|
||||
case editor(completion: (UIImage) -> Void)
|
||||
case avatar(initialQuery: String?, completion: (UIImage) -> Void)
|
||||
|
||||
var mode: WebSearchMode {
|
||||
switch self {
|
||||
case .media:
|
||||
return .media
|
||||
case .avatar:
|
||||
return .avatar
|
||||
case .media, .editor:
|
||||
return .media
|
||||
case .avatar:
|
||||
return .avatar
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -81,7 +82,7 @@ public final class WebSearchController: ViewController {
|
||||
private var validLayout: ContainerViewLayout?
|
||||
|
||||
private let context: AccountContext
|
||||
private let mode: WebSearchControllerMode
|
||||
let mode: WebSearchControllerMode
|
||||
private let peer: EnginePeer?
|
||||
private let chatLocation: ChatLocation?
|
||||
private let configuration: EngineConfiguration.SearchBots
|
||||
@ -193,6 +194,8 @@ public final class WebSearchController: ViewController {
|
||||
var attachment = false
|
||||
if case let .media(attachmentValue, _) = mode {
|
||||
attachment = attachmentValue
|
||||
} else if case .editor = mode {
|
||||
attachment = true
|
||||
}
|
||||
let navigationContentNode = WebSearchNavigationContentNode(theme: presentationData.theme, strings: presentationData.strings, attachment: attachment)
|
||||
self.navigationContentNode = navigationContentNode
|
||||
@ -218,6 +221,8 @@ public final class WebSearchController: ViewController {
|
||||
selectionState = TGMediaSelectionContext()
|
||||
case .avatar:
|
||||
selectionState = nil
|
||||
case .editor:
|
||||
selectionState = nil
|
||||
}
|
||||
let editingState = TGMediaEditingContext()
|
||||
self.controllerInteraction = WebSearchControllerInteraction(openResult: { [weak self] result in
|
||||
@ -345,10 +350,13 @@ public final class WebSearchController: ViewController {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
var select = false
|
||||
if case let .avatar(initialQuery, _) = mode, let _ = initialQuery {
|
||||
if case let .avatar(initialQuery, _) = self.mode, let _ = initialQuery {
|
||||
select = true
|
||||
}
|
||||
if case let .media(attachment, _) = mode, attachment && !self.didPlayPresentationAnimation {
|
||||
if case let .media(attachment, _) = self.mode, attachment && !self.didPlayPresentationAnimation {
|
||||
self.didPlayPresentationAnimation = true
|
||||
self.controllerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
} else if case .editor = self.mode, !self.didPlayPresentationAnimation {
|
||||
self.didPlayPresentationAnimation = true
|
||||
self.controllerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
@ -414,7 +422,7 @@ public final class WebSearchController: ViewController {
|
||||
return state.state?.scope
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
case .avatar:
|
||||
case .avatar, .editor:
|
||||
scope = .single(.images)
|
||||
}
|
||||
|
||||
@ -467,9 +475,7 @@ public final class WebSearchController: ViewController {
|
||||
let delayRequest = true
|
||||
let signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = .single({ _ in return .contextRequestResult(nil, nil) })
|
||||
|
||||
guard let peerId = self.peer?.id else {
|
||||
return .single({ _ in return .contextRequestResult(nil, nil) })
|
||||
}
|
||||
let peerId = self.peer?.id ?? self.context.account.peerId
|
||||
|
||||
let botName: String?
|
||||
switch scope {
|
||||
|
@ -791,18 +791,29 @@ class WebSearchControllerNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
presentLegacyWebSearchEditor(context: self.context, theme: self.theme, result: currentResult, initialLayout: self.containerLayout?.0, updateHiddenMedia: { [weak self] id in
|
||||
self?.hiddenMediaId.set(.single(id))
|
||||
}, transitionHostView: { [weak self] in
|
||||
return self?.gridNode.view
|
||||
}, transitionView: { [weak self] result in
|
||||
return self?.transitionNode(for: result)?.transitionView()
|
||||
}, completed: { [weak self] result in
|
||||
if let strongSelf = self {
|
||||
strongSelf.controllerInteraction.avatarCompleted(result)
|
||||
strongSelf.cancel?()
|
||||
if let mode = self.controller?.mode, case let .editor(completion) = mode {
|
||||
if let item = legacyWebSearchItem(account: self.context.account, result: currentResult) {
|
||||
let _ = (item.originalImage
|
||||
|> deliverOnMainQueue).start(next: { image in
|
||||
if !image.degraded() {
|
||||
completion(image)
|
||||
}
|
||||
})
|
||||
}
|
||||
}, present: present)
|
||||
} else {
|
||||
presentLegacyWebSearchEditor(context: self.context, theme: self.theme, result: currentResult, initialLayout: self.containerLayout?.0, updateHiddenMedia: { [weak self] id in
|
||||
self?.hiddenMediaId.set(.single(id))
|
||||
}, transitionHostView: { [weak self] in
|
||||
return self?.gridNode.view
|
||||
}, transitionView: { [weak self] result in
|
||||
return self?.transitionNode(for: result)?.transitionView()
|
||||
}, completed: { [weak self] result in
|
||||
if let strongSelf = self {
|
||||
strongSelf.controllerInteraction.avatarCompleted(result)
|
||||
strongSelf.cancel?()
|
||||
}
|
||||
}, present: present)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user