GIFs in media editor

Image search in media editor
This commit is contained in:
Ilya Laktyushin 2023-08-07 08:38:02 +02:00
parent 9ddb8fa2d6
commit b71304a3d2
24 changed files with 1231 additions and 686 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -39,6 +39,7 @@ swift_library(
"//submodules/TelegramUI/Components/ChatControllerInteraction:ChatControllerInteraction",
"//submodules/FeaturedStickersScreen:FeaturedStickersScreen",
"//submodules/StickerPackPreviewUI",
"//submodules/TelegramUI/Components/EntityKeyboardGifContent:EntityKeyboardGifContent",
],
visibility = [
"//visibility:public",

View File

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

View File

@ -11,6 +11,7 @@ import AppBundle
import ChatControllerInteraction
import MultiplexedVideoNode
import ChatPresentationInterfaceState
import EntityKeyboardGifContent
final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
private let context: AccountContext

View File

@ -13,6 +13,7 @@ import ChatControllerInteraction
import MultiplexedVideoNode
import FeaturedStickersScreen
import StickerPeekUI
import EntityKeyboardGifContent
private let searchBarHeight: CGFloat = 52.0

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -84,6 +84,8 @@ swift_library(
"//submodules/Speak",
"//submodules/TranslateUI",
"//submodules/TelegramNotices",
"//submodules/MediaPlayer:UniversalMediaPlayer",
"//submodules/TelegramUniversalVideoContent",
],
visibility = [
"//visibility:public",

View File

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

View File

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

View File

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

View File

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