Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
+40
View File
@@ -0,0 +1,40 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "WebSearchUI",
module_name = "WebSearchUI",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/AccountContext:AccountContext",
"//submodules/LegacyComponents:LegacyComponents",
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
"//submodules/MergeLists:MergeLists",
"//submodules/GalleryUI:GalleryUI",
"//submodules/ChatListSearchItemHeader:ChatListSearchItemHeader",
"//submodules/TelegramUniversalVideoContent:TelegramUniversalVideoContent",
"//submodules/CheckNode:CheckNode",
"//submodules/PhotoResources:PhotoResources",
"//submodules/SearchBarNode:SearchBarNode",
"//submodules/ItemListUI:ItemListUI",
"//submodules/LegacyMediaPickerUI:LegacyMediaPickerUI",
"//submodules/SegmentedControlNode:SegmentedControlNode",
"//submodules/AppBundle:AppBundle",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/AttachmentUI:AttachmentUI",
"//submodules/RadialStatusNode:RadialStatusNode",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,97 @@
import Foundation
import UIKit
import LegacyComponents
import SwiftSignalKit
import TelegramCore
import SSignalKit
import Display
import TelegramPresentationData
import AccountContext
import LegacyUI
import LegacyMediaPickerUI
func presentLegacyWebSearchEditor(context: AccountContext, theme: PresentationTheme, result: ChatContextResult, initialLayout: ContainerViewLayout?, updateHiddenMedia: @escaping (String?) -> Void, transitionHostView: @escaping () -> UIView?, transitionView: @escaping (ChatContextResult) -> UIView?, completed: @escaping (UIImage) -> Void, present: @escaping (ViewController, Any?) -> Void) {
guard let item = legacyWebSearchItem(account: context.account, result: result) else {
return
}
var screenImage: Signal<UIImage?, NoError> = .single(nil)
if let resource = item.thumbnailResource {
screenImage = context.account.postbox.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: true)
|> map { maybeData -> UIImage? in
if maybeData.complete {
if let loadedData = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []), let image = UIImage(data: loadedData) {
return image
}
}
return nil
}
}
let _ = (screenImage
|> take(1)
|> deliverOnMainQueue).start(next: { screenImage in
let legacyController = LegacyController(presentation: .custom, theme: theme, initialLayout: initialLayout)
legacyController.statusBar.statusBarStyle = theme.rootController.statusBarStyle.style
let paintStickersContext = LegacyPaintStickersContext(context: context)
let controller = TGPhotoEditorController(context: legacyController.context, item: item, intent: TGPhotoEditorControllerAvatarIntent, adjustments: nil, caption: nil, screenImage: screenImage ?? UIImage(), availableTabs: TGPhotoEditorController.defaultTabs(forAvatarIntent: true), selectedTab: .cropTab)!
controller.stickersContext = paintStickersContext
legacyController.bind(controller: controller)
controller.editingContext = TGMediaEditingContext()
controller.didFinishEditing = { [weak controller] _, result, _, hasChanges, commit in
if !hasChanges {
return
}
if let result = result {
completed(result)
}
commit?()
controller?.dismiss(animated: true)
}
controller.requestThumbnailImage = { _ -> SSignal in
return item.thumbnailImageSignal()
}
controller.requestOriginalScreenSizeImage = { _, position -> SSignal in
return item.screenImageSignal(position)
}
controller.requestOriginalFullSizeImage = { _, position -> SSignal in
return item.originalImageSignal(position)
}
let fromView = transitionView(result)!
let transition = TGMediaAvatarEditorTransition(controller: controller, from: fromView)!
transition.transitionHostView = transitionHostView()
transition.referenceFrame = {
return fromView.frame
}
transition.referenceImageSize = {
return item.dimensions
}
transition.referenceScreenImageSignal = {
return item.screenImageSignal(0.0)
}
transition.imageReady = {
updateHiddenMedia(result.id)
}
controller.beginCustomTransitionOut = { [weak legacyController] outFrame, outView, completion in
transition.outReferenceFrame = outFrame
transition.repView = outView
transition.dismiss(animated: true, completion: {
updateHiddenMedia(nil)
if let completion = completion {
DispatchQueue.main.async {
completion()
}
}
legacyController?.dismiss()
})
}
present(legacyController, nil)
transition.present(animated: true)
})
}
@@ -0,0 +1,475 @@
import Foundation
import UIKit
import LegacyComponents
import SwiftSignalKit
import TelegramCore
import SSignalKit
import UIKit
import Display
import TelegramPresentationData
import AccountContext
import PhotoResources
import LegacyUI
import LegacyMediaPickerUI
import Postbox
class LegacyWebSearchItem: NSObject, TGMediaEditableItem, TGMediaSelectableItem {
var isVideo: Bool {
return false
}
var uniqueIdentifier: String! {
return self.result.id
}
let result: ChatContextResult
private(set) var thumbnailResource: TelegramMediaResource?
private(set) var imageResource: TelegramMediaResource?
let dimensions: CGSize
let thumbnailImage: Signal<UIImage, NoError>
let originalImage: Signal<UIImage, NoError>
let progress: Signal<Float, NoError>
init(result: ChatContextResult) {
self.result = result
self.dimensions = CGSize()
self.thumbnailImage = .complete()
self.originalImage = .complete()
self.progress = .complete()
}
init(result: ChatContextResult, thumbnailResource: TelegramMediaResource?, imageResource: TelegramMediaResource?, dimensions: CGSize, thumbnailImage: Signal<UIImage, NoError>, originalImage: Signal<UIImage, NoError>, progress: Signal<Float, NoError>) {
self.result = result
self.thumbnailResource = thumbnailResource
self.imageResource = imageResource
self.dimensions = dimensions
self.thumbnailImage = thumbnailImage
self.originalImage = originalImage
self.progress = progress
}
var originalSize: CGSize {
return self.dimensions
}
func thumbnailImageSignal() -> SSignal! {
return SSignal(generator: { subscriber -> SDisposable? in
let disposable = self.thumbnailImage.start(next: { image in
subscriber.putNext(image)
subscriber.putCompletion()
})
return SBlockDisposable(block: {
disposable.dispose()
})
})
}
func screenImageAndProgressSignal() -> SSignal {
return SSignal { subscriber in
let imageDisposable = self.originalImage.start(next: { image in
if !image.degraded() {
subscriber.putNext(1.0)
}
subscriber.putNext(image)
if !image.degraded() {
subscriber.putCompletion()
}
})
let progressDisposable = (self.progress
|> deliverOnMainQueue).start(next: { next in
subscriber.putNext(next)
})
return SBlockDisposable {
imageDisposable.dispose()
progressDisposable.dispose()
}
}
}
func screenImageSignal(_ position: TimeInterval) -> SSignal! {
return self.originalImageSignal(position)
}
func originalImageSignal(_ position: TimeInterval) -> SSignal! {
return SSignal(generator: { subscriber -> SDisposable? in
let disposable = self.originalImage.start(next: { image in
subscriber.putNext(image)
if !image.degraded() {
subscriber.putCompletion()
}
})
return SBlockDisposable(block: {
disposable.dispose()
})
})
}
}
private class LegacyWebSearchGalleryItem: TGModernGalleryImageItem, TGModernGalleryEditableItem, TGModernGallerySelectableItem {
var selectionContext: TGMediaSelectionContext!
var editingContext: TGMediaEditingContext!
var stickersContext: TGPhotoPaintStickersContext!
let item: LegacyWebSearchItem
init(item: LegacyWebSearchItem) {
self.item = item
super.init()
}
func editableMediaItem() -> TGMediaEditableItem! {
return self.item
}
func selectableMediaItem() -> TGMediaSelectableItem! {
return self.item
}
func toolbarTabs() -> TGPhotoEditorTab {
return [.cropTab, .paintTab, .toolsTab]
}
func uniqueId() -> String! {
return self.item.uniqueIdentifier
}
override func viewClass() -> AnyClass! {
return LegacyWebSearchGalleryItemView.self
}
override func isEqual(_ object: Any?) -> Bool {
if let item = object as? LegacyWebSearchGalleryItem {
return item.item.result.id == self.item.result.id
}
return false
}
}
private class LegacyWebSearchGalleryItemView: TGModernGalleryImageItemView, TGModernGalleryEditableItemView {
private let readyForTransition = SVariable()
@objc func setHiddenAsBeingEdited(_ hidden: Bool) {
self.imageView.isHidden = hidden
}
@objc func singleTap() {
if let item = item as? LegacyWebSearchGalleryItem, let selectionContext = item.selectionContext {
selectionContext.toggleItemSelection(item.selectableMediaItem(), success: nil)
}
}
override func readyForTransitionIn() -> SSignal! {
return self.readyForTransition.signal().take(1)
}
override func setItem(_ item: TGModernGalleryItem!, synchronously: Bool) {
if let item = item as? LegacyWebSearchGalleryItem {
self._setItem(item)
self.imageSize = TGFitSize(item.editableMediaItem().originalSize!, CGSize(width: 1600, height: 1600))
let signal = item.editingContext.imageSignal(for: item.editableMediaItem())?.map(toSignal: { result -> SSignal in
if let image = result as? UIImage {
return SSignal.single(image)
} else if result == nil, let mediaItem = item.editableMediaItem() as? LegacyWebSearchItem {
return mediaItem.screenImageAndProgressSignal()
} else {
return SSignal.complete()
}
})
self.imageView.setSignal(signal?.deliver(on: SQueue.main()).afterNext({ [weak self] next in
if let strongSelf = self, let image = next as? UIImage {
strongSelf.imageSize = image.size
strongSelf.reset()
strongSelf.readyForTransition.set(SSignal.single(true))
}
}))
self.reset()
} else {
self.imageView.setSignal(nil)
super.setItem(item, synchronously: synchronously)
}
}
override func contentView() -> UIView! {
return self.imageView
}
override func transitionContentView() -> UIView! {
return self.contentView()
}
override func transitionViewContentRect() -> CGRect {
let contentView = self.transitionContentView()!
return contentView.convert(contentView.bounds, to: self.transitionView())
}
}
func legacyWebSearchItem(account: Account, result: ChatContextResult) -> LegacyWebSearchItem? {
var thumbnailDimensions: CGSize?
var thumbnailResource: TelegramMediaResource?
var imageResource: TelegramMediaResource?
var imageDimensions = CGSize()
var immediateThumbnailData: Data?
let thumbnailSignal: Signal<UIImage, NoError>
let originalSignal: Signal<UIImage, NoError>
switch result {
case let .externalReference(externalReference):
if let content = externalReference.content {
imageResource = content.resource
}
if let thumbnail = externalReference.thumbnail {
thumbnailResource = thumbnail.resource
thumbnailDimensions = thumbnail.dimensions?.cgSize
}
if let dimensions = externalReference.content?.dimensions {
imageDimensions = dimensions.cgSize
}
case let .internalReference(internalReference):
immediateThumbnailData = internalReference.image?.immediateThumbnailData
if let image = internalReference.image {
if let imageRepresentation = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 1000, height: 800)) {
imageDimensions = imageRepresentation.dimensions.cgSize
imageResource = imageRepresentation.resource
}
if let thumbnailRepresentation = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 200, height: 100)) {
thumbnailDimensions = thumbnailRepresentation.dimensions.cgSize
thumbnailResource = thumbnailRepresentation.resource
}
}
}
if let imageResource = imageResource {
let progressSignal = account.postbox.mediaBox.resourceStatus(imageResource)
|> map { status -> Float in
switch status {
case .Local:
return 1.0
case .Remote, .Paused:
return 0.027
case let .Fetching(_, progress):
return max(progress, 0.1)
}
}
var representations: [TelegramMediaImageRepresentation] = []
if let thumbnailResource = thumbnailResource, let thumbnailDimensions = thumbnailDimensions {
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false))
}
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(imageDimensions), resource: imageResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false))
let tmpImage = TelegramMediaImage(imageId: EngineMedia.Id(namespace: 0, id: 0), representations: representations, immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
thumbnailSignal = chatMessagePhotoDatas(postbox: account.postbox, userLocation: .other, photoReference: .standalone(media: tmpImage), autoFetchFullSize: false)
|> mapToSignal { value -> Signal<UIImage, NoError> in
let thumbnailData = value._0
if let data = thumbnailData, let image = UIImage(data: data) {
return .single(image)
} else {
return .complete()
}
}
originalSignal = chatMessagePhotoDatas(postbox: account.postbox, userLocation: .other, photoReference: .standalone(media: tmpImage), autoFetchFullSize: true)
|> mapToSignal { value -> Signal<UIImage, NoError> in
let thumbnailData = value._0
let fullSizeData = value._1
let fullSizeComplete = value._3
if fullSizeComplete, let data = fullSizeData, let image = UIImage(data: data) {
return .single(image)
} else if let data = thumbnailData, let image = UIImage(data: data) {
image.setDegraded(true)
return .single(image)
} else {
return .complete()
}
}
return LegacyWebSearchItem(result: result, thumbnailResource: thumbnailResource, imageResource: imageResource, dimensions: imageDimensions, thumbnailImage: thumbnailSignal, originalImage: originalSignal, progress: progressSignal)
} else {
return nil
}
}
private func galleryItems(account: Account, results: [ChatContextResult], current: ChatContextResult, selectionContext: TGMediaSelectionContext?, editingContext: TGMediaEditingContext) -> ([TGModernGalleryItem], TGModernGalleryItem?) {
var focusItem: TGModernGalleryItem?
var galleryItems: [TGModernGalleryItem] = []
for result in results {
if let item = legacyWebSearchItem(account: account, result: result) {
let galleryItem = LegacyWebSearchGalleryItem(item: item)
galleryItem.selectionContext = selectionContext
galleryItem.editingContext = editingContext
if result.id == current.id {
focusItem = galleryItem
}
galleryItems.append(galleryItem)
}
}
return (galleryItems, focusItem)
}
func presentLegacyWebSearchGallery(context: AccountContext, peer: EnginePeer?, threadTitle: String?, chatLocation: ChatLocation?, presentationData: PresentationData, results: [ChatContextResult], current: ChatContextResult, selectionContext: TGMediaSelectionContext?, editingContext: TGMediaEditingContext, updateHiddenMedia: @escaping (String?) -> Void, initialLayout: ContainerViewLayout?, transitionHostView: @escaping () -> UIView?, transitionView: @escaping (ChatContextResult) -> UIView?, completed: @escaping (ChatContextResult) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, present: (ViewController, Any?) -> Void) {
let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme, initialLayout: nil)
legacyController.statusBar.statusBarStyle = presentationData.theme.rootController.statusBarStyle.style
let recipientName: String?
if let threadTitle {
recipientName = threadTitle
} else {
if peer?.id == context.account.peerId {
recipientName = presentationData.strings.DialogList_SavedMessages
} else {
recipientName = peer?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
}
}
let paintStickersContext = LegacyPaintStickersContext(context: context)
paintStickersContext.captionPanelView = {
return getCaptionPanelView()
}
let controller = TGModernGalleryController(context: legacyController.context)!
controller.asyncTransitionIn = true
legacyController.bind(controller: controller)
let (items, focusItem) = galleryItems(account: context.account, results: results, current: current, selectionContext: selectionContext, editingContext: editingContext)
let model = TGMediaPickerGalleryModel(context: legacyController.context, items: items, focus: focusItem, selectionContext: selectionContext, editingContext: editingContext, hasCaptions: false, allowCaptionEntities: true, hasTimer: false, onlyCrop: false, inhibitDocumentCaptions: false, hasSelectionPanel: false, hasCamera: false, recipientName: recipientName, isScheduledMessages: false, hasCoverButton: false)!
model.stickersContext = paintStickersContext
controller.model = model
model.controller = controller
model.useGalleryImageAsEditableItemImage = true
model.storeOriginalImageForItem = { item, image in
editingContext.setOriginalImage(image, for: item, synchronous: false)
}
model.willFinishEditingItem = { item, adjustments, representation, hasChanges in
if hasChanges {
editingContext.setAdjustments(adjustments, for: item)
}
editingContext.setTemporaryRep(representation, for: item)
if let selectionContext = selectionContext, adjustments != nil, let item = item as? TGMediaSelectableItem {
selectionContext.setItem(item, selected: true)
}
}
model.didFinishEditingItem = { item, adjustments, result, thumbnail in
editingContext.setImage(result, thumbnailImage: thumbnail, for: item, synchronous: true)
}
model.saveItemCaption = { item, caption in
editingContext.setCaption(caption, for: item)
if let selectionContext = selectionContext, let caption = caption, caption.length > 0, let item = item as? TGMediaSelectableItem {
selectionContext.setItem(item, selected: true)
}
}
if let selectionContext = selectionContext {
model.interfaceView.updateSelectionInterface(selectionContext.count(), counterVisible: selectionContext.count() > 0, animated: false)
}
model.interfaceView.donePressed = { item in
if let item = item as? LegacyWebSearchGalleryItem {
controller.dismissWhenReady(animated: true)
completed(item.item.result)
}
}
controller.transitionHost = {
return transitionHostView()
}
var transitionedIn = false
controller.itemFocused = { item in
if let item = item as? LegacyWebSearchGalleryItem, transitionedIn {
updateHiddenMedia(item.item.result.id)
}
}
controller.beginTransitionIn = { item, _ in
if let item = item as? LegacyWebSearchGalleryItem {
return transitionView(item.item.result)
} else {
return nil
}
}
controller.startedTransitionIn = {
transitionedIn = true
updateHiddenMedia(current.id)
}
controller.beginTransitionOut = { item, _ in
if let item = item as? LegacyWebSearchGalleryItem {
return transitionView(item.item.result)
} else {
return nil
}
}
controller.completedTransitionOut = { [weak legacyController] in
updateHiddenMedia(nil)
legacyController?.dismiss()
}
present(legacyController, nil)
}
public func legacyEnqueueWebSearchMessages(_ selectionState: TGMediaSelectionContext, _ editingState: TGMediaEditingContext, enqueueChatContextResult: (ChatContextResult) -> Void, enqueueMediaMessages: ([Any]) -> Void)
{
var results: [ChatContextResult] = []
for item in selectionState.selectedItems() {
if let item = item as? LegacyWebSearchItem {
results.append(item.result)
}
}
if !results.isEmpty {
var signals: [Any] = []
for result in results {
let editableItem = LegacyWebSearchItem(result: result)
if let adjustments = editingState.adjustments(for: editableItem) {
let animated = adjustments.paintingData?.hasAnimation ?? false
if let imageSignal = editingState.imageSignal(for: editableItem) {
let signal = imageSignal.map { image -> Any in
if let image = image as? UIImage {
var dict: [AnyHashable: Any] = [
"type": "editedPhoto",
"image": image
]
if animated {
dict["isAnimation"] = true
if let photoEditorValues = adjustments as? PGPhotoEditorValues {
dict["adjustments"] = TGVideoEditAdjustments(photoEditorValues: photoEditorValues, preset: TGMediaVideoConversionPresetAnimation)
}
let filePath = NSTemporaryDirectory().appending("/gifvideo_\(arc4random()).jpg")
let data = image.jpegData(compressionQuality: 0.8)
if let data = data {
let _ = try? data.write(to: URL(fileURLWithPath: filePath), options: [])
}
dict["url"] = NSURL(fileURLWithPath: filePath)
if adjustments.cropApplied(forAvatar: false) || adjustments.hasPainting() || adjustments.toolsApplied() {
var paintingImage: UIImage? = adjustments.paintingData?.stillImage
if paintingImage == nil {
paintingImage = adjustments.paintingData?.image
}
let thumbnailImage = TGPhotoEditorVideoExtCrop(image, paintingImage, adjustments.cropOrientation, adjustments.cropRotation, adjustments.cropRect, adjustments.cropMirrored, TGScaleToFill(image.size, CGSize(width: 512.0, height: 512.0)), adjustments.originalSize, true, true, true, false)
if let thumbnailImage = thumbnailImage {
dict["previewImage"] = thumbnailImage
}
}
}
return legacyAssetPickerItemGenerator()(dict, nil, nil, nil) as Any
} else {
return SSignal.complete()
}
}
signals.append(signal as Any)
}
} else {
enqueueChatContextResult(result)
}
}
if !signals.isEmpty {
enqueueMediaMessages(signals)
}
}
}
@@ -0,0 +1,96 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
final class WebSearchBadgeNode: ASDisplayNode {
private var fillColor: UIColor
private var strokeColor: UIColor
private var textColor: UIColor
private let textNode: ASTextNode
private let backgroundNode: ASImageNode
private let font: UIFont = Font.with(size: 17.0, design: .round, weight: .bold)
var text: String = "" {
didSet {
self.textNode.attributedText = NSAttributedString(string: self.text, font: self.font, textColor: self.textColor)
self.invalidateCalculatedLayout()
}
}
convenience init(theme: PresentationTheme) {
self.init(fillColor: theme.list.itemCheckColors.fillColor, strokeColor: theme.list.itemCheckColors.fillColor, textColor: theme.list.itemCheckColors.foregroundColor)
}
init(fillColor: UIColor, strokeColor: UIColor, textColor: UIColor) {
self.fillColor = fillColor
self.strokeColor = strokeColor
self.textColor = textColor
self.textNode = ASTextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.displaysAsynchronously = false
self.backgroundNode = ASImageNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.displayWithoutProcessing = true
self.backgroundNode.displaysAsynchronously = false
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 22.0, color: fillColor, strokeColor: strokeColor, strokeWidth: 1.0)
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.textNode)
}
func updateTheme(fillColor: UIColor, strokeColor: UIColor, textColor: UIColor) {
self.fillColor = fillColor
self.strokeColor = strokeColor
self.textColor = textColor
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 22.0, color: fillColor, strokeColor: strokeColor, strokeWidth: 1.0)
self.textNode.attributedText = NSAttributedString(string: self.text, font: self.font, textColor: self.textColor)
}
func animateBump(incremented: Bool) {
if incremented {
let firstTransition = ContainedViewLayoutTransition.animated(duration: 0.1, curve: .easeInOut)
firstTransition.updateTransformScale(layer: self.backgroundNode.layer, scale: 1.2)
firstTransition.updateTransformScale(layer: self.textNode.layer, scale: 1.2, completion: { finished in
if finished {
let secondTransition = ContainedViewLayoutTransition.animated(duration: 0.1, curve: .easeInOut)
secondTransition.updateTransformScale(layer: self.backgroundNode.layer, scale: 1.0)
secondTransition.updateTransformScale(layer: self.textNode.layer, scale: 1.0)
}
})
} else {
let firstTransition = ContainedViewLayoutTransition.animated(duration: 0.1, curve: .easeInOut)
firstTransition.updateTransformScale(layer: self.backgroundNode.layer, scale: 0.8)
firstTransition.updateTransformScale(layer: self.textNode.layer, scale: 0.8, completion: { finished in
if finished {
let secondTransition = ContainedViewLayoutTransition.animated(duration: 0.1, curve: .easeInOut)
secondTransition.updateTransformScale(layer: self.backgroundNode.layer, scale: 1.0)
secondTransition.updateTransformScale(layer: self.textNode.layer, scale: 1.0)
}
})
}
}
func animateOut() {
let timingFunction = CAMediaTimingFunctionName.easeInEaseOut.rawValue
self.backgroundNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.3, delay: 0.0, timingFunction: timingFunction, removeOnCompletion: true, completion: nil)
self.textNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.3, delay: 0.0, timingFunction: timingFunction, removeOnCompletion: true, completion: nil)
}
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
let badgeSize = self.textNode.measure(constrainedSize)
let backgroundSize = CGSize(width: max(22.0, badgeSize.width + 12.0), height: 22.0)
let backgroundFrame = CGRect(origin: CGPoint(), size: backgroundSize)
self.backgroundNode.frame = backgroundFrame
self.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(backgroundFrame.midX - badgeSize.width / 2.0), y: floorToScreenPixels((backgroundFrame.size.height - badgeSize.height) / 2.0) - UIScreenPixel), size: badgeSize)
return backgroundSize
}
}
@@ -0,0 +1,608 @@
import Foundation
import UIKit
import SwiftSignalKit
import Display
import AsyncDisplayKit
import TelegramCore
import LegacyComponents
import TelegramUIPreferences
import TelegramPresentationData
import AccountContext
import AttachmentUI
public enum WebSearchMode {
case media
case avatar
}
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, .editor:
return .media
case .avatar:
return .avatar
}
}
}
final class WebSearchControllerInteraction {
let openResult: (ChatContextResult) -> Void
let setSearchQuery: (String) -> Void
let deleteRecentQuery: (String) -> Void
let toggleSelection: (ChatContextResult, Bool) -> Bool
let sendSelected: (ChatContextResult?, Bool, Int32?, ChatSendMessageActionSheetController.SendParameters?) -> Void
let schedule: (ChatSendMessageActionSheetController.SendParameters?) -> Void
let avatarCompleted: (UIImage) -> Void
let selectionState: TGMediaSelectionContext?
let editingState: TGMediaEditingContext
var hiddenMediaId: String?
init(openResult: @escaping (ChatContextResult) -> Void, setSearchQuery: @escaping (String) -> Void, deleteRecentQuery: @escaping (String) -> Void, toggleSelection: @escaping (ChatContextResult, Bool) -> Bool, sendSelected: @escaping (ChatContextResult?, Bool, Int32?, ChatSendMessageActionSheetController.SendParameters?) -> Void, schedule: @escaping (ChatSendMessageActionSheetController.SendParameters?) -> Void, avatarCompleted: @escaping (UIImage) -> Void, selectionState: TGMediaSelectionContext?, editingState: TGMediaEditingContext) {
self.openResult = openResult
self.setSearchQuery = setSearchQuery
self.deleteRecentQuery = deleteRecentQuery
self.toggleSelection = toggleSelection
self.sendSelected = sendSelected
self.schedule = schedule
self.avatarCompleted = avatarCompleted
self.selectionState = selectionState
self.editingState = editingState
}
}
private func selectionChangedSignal(selectionState: TGMediaSelectionContext) -> Signal<Void, NoError> {
return Signal { subscriber in
let disposable = selectionState.selectionChangedSignal()?.start(next: { next in
subscriber.putNext(Void())
}, completed: {})
return ActionDisposable {
disposable?.dispose()
}
}
}
public struct WebSearchConfiguration: Equatable {
public let gifProvider: String?
public init(appConfiguration: AppConfiguration) {
var gifProvider: String? = nil
if let data = appConfiguration.data, let value = data["gif_search_branding"] as? String {
gifProvider = value
}
self.gifProvider = gifProvider
}
}
public final class WebSearchController: ViewController {
private var validLayout: ContainerViewLayout?
private let context: AccountContext
let mode: WebSearchControllerMode
private let peer: EnginePeer?
private let chatLocation: ChatLocation?
private let configuration: EngineConfiguration.SearchBots
private let activateOnDisplay: Bool
private var controllerNode: WebSearchControllerNode {
return self.displayNode as! WebSearchControllerNode
}
private var _ready = Promise<Bool>()
override public var ready: Promise<Bool> {
return self._ready
}
private var didPlayPresentationAnimation = false
private var controllerInteraction: WebSearchControllerInteraction?
private var interfaceState: WebSearchInterfaceState
private let interfaceStatePromise = ValuePromise<WebSearchInterfaceState>()
private var disposable: Disposable?
private let resultsDisposable = MetaDisposable()
private var selectionDisposable: Disposable?
private var navigationContentNode: WebSearchNavigationContentNode?
public var getCaptionPanelView: () -> TGCaptionPanelView? = { return nil } {
didSet {
self.controllerNode.getCaptionPanelView = self.getCaptionPanelView
}
}
public var presentSchedulePicker: (Bool, @escaping (Int32) -> Void) -> Void = { _, _ in }
public var dismissed: () -> Void = { }
public var searchingUpdated: (Bool) -> Void = { _ in }
public var attemptItemSelection: (ChatContextResult) -> Bool = { _ in return true }
private var searchQueryPromise = ValuePromise<String>()
private var searchQueryDisposable: Disposable?
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peer: EnginePeer?, chatLocation: ChatLocation?, configuration: EngineConfiguration.SearchBots, mode: WebSearchControllerMode, activateOnDisplay: Bool = true) {
self.context = context
self.mode = mode
self.peer = peer
self.chatLocation = chatLocation
self.configuration = configuration
self.activateOnDisplay = activateOnDisplay
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
self.interfaceState = WebSearchInterfaceState(presentationData: presentationData)
var searchQuery: String?
if case let .avatar(initialQuery, _) = mode, let query = initialQuery {
searchQuery = query
self.interfaceState = self.interfaceState.withUpdatedQuery(query)
}
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: presentationData.theme).withUpdatedSeparatorColor(presentationData.theme.list.plainBackgroundColor).withUpdatedBackgroundColor(presentationData.theme.list.plainBackgroundColor), strings: NavigationBarStrings(presentationStrings: presentationData.strings)))
self.statusBar.statusBarStyle = presentationData.theme.rootController.statusBarStyle.style
self.scrollToTop = { [weak self] in
if let strongSelf = self {
strongSelf.controllerNode.scrollToTop(animated: true)
}
}
let settings = self.context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.webSearchSettings])
|> map { sharedData -> WebSearchSettings in
if let current = sharedData.entries[ApplicationSpecificSharedDataKeys.webSearchSettings]?.get(WebSearchSettings.self) {
return current
} else {
return WebSearchSettings.defaultSettings
}
}
let gifProvider = self.context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration])
|> map { view -> String? in
guard let appConfiguration = view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) else {
return nil
}
let configuration = WebSearchConfiguration(appConfiguration: appConfiguration)
return configuration.gifProvider
}
|> distinctUntilChanged
self.disposable = ((combineLatest(settings, (updatedPresentationData?.signal ?? context.sharedContext.presentationData), gifProvider))
|> deliverOnMainQueue).start(next: { [weak self] settings, presentationData, gifProvider in
guard let strongSelf = self else {
return
}
strongSelf.updateInterfaceState { current -> WebSearchInterfaceState in
var updated = current
if case .media = mode, current.state?.scope != settings.scope {
updated = updated.withUpdatedScope(settings.scope)
}
if current.presentationData !== presentationData {
updated = updated.withUpdatedPresentationData(presentationData)
}
if current.gifProvider != gifProvider {
updated = updated.withUpdatedGifProvider(gifProvider)
}
return updated
}
})
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
navigationContentNode.setQueryUpdated { [weak self] query in
if let strongSelf = self, strongSelf.isNodeLoaded {
strongSelf.searchQueryPromise.set(query)
strongSelf.searchingUpdated(!query.isEmpty)
}
}
navigationContentNode.cancel = { [weak self] in
if let strongSelf = self {
strongSelf.cancel()
}
}
self.navigationBar?.setContentNode(navigationContentNode, animated: false)
if let query = searchQuery {
navigationContentNode.setQuery(query)
}
let selectionState: TGMediaSelectionContext?
switch self.mode {
case .media:
selectionState = TGMediaSelectionContext()
case .avatar:
selectionState = nil
case .editor:
selectionState = nil
}
let editingState = TGMediaEditingContext()
self.controllerInteraction = WebSearchControllerInteraction(openResult: { [weak self] result in
if let strongSelf = self {
strongSelf.controllerNode.openResult(currentResult: result, present: { [weak self] viewController, arguments in
if let strongSelf = self {
strongSelf.present(viewController, in: .window(.root), with: arguments, blockInteraction: true)
}
})
}
}, setSearchQuery: { [weak self] query in
if let strongSelf = self {
strongSelf.navigationContentNode?.setQuery(query)
strongSelf.updateSearchQuery(query)
strongSelf.navigationContentNode?.deactivate()
}
}, deleteRecentQuery: { [weak self] query in
if let strongSelf = self {
let _ = removeRecentWebSearchQuery(engine: strongSelf.context.engine, string: query).start()
}
}, toggleSelection: { [weak self] result, value in
if let strongSelf = self {
if !strongSelf.attemptItemSelection(result) {
return false
}
let item = LegacyWebSearchItem(result: result)
strongSelf.controllerInteraction?.selectionState?.setItem(item, selected: value)
return true
} else {
return false
}
}, sendSelected: { [weak self] current, silently, scheduleTime, messageEffect in
if let selectionState = selectionState, let results = self?.controllerNode.currentExternalResults {
if let current = current {
let currentItem = LegacyWebSearchItem(result: current)
selectionState.setItem(currentItem, selected: true)
}
if case let .media(_, sendSelected) = mode {
sendSelected(results, selectionState, editingState, false)
}
}
}, schedule: { [weak self] messageEffect in
if let strongSelf = self {
strongSelf.presentSchedulePicker(false, { [weak self] time in
self?.controllerInteraction?.sendSelected(nil, false, time, nil)
})
}
}, avatarCompleted: { result in
if case let .avatar(_, avatarCompleted) = mode {
avatarCompleted(result)
}
}, selectionState: selectionState, editingState: editingState)
selectionState?.attemptSelectingItem = { [weak self] item in
guard let self else {
return false
}
if let item = item as? LegacyWebSearchItem {
return self.attemptItemSelection(item.result)
}
return true
}
if let selectionState = selectionState {
self.selectionDisposable = (selectionChangedSignal(selectionState: selectionState)
|> deliverOnMainQueue).start(next: { [weak self] _ in
if let strongSelf = self {
strongSelf.controllerNode.updateSelectionState(animated: true)
}
})
}
let throttledSearchQuery = self.searchQueryPromise.get()
|> mapToSignal { query -> Signal<String, NoError> in
if !query.isEmpty {
return (.complete() |> delay(1.0, queue: Queue.mainQueue()))
|> then(.single(query))
} else {
return .single(query)
}
}
self.searchQueryDisposable = (throttledSearchQuery
|> deliverOnMainQueue).start(next: { [weak self] query in
if let self {
self.updateSearchQuery(query)
}
})
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.disposable?.dispose()
self.resultsDisposable.dispose()
self.selectionDisposable?.dispose()
self.searchQueryDisposable?.dispose()
}
public func cancel() {
self.controllerNode.dismissInput?()
self.controllerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak self] _ in
self?.dismissed()
self?.dismiss()
})
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if let presentationArguments = self.presentationArguments as? ViewControllerPresentationArguments, !self.didPlayPresentationAnimation {
self.didPlayPresentationAnimation = true
if case .modalSheet = presentationArguments.presentationAnimation {
self.controllerNode.animateIn()
}
}
}
private var didActivateSearch = false
override public func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
var select = false
if case let .avatar(initialQuery, _) = self.mode, let _ = initialQuery {
select = true
}
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)
}
if !self.didActivateSearch && self.activateOnDisplay {
self.didActivateSearch = true
self.navigationContentNode?.activate(select: select)
}
}
override public func loadDisplayNode() {
var attachment: Bool = false
if case let .media(attachmentValue, _) = self.mode, attachmentValue {
attachment = true
}
self.displayNode = WebSearchControllerNode(controller: self, context: self.context, presentationData: self.interfaceState.presentationData, controllerInteraction: self.controllerInteraction!, peer: self.peer, chatLocation: self.chatLocation, mode: self.mode.mode, attachment: attachment)
self.controllerNode.requestUpdateInterfaceState = { [weak self] animated, f in
if let strongSelf = self {
strongSelf.updateInterfaceState(f)
}
}
self.controllerNode.cancel = { [weak self] in
if let strongSelf = self {
strongSelf.dismiss()
}
}
self.controllerNode.dismissInput = { [weak self] in
if let strongSelf = self {
strongSelf.navigationContentNode?.deactivate()
}
}
self.controllerNode.updateInterfaceState(self.interfaceState, animated: false)
self._ready.set(.single(true))
self.displayNodeDidLoad()
}
private func updateInterfaceState(animated: Bool = true, _ f: (WebSearchInterfaceState) -> WebSearchInterfaceState) {
let previousInterfaceState = self.interfaceState
let previousTheme = self.interfaceState.presentationData.theme
let previousStrings = self.interfaceState.presentationData.theme
let previousGifProvider = self.interfaceState.gifProvider
let updatedInterfaceState = f(self.interfaceState)
self.interfaceState = updatedInterfaceState
self.interfaceStatePromise.set(updatedInterfaceState)
if self.isNodeLoaded {
if previousTheme !== updatedInterfaceState.presentationData.theme || previousStrings !== updatedInterfaceState.presentationData.strings || previousGifProvider != updatedInterfaceState.gifProvider {
self.controllerNode.updatePresentationData(theme: updatedInterfaceState.presentationData.theme, strings: updatedInterfaceState.presentationData.strings)
}
if previousInterfaceState != self.interfaceState {
self.controllerNode.updateInterfaceState(self.interfaceState, animated: animated)
}
}
}
private func updateSearchQuery(_ query: String) {
let scope: Signal<WebSearchScope?, NoError>
switch self.mode {
case .media:
scope = self.interfaceStatePromise.get()
|> map { state -> WebSearchScope? in
return state.state?.scope
}
|> distinctUntilChanged
case .avatar, .editor:
scope = .single(.images)
}
self.updateInterfaceState { $0.withUpdatedQuery(query) }
let scopes: [WebSearchScope: Promise<((ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, Bool)>] = [.images: Promise(initializeOnFirstAccess: self.signalForQuery(query, scope: .images)
|> mapToSignal { result -> Signal<((ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, Bool), NoError> in
return .single((result, false))
|> then(.single((result, true)))
}), .gifs: Promise(initializeOnFirstAccess: self.signalForQuery(query, scope: .gifs)
|> mapToSignal { result -> Signal<((ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, Bool), NoError> in
return .single((result, false))
|> then(.single((result, true)))
})]
var results = scope
|> mapToSignal { scope -> (Signal<((ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, Bool), NoError>) in
if let scope = scope, let scopeResults = scopes[scope] {
return scopeResults.get()
} else {
return .complete()
}
}
if query.isEmpty {
results = .single(({ _ in return nil}, false))
self.navigationContentNode?.setActivity(false)
}
let previousResults = Atomic<(ChatContextResultCollection, Bool)?>(value: nil)
self.resultsDisposable.set((results
|> deliverOnMainQueue).start(next: { [weak self] result, immediate in
if let strongSelf = self {
if let result = result(nil), case let .contextRequestResult(_, results) = result {
if let results = results {
let previous = previousResults.swap((results, immediate))
if let previous = previous, previous.0.queryId == results.queryId && !previous.1 {
} else {
strongSelf.controllerNode.updateResults(results, immediate: immediate)
}
}
} else {
strongSelf.controllerNode.updateResults(nil)
}
}
}))
}
private func signalForQuery(_ query: String, scope: WebSearchScope) -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> {
let delayRequest = true
let signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = .single({ _ in return .contextRequestResult(nil, nil) })
let peerId = self.peer?.id ?? self.context.account.peerId
let botName: String?
switch scope {
case .images:
botName = self.configuration.imageBotUsername
case .gifs:
botName = self.configuration.gifBotUsername
}
guard let name = botName else {
return .single({ _ in return .contextRequestResult(nil, nil) })
}
let context = self.context
let contextBot = self.context.engine.peers.resolvePeerByName(name: name, referrer: nil)
|> mapToSignal { result -> Signal<EnginePeer?, NoError> in
guard case let .result(result) = result else {
return .complete()
}
return .single(result)
}
|> mapToSignal { peer -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, 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: peerId, limit: 64)
|> map { results -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in
return { _ in
return .contextRequestResult(.user(user), results?.results)
}
}
let botResult: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = .single({ previousResult in
var passthroughPreviousResult: ChatContextResultCollection?
if let previousResult = previousResult {
if case let .contextRequestResult(previousUser, previousResults) = previousResult {
if previousUser?.id == user.id {
passthroughPreviousResult = previousResults
}
}
}
return .contextRequestResult(nil, passthroughPreviousResult)
})
let maybeDelayedContextResults: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError>
if delayRequest {
maybeDelayedContextResults = results |> delay(0.4, queue: Queue.concurrentDefaultQueue())
} else {
maybeDelayedContextResults = results
}
return botResult |> then(maybeDelayedContextResults)
} else {
return .single({ _ in return nil })
}
}
return (signal |> then(contextBot))
|> deliverOnMainQueue
|> beforeStarted { [weak self] in
if let strongSelf = self {
strongSelf.navigationContentNode?.setActivity(true)
}
}
|> afterCompleted { [weak self] in
if let strongSelf = self {
strongSelf.navigationContentNode?.setActivity(false)
}
}
}
public var mediaPickerContext: WebSearchPickerContext? {
if let interaction = self.controllerInteraction {
return WebSearchPickerContext(interaction: interaction)
} else {
return nil
}
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.validLayout = layout
let navigationBarHeight = self.navigationLayout(layout: layout).navigationFrame.maxY
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
}
}
public class WebSearchPickerContext: AttachmentMediaPickerContext {
private weak var interaction: WebSearchControllerInteraction?
public var selectionCount: Signal<Int, NoError> {
return Signal { [weak self] subscriber in
let disposable = self?.interaction?.selectionState?.selectionChangedSignal().start(next: { [weak self] value in
subscriber.putNext(Int(self?.interaction?.selectionState?.count() ?? 0))
}, error: { _ in }, completed: { })
return ActionDisposable {
disposable?.dispose()
}
}
}
public var caption: Signal<NSAttributedString?, NoError> {
return Signal { [weak self] subscriber in
let disposable = self?.interaction?.editingState.forcedCaption().start(next: { caption in
if let caption = caption as? NSAttributedString {
subscriber.putNext(caption)
} else {
subscriber.putNext(nil)
}
}, error: { _ in }, completed: { })
return ActionDisposable {
disposable?.dispose()
}
}
}
init(interaction: WebSearchControllerInteraction) {
self.interaction = interaction
}
public func setCaption(_ caption: NSAttributedString) {
self.interaction?.editingState.setForcedCaption(caption, skipUpdate: true)
}
public func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode, parameters: ChatSendMessageActionSheetController.SendParameters?) {
self.interaction?.sendSelected(nil, mode == .silently, nil, parameters)
}
public func schedule(parameters: ChatSendMessageActionSheetController.SendParameters?) {
self.interaction?.schedule(parameters)
}
}
@@ -0,0 +1,829 @@
import Foundation
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
import Display
import TelegramCore
import LegacyComponents
import TelegramPresentationData
import TelegramUIPreferences
import MergeLists
import AccountContext
import GalleryUI
import ChatListSearchItemHeader
import SegmentedControlNode
import AppBundle
private struct WebSearchContextResultStableId: Hashable {
let result: ChatContextResult
func hash(into hasher: inout Hasher) {
hasher.combine(result.id)
}
static func ==(lhs: WebSearchContextResultStableId, rhs: WebSearchContextResultStableId) -> Bool {
return lhs.result == rhs.result
}
}
private struct WebSearchEntry: Comparable, Identifiable {
let index: Int
let result: ChatContextResult
var stableId: WebSearchContextResultStableId {
return WebSearchContextResultStableId(result: self.result)
}
static func ==(lhs: WebSearchEntry, rhs: WebSearchEntry) -> Bool {
return lhs.index == rhs.index && lhs.result == rhs.result
}
static func <(lhs: WebSearchEntry, rhs: WebSearchEntry) -> Bool {
return lhs.index < rhs.index
}
func item(account: Account, theme: PresentationTheme, interfaceState: WebSearchInterfaceState, controllerInteraction: WebSearchControllerInteraction) -> GridItem {
return WebSearchItem(account: account, theme: theme, interfaceState: interfaceState, result: self.result, controllerInteraction: controllerInteraction)
}
}
private struct WebSearchTransition {
let deleteItems: [Int]
let insertItems: [GridNodeInsertItem]
let updateItems: [GridNodeUpdateItem]
let entryCount: Int
let hasMore: Bool
}
private func preparedTransition(from fromEntries: [WebSearchEntry], to toEntries: [WebSearchEntry], hasMore: Bool, account: Account, theme: PresentationTheme, interfaceState: WebSearchInterfaceState, controllerInteraction: WebSearchControllerInteraction) -> WebSearchTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, theme: theme, interfaceState: interfaceState, controllerInteraction: controllerInteraction), previousIndex: $0.2) }
let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, interfaceState: interfaceState, controllerInteraction: controllerInteraction)) }
return WebSearchTransition(deleteItems: deleteIndices, insertItems: insertions, updateItems: updates, entryCount: toEntries.count, hasMore: hasMore)
}
private func gridNodeLayoutForContainerLayout(_ layout: ContainerViewLayout) -> GridNodeLayoutType {
let itemsPerRow: Int
if case .compact = layout.metrics.widthClass {
switch layout.orientation {
case .portrait:
itemsPerRow = 3
case .landscape:
itemsPerRow = 5
}
} else {
itemsPerRow = 3
}
let side = floorToScreenPixels((layout.size.width - layout.safeInsets.left - layout.safeInsets.right - CGFloat(itemsPerRow - 1)) / CGFloat(itemsPerRow))
return .fixed(itemSize: CGSize(width: side, height: side), fillWidth: true, lineSpacing: 1.0, itemSpacing: 1.0)
}
private struct WebSearchRecentQueryStableId: Hashable {
let query: String
}
private struct WebSearchRecentQueryEntry: Comparable, Identifiable {
let index: Int
let query: String
var stableId: WebSearchRecentQueryStableId {
return WebSearchRecentQueryStableId(query: self.query)
}
static func ==(lhs: WebSearchRecentQueryEntry, rhs: WebSearchRecentQueryEntry) -> Bool {
return lhs.index == rhs.index && lhs.query == rhs.query
}
static func <(lhs: WebSearchRecentQueryEntry, rhs: WebSearchRecentQueryEntry) -> Bool {
return lhs.index < rhs.index
}
func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: WebSearchControllerInteraction, header: ListViewItemHeader) -> ListViewItem {
return WebSearchRecentQueryItem(account: account, theme: theme, strings: strings, query: self.query, tapped: { query in
controllerInteraction.setSearchQuery(query)
}, deleted: { query in
controllerInteraction.deleteRecentQuery(query)
}, header: header)
}
}
private struct WebSearchRecentTransition {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
}
private func preparedWebSearchRecentTransition(from fromEntries: [WebSearchRecentQueryEntry], to toEntries: [WebSearchRecentQueryEntry], account: Account, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: WebSearchControllerInteraction, header: ListViewItemHeader) -> WebSearchRecentTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, controllerInteraction: controllerInteraction, header: header), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, controllerInteraction: controllerInteraction, header: header), directionHint: nil) }
return WebSearchRecentTransition(deletions: deletions, insertions: insertions, updates: updates)
}
class WebSearchControllerNode: ASDisplayNode {
private weak var controller: WebSearchController?
private let context: AccountContext
private let peer: EnginePeer?
private let chatLocation: ChatLocation?
private var theme: PresentationTheme
private var strings: PresentationStrings
private var presentationData: PresentationData
private let mode: WebSearchMode
private let attachment: Bool
private let controllerInteraction: WebSearchControllerInteraction
private var webSearchInterfaceState: WebSearchInterfaceState
private let webSearchInterfaceStatePromise: ValuePromise<WebSearchInterfaceState>
private let segmentedContainerNode: ASDisplayNode
private let segmentedBackgroundNode: ASDisplayNode
private let segmentedSeparatorNode: ASDisplayNode
private let segmentedControlNode: SegmentedControlNode
private let toolbarBackgroundNode: ASDisplayNode
private let toolbarSeparatorNode: ASDisplayNode
private let cancelButton: HighlightableButtonNode
private let sendButton: HighlightableButtonNode
private let badgeNode: WebSearchBadgeNode
private let attributionNode: ASImageNode
private let recentQueriesNode: ListView
private var enqueuedRecentTransitions: [(WebSearchRecentTransition, Bool)] = []
private var gridNode: GridNode
private var enqueuedTransitions: [(WebSearchTransition, Bool)] = []
private var dequeuedInitialTransitionOnLayout = false
private(set) var currentExternalResults: ChatContextResultCollection?
private var currentProcessedResults: ChatContextResultCollection?
private var currentEntries: [WebSearchEntry]?
private var hasMore = false
private var isLoadingMore = false
private let hiddenMediaId = Promise<String?>(nil)
private var hiddenMediaDisposable: Disposable?
private let results = ValuePromise<ChatContextResultCollection?>(nil, ignoreRepeated: true)
private let disposable = MetaDisposable()
private let loadMoreDisposable = MetaDisposable()
private var recentDisposable: Disposable?
private var containerLayout: (ContainerViewLayout, CGFloat)?
var requestUpdateInterfaceState: (Bool, (WebSearchInterfaceState) -> WebSearchInterfaceState) -> Void = { _, _ in }
var cancel: (() -> Void)?
var dismissInput: (() -> Void)?
var getCaptionPanelView: () -> TGCaptionPanelView? = { return nil }
init(controller: WebSearchController, context: AccountContext, presentationData: PresentationData, controllerInteraction: WebSearchControllerInteraction, peer: EnginePeer?, chatLocation: ChatLocation?, mode: WebSearchMode, attachment: Bool) {
self.controller = controller
self.context = context
self.theme = presentationData.theme
self.strings = presentationData.strings
self.presentationData = presentationData
self.controllerInteraction = controllerInteraction
self.peer = peer
self.chatLocation = chatLocation
self.mode = mode
self.attachment = attachment
self.webSearchInterfaceState = WebSearchInterfaceState(presentationData: context.sharedContext.currentPresentationData.with { $0 })
self.webSearchInterfaceStatePromise = ValuePromise(self.webSearchInterfaceState, ignoreRepeated: true)
self.segmentedContainerNode = ASDisplayNode()
self.segmentedContainerNode.clipsToBounds = true
self.segmentedBackgroundNode = ASDisplayNode()
self.segmentedSeparatorNode = ASDisplayNode()
let items = [
strings.WebSearch_Images,
strings.WebSearch_GIFs
]
self.segmentedControlNode = SegmentedControlNode(theme: SegmentedControlTheme(theme: theme), items: items.map { SegmentedControlItem(title: $0) }, selectedIndex: 0)
self.toolbarBackgroundNode = ASDisplayNode()
self.toolbarSeparatorNode = ASDisplayNode()
self.attributionNode = ASImageNode()
self.cancelButton = HighlightableButtonNode()
self.sendButton = HighlightableButtonNode()
self.badgeNode = WebSearchBadgeNode(theme: theme)
self.gridNode = GridNode()
self.gridNode.backgroundColor = theme.list.plainBackgroundColor
self.recentQueriesNode = ListView()
self.recentQueriesNode.backgroundColor = theme.list.plainBackgroundColor
self.recentQueriesNode.accessibilityPageScrolledString = { row, count in
return presentationData.strings.VoiceOver_ScrollStatus(row, count).string
}
super.init()
self.setViewBlock({
return UITracingLayerView()
})
self.addSubnode(self.gridNode)
if !attachment {
self.addSubnode(self.recentQueriesNode)
}
self.addSubnode(self.segmentedContainerNode)
self.segmentedContainerNode.addSubnode(self.segmentedBackgroundNode)
self.segmentedContainerNode.addSubnode(self.segmentedSeparatorNode)
// if case .media = mode {
// self.segmentedContainerNode.addSubnode(self.segmentedControlNode)
// }
if !attachment {
self.addSubnode(self.toolbarBackgroundNode)
self.addSubnode(self.toolbarSeparatorNode)
self.addSubnode(self.cancelButton)
self.addSubnode(self.sendButton)
self.addSubnode(self.attributionNode)
self.addSubnode(self.badgeNode)
}
self.segmentedControlNode.selectedIndexChanged = { [weak self] index in
if let strongSelf = self, let scope = WebSearchScope(rawValue: Int32(index)) {
let _ = updateWebSearchSettingsInteractively(accountManager: strongSelf.context.sharedContext.accountManager) { _ -> WebSearchSettings in
return WebSearchSettings(scope: scope)
}.start()
strongSelf.requestUpdateInterfaceState(true) { current in
return current.withUpdatedScope(scope)
}
}
}
self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside)
self.sendButton.addTarget(self, action: #selector(self.sendPressed), forControlEvents: .touchUpInside)
self.applyPresentationData()
self.disposable.set((combineLatest(self.results.get(), self.webSearchInterfaceStatePromise.get())
|> deliverOnMainQueue).start(next: { [weak self] results, interfaceState in
if let strongSelf = self {
strongSelf.updateInternalResults(results, interfaceState: interfaceState)
}
}))
if !attachment {
let previousRecentItems = Atomic<[WebSearchRecentQueryEntry]?>(value: nil)
self.recentDisposable = (combineLatest(webSearchRecentQueries(engine: self.context.engine), self.webSearchInterfaceStatePromise.get())
|> deliverOnMainQueue).start(next: { [weak self] queries, interfaceState in
if let strongSelf = self {
var entries: [WebSearchRecentQueryEntry] = []
for i in 0 ..< queries.count {
entries.append(WebSearchRecentQueryEntry(index: i, query: queries[i]))
}
let header = ChatListSearchItemHeader(type: .recentPeers, theme: interfaceState.presentationData.theme, strings: interfaceState.presentationData.strings, actionTitle: interfaceState.presentationData.strings.WebSearch_RecentSectionClear, action: { _ in
let _ = clearRecentWebSearchQueries(engine: strongSelf.context.engine).start()
})
let previousEntries = previousRecentItems.swap(entries)
let transition = preparedWebSearchRecentTransition(from: previousEntries ?? [], to: entries, account: strongSelf.context.account, theme: interfaceState.presentationData.theme, strings: interfaceState.presentationData.strings, controllerInteraction: strongSelf.controllerInteraction, header: header)
strongSelf.enqueueRecentTransition(transition, firstTime: previousEntries == nil)
}
})
}
self.recentQueriesNode.beganInteractiveDragging = { [weak self] _ in
self?.dismissInput?()
}
self.gridNode.visibleItemsUpdated = { [weak self] visibleItems in
if let strongSelf = self, let bottom = visibleItems.bottom, let entries = strongSelf.currentEntries {
if bottom.0 >= entries.count - 8 {
strongSelf.loadMore()
}
}
}
self.gridNode.scrollingInitiated = { [weak self] in
self?.dismissInput?()
}
self.sendButton.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self, strongSelf.badgeNode.alpha > 0.0 {
if highlighted {
strongSelf.badgeNode.layer.removeAnimation(forKey: "opacity")
strongSelf.badgeNode.alpha = 0.4
} else {
strongSelf.badgeNode.alpha = 1.0
strongSelf.badgeNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.hiddenMediaDisposable = (self.hiddenMediaId.get()
|> deliverOnMainQueue).start(next: { [weak self] id in
if let strongSelf = self {
strongSelf.controllerInteraction.hiddenMediaId = id
strongSelf.gridNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? WebSearchItemNode {
itemNode.updateHiddenMedia()
}
}
}
})
}
deinit {
self.disposable.dispose()
self.recentDisposable?.dispose()
self.loadMoreDisposable.dispose()
self.hiddenMediaDisposable?.dispose()
}
func updatePresentationData(theme: PresentationTheme, strings: PresentationStrings) {
let themeUpdated = theme !== self.theme
self.theme = theme
self.strings = strings
self.applyPresentationData(themeUpdated: themeUpdated)
}
func updateBackgroundAlpha(_ alpha: CGFloat, transition: ContainedViewLayoutTransition) {
self.controller?.navigationBar?.updateBackgroundAlpha(0.0, transition: transition)
transition.updateAlpha(node: self.segmentedBackgroundNode, alpha: alpha)
}
func applyPresentationData(themeUpdated: Bool = true) {
self.cancelButton.setTitle(self.strings.Common_Cancel, with: Font.regular(17.0), with: self.theme.rootController.navigationBar.accentTextColor, for: .normal)
if let selectionState = self.controllerInteraction.selectionState {
let sendEnabled = selectionState.count() > 0
let color = sendEnabled ? self.theme.rootController.navigationBar.accentTextColor : self.theme.rootController.navigationBar.disabledButtonColor
self.sendButton.setTitle(self.strings.MediaPicker_Send, with: Font.medium(17.0), with: color, for: .normal)
}
if themeUpdated {
self.gridNode.backgroundColor = self.theme.list.plainBackgroundColor
self.segmentedBackgroundNode.backgroundColor = self.theme.list.plainBackgroundColor
self.segmentedSeparatorNode.backgroundColor = self.theme.rootController.navigationBar.separatorColor
self.segmentedControlNode.updateTheme(SegmentedControlTheme(theme: self.theme))
self.toolbarBackgroundNode.backgroundColor = self.theme.rootController.navigationBar.opaqueBackgroundColor
self.toolbarSeparatorNode.backgroundColor = self.theme.rootController.navigationBar.separatorColor
}
let gifProviderImage: UIImage?
if let gifProvider = self.webSearchInterfaceState.gifProvider {
switch gifProvider {
case "tenor":
gifProviderImage = generateTintedImage(image: UIImage(bundleImageName: "Media Grid/Tenor"), color: self.theme.list.itemSecondaryTextColor)
case "giphy":
gifProviderImage = generateTintedImage(image: UIImage(bundleImageName: "Media Grid/Giphy"), color: self.theme.list.itemSecondaryTextColor)
default:
gifProviderImage = nil
}
} else {
gifProviderImage = nil
}
let previousGifProviderImage = self.attributionNode.image
self.attributionNode.image = gifProviderImage
if previousGifProviderImage == nil, let validLayout = self.containerLayout {
self.containerLayoutUpdated(validLayout.0, navigationBarHeight: validLayout.1, transition: .immediate)
}
}
func animateIn(completion: (() -> Void)? = nil) {
self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
}
func animateOut(completion: (() -> Void)? = nil) {
self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { _ in
completion?()
})
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.containerLayout = (layout, navigationBarHeight)
var insets = layout.insets(options: [.input])
insets.top += navigationBarHeight
let hasQuery = !(self.webSearchInterfaceState.state?.query ?? "").isEmpty
let segmentedHeight: CGFloat = self.segmentedControlNode.supernode != nil ? 44.0 : 5.0
let panelY: CGFloat = insets.top - UIScreenPixel - 4.0
transition.updateSublayerTransformOffset(layer: self.segmentedContainerNode.layer, offset: CGPoint(x: 0.0, y: !hasQuery ? -44.0 : 0.0), completion: nil)
transition.updateFrame(node: self.segmentedContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelY), size: CGSize(width: layout.size.width, height: segmentedHeight)))
transition.updateFrame(node: self.segmentedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: segmentedHeight)))
transition.updateFrame(node: self.segmentedSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: segmentedHeight - UIScreenPixel), size: CGSize(width: layout.size.width, height: UIScreenPixel)))
let controlSize = self.segmentedControlNode.updateLayout(.stretchToFill(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - 10.0 * 2.0), transition: transition)
transition.updateFrame(node: self.segmentedControlNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + floor((layout.size.width - layout.safeInsets.left - layout.safeInsets.right - controlSize.width) / 2.0), y: 5.0), size: controlSize))
insets.top -= 4.0
let toolbarHeight: CGFloat = 44.0
let toolbarY = layout.size.height - toolbarHeight - insets.bottom
transition.updateFrame(node: self.toolbarBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: toolbarY), size: CGSize(width: layout.size.width, height: toolbarHeight + insets.bottom)))
transition.updateFrame(node: self.toolbarSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: toolbarY), size: CGSize(width: layout.size.width, height: UIScreenPixel)))
if let image = self.attributionNode.image {
self.attributionNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - image.size.width) / 2.0), y: toolbarY + floor((toolbarHeight - image.size.height) / 2.0)), size: image.size)
transition.updateAlpha(node: self.attributionNode, alpha: self.webSearchInterfaceState.state?.scope == .gifs ? 1.0 : 0.0)
}
let toolbarPadding: CGFloat = 10.0
let cancelSize = self.cancelButton.measure(CGSize(width: layout.size.width, height: toolbarHeight))
transition.updateFrame(node: self.cancelButton, frame: CGRect(origin: CGPoint(x: toolbarPadding + layout.safeInsets.left, y: toolbarY), size: CGSize(width: cancelSize.width, height: toolbarHeight)))
let sendSize = self.sendButton.measure(CGSize(width: layout.size.width, height: toolbarHeight))
let sendFrame = CGRect(origin: CGPoint(x: layout.size.width - toolbarPadding - layout.safeInsets.right - sendSize.width, y: toolbarY), size: CGSize(width: sendSize.width, height: toolbarHeight))
transition.updateFrame(node: self.sendButton, frame: sendFrame)
if let selectionState = self.controllerInteraction.selectionState {
self.sendButton.isHidden = false
let previousSendEnabled = self.sendButton.isEnabled
let sendEnabled = selectionState.count() > 0
self.sendButton.isEnabled = sendEnabled
if sendEnabled != previousSendEnabled {
let color = sendEnabled ? self.theme.rootController.navigationBar.accentTextColor : self.theme.rootController.navigationBar.disabledButtonColor
self.sendButton.setTitle(self.strings.MediaPicker_Send, with: Font.medium(17.0), with: color, for: .normal)
}
let selectedCount = selectionState.count()
let badgeText = String(selectedCount)
if selectedCount > 0 && (self.badgeNode.text != badgeText || self.badgeNode.alpha < 1.0) {
if transition.isAnimated {
var incremented = true
if let previousCount = Int(self.badgeNode.text) {
incremented = selectedCount > previousCount || self.badgeNode.alpha < 1.0
}
self.badgeNode.animateBump(incremented: incremented)
}
self.badgeNode.text = badgeText
let badgeSize = self.badgeNode.measure(layout.size)
transition.updateFrame(node: self.badgeNode, frame: CGRect(origin: CGPoint(x: sendFrame.minX - badgeSize.width - 6.0, y: toolbarY + 11.0), size: badgeSize))
transition.updateAlpha(node: self.badgeNode, alpha: 1.0)
} else if selectedCount == 0 {
if transition.isAnimated {
self.badgeNode.animateOut()
}
let badgeSize = CGSize(width: 22.0, height: 22.0)
transition.updateFrame(node: self.badgeNode, frame: CGRect(origin: CGPoint(x: sendFrame.minX - badgeSize.width - 6.0, y: toolbarY + 11.0), size: badgeSize))
transition.updateAlpha(node: self.badgeNode, alpha: 0.0)
}
} else {
self.sendButton.isHidden = true
}
let previousBounds = self.gridNode.bounds
self.gridNode.bounds = CGRect(x: previousBounds.origin.x, y: previousBounds.origin.y, width: layout.size.width, height: layout.size.height)
self.gridNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0)
insets.top += segmentedHeight
insets.bottom += toolbarHeight
let gridInsets = UIEdgeInsets(top: insets.top, left: layout.safeInsets.left, bottom: insets.bottom, right: layout.safeInsets.right)
self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: layout.size, insets: gridInsets, preloadSize: 400.0, type: gridNodeLayoutForContainerLayout(layout)), transition: .immediate), itemTransition: .immediate, stationaryItems: .none,updateFirstIndexInSectionOffset: nil), completion: { _ in })
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
self.recentQueriesNode.frame = CGRect(origin: CGPoint(), size: layout.size)
self.recentQueriesNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
if !self.dequeuedInitialTransitionOnLayout {
self.dequeuedInitialTransitionOnLayout = true
self.dequeueTransition()
}
}
func updateInterfaceState(_ interfaceState: WebSearchInterfaceState, animated: Bool) {
let previousGifProvider = self.webSearchInterfaceState.gifProvider
self.webSearchInterfaceState = interfaceState
self.webSearchInterfaceStatePromise.set(self.webSearchInterfaceState)
if let state = interfaceState.state {
self.segmentedControlNode.selectedIndex = Int(state.scope.rawValue)
}
if previousGifProvider != interfaceState.gifProvider {
self.applyPresentationData(themeUpdated: false)
}
if let validLayout = self.containerLayout {
self.containerLayoutUpdated(validLayout.0, navigationBarHeight: validLayout.1, transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate)
}
}
func updateSelectionState(animated: Bool) {
self.gridNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? WebSearchItemNode {
itemNode.updateSelectionState(animated: animated)
}
}
if let validLayout = self.containerLayout {
self.containerLayoutUpdated(validLayout.0, navigationBarHeight: validLayout.1, transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate)
}
}
func updateResults(_ results: ChatContextResultCollection?, immediate: Bool = false) {
if self.currentExternalResults == results {
return
}
let previousResults = self.currentExternalResults
self.currentExternalResults = results
self.currentProcessedResults = results
self.isLoadingMore = false
self.loadMoreDisposable.set(nil)
if immediate && previousResults?.query == results?.query && previousResults?.botId != results?.botId {
let previousNode = self.gridNode
let gridNode = GridNode()
gridNode.backgroundColor = theme.list.plainBackgroundColor
gridNode.frame = previousNode.frame
gridNode.visibleItemsUpdated = { [weak self] visibleItems in
if let strongSelf = self, let bottom = visibleItems.bottom, let entries = strongSelf.currentEntries {
if bottom.0 >= entries.count - 8 {
strongSelf.loadMore()
}
}
}
gridNode.scrollingInitiated = { [weak self] in
self?.dismissInput?()
}
if self.recentQueriesNode.supernode != nil {
self.insertSubnode(gridNode, belowSubnode: self.recentQueriesNode)
} else {
self.insertSubnode(gridNode, aboveSubnode: previousNode)
}
self.gridNode = gridNode
self.currentEntries = nil
let directionMultiplier: CGFloat
if let state = self.webSearchInterfaceState.state {
switch state.scope {
case .images:
directionMultiplier = 1.0
case .gifs:
directionMultiplier = -1.0
}
} else {
directionMultiplier = 1.0
}
previousNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: -directionMultiplier * self.bounds.width, y: 0.0), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { [weak previousNode] _ in
previousNode?.removeFromSupernode()
})
gridNode.layer.animatePosition(from: CGPoint(x: directionMultiplier * bounds.width, y: 0.0), to: CGPoint(), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
} else if previousResults?.botId != results?.botId || previousResults?.query != results?.query {
self.scrollToTop()
}
self.results.set(results)
}
func clearResults() {
self.results.set(nil)
}
private func loadMore() {
guard !self.isLoadingMore, let currentProcessedResults = self.currentProcessedResults, currentProcessedResults.results.count > 55, let nextOffset = currentProcessedResults.nextOffset else {
return
}
self.isLoadingMore = true
let geoPoint = currentProcessedResults.geoPoint.flatMap { geoPoint -> (Double, Double) in
return (geoPoint.latitude, geoPoint.longitude)
}
self.loadMoreDisposable.set((self.context.engine.messages.requestChatContextResults(botId: currentProcessedResults.botId, peerId: currentProcessedResults.peerId, query: currentProcessedResults.query, location: .single(geoPoint), offset: nextOffset)
|> deliverOnMainQueue).start(next: { [weak self] nextResults in
guard let strongSelf = self, let nextResults = nextResults else {
return
}
strongSelf.isLoadingMore = false
var results: [ChatContextResult] = []
var existingIds = Set<String>()
for result in currentProcessedResults.results {
results.append(result)
existingIds.insert(result.id)
}
for result in nextResults.results.results {
if !existingIds.contains(result.id) {
results.append(result)
existingIds.insert(result.id)
}
}
let mergedResults = ChatContextResultCollection(botId: currentProcessedResults.botId, peerId: currentProcessedResults.peerId, query: currentProcessedResults.query, geoPoint: currentProcessedResults.geoPoint, queryId: nextResults.results.queryId, nextOffset: nextResults.results.nextOffset, presentation: currentProcessedResults.presentation, switchPeer: currentProcessedResults.switchPeer, webView: currentProcessedResults.webView, results: results, cacheTimeout: currentProcessedResults.cacheTimeout)
strongSelf.currentProcessedResults = mergedResults
strongSelf.results.set(mergedResults)
}))
}
private func updateInternalResults(_ results: ChatContextResultCollection?, interfaceState: WebSearchInterfaceState) {
var entries: [WebSearchEntry] = []
var hasMore = false
if let state = interfaceState.state, state.query.isEmpty {
} else if let results = results {
hasMore = results.nextOffset != nil
var index = 0
var resultIds = Set<WebSearchContextResultStableId>()
for result in results.results {
let entry = WebSearchEntry(index: index, result: result)
if resultIds.contains(entry.stableId) {
continue
} else {
resultIds.insert(entry.stableId)
}
entries.append(entry)
index += 1
}
}
let firstTime = self.currentEntries == nil
let transition = preparedTransition(from: self.currentEntries ?? [], to: entries, hasMore: hasMore, account: self.context.account, theme: interfaceState.presentationData.theme, interfaceState: interfaceState, controllerInteraction: self.controllerInteraction)
self.currentEntries = entries
self.enqueueTransition(transition, firstTime: firstTime)
}
private func enqueueTransition(_ transition: WebSearchTransition, firstTime: Bool) {
self.enqueuedTransitions.append((transition, firstTime))
if self.containerLayout != nil {
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
}
}
private func dequeueTransition() {
if let (transition, _) = self.enqueuedTransitions.first {
self.enqueuedTransitions.remove(at: 0)
if let state = self.webSearchInterfaceState.state {
self.recentQueriesNode.isHidden = !state.query.isEmpty
}
self.hasMore = transition.hasMore
self.gridNode.transaction(GridNodeTransaction(deleteItems: transition.deleteItems, insertItems: transition.insertItems, updateItems: transition.updateItems, scrollToItem: nil, updateLayout: nil, itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil, synchronousLoads: true), completion: { _ in })
}
}
private func enqueueRecentTransition(_ transition: WebSearchRecentTransition, firstTime: Bool) {
enqueuedRecentTransitions.append((transition, firstTime))
if self.containerLayout != nil {
while !self.enqueuedRecentTransitions.isEmpty {
self.dequeueRecentTransition()
}
}
}
private func dequeueRecentTransition() {
if let (transition, firstTime) = self.enqueuedRecentTransitions.first {
self.enqueuedRecentTransitions.remove(at: 0)
var options = ListViewDeleteAndInsertOptions()
if firstTime {
options.insert(.PreferSynchronousDrawing)
} else {
options.insert(.AnimateInsertion)
}
self.recentQueriesNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in
})
}
}
@objc private func cancelPressed() {
self.cancel?()
}
@objc private func sendPressed() {
self.controllerInteraction.sendSelected(nil, false, nil, nil)
self.cancel?()
}
func scrollToTop(animated: Bool = false) {
self.gridNode.scrollView.setContentOffset(CGPoint(x: 0.0, y: -self.gridNode.scrollView.contentInset.top), animated: animated)
}
func openResult(currentResult: ChatContextResult, present: @escaping (ViewController, Any?) -> Void) {
self.view.endEditing(true)
if self.controllerInteraction.selectionState != nil {
if let state = self.webSearchInterfaceState.state, state.scope == .images {
if let results = self.currentProcessedResults?.results {
presentLegacyWebSearchGallery(context: self.context, peer: self.peer, threadTitle: nil, chatLocation: self.chatLocation, presentationData: self.presentationData, results: results, current: currentResult, selectionContext: self.controllerInteraction.selectionState, editingContext: self.controllerInteraction.editingState, updateHiddenMedia: { [weak self] id in
self?.hiddenMediaId.set(.single(id))
}, initialLayout: self.containerLayout?.0, 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.sendSelected(result, false, nil, nil)
strongSelf.cancel?()
}
}, getCaptionPanelView: self.getCaptionPanelView, present: present)
}
} else {
if let results = self.currentProcessedResults?.results {
var entries: [WebSearchGalleryEntry] = []
var centralIndex: Int = 0
for i in 0 ..< results.count {
entries.append(WebSearchGalleryEntry(index: entries.count, result: results[i]))
if results[i] == currentResult {
centralIndex = i
}
}
let controller = WebSearchGalleryController(context: self.context, peer: self.peer, selectionState: self.controllerInteraction.selectionState, editingState: self.controllerInteraction.editingState, entries: entries, centralIndex: centralIndex, replaceRootController: { (controller, _) in
}, baseNavigationController: nil, sendCurrent: { [weak self] result in
if let strongSelf = self {
strongSelf.controllerInteraction.sendSelected(result, false, nil, nil)
strongSelf.cancel?()
}
})
self.hiddenMediaId.set((controller.hiddenMedia |> deliverOnMainQueue)
|> map { entry in
return entry?.result.id
})
present(controller, WebSearchGalleryControllerPresentationArguments(transitionArguments: { [weak self] entry -> GalleryTransitionArguments? in
if let strongSelf = self {
var transitionNode: WebSearchItemNode?
strongSelf.gridNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? WebSearchItemNode, itemNode.item?.result.id == entry.result.id {
transitionNode = itemNode
}
}
if let transitionNode = transitionNode {
return GalleryTransitionArguments(transitionNode: (transitionNode, transitionNode.bounds, { [weak transitionNode] in
return (transitionNode?.transitionView().snapshotContentTree(unhide: true), nil)
}), addToTransitionSurface: { view in
if let strongSelf = self {
strongSelf.gridNode.view.superview?.insertSubview(view, aboveSubview: strongSelf.gridNode.view)
}
})
}
}
return nil
}))
}
}
} else {
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)
}
})
}
} 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)
}
}
}
private func transitionNode(for result: ChatContextResult) -> WebSearchItemNode? {
var transitionNode: WebSearchItemNode?
self.gridNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? WebSearchItemNode, itemNode.item?.result.id == result.id {
transitionNode = itemNode
}
}
return transitionNode
}
}
@@ -0,0 +1,348 @@
import Foundation
import UIKit
import Display
import QuickLook
import SwiftSignalKit
import AsyncDisplayKit
import TelegramCore
import LegacyComponents
import TelegramPresentationData
import AccountContext
import GalleryUI
import TelegramUniversalVideoContent
final class WebSearchGalleryControllerInteraction {
let dismiss: (Bool) -> Void
let send: (ChatContextResult) -> Void
let selectionState: TGMediaSelectionContext?
let editingState: TGMediaEditingContext
init(dismiss: @escaping (Bool) -> Void, send: @escaping (ChatContextResult) -> Void, selectionState: TGMediaSelectionContext?, editingState: TGMediaEditingContext) {
self.dismiss = dismiss
self.send = send
self.selectionState = selectionState
self.editingState = editingState
}
}
struct WebSearchGalleryEntry: Equatable {
let index: Int
let result: ChatContextResult
static func ==(lhs: WebSearchGalleryEntry, rhs: WebSearchGalleryEntry) -> Bool {
return lhs.result == rhs.result
}
func item(context: AccountContext, presentationData: PresentationData, controllerInteraction: WebSearchGalleryControllerInteraction?) -> GalleryItem {
switch self.result {
case let .externalReference(externalReference):
if let content = externalReference.content, externalReference.type == "gif", let thumbnailResource = externalReference.thumbnail?.resource, let dimensions = content.dimensions {
let fileReference = FileMediaReference.standalone(media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: content.resource, previewRepresentations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: []))
return WebSearchVideoGalleryItem(context: context, presentationData: presentationData, index: self.index, result: self.result, content: NativeVideoContent(id: .contextResult(self.result.queryId, self.result.id), userLocation: .other, fileReference: fileReference, loopVideo: true, enableSound: false, fetchAutomatically: true, storeAfterDownload: nil), controllerInteraction: controllerInteraction)
}
case let .internalReference(internalReference):
if let file = internalReference.file {
return WebSearchVideoGalleryItem(context: context, presentationData: presentationData, index: self.index, result: self.result, content: NativeVideoContent(id: .contextResult(self.result.queryId, self.result.id), userLocation: .other, fileReference: .standalone(media: file), loopVideo: true, enableSound: false, fetchAutomatically: true, storeAfterDownload: nil), controllerInteraction: controllerInteraction)
}
}
preconditionFailure()
}
}
final class WebSearchGalleryControllerPresentationArguments {
let animated: Bool
let transitionArguments: (WebSearchGalleryEntry) -> GalleryTransitionArguments?
init(animated: Bool = true, transitionArguments: @escaping (WebSearchGalleryEntry) -> GalleryTransitionArguments?) {
self.animated = animated
self.transitionArguments = transitionArguments
}
}
class WebSearchGalleryController: ViewController {
private static let navigationTheme = NavigationBarTheme(buttonColor: .white, disabledButtonColor: UIColor(rgb: 0x525252), primaryTextColor: .white, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear)
private var galleryNode: GalleryControllerNode {
return self.displayNode as! GalleryControllerNode
}
private let context: AccountContext
private var presentationData: PresentationData
private var controllerInteraction: WebSearchGalleryControllerInteraction?
private let _ready = Promise<Bool>()
override var ready: Promise<Bool> {
return self._ready
}
private var didSetReady = false
private let disposable = MetaDisposable()
private var entries: [WebSearchGalleryEntry] = []
private var centralEntryIndex: Int?
private let centralItemTitle = Promise<String>()
private let centralItemTitleView = Promise<UIView?>()
private let centralItemNavigationStyle = Promise<GalleryItemNodeNavigationStyle>()
private let centralItemFooterContentNode = Promise<(GalleryFooterContentNode?, GalleryOverlayContentNode?)>()
private let centralItemAttributesDisposable = DisposableSet();
private let checkedDisposable = MetaDisposable()
private var checkNode: GalleryNavigationCheckNode?
private let _hiddenMedia = Promise<WebSearchGalleryEntry?>(nil)
var hiddenMedia: Signal<WebSearchGalleryEntry?, NoError> {
return self._hiddenMedia.get()
}
private let replaceRootController: (ViewController, Promise<Bool>?) -> Void
private let baseNavigationController: NavigationController?
init(context: AccountContext, peer: EnginePeer?, selectionState: TGMediaSelectionContext?, editingState: TGMediaEditingContext, entries: [WebSearchGalleryEntry], centralIndex: Int, replaceRootController: @escaping (ViewController, Promise<Bool>?) -> Void, baseNavigationController: NavigationController?, sendCurrent: @escaping (ChatContextResult) -> Void) {
self.context = context
self.replaceRootController = replaceRootController
self.baseNavigationController = baseNavigationController
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: WebSearchGalleryController.navigationTheme, strings: NavigationBarStrings(presentationStrings: self.presentationData.strings)))
self.controllerInteraction = WebSearchGalleryControllerInteraction(dismiss: { [weak self] animated in
self?.dismiss(forceAway: false)
}, send: { [weak self] current in
sendCurrent(current)
self?.dismiss(forceAway: true)
}, selectionState: selectionState, editingState: editingState)
if let title = peer?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) {
let recipientNode = GalleryNavigationRecipientNode(color: .white, title: title)
let leftItem = UIBarButtonItem(customDisplayNode: recipientNode)
self.navigationItem.leftBarButtonItem = leftItem
}
let checkNode = GalleryNavigationCheckNode(theme: self.presentationData.theme)
checkNode.addTarget(target: self, action: #selector(self.checkPressed))
let rightItem = UIBarButtonItem(customDisplayNode: checkNode)
self.navigationItem.rightBarButtonItem = rightItem
self.checkNode = checkNode
self.statusBar.statusBarStyle = .White
let entriesSignal: Signal<[WebSearchGalleryEntry], NoError> = .single(entries)
self.disposable.set((entriesSignal |> deliverOnMainQueue).start(next: { [weak self] entries in
if let strongSelf = self {
strongSelf.entries = entries
strongSelf.centralEntryIndex = centralIndex
if strongSelf.isViewLoaded {
strongSelf.galleryNode.pager.replaceItems(strongSelf.entries.map({
$0.item(context: context, presentationData: strongSelf.presentationData, controllerInteraction: strongSelf.controllerInteraction)
}), centralItemIndex: centralIndex)
let ready = strongSelf.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak strongSelf] _ in
strongSelf?.didSetReady = true
}
strongSelf._ready.set(ready |> map { true })
}
}
}))
self.centralItemAttributesDisposable.add(self.centralItemTitle.get().start(next: { [weak self] title in
self?.navigationItem.title = title
}))
self.centralItemAttributesDisposable.add(self.centralItemTitleView.get().start(next: { [weak self] titleView in
self?.navigationItem.titleView = titleView
}))
self.centralItemAttributesDisposable.add(self.centralItemFooterContentNode.get().start(next: { [weak self] footerContentNode, _ in
self?.galleryNode.updatePresentationState({
$0.withUpdatedFooterContentNode(footerContentNode)
}, transition: .immediate)
}))
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.disposable.dispose()
self.checkedDisposable.dispose()
self.centralItemAttributesDisposable.dispose()
}
@objc func checkPressed() {
if let checkNode = self.checkNode, let controllerInteraction = self.controllerInteraction, let centralItemNode = self.galleryNode.pager.centralItemNode() as? WebSearchVideoGalleryItemNode, let item = centralItemNode.item {
let legacyItem = LegacyWebSearchItem(result: item.result)
controllerInteraction.selectionState?.setItem(legacyItem, selected: checkNode.isChecked)
}
}
private func dismiss(forceAway: Bool, animated: Bool = true) {
var animatedOutNode = true
var animatedOutInterface = false
let completion = { [weak self] in
if animatedOutNode && animatedOutInterface {
self?._hiddenMedia.set(.single(nil))
self?.presentingViewController?.dismiss(animated: false, completion: nil)
}
}
if animated {
if let centralItemNode = self.galleryNode.pager.centralItemNode(), let presentationArguments = self.presentationArguments as? WebSearchGalleryControllerPresentationArguments {
if !self.entries.isEmpty {
if let transitionArguments = presentationArguments.transitionArguments(self.entries[centralItemNode.index]), !forceAway {
animatedOutNode = false
centralItemNode.animateOut(to: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface, completion: {
animatedOutNode = true
completion()
})
}
}
}
self.galleryNode.animateOut(animateContent: animatedOutNode, completion: {
animatedOutInterface = true
completion()
})
} else {
animatedOutInterface = true
completion()
}
}
override func loadDisplayNode() {
let controllerInteraction = GalleryControllerInteraction(presentController: { [weak self] controller, arguments in
if let strongSelf = self {
strongSelf.present(controller, in: .window(.root), with: arguments, blockInteraction: true)
}
}, pushController: { _ in
}, dismissController: { [weak self] in
self?.dismiss(forceAway: true)
}, replaceRootController: { [weak self] controller, ready in
if let strongSelf = self {
strongSelf.replaceRootController(controller, ready)
}
}, editMedia: { _ in
}, controller: { [weak self] in
return self
})
self.displayNode = GalleryControllerNode(context: self.context, controllerInteraction: controllerInteraction)
self.displayNodeDidLoad()
self.galleryNode.statusBar = self.statusBar
self.galleryNode.navigationBar = self.navigationBar
self.galleryNode.transitionDataForCentralItem = { [weak self] in
if let strongSelf = self {
if let centralItemNode = strongSelf.galleryNode.pager.centralItemNode(), let presentationArguments = strongSelf.presentationArguments as? WebSearchGalleryControllerPresentationArguments {
if let transitionArguments = presentationArguments.transitionArguments(strongSelf.entries[centralItemNode.index]) {
return (transitionArguments.transitionNode, transitionArguments.addToTransitionSurface)
}
}
}
return nil
}
self.galleryNode.dismiss = { [weak self] in
self?._hiddenMedia.set(.single(nil))
self?.presentingViewController?.dismiss(animated: false, completion: nil)
}
self.galleryNode.pager.replaceItems(self.entries.map({
$0.item(context: self.context, presentationData: self.presentationData, controllerInteraction: self.controllerInteraction)
}), centralItemIndex: self.centralEntryIndex)
self.galleryNode.pager.centralItemIndexUpdated = { [weak self] index in
if let strongSelf = self {
var item: WebSearchGalleryEntry?
if let index = index {
item = strongSelf.entries[index]
if let node = strongSelf.galleryNode.pager.centralItemNode() {
strongSelf.centralItemTitle.set(node.title())
strongSelf.centralItemTitleView.set(node.titleView())
strongSelf.centralItemNavigationStyle.set(node.navigationStyle())
strongSelf.centralItemFooterContentNode.set(node.footerContent())
}
if let checkNode = strongSelf.checkNode, let controllerInteraction = strongSelf.controllerInteraction, let selectionState = controllerInteraction.selectionState, let item = item {
checkNode.setIsChecked(selectionState.isIdentifierSelected(item.result.id), animated: false)
}
}
if strongSelf.didSetReady {
strongSelf._hiddenMedia.set(.single(item))
}
}
}
let selectionState = self.controllerInteraction?.selectionState
let selectionUpdated = Signal<Void, NoError> { subscriber in
if let selectionState = selectionState {
let disposable = selectionState.selectionChangedSignal()!.start(next: { _ in
subscriber.putNext(Void())
}, error: { _ in }, completed: {})!
return ActionDisposable {
disposable.dispose()
}
} else {
subscriber.putCompletion()
return EmptyDisposable
}
}
self.checkedDisposable.set((selectionUpdated
|> deliverOnMainQueue).start(next: { [weak self] _ in
if let strongSelf = self, let centralItemNode = strongSelf.galleryNode.pager.centralItemNode() {
let item = strongSelf.entries[centralItemNode.index]
if let checkNode = strongSelf.checkNode, let controllerInteraction = strongSelf.controllerInteraction, let selectionState = controllerInteraction.selectionState {
checkNode.setIsChecked(selectionState.isIdentifierSelected(item.result.id), animated: true)
}
}
}))
let ready = self.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak self] _ in
self?.didSetReady = true
}
self._ready.set(ready |> map { true })
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
var nodeAnimatesItself = false
if let centralItemNode = self.galleryNode.pager.centralItemNode(), let presentationArguments = self.presentationArguments as? WebSearchGalleryControllerPresentationArguments {
self.centralItemTitle.set(centralItemNode.title())
self.centralItemTitleView.set(centralItemNode.titleView())
self.centralItemNavigationStyle.set(centralItemNode.navigationStyle())
self.centralItemFooterContentNode.set(centralItemNode.footerContent())
let item = self.entries[centralItemNode.index]
if let transitionArguments = presentationArguments.transitionArguments(item) {
nodeAnimatesItself = true
centralItemNode.activateAsInitial()
if presentationArguments.animated {
centralItemNode.animateIn(from: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface, completion: {})
}
if let checkNode = self.checkNode, let controllerInteraction = self.controllerInteraction, let selectionState = controllerInteraction.selectionState {
checkNode.setIsChecked(selectionState.isIdentifierSelected(item.result.id), animated: false)
}
self._hiddenMedia.set(.single(self.entries[centralItemNode.index]))
}
}
self.galleryNode.animateIn(animateContent: !nodeAnimatesItself, useSimpleAnimation: false)
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.galleryNode.frame = CGRect(origin: CGPoint(), size: layout.size)
self.galleryNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
}
@@ -0,0 +1,76 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramCore
import SwiftSignalKit
import LegacyComponents
import TelegramPresentationData
import AccountContext
import GalleryUI
final class WebSearchGalleryFooterContentNode: GalleryFooterContentNode {
private let context: AccountContext
private var theme: PresentationTheme
private var strings: PresentationStrings
private let cancelButton: HighlightableButtonNode
private let sendButton: HighlightableButtonNode
var cancel: (() -> Void)?
var send: (() -> Void)?
init(context: AccountContext, presentationData: PresentationData) {
self.context = context
self.theme = presentationData.theme
self.strings = presentationData.strings
self.cancelButton = HighlightableButtonNode()
self.cancelButton.setImage(TGComponentsImageNamed("PhotoPickerBackIcon"), for: [.normal])
self.sendButton = HighlightableButtonNode()
self.sendButton.setImage(PresentationResourcesChat.chatInputPanelSendButtonImage(self.theme), for: [.normal])
super.init()
self.addSubnode(self.cancelButton)
self.addSubnode(self.sendButton)
self.cancelButton.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside)
self.sendButton.addTarget(self, action: #selector(self.sendButtonPressed), forControlEvents: .touchUpInside)
}
func setCaption(_ caption: String) {
}
override func updateLayout(size: CGSize, metrics: LayoutMetrics, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, contentInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
let width = size.width
let panelSize: CGFloat = 49.0
var panelHeight: CGFloat = panelSize + bottomInset
panelHeight += contentInset
self.cancelButton.frame = CGRect(origin: CGPoint(x: leftInset, y: panelHeight - bottomInset - panelSize), size: CGSize(width: panelSize, height: panelSize))
self.sendButton.frame = CGRect(origin: CGPoint(x: width - panelSize - rightInset, y: panelHeight - bottomInset - panelSize), size: CGSize(width: panelSize, height: panelSize))
return panelHeight
}
override func animateIn(fromHeight: CGFloat, previousContentNode: GalleryFooterContentNode, transition: ContainedViewLayoutTransition) {
self.cancelButton.alpha = 1.0
self.sendButton.alpha = 1.0
}
override func animateOut(toHeight: CGFloat, nextContentNode: GalleryFooterContentNode, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
self.cancelButton.alpha = 0.0
self.sendButton.alpha = 0.0
completion()
}
@objc func cancelButtonPressed() {
self.cancel?()
}
@objc func sendButtonPressed() {
self.send?()
}
}
@@ -0,0 +1,43 @@
import Foundation
import UIKit
import TelegramPresentationData
import TelegramUIPreferences
struct WebSearchInterfaceInnerState: Equatable {
let scope: WebSearchScope
let query: String
}
struct WebSearchInterfaceState: Equatable {
let state: WebSearchInterfaceInnerState?
let presentationData: PresentationData
let gifProvider: String?
init (presentationData: PresentationData) {
self.state = nil
self.presentationData = presentationData
self.gifProvider = nil
}
init(state: WebSearchInterfaceInnerState?, presentationData: PresentationData, gifProvider: String? = nil) {
self.state = state
self.presentationData = presentationData
self.gifProvider = gifProvider
}
func withUpdatedScope(_ scope: WebSearchScope) -> WebSearchInterfaceState {
return WebSearchInterfaceState(state: WebSearchInterfaceInnerState(scope: scope, query: self.state?.query ?? ""), presentationData: self.presentationData, gifProvider: self.gifProvider)
}
func withUpdatedQuery(_ query: String) -> WebSearchInterfaceState {
return WebSearchInterfaceState(state: WebSearchInterfaceInnerState(scope: self.state?.scope ?? .images, query: query), presentationData: self.presentationData, gifProvider: self.gifProvider)
}
func withUpdatedPresentationData(_ presentationData: PresentationData) -> WebSearchInterfaceState {
return WebSearchInterfaceState(state: self.state, presentationData: presentationData, gifProvider: self.gifProvider)
}
func withUpdatedGifProvider(_ gifProvider: String?) -> WebSearchInterfaceState {
return WebSearchInterfaceState(state: self.state, presentationData: self.presentationData, gifProvider: gifProvider)
}
}
@@ -0,0 +1,316 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import CheckNode
import PhotoResources
import RadialStatusNode
final class WebSearchItem: GridItem {
var section: GridSection?
let account: Account
let theme: PresentationTheme
let interfaceState: WebSearchInterfaceState
let result: ChatContextResult
let controllerInteraction: WebSearchControllerInteraction
public init(account: Account, theme: PresentationTheme, interfaceState: WebSearchInterfaceState, result: ChatContextResult, controllerInteraction: WebSearchControllerInteraction) {
self.account = account
self.theme = theme
self.result = result
self.interfaceState = interfaceState
self.controllerInteraction = controllerInteraction
}
func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
let node = WebSearchItemNode()
node.setup(item: self, synchronousLoad: synchronousLoad)
return node
}
func update(node: GridItemNode) {
guard let node = node as? WebSearchItemNode else {
assertionFailure()
return
}
node.setup(item: self, synchronousLoad: false)
}
}
final class WebSearchItemNode: GridItemNode {
private let imageNodeBackground: ASDisplayNode
private let imageNode: TransformImageNode
private var checkNode: CheckNode?
private var statusNode: RadialStatusNode?
private(set) var item: WebSearchItem?
private var currentDimensions: CGSize?
private let fetchStatusDisposable = MetaDisposable()
private let fetchDisposable = MetaDisposable()
private var resourceStatus: EngineMediaResource.FetchStatus?
override init() {
self.imageNodeBackground = ASDisplayNode()
self.imageNodeBackground.isLayerBacked = true
self.imageNode = TransformImageNode()
self.imageNode.contentAnimations = [.subsequentUpdates]
self.imageNode.displaysAsynchronously = false
super.init()
self.addSubnode(self.imageNodeBackground)
self.addSubnode(self.imageNode)
}
deinit {
self.fetchStatusDisposable.dispose()
self.fetchDisposable.dispose()
}
override func didLoad() {
super.didLoad()
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
recognizer.tapActionAtPoint = { _ in
return .waitForSingleTap
}
self.imageNode.view.addGestureRecognizer(recognizer)
}
func updateProgress(_ value: Float?, animated: Bool) {
if let value {
let statusNode: RadialStatusNode
if let current = self.statusNode {
statusNode = current
} else {
statusNode = RadialStatusNode(backgroundNodeColor: UIColor(rgb: 0x000000, alpha: 0.6))
statusNode.isUserInteractionEnabled = false
self.addSubnode(statusNode)
self.statusNode = statusNode
}
let adjustedProgress = max(0.027, CGFloat(value))
let state: RadialStatusNodeState = .progress(color: .white, lineWidth: nil, value: adjustedProgress, cancelEnabled: true, animateRotation: true)
statusNode.transitionToState(state)
} else if let statusNode = self.statusNode {
self.statusNode = nil
if animated {
statusNode.transitionToState(.none, animated: true, completion: { [weak statusNode] in
statusNode?.removeFromSupernode()
})
} else {
statusNode.removeFromSupernode()
}
}
}
func setup(item: WebSearchItem, synchronousLoad: Bool) {
if self.item !== item {
var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
var thumbnailDimensions: CGSize?
var thumbnailResource: TelegramMediaResource?
var imageResource: TelegramMediaResource?
var imageDimensions: CGSize?
var immediateThumbnailData: Data?
switch item.result {
case let .externalReference(externalReference):
if let content = externalReference.content, externalReference.type != "gif" {
imageResource = content.resource
} else if let thumbnail = externalReference.thumbnail {
imageResource = thumbnail.resource
}
imageDimensions = externalReference.content?.dimensions?.cgSize
case let .internalReference(internalReference):
if let image = internalReference.image {
immediateThumbnailData = image.immediateThumbnailData
if let largestRepresentation = largestImageRepresentation(image.representations) {
imageDimensions = largestRepresentation.dimensions.cgSize
}
imageResource = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 200, height: 100))?.resource
if let file = internalReference.file {
if let thumbnailRepresentation = smallestImageRepresentation(file.previewRepresentations) {
thumbnailDimensions = thumbnailRepresentation.dimensions.cgSize
thumbnailResource = thumbnailRepresentation.resource
}
} else {
if let thumbnailRepresentation = smallestImageRepresentation(image.representations) {
thumbnailDimensions = thumbnailRepresentation.dimensions.cgSize
thumbnailResource = thumbnailRepresentation.resource
}
}
} else if let file = internalReference.file {
immediateThumbnailData = file.immediateThumbnailData
if let dimensions = file.dimensions {
imageDimensions = dimensions.cgSize
} else if let largestRepresentation = largestImageRepresentation(file.previewRepresentations) {
imageDimensions = largestRepresentation.dimensions.cgSize
}
imageResource = smallestImageRepresentation(file.previewRepresentations)?.resource
}
}
var representations: [TelegramMediaImageRepresentation] = []
if let thumbnailResource = thumbnailResource, let thumbnailDimensions = thumbnailDimensions {
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false))
}
if let imageResource = imageResource, let imageDimensions = imageDimensions {
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(imageDimensions), resource: imageResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false))
}
if !representations.isEmpty {
let tmpImage = TelegramMediaImage(imageId: EngineMedia.Id(namespace: 0, id: 0), representations: representations, immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
updateImageSignal = mediaGridMessagePhoto(account: item.account, userLocation: .other, photoReference: .standalone(media: tmpImage))
} else {
updateImageSignal = .complete()
}
if let updateImageSignal = updateImageSignal {
let editingContext = item.controllerInteraction.editingState
let editableItem = LegacyWebSearchItem(result: item.result)
let editedImageSignal = Signal<UIImage?, NoError> { subscriber in
if let signal = editingContext.thumbnailImageSignal(for: editableItem) {
let disposable = signal.start(next: { next in
if let image = next as? UIImage {
subscriber.putNext(image)
} else {
subscriber.putNext(nil)
}
}, error: { _ in
}, completed: nil)!
return ActionDisposable {
disposable.dispose()
}
} else {
return EmptyDisposable
}
}
let editedSignal: Signal<((TransformImageArguments) -> DrawingContext?)?, NoError> = editedImageSignal
|> map { image in
if let image = image {
return { arguments in
guard let context = DrawingContext(size: arguments.drawingSize, clear: true) else {
return nil
}
let drawingRect = arguments.drawingRect
let imageSize = image.size
let fittedSize = imageSize.aspectFilled(arguments.boundingSize).fitted(imageSize)
let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize)
context.withFlippedContext { c in
c.setBlendMode(.copy)
if let cgImage = image.cgImage {
drawImage(context: c, image: cgImage, orientation: .up, in: fittedRect)
}
}
return context
}
} else {
return nil
}
}
let imageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError> = editedSignal
|> mapToSignal { result in
if result != nil {
return .single(result!)
} else {
return updateImageSignal
}
}
self.imageNode.setSignal(imageSignal)
}
self.currentDimensions = imageDimensions
if let _ = imageDimensions {
self.setNeedsLayout()
}
self.updateHiddenMedia()
}
self.item = item
self.updateSelectionState(animated: false)
}
func updateSelectionState(animated: Bool) {
if self.checkNode == nil, let item = self.item, let _ = item.controllerInteraction.selectionState {
let checkNode = InteractiveCheckNode(theme: CheckNodeTheme(theme: item.theme, style: .overlay))
checkNode.valueChanged = { [weak self] value in
guard let self else {
return
}
if !item.controllerInteraction.toggleSelection(item.result, value) {
self.checkNode?.setSelected(false, animated: false)
}
}
self.addSubnode(checkNode)
self.checkNode = checkNode
self.setNeedsLayout()
}
if let item = self.item {
if let selectionState = item.controllerInteraction.selectionState {
let selected = selectionState.isIdentifierSelected(item.result.id)
self.checkNode?.setSelected(selected, animated: animated)
}
}
}
func updateHiddenMedia() {
if let item = self.item {
let wasHidden = self.isHidden
self.isHidden = item.controllerInteraction.hiddenMediaId == item.result.id
if !self.isHidden && wasHidden {
self.checkNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
}
func transitionView() -> UIView {
let view = self.imageNode.view.snapshotContentTree(unhide: true, keepTransform: true)!
view.frame = self.convert(self.bounds, to: nil)
return view
}
override func layout() {
super.layout()
let imageFrame = self.bounds
self.imageNode.frame = imageFrame
if let item = self.item, let dimensions = self.currentDimensions {
let imageSize = dimensions.aspectFilled(imageFrame.size)
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageFrame.size, intrinsicInsets: UIEdgeInsets(), emptyColor: item.theme.list.mediaPlaceholderColor))()
}
let checkSize = CGSize(width: 28.0, height: 28.0)
self.checkNode?.frame = CGRect(origin: CGPoint(x: imageFrame.width - checkSize.width - 2.0, y: 2.0), size: checkSize)
}
@objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
guard let item = self.item else {
return
}
switch recognizer.state {
case .ended:
if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation {
switch gesture {
case .tap:
item.controllerInteraction.openResult(item.result)
default:
break
}
}
default:
break
}
}
}
@@ -0,0 +1,71 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramCore
import TelegramPresentationData
import SearchBarNode
private let searchBarFont = Font.regular(17.0)
final class WebSearchNavigationContentNode: NavigationBarContentNode {
private let theme: PresentationTheme
private let strings: PresentationStrings
private let searchBar: SearchBarNode
private var queryUpdated: ((String) -> Void)?
var cancel: (() -> Void)?
init(theme: PresentationTheme, strings: PresentationStrings, attachment: Bool) {
self.theme = theme
self.strings = strings
self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme, hasSeparator: false), strings: strings, fieldStyle: .modern, displayBackground: !attachment)
self.searchBar.hasCancelButton = attachment
self.searchBar.placeholderString = NSAttributedString(string: attachment ? strings.Attachment_SearchWeb : strings.Common_Search, font: searchBarFont, textColor: theme.rootController.navigationSearchBar.inputPlaceholderTextColor)
super.init()
self.addSubnode(self.searchBar)
self.searchBar.textUpdated = { [weak self] query, _ in
self?.queryUpdated?(query)
}
self.searchBar.cancel = { [weak self] in
self?.cancel?()
}
}
func setQueryUpdated(_ f: @escaping (String) -> Void) {
self.queryUpdated = f
}
func setActivity(_ activity: Bool) {
self.searchBar.activity = activity
}
func setQuery(_ query: String) {
self.searchBar.text = query
}
override var nominalHeight: CGFloat {
return 56.0
}
override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
let searchBarFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - self.nominalHeight), size: CGSize(width: size.width, height: 56.0))
self.searchBar.frame = searchBarFrame
self.searchBar.updateLayout(boundingSize: searchBarFrame.size, leftInset: leftInset, rightInset: rightInset, transition: transition)
}
func activate(select: Bool = false) {
self.searchBar.activate()
self.searchBar.selectAll()
}
func deactivate() {
self.searchBar.deactivate(clear: false)
}
}
@@ -0,0 +1,69 @@
import Foundation
import UIKit
import Postbox
import TelegramCore
import SwiftSignalKit
import TelegramUIPreferences
private struct WebSearchRecentQueryItemId {
public let rawValue: MemoryBuffer
var value: String {
return String(data: self.rawValue.makeData(), encoding: .utf8) ?? ""
}
init(_ rawValue: MemoryBuffer) {
self.rawValue = rawValue
}
init?(_ value: String) {
if let data = value.data(using: .utf8) {
self.rawValue = MemoryBuffer(data: data)
} else {
return nil
}
}
}
public final class RecentWebSearchQueryItem: Codable {
init() {
}
public init(from decoder: Decoder) throws {
}
public func encode(to encoder: Encoder) throws {
}
}
func addRecentWebSearchQuery(engine: TelegramEngine, string: String) -> Signal<Never, NoError> {
if let itemId = WebSearchRecentQueryItemId(string) {
return engine.orderedLists.addOrMoveToFirstPosition(collectionId: ApplicationSpecificOrderedItemListCollectionId.webSearchRecentQueries, id: itemId.rawValue, item: RecentWebSearchQueryItem(), removeTailIfCountExceeds: 100)
} else {
return .complete()
}
}
func removeRecentWebSearchQuery(engine: TelegramEngine, string: String) -> Signal<Never, NoError> {
if let itemId = WebSearchRecentQueryItemId(string) {
return engine.orderedLists.removeItem(collectionId: ApplicationSpecificOrderedItemListCollectionId.webSearchRecentQueries, id: itemId.rawValue)
} else {
return .complete()
}
}
func clearRecentWebSearchQueries(engine: TelegramEngine) -> Signal<Never, NoError> {
return engine.orderedLists.clear(collectionId: ApplicationSpecificOrderedItemListCollectionId.webSearchRecentQueries)
}
func webSearchRecentQueries(engine: TelegramEngine) -> Signal<[String], NoError> {
return engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: ApplicationSpecificOrderedItemListCollectionId.webSearchRecentQueries))
|> map { items -> [String] in
var result: [String] = []
for item in items {
let value = WebSearchRecentQueryItemId(item.id).value
result.append(value)
}
return result
}
}
@@ -0,0 +1,233 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
private enum RevealOptionKey: Int32 {
case delete
}
public class WebSearchRecentQueryItem: ListViewItem {
let theme: PresentationTheme
let strings: PresentationStrings
let account: Account
let query: String
let tapped: (String) -> Void
let deleted: (String) -> Void
let header: ListViewItemHeader?
public init(account: Account, theme: PresentationTheme, strings: PresentationStrings, query: String, tapped: @escaping (String) -> Void, deleted: @escaping (String) -> Void, header: ListViewItemHeader) {
self.theme = theme
self.strings = strings
self.account = account
self.query = query
self.tapped = tapped
self.deleted = deleted
self.header = header
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = WebSearchRecentQueryItemNode()
let makeLayout = node.asyncLayout()
let (nodeLayout, nodeApply) = makeLayout(self, params, nextItem == nil, !(previousItem is WebSearchRecentQueryItem))
node.contentSize = nodeLayout.contentSize
node.insets = nodeLayout.insets
completion(node, nodeApply)
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? WebSearchRecentQueryItemNode {
let layout = nodeValue.asyncLayout()
async {
let (nodeLayout, apply) = layout(self, params, nextItem == nil, !(previousItem is WebSearchRecentQueryItem))
Queue.mainQueue().async {
completion(nodeLayout, { info in
apply().1(info)
})
}
}
}
}
}
public var selectable: Bool {
return true
}
public func selected(listView: ListView) {
listView.clearHighlightAnimated(true)
self.tapped(self.query)
}
}
class WebSearchRecentQueryItemNode: ItemListRevealOptionsItemNode {
private let backgroundNode: ASDisplayNode
private let separatorNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private var textNode: TextNode?
private var item: WebSearchRecentQueryItem?
private var layoutParams: ListViewItemLayoutParams?
required init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.separatorNode = ASDisplayNode()
self.separatorNode.isLayerBacked = true
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.addSubnode(self.backgroundNode)
self.addSubnode(self.separatorNode)
}
override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
if let item = self.item {
let makeLayout = self.asyncLayout()
let (nodeLayout, nodeApply) = makeLayout(item, params, nextItem == nil, previousItem == nil)
self.contentSize = nodeLayout.contentSize
self.insets = nodeLayout.insets
let _ = nodeApply()
}
}
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
if highlighted {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.separatorNode)
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if animated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
}
func asyncLayout() -> (_ item: WebSearchRecentQueryItem, _ params: ListViewItemLayoutParams, _ last: Bool, _ firstWithHeader: Bool) -> (ListViewItemNodeLayout, () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) {
let currentItem = self.item
let textLayout = TextNode.asyncLayout(self.textNode)
return { [weak self] item, params, last, firstWithHeader in
let leftInset: CGFloat = 15.0 + params.leftInset
let rightInset: CGFloat = params.rightInset
let attributedString = NSAttributedString(string: item.query, font: Font.regular(17.0), textColor: item.theme.list.itemPrimaryTextColor)
let textApply = textLayout(TextNodeLayoutArguments(attributedString: attributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 15.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 44.0), insets: UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0))
return (nodeLayout, { [weak self] in
var updatedTheme: PresentationTheme?
if currentItem?.theme !== item.theme {
updatedTheme = item.theme
}
return (nil, { _ in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
if let _ = updatedTheme {
strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor
strongSelf.backgroundNode.backgroundColor = item.theme.list.plainBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor
}
let (textLayout, textApply) = textApply
let textNode = textApply()
if strongSelf.textNode == nil {
strongSelf.textNode = textNode
strongSelf.addSubnode(textNode)
}
let textFrame = CGRect(origin: CGPoint(x: leftInset, y: floorToScreenPixels((44.0 - textLayout.size.height) / 2.0)), size: textLayout.size)
textNode.frame = textFrame
let separatorHeight = UIScreenPixel
let topHighlightInset: CGFloat = (firstWithHeader || !nodeLayout.insets.top.isZero) ? 0.0 : separatorHeight
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: nodeLayout.contentSize.width, height: nodeLayout.contentSize.height))
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top - topHighlightInset), size: CGSize(width: nodeLayout.size.width, height: nodeLayout.size.height + topHighlightInset))
strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - separatorHeight), size: CGSize(width: nodeLayout.size.width, height: separatorHeight))
strongSelf.separatorNode.isHidden = last
strongSelf.updateLayout(size: nodeLayout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: item.strings.Common_Delete, icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)]))
}
})
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false)
}
override public func headers() -> [ListViewItemHeader]? {
if let item = self.item {
return item.header.flatMap { [$0] }
} else {
return nil
}
}
override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
super.updateRevealOffset(offset: offset, transition: transition)
if let params = self.layoutParams, let textNode = self.textNode {
let leftInset: CGFloat = 15.0 + params.leftInset
var textFrame = textNode.frame
textFrame.origin.x = leftInset + offset
transition.updateFrame(node: textNode, frame: textFrame)
}
}
override func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) {
if let item = self.item {
switch option.key {
case RevealOptionKey.delete.rawValue:
item.deleted(item.query)
default:
break
}
}
self.setRevealOptionsOpened(false, animated: true)
self.revealOptionsInteractivelyClosed()
}
}
@@ -0,0 +1,548 @@
import Foundation
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import Display
import TelegramPresentationData
import AccountContext
import RadialStatusNode
import GalleryUI
import TelegramUniversalVideoContent
import GalleryUI
class WebSearchVideoGalleryItem: GalleryItem {
var id: AnyHashable {
return self.index
}
let index: Int
let context: AccountContext
let presentationData: PresentationData
let result: ChatContextResult
let content: UniversalVideoContent
let controllerInteraction: WebSearchGalleryControllerInteraction?
init(context: AccountContext, presentationData: PresentationData, index: Int, result: ChatContextResult, content: UniversalVideoContent, controllerInteraction: WebSearchGalleryControllerInteraction?) {
self.context = context
self.presentationData = presentationData
self.index = index
self.result = result
self.content = content
self.controllerInteraction = controllerInteraction
}
func node(synchronous: Bool) -> GalleryItemNode {
let node = WebSearchVideoGalleryItemNode(context: self.context, presentationData: self.presentationData, controllerInteraction: self.controllerInteraction)
node.setupItem(self)
return node
}
func updateNode(node: GalleryItemNode, synchronous: Bool) {
if let node = node as? WebSearchVideoGalleryItemNode {
node.setupItem(self)
}
}
func thumbnailItem() -> (Int64, GalleryThumbnailItem)? {
return nil
}
}
private struct FetchControls {
let fetch: () -> Void
let cancel: () -> Void
}
final class WebSearchVideoGalleryItemNode: ZoomableContentGalleryItemNode {
private let context: AccountContext
private let strings: PresentationStrings
private let controllerInteraction: WebSearchGalleryControllerInteraction?
fileprivate let _ready = Promise<Void>()
private let footerContentNode: WebSearchGalleryFooterContentNode
private var videoNode: UniversalVideoNode?
private let statusButtonNode: HighlightableButtonNode
private let statusNode: RadialStatusNode
private var isCentral = false
private var validLayout: (ContainerViewLayout, CGFloat)?
private var didPause = false
private var isPaused = true
private var requiresDownload = false
var item: WebSearchVideoGalleryItem?
private let statusDisposable = MetaDisposable()
private let fetchDisposable = MetaDisposable()
private var fetchStatus: EngineMediaResource.FetchStatus?
private var fetchControls: FetchControls?
var playbackCompleted: (() -> Void)?
init(context: AccountContext, presentationData: PresentationData, controllerInteraction: WebSearchGalleryControllerInteraction?) {
self.context = context
self.strings = presentationData.strings
self.controllerInteraction = controllerInteraction
self.footerContentNode = WebSearchGalleryFooterContentNode(context: context, presentationData: presentationData)
self.statusButtonNode = HighlightableButtonNode()
self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.5))
super.init()
self.statusButtonNode.addSubnode(self.statusNode)
self.statusButtonNode.addTarget(self, action: #selector(statusButtonPressed), forControlEvents: .touchUpInside)
self.addSubnode(self.statusButtonNode)
self.footerContentNode.cancel = {
controllerInteraction?.dismiss(true)
}
self.footerContentNode.send = { [weak self] in
if let strongSelf = self, let item = strongSelf.item {
controllerInteraction?.send(item.result)
}
}
}
deinit {
self.statusDisposable.dispose()
}
@objc override func contentTap(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
if recognizer.state == .ended {
if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation {
switch gesture {
case .tap:
if let item = self.item, let selectionState = item.controllerInteraction?.selectionState {
let legacyItem = legacyWebSearchItem(account: item.context.account, result: item.result)
selectionState.toggleItemSelection(legacyItem, success: nil)
}
case .doubleTap:
super.contentTap(recognizer)
default:
break
}
}
}
}
override func ready() -> Signal<Void, NoError> {
return self._ready.get()
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
self.validLayout = (layout, navigationBarHeight)
let statusDiameter: CGFloat = 50.0
let statusFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - statusDiameter) / 2.0), y: floor((layout.size.height - statusDiameter) / 2.0)), size: CGSize(width: statusDiameter, height: statusDiameter))
transition.updateFrame(node: self.statusButtonNode, frame: statusFrame)
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(), size: statusFrame.size))
}
func setupItem(_ item: WebSearchVideoGalleryItem) {
if self.item?.content.id != item.content.id {
var isAnimated = false
var mediaResource: EngineMediaResource?
if let content = item.content as? NativeVideoContent {
isAnimated = content.fileReference.media.isAnimated
mediaResource = EngineMediaResource(content.fileReference.media.resource)
}
if let videoNode = self.videoNode {
videoNode.canAttachContent = false
videoNode.removeFromSupernode()
}
let mediaManager = item.context.sharedContext.mediaManager
let videoNode = UniversalVideoNode(context: item.context, postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: item.content, priority: .gallery)
let videoSize = CGSize(width: item.content.dimensions.width * 2.0, height: item.content.dimensions.height * 2.0)
videoNode.updateLayout(size: videoSize, transition: .immediate)
self.videoNode = videoNode
videoNode.isUserInteractionEnabled = false
videoNode.backgroundColor = videoNode.ownsContentNode ? UIColor.black : UIColor(rgb: 0x333335)
videoNode.canAttachContent = true
self.requiresDownload = true
var mediaFileStatus: Signal<EngineMediaResource.FetchStatus?, NoError> = .single(nil)
if let mediaResource = mediaResource {
mediaFileStatus = item.context.account.postbox.mediaBox.resourceStatus(mediaResource._asResource())
|> map { status in
return EngineMediaResource.FetchStatus(status)
}
|> map(Optional.init)
}
self.statusDisposable.set((combineLatest(videoNode.status, mediaFileStatus)
|> deliverOnMainQueue).start(next: { [weak self] value, fetchStatus in
if let strongSelf = self {
var initialBuffering = false
var isPaused = true
if let value = value {
if let zoomableContent = strongSelf.zoomableContent, !value.dimensions.width.isZero && !value.dimensions.height.isZero {
let videoSize = CGSize(width: value.dimensions.width * 2.0, height: value.dimensions.height * 2.0)
if !zoomableContent.0.equalTo(videoSize) {
strongSelf.zoomableContent = (videoSize, zoomableContent.1)
strongSelf.videoNode?.updateLayout(size: videoSize, transition: .immediate)
}
}
switch value.status {
case .playing:
isPaused = false
case let .buffering(_, whilePlaying, _, _):
initialBuffering = true
isPaused = !whilePlaying
var isStreaming = false
if let fetchStatus = strongSelf.fetchStatus {
switch fetchStatus {
case .Local:
break
default:
isStreaming = true
}
}
if let content = item.content as? NativeVideoContent, !isStreaming {
initialBuffering = false
if !content.enableSound {
isPaused = false
}
}
default:
if let content = item.content as? NativeVideoContent, !content.streamVideo.enabled {
if !content.enableSound {
isPaused = false
}
}
}
}
var fetching = false
if initialBuffering {
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: nil, cancelEnabled: false, animateRotation: true), animated: false, completion: {})
} else {
var state: RadialStatusNodeState = .none
if let fetchStatus = fetchStatus {
if strongSelf.requiresDownload {
switch fetchStatus {
case let .Fetching(_, progress):
fetching = true
isPaused = true
state = .progress(color: .white, lineWidth: nil, value: CGFloat(max(0.027, progress)), cancelEnabled: false, animateRotation: true)
default:
break
}
}
}
strongSelf.statusNode.transitionToState(state, animated: false, completion: {})
}
strongSelf.isPaused = isPaused
strongSelf.fetchStatus = fetchStatus
strongSelf.statusButtonNode.isHidden = !initialBuffering && !isPaused && !fetching
}
}))
self.zoomableContent = (videoSize, videoNode)
videoNode.playbackCompleted = { [weak videoNode] in
Queue.mainQueue().async {
if !isAnimated {
videoNode?.seek(0.0)
}
}
}
self._ready.set(videoNode.ready)
}
self.item = item
}
override func centralityUpdated(isCentral: Bool) {
super.centralityUpdated(isCentral: isCentral)
if self.isCentral != isCentral {
self.isCentral = isCentral
if let videoNode = self.videoNode, videoNode.ownsContentNode {
if isCentral {
videoNode.play()
} else {
videoNode.pause()
}
}
}
}
override func activateAsInitial() {
if self.isCentral {
self.videoNode?.play()
}
}
override func animateIn(from node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) {
guard let videoNode = self.videoNode else {
return
}
if let node = node.0 as? OverlayMediaItemNode {
var transformedFrame = node.view.convert(node.view.bounds, to: videoNode.view)
let transformedSuperFrame = node.view.convert(node.view.bounds, to: videoNode.view.superview)
videoNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: videoNode.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
transformedFrame.origin = CGPoint()
let transform = CATransform3DScale(videoNode.layer.transform, transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height, 1.0)
videoNode.layer.animate(from: NSValue(caTransform3D: transform), to: NSValue(caTransform3D: videoNode.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25)
self.context.sharedContext.mediaManager.setOverlayVideoNode(nil)
} else {
var transformedFrame = node.0.view.convert(node.0.view.bounds, to: videoNode.view)
let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: videoNode.view.superview)
let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view)
let transformedCopyViewFinalFrame = videoNode.view.convert(videoNode.view.bounds, to: self.view)
let surfaceCopyView = node.2().0!
let copyView = node.2().0!
addToTransitionSurface(surfaceCopyView)
var transformedSurfaceFrame: CGRect?
var transformedSurfaceFinalFrame: CGRect?
if let contentSurface = surfaceCopyView.superview {
transformedSurfaceFrame = node.0.view.convert(node.0.view.bounds, to: contentSurface)
transformedSurfaceFinalFrame = videoNode.view.convert(videoNode.view.bounds, to: contentSurface)
}
if let transformedSurfaceFrame = transformedSurfaceFrame {
surfaceCopyView.frame = transformedSurfaceFrame
}
self.view.insertSubview(copyView, belowSubview: self.scrollNode.view)
copyView.frame = transformedSelfFrame
copyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, removeOnCompletion: false)
surfaceCopyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
copyView.layer.animatePosition(from: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), to: CGPoint(x: transformedCopyViewFinalFrame.midX, y: transformedCopyViewFinalFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak copyView] _ in
copyView?.removeFromSuperview()
})
let scale = CGSize(width: transformedCopyViewFinalFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewFinalFrame.size.height / transformedSelfFrame.size.height)
copyView.layer.animate(from: NSValue(caTransform3D: CATransform3DIdentity), to: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false)
if let transformedSurfaceFrame = transformedSurfaceFrame, let transformedSurfaceFinalFrame = transformedSurfaceFinalFrame {
surfaceCopyView.layer.animatePosition(from: CGPoint(x: transformedSurfaceFrame.midX, y: transformedSurfaceFrame.midY), to: CGPoint(x: transformedCopyViewFinalFrame.midX, y: transformedCopyViewFinalFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak surfaceCopyView] _ in
surfaceCopyView?.removeFromSuperview()
})
let scale = CGSize(width: transformedSurfaceFinalFrame.size.width / transformedSurfaceFrame.size.width, height: transformedSurfaceFinalFrame.size.height / transformedSurfaceFrame.size.height)
surfaceCopyView.layer.animate(from: NSValue(caTransform3D: CATransform3DIdentity), to: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false)
}
videoNode.allowsGroupOpacity = true
videoNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, completion: { [weak videoNode] _ in
videoNode?.allowsGroupOpacity = false
})
videoNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: videoNode.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
transformedFrame.origin = CGPoint()
let transform = CATransform3DScale(videoNode.layer.transform, transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height, 1.0)
videoNode.layer.animate(from: NSValue(caTransform3D: transform), to: NSValue(caTransform3D: videoNode.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25)
self.statusButtonNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: self.statusButtonNode.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
self.statusButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
self.statusButtonNode.layer.animateScale(from: 0.5, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
}
}
override func animateOut(to node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) {
guard let videoNode = self.videoNode else {
completion()
return
}
var transformedFrame = node.0.view.convert(node.0.view.bounds, to: videoNode.view)
let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: videoNode.view.superview)
let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view)
let transformedCopyViewInitialFrame = videoNode.view.convert(videoNode.view.bounds, to: self.view)
var positionCompleted = false
var boundsCompleted = false
var copyCompleted = false
let copyView = node.2().0!
let surfaceCopyView = node.2().0!
addToTransitionSurface(surfaceCopyView)
var transformedSurfaceFrame: CGRect?
var transformedSurfaceCopyViewInitialFrame: CGRect?
if let contentSurface = surfaceCopyView.superview {
transformedSurfaceFrame = node.0.view.convert(node.0.view.bounds, to: contentSurface)
transformedSurfaceCopyViewInitialFrame = videoNode.view.convert(videoNode.view.bounds, to: contentSurface)
}
self.view.insertSubview(copyView, belowSubview: self.scrollNode.view)
copyView.frame = transformedSelfFrame
let intermediateCompletion = { [weak copyView, weak surfaceCopyView] in
if positionCompleted && boundsCompleted && copyCompleted {
copyView?.removeFromSuperview()
surfaceCopyView?.removeFromSuperview()
completion()
}
}
copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false)
surfaceCopyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, removeOnCompletion: false)
copyView.layer.animatePosition(from: CGPoint(x: transformedCopyViewInitialFrame.midX, y: transformedCopyViewInitialFrame.midY), to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
let scale = CGSize(width: transformedCopyViewInitialFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewInitialFrame.size.height / transformedSelfFrame.size.height)
copyView.layer.animate(from: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), to: NSValue(caTransform3D: CATransform3DIdentity), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in
copyCompleted = true
intermediateCompletion()
})
if let transformedSurfaceFrame = transformedSurfaceFrame, let transformedCopyViewInitialFrame = transformedSurfaceCopyViewInitialFrame {
surfaceCopyView.layer.animatePosition(from: CGPoint(x: transformedCopyViewInitialFrame.midX, y: transformedCopyViewInitialFrame.midY), to: CGPoint(x: transformedSurfaceFrame.midX, y: transformedSurfaceFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
let scale = CGSize(width: transformedCopyViewInitialFrame.size.width / transformedSurfaceFrame.size.width, height: transformedCopyViewInitialFrame.size.height / transformedSurfaceFrame.size.height)
surfaceCopyView.layer.animate(from: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), to: NSValue(caTransform3D: CATransform3DIdentity), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false)
}
videoNode.layer.animatePosition(from: videoNode.layer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
positionCompleted = true
intermediateCompletion()
})
videoNode.allowsGroupOpacity = true
videoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak videoNode] _ in
videoNode?.allowsGroupOpacity = false
})
self.statusButtonNode.layer.animatePosition(from: self.statusButtonNode.layer.position, to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
//positionCompleted = true
//intermediateCompletion()
})
self.statusButtonNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
self.statusButtonNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.25, removeOnCompletion: false)
transformedFrame.origin = CGPoint()
let transform = CATransform3DScale(videoNode.layer.transform, transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height, 1.0)
videoNode.layer.animate(from: NSValue(caTransform3D: videoNode.layer.transform), to: NSValue(caTransform3D: transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in
boundsCompleted = true
intermediateCompletion()
})
}
func animateOut(toOverlay node: ASDisplayNode, completion: @escaping () -> Void) {
guard let videoNode = self.videoNode else {
completion()
return
}
var transformedFrame = node.view.convert(node.view.bounds, to: videoNode.view)
let transformedSuperFrame = node.view.convert(node.view.bounds, to: videoNode.view.superview)
let transformedSelfFrame = node.view.convert(node.view.bounds, to: self.view)
let transformedCopyViewInitialFrame = videoNode.view.convert(videoNode.view.bounds, to: self.view)
let transformedSelfTargetSuperFrame = videoNode.view.convert(videoNode.view.bounds, to: node.view.superview)
var positionCompleted = false
var boundsCompleted = false
var copyCompleted = false
var nodeCompleted = false
let copyView = node.view.snapshotContentTree()!
videoNode.isHidden = true
copyView.frame = transformedSelfFrame
let intermediateCompletion = { [weak copyView] in
if positionCompleted && boundsCompleted && copyCompleted && nodeCompleted {
copyView?.removeFromSuperview()
completion()
}
}
copyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, removeOnCompletion: false)
copyView.layer.animatePosition(from: CGPoint(x: transformedCopyViewInitialFrame.midX, y: transformedCopyViewInitialFrame.midY), to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
let scale = CGSize(width: transformedCopyViewInitialFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewInitialFrame.size.height / transformedSelfFrame.size.height)
copyView.layer.animate(from: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), to: NSValue(caTransform3D: CATransform3DIdentity), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in
copyCompleted = true
intermediateCompletion()
})
videoNode.layer.animatePosition(from: videoNode.layer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
positionCompleted = true
intermediateCompletion()
})
videoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
self.statusButtonNode.layer.animatePosition(from: self.statusButtonNode.layer.position, to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
//positionCompleted = true
//intermediateCompletion()
})
self.statusButtonNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
self.statusButtonNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.25, removeOnCompletion: false)
transformedFrame.origin = CGPoint()
let videoTransform = CATransform3DScale(videoNode.layer.transform, transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height, 1.0)
videoNode.layer.animate(from: NSValue(caTransform3D: videoNode.layer.transform), to: NSValue(caTransform3D: videoTransform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in
boundsCompleted = true
intermediateCompletion()
})
let nodeTransform = CATransform3DScale(node.layer.transform, videoNode.layer.bounds.size.width / transformedFrame.size.width, videoNode.layer.bounds.size.height / transformedFrame.size.height, 1.0)
node.layer.animatePosition(from: CGPoint(x: transformedSelfTargetSuperFrame.midX, y: transformedSelfTargetSuperFrame.midY), to: node.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
node.layer.animate(from: NSValue(caTransform3D: nodeTransform), to: NSValue(caTransform3D: node.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in
nodeCompleted = true
intermediateCompletion()
})
}
@objc func statusButtonPressed() {
if let videoNode = self.videoNode {
if let fetchStatus = self.fetchStatus, case .Local = fetchStatus {
self.toggleControlsVisibility()
}
if let fetchStatus = self.fetchStatus {
switch fetchStatus {
case .Local:
videoNode.togglePlayPause()
case .Remote, .Paused:
if self.requiresDownload {
self.fetchControls?.fetch()
} else {
videoNode.togglePlayPause()
}
case .Fetching:
self.fetchControls?.cancel()
}
} else {
videoNode.togglePlayPause()
}
}
}
override func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> {
return .single((self.footerContentNode, nil))
}
}