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
@@ -0,0 +1,212 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import ComponentFlow
import SemanticStatusNode
import AnimatedTextComponent
public final class AdRemainingProgressComponent: Component {
public let initialTimestamp: Int32
public let minDisplayDuration: Int32
public let maxDisplayDuration: Int32
public let action: (Bool) -> Void
public init(
initialTimestamp: Int32,
minDisplayDuration: Int32,
maxDisplayDuration: Int32,
action: @escaping (Bool) -> Void
) {
self.initialTimestamp = initialTimestamp
self.minDisplayDuration = minDisplayDuration
self.maxDisplayDuration = maxDisplayDuration
self.action = action
}
public static func ==(lhs: AdRemainingProgressComponent, rhs: AdRemainingProgressComponent) -> Bool {
if lhs.initialTimestamp != rhs.initialTimestamp {
return false
}
if lhs.minDisplayDuration != rhs.minDisplayDuration {
return false
}
if lhs.maxDisplayDuration != rhs.maxDisplayDuration {
return false
}
return true
}
public final class View: HighlightTrackingButton {
private var component: AdRemainingProgressComponent?
private weak var componentState: EmptyComponentState?
private let node: SemanticStatusNode
private var textComponent = ComponentView<Empty>()
private var cancelIcon = UIImageView()
private var progress: Double = 1.0
override init(frame: CGRect) {
self.node = SemanticStatusNode(backgroundNodeColor: .clear, foregroundNodeColor: .white)
self.node.isUserInteractionEnabled = false
self.cancelIcon.alpha = 0.0
self.cancelIcon.isUserInteractionEnabled = false
super.init(frame: frame)
self.addSubview(self.node.view)
self.addSubview(self.cancelIcon)
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
self.highligthedChanged = { [weak self] highlighted in
if let self {
if highlighted {
self.layer.removeAnimation(forKey: "opacity")
self.alpha = 0.7
} else {
self.alpha = 1.0
self.layer.animateAlpha(from: 7, to: 1.0, duration: 0.2)
}
}
}
}
required init(coder: NSCoder) {
preconditionFailure()
}
@objc private func pressed() {
guard let component = self.component else {
return
}
component.action(self.progress < .ulpOfOne)
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return super.hitTest(point, with: event)
}
var timer: SwiftSignalKit.Timer?
func update(component: AdRemainingProgressComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
if self.component == nil {
self.timer = SwiftSignalKit.Timer(timeout: 0.25, repeat: true, completion: { [weak self] progress in
guard let self else {
return
}
self.componentState?.updated(transition: .easeInOut(duration: 0.2))
}, queue: Queue.mainQueue())
self.timer?.start()
}
self.component = component
self.componentState = state
let size = CGSize(width: 24.0, height: 24.0)
let color = UIColor(rgb: 0x64d2ff)
if self.cancelIcon.image == nil {
self.cancelIcon.image = generateCancelIcon(color: color)
self.node.foregroundNodeColor = color
}
var progress = 0.0
let currentTimestamp = CFAbsoluteTimeGetCurrent()
let minTimestamp = Double(component.initialTimestamp + component.minDisplayDuration)
let initialTimestamp = Double(component.initialTimestamp)
let remaining = min(9, max(1, minTimestamp - currentTimestamp))
var textIsHidden = false
if currentTimestamp >= initialTimestamp && currentTimestamp <= minTimestamp {
progress = (minTimestamp - currentTimestamp) / (minTimestamp - initialTimestamp)
} else {
progress = 0
textIsHidden = true
}
self.progress = progress
let textSize = self.textComponent.update(
transition: transition,
component: AnyComponent(
AnimatedTextComponent(
font: Font.regular(14.0),
color: color,
items: [AnimatedTextComponent.Item(id: 0, content: .number(Int(remaining), minDigits: 1))]
)
),
environment: {},
containerSize: size
)
let iconTransition = ComponentTransition(animation: .curve(duration: 0.25, curve: .spring))
if let textView = self.textComponent.view {
if textView.superview == nil {
textView.isUserInteractionEnabled = false
self.addSubview(textView)
}
textView.frame = CGRect(origin: CGPoint(x: (size.width - textSize.width) / 2.0, y: (size.height - textSize.height) / 2.0), size: textSize)
iconTransition.setAlpha(view: textView, alpha: textIsHidden ? 0.0 : 1.0)
iconTransition.setAlpha(view: self.cancelIcon, alpha: textIsHidden ? 1.0 : 0.0)
}
if let icon = self.cancelIcon.image {
self.cancelIcon.bounds = CGRect(origin: .zero, size: icon.size)
var iconScale = 0.7
var iconAlpha = 1.0
var iconPosition = CGPoint(x: size.width / 2.0 + 10.0, y: size.height / 2.0 - 10.0)
if progress > 0.8 {
iconScale = 0.01
iconAlpha = 0.0
} else if progress < .ulpOfOne {
iconScale = 1.0
iconPosition = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
}
iconTransition.setAlpha(view: self.cancelIcon, alpha: iconAlpha)
iconTransition.setScale(view: self.cancelIcon, scale: iconScale)
iconTransition.setPosition(view: self.cancelIcon, position: iconPosition)
}
self.node.frame = CGRect(origin: .zero, size: size)
self.node.transitionToState(.progress(value: max(0.0, min(1.0, progress)), cancelEnabled: false, appearance: SemanticStatusNodeState.ProgressAppearance(inset: 1.0, lineWidth: 1.0 + UIScreenPixel), animateRotation: false), updateCutout: false)
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private func generateCancelIcon(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 12.0, height: 12.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
let lineWidth = 2.0 - UIScreenPixel
context.setLineWidth(lineWidth)
context.setLineCap(.round)
context.setStrokeColor(color.cgColor)
context.move(to: CGPoint(x: lineWidth / 2.0, y: lineWidth / 2.0))
context.addLine(to: CGPoint(x: size.width - lineWidth / 2.0, y: size.height - lineWidth / 2.0))
context.strokePath()
context.move(to: CGPoint(x: size.width - lineWidth / 2.0, y: lineWidth / 2.0))
context.addLine(to: CGPoint(x: lineWidth / 2.0, y: size.height - lineWidth / 2.0))
context.strokePath()
})
}
@@ -0,0 +1,353 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import Lottie
import TelegramPresentationData
import AnimationUI
import AccountContext
import RadialStatusNode
import StickerResources
import AppBundle
class ChatAnimationGalleryItem: GalleryItem {
var id: AnyHashable {
return self.message.stableId
}
let context: AccountContext
let presentationData: PresentationData
let message: Message
let location: MessageHistoryEntryLocation?
init(context: AccountContext, presentationData: PresentationData, message: Message, location: MessageHistoryEntryLocation?) {
self.context = context
self.presentationData = presentationData
self.message = message
self.location = location
}
func node(synchronous: Bool) -> GalleryItemNode {
let node = ChatAnimationGalleryItemNode(context: self.context, presentationData: self.presentationData)
for media in self.message.media {
if let file = media as? TelegramMediaFile {
node.setFile(context: self.context, fileReference: .message(message: MessageReference(self.message), media: file))
break
}
}
node.setMessage(self.message)
return node
}
func updateNode(node: GalleryItemNode, synchronous: Bool) {
if let node = node as? ChatAnimationGalleryItemNode {
node.setMessage(self.message)
}
}
func thumbnailItem() -> (Int64, GalleryThumbnailItem)? {
return nil
}
}
private var backgroundButtonIcon: UIImage = {
return generateImage(CGSize(width: 20.0, height: 20.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
context.setLineWidth(1.0)
context.setStrokeColor(UIColor.white.cgColor)
context.setFillColor(UIColor.white.cgColor)
context.strokeEllipse(in: bounds.insetBy(dx: 0.5, dy: 0.5))
context.addEllipse(in: bounds.insetBy(dx: 0.5, dy: 0.5))
context.clip()
context.fill(CGRect(x: 0.0, y: 0.0, width: 10.0, height: 20.0))
})!
}()
final class ChatAnimationGalleryItemNode: ZoomableContentGalleryItemNode {
private let context: AccountContext
private var presentationData: PresentationData
private var message: Message?
fileprivate let _title = Promise<String>()
fileprivate let _rightBarButtonItems = Promise<[UIBarButtonItem]?>()
private let containerNode: ASDisplayNode
private let animationNode: AnimationNode
private let statusNodeContainer: HighlightableButtonNode
private let statusNode: RadialStatusNode
private let footerContentNode: ChatItemGalleryFooterContentNode
private var contextAndMedia: (AccountContext, AnyMediaReference)?
private var disposable = MetaDisposable()
private var fetchDisposable = MetaDisposable()
private let statusDisposable = MetaDisposable()
private var status: MediaResourceStatus?
init(context: AccountContext, presentationData: PresentationData) {
self.context = context
self.presentationData = presentationData
self.containerNode = ASDisplayNode()
self.containerNode.backgroundColor = .black
self.animationNode = AnimationNode()
self.containerNode.addSubnode(self.animationNode)
self.footerContentNode = ChatItemGalleryFooterContentNode(context: context, presentationData: presentationData)
self.statusNodeContainer = HighlightableButtonNode()
self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.5))
self.statusNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 50.0, height: 50.0))
self.statusNode.isHidden = true
super.init()
self.statusNodeContainer.addSubnode(self.statusNode)
self.addSubnode(self.statusNodeContainer)
self.statusNodeContainer.addTarget(self, action: #selector(self.statusPressed), forControlEvents: .touchUpInside)
self.statusNodeContainer.isUserInteractionEnabled = false
}
deinit {
self.disposable.dispose()
self.fetchDisposable.dispose()
self.statusDisposable.dispose()
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
let statusSize = CGSize(width: 50.0, height: 50.0)
transition.updateFrame(node: self.statusNodeContainer, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - statusSize.width) / 2.0), y: floor((layout.size.height - statusSize.height) / 2.0)), size: statusSize))
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(), size: statusSize))
}
fileprivate func setMessage(_ message: Message) {
self.footerContentNode.setMessage(message)
}
func setFile(context: AccountContext, fileReference: FileMediaReference) {
if self.contextAndMedia == nil || !self.contextAndMedia!.1.media.isEqual(to: fileReference.media) {
let signal = chatMessageAnimatedStickerBackingData(postbox: context.account.postbox, fileReference: fileReference, synchronousLoad: false)
|> mapToSignal { value -> Signal<Data, NoError> in
if value._1, let data = value._0 {
return .single(data)
} else {
return .complete()
}
}
self.disposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] next in
guard let strongSelf = self else {
return
}
if let json = try? JSONSerialization.jsonObject(with: next, options: []) as? [String: Any] {
let containerSize = CGSize(width: 640.0, height: 640.0)
strongSelf.animationNode.setAnimation(json: json)
strongSelf.zoomableContent = (containerSize, strongSelf.containerNode)
if let animationSize = strongSelf.animationNode.preferredSize() {
let size = animationSize.fitted(containerSize)
strongSelf.animationNode.frame = CGRect(origin: CGPoint(x: floor((containerSize.width - size.width) / 2.0), y: floor((containerSize.height - size.height) / 2.0)), size: size)
}
strongSelf.animationNode.loop()
}
}))
self.setupStatus(resource: fileReference.media.resource)
self._title.set(.single("\(fileReference.media.fileName ?? "") - \(dataSizeString(fileReference.media.size ?? 0, forceDecimal: false, formatting: DataSizeStringFormatting(presentationData: self.presentationData)))"))
self._rightBarButtonItems.set(.single([]))
}
self.contextAndMedia = (context, fileReference.abstract)
}
@objc private func toggleSpeedButtonPressed() {
if self.animationNode.speed == 1.0 {
self.animationNode.speed = 0.1
} else {
self.animationNode.speed = 1.0
}
}
@objc private func toggleBackgroundButtonPressed() {
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
if self.containerNode.backgroundColor == .white {
transition.updateBackgroundColor(node: self.containerNode, color: .black)
} else {
transition.updateBackgroundColor(node: self.containerNode, color: .white)
}
}
private func setupStatus(resource: MediaResource) {
self.statusDisposable.set((self.context.account.postbox.mediaBox.resourceStatus(resource)
|> deliverOnMainQueue).start(next: { [weak self] status in
if let strongSelf = self {
let previousStatus = strongSelf.status
strongSelf.status = status
switch status {
case .Remote, .Paused:
strongSelf.statusNode.isHidden = false
strongSelf.statusNode.alpha = 1.0
strongSelf.statusNodeContainer.isUserInteractionEnabled = true
strongSelf.statusNode.transitionToState(.download(.white), completion: {})
case let .Fetching(_, progress):
strongSelf.statusNode.isHidden = false
strongSelf.statusNode.alpha = 1.0
strongSelf.statusNodeContainer.isUserInteractionEnabled = true
let adjustedProgress = max(progress, 0.027)
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true, animateRotation: true), completion: {})
case .Local:
if let previousStatus = previousStatus, case .Fetching = previousStatus {
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: 1.0, cancelEnabled: true, animateRotation: true), completion: {
if let strongSelf = self {
strongSelf.statusNode.alpha = 0.0
strongSelf.statusNodeContainer.isUserInteractionEnabled = false
strongSelf.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { _ in
if let strongSelf = self {
strongSelf.statusNode.transitionToState(.none, animated: false, completion: {})
}
})
}
})
} else if !strongSelf.statusNode.isHidden && !strongSelf.statusNode.alpha.isZero {
strongSelf.statusNode.alpha = 0.0
strongSelf.statusNodeContainer.isUserInteractionEnabled = false
strongSelf.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { _ in
if let strongSelf = self {
strongSelf.statusNode.transitionToState(.none, animated: false, completion: {})
}
})
}
}
}
}))
}
override func animateIn(from node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) {
var transformedFrame = node.0.view.convert(node.0.view.bounds, to: self.containerNode.view)
let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: self.containerNode.view.superview)
self.containerNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: self.containerNode.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
transformedFrame.origin = CGPoint()
let transform = CATransform3DScale(self.containerNode.layer.transform, transformedFrame.size.width / self.containerNode.layer.bounds.size.width, transformedFrame.size.height / self.containerNode.layer.bounds.size.height, 1.0)
self.containerNode.layer.animate(from: NSValue(caTransform3D: transform), to: NSValue(caTransform3D: self.containerNode.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25)
self.statusNodeContainer.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: self.statusNodeContainer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
self.statusNodeContainer.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
self.statusNodeContainer.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) {
var transformedFrame = node.0.view.convert(node.0.view.bounds, to: self.containerNode.view)
let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: self.containerNode.view.superview)
let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view)
let transformedCopyViewInitialFrame = self.containerNode.view.convert(self.containerNode.view.bounds, to: self.view)
var positionCompleted = false
var boundsCompleted = false
var copyCompleted = false
let (maybeCopyView, copyViewBackgrond) = node.2()
copyViewBackgrond?.alpha = 0.0
let copyView = maybeCopyView!
self.view.insertSubview(copyView, belowSubview: self.containerNode.view)
copyView.frame = transformedSelfFrame
let intermediateCompletion = { [weak copyView] in
if positionCompleted && boundsCompleted && copyCompleted {
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()
})
self.containerNode.layer.animatePosition(from: self.containerNode.layer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
positionCompleted = true
intermediateCompletion()
})
self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
transformedFrame.origin = CGPoint()
let transform = CATransform3DScale(self.containerNode.layer.transform, transformedFrame.size.width / self.containerNode.layer.bounds.size.width, transformedFrame.size.height / self.containerNode.layer.bounds.size.height, 1.0)
self.containerNode.layer.animate(from: NSValue(caTransform3D: self.containerNode.layer.transform), to: NSValue(caTransform3D: transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in
boundsCompleted = true
intermediateCompletion()
})
self.statusNodeContainer.layer.animatePosition(from: self.statusNodeContainer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
self.statusNodeContainer.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue, removeOnCompletion: false)
}
override func visibilityUpdated(isVisible: Bool) {
super.visibilityUpdated(isVisible: isVisible)
if let (_, mediaReference) = self.contextAndMedia, let _ = mediaReference.concrete(TelegramMediaFile.self) {
if isVisible {
} else {
self.fetchDisposable.set(nil)
}
}
}
override func title() -> Signal<String, NoError> {
return self._title.get()
}
override func rightBarButtonItems() -> Signal<[UIBarButtonItem]?, NoError> {
return self._rightBarButtonItems.get()
}
override func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> {
return .single((self.footerContentNode, nil))
}
@objc func statusPressed() {
if let (_, mediaReference) = self.contextAndMedia, let status = self.status {
var resource: MediaResourceReference?
var statsCategory: MediaResourceStatsCategory?
if let fileReference = mediaReference.concrete(TelegramMediaFile.self) {
resource = fileReference.resourceReference(fileReference.media.resource)
statsCategory = statsCategoryForFileWithAttributes(fileReference.media.attributes)
}
if let resource = resource {
switch status {
case .Fetching:
self.context.account.postbox.mediaBox.cancelInteractiveResourceFetch(resource.resource)
case .Remote:
self.fetchDisposable.set(fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, userLocation: (self.message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, userContentType: .file, reference: resource, statsCategory: statsCategory ?? .generic).start())
default:
break
}
}
}
}
}
@@ -0,0 +1,397 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Postbox
import Display
import SwiftSignalKit
import WebKit
import TelegramCore
import TelegramPresentationData
import AccountContext
import RadialStatusNode
class ChatDocumentGalleryItem: GalleryItem {
var id: AnyHashable {
return self.message.stableId
}
let context: AccountContext
let presentationData: PresentationData
let message: Message
let location: MessageHistoryEntryLocation?
init(context: AccountContext, presentationData: PresentationData, message: Message, location: MessageHistoryEntryLocation?) {
self.context = context
self.presentationData = presentationData
self.message = message
self.location = location
}
func node(synchronous: Bool) -> GalleryItemNode {
let node = ChatDocumentGalleryItemNode(context: self.context, presentationData: self.presentationData)
for media in self.message.media {
if let file = media as? TelegramMediaFile {
node.setFile(context: context, fileReference: .message(message: MessageReference(self.message), media: file))
break
} else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
if let file = content.file {
node.setFile(context: context, fileReference: .message(message: MessageReference(self.message), media: file))
break
}
}
}
if let location = self.location {
node._title.set(.single(self.presentationData.strings.Items_NOfM("\(location.index + 1)", "\(location.count)").string))
}
node.setMessage(self.message)
return node
}
func updateNode(node: GalleryItemNode, synchronous: Bool) {
if let node = node as? ChatDocumentGalleryItemNode, let location = self.location {
node._title.set(.single(self.presentationData.strings.Items_NOfM("\(location.index + 1)", "\(location.count)").string))
node.setMessage(self.message)
}
}
func thumbnailItem() -> (Int64, GalleryThumbnailItem)? {
return nil
}
}
private let registeredURLProtocol: Void = {
URLProtocol.registerClass(ChatDocumentURLProtocol.self)
}()
private final class ChatDocumentURLProtocol: URLProtocol {
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
return request
}
override class func canInit(with request: URLRequest) -> Bool {
if let mainDocumentURL = request.mainDocumentURL {
if mainDocumentURL.scheme == "file" && request.url != mainDocumentURL {
return true
}
}
return false
}
override class func requestIsCacheEquivalent(_ a: URLRequest, to b: URLRequest) -> Bool {
return super.requestIsCacheEquivalent(a, to: b)
}
override func startLoading() {
}
override func stopLoading() {
}
}
class ChatDocumentGalleryItemNode: ZoomableContentGalleryItemNode, WKNavigationDelegate {
fileprivate let _title = Promise<String>()
private let statusNodeContainer: HighlightableButtonNode
private let statusNode: RadialStatusNode
private let webView: UIView
private var contextAndFile: (AccountContext, FileMediaReference)?
private let dataDisposable = MetaDisposable()
private var itemIsVisible = false
private var message: Message?
private let footerContentNode: ChatItemGalleryFooterContentNode
private var fetchDisposable = MetaDisposable()
private let statusDisposable = MetaDisposable()
private var status: MediaResourceStatus?
init(context: AccountContext, presentationData: PresentationData) {
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
let preferences = WKPreferences()
preferences.javaScriptEnabled = false
let configuration = WKWebViewConfiguration()
configuration.preferences = preferences
let webView = WKWebView(frame: CGRect(), configuration: configuration)
webView.allowsLinkPreview = false
webView.allowsBackForwardNavigationGestures = false
self.webView = webView
} else {
let _ = registeredURLProtocol
let webView = UIWebView()
webView.scalesPageToFit = true
self.webView = webView
}
self.footerContentNode = ChatItemGalleryFooterContentNode(context: context, presentationData: presentationData)
self.statusNodeContainer = HighlightableButtonNode()
self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.5))
self.statusNode.isHidden = true
super.init()
self.view.insertSubview(self.webView, belowSubview: self.scrollNode.view)
self.statusNodeContainer.addSubnode(self.statusNode)
self.addSubnode(self.statusNodeContainer)
self.statusNodeContainer.addTarget(self, action: #selector(self.statusPressed), forControlEvents: .touchUpInside)
self.statusNodeContainer.isUserInteractionEnabled = false
}
deinit {
self.dataDisposable.dispose()
self.fetchDisposable.dispose()
self.statusDisposable.dispose()
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
self.webView.frame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: layout.size.height - navigationBarHeight - 44.0 - layout.insets(options: []).bottom))
let statusSize = CGSize(width: 50.0, height: 50.0)
transition.updateFrame(node: self.statusNodeContainer, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - statusSize.width) / 2.0), y: floor((layout.size.height - statusSize.height) / 2.0)), size: statusSize))
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(), size: statusSize))
}
fileprivate func setMessage(_ message: Message) {
self.footerContentNode.setMessage(message)
}
override func navigationStyle() -> Signal<GalleryItemNodeNavigationStyle, NoError> {
return .single(.dark)
}
func setFile(context: AccountContext, fileReference: FileMediaReference) {
let updateFile = self.contextAndFile?.1.media != fileReference.media
self.contextAndFile = (context, fileReference)
if updateFile {
if fileReference.media.mimeType.hasPrefix("image/") {
if let webView = self.webView as? WKWebView {
webView.backgroundColor = .black
}
}
self.maybeLoadContent()
self.setupStatus(context: context, resource: fileReference.media.resource)
self.fetchDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: (self.message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, userContentType: .file, reference: fileReference.resourceReference(fileReference.media.resource)).start())
}
}
private func setupStatus(context: AccountContext, resource: MediaResource) {
self.statusDisposable.set((context.account.postbox.mediaBox.resourceStatus(resource)
|> deliverOnMainQueue).start(next: { [weak self] status in
if let strongSelf = self {
let previousStatus = strongSelf.status
strongSelf.status = status
switch status {
case .Remote, .Paused:
strongSelf.statusNode.isHidden = false
strongSelf.statusNode.alpha = 1.0
strongSelf.statusNodeContainer.isUserInteractionEnabled = true
strongSelf.statusNode.transitionToState(.download(.white), completion: {})
case let .Fetching(_, progress):
strongSelf.statusNode.isHidden = false
strongSelf.statusNode.alpha = 1.0
strongSelf.statusNodeContainer.isUserInteractionEnabled = true
let adjustedProgress = max(progress, 0.027)
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true, animateRotation: true), completion: {})
case .Local:
if let previousStatus = previousStatus, case .Fetching = previousStatus {
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: 1.0, cancelEnabled: true, animateRotation: true), completion: {
if let strongSelf = self {
strongSelf.statusNode.alpha = 0.0
strongSelf.statusNodeContainer.isUserInteractionEnabled = false
strongSelf.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { _ in
if let strongSelf = self {
strongSelf.statusNode.transitionToState(.none, animated: false, completion: {})
}
})
}
})
} else if !strongSelf.statusNode.isHidden && !strongSelf.statusNode.alpha.isZero {
strongSelf.statusNode.alpha = 0.0
strongSelf.statusNodeContainer.isUserInteractionEnabled = false
strongSelf.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { _ in
if let strongSelf = self {
strongSelf.statusNode.transitionToState(.none, animated: false, completion: {})
}
})
}
}
}
}))
}
private func maybeLoadContent() {
if let (context, fileReference) = self.contextAndFile {
var pathExtension: String?
if let fileName = fileReference.media.fileName {
pathExtension = (fileName as NSString).pathExtension
}
let data = context.account.postbox.mediaBox.resourceData(fileReference.media.resource, pathExtension: pathExtension, option: .complete(waitUntilFetchStatus: false))
|> deliverOnMainQueue
self.dataDisposable.set(data.start(next: { [weak self] data in
if let strongSelf = self {
if data.complete {
if let webView = strongSelf.webView as? WKWebView {
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
let blockRules = """
[{
"trigger": {
"url-filter": ".*"
},
"action": {
"type": "block"
}
},
{
"trigger": {
"url-filter": "file://\(data.path)"
},
"action": {
"type": "ignore-previous-rules"
}
}]
"""
WKContentRuleListStore.default().compileContentRuleList(
forIdentifier: "ContentBlockingRules",
encodedContentRuleList: blockRules) { [weak webView] contentRuleList, error in
guard let webView = webView, let contentRuleList = contentRuleList else {
return
}
if let _ = error {
return
}
let configuration = webView.configuration
configuration.userContentController.add(contentRuleList)
webView.loadFileURL(URL(fileURLWithPath: data.path), allowingReadAccessTo: URL(fileURLWithPath: data.path))
}
}
}
}
}
}))
}
}
/*private func unloadContent() {
self.dataDisposable.set(nil)
self.webView.stopLoading()
self.webView.loadHTMLString("<html></html>", baseURL: nil)
}*/
override func visibilityUpdated(isVisible: Bool) {
super.visibilityUpdated(isVisible: isVisible)
if self.itemIsVisible != isVisible {
self.itemIsVisible = isVisible
if isVisible {
} else {
self.fetchDisposable.set(nil)
}
}
}
override func title() -> Signal<String, NoError> {
return self._title.get()
}
override func animateIn(from node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) {
var transformedFrame = node.0.view.convert(node.0.view.bounds, to: self.webView)
let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: self.webView.superview)
self.webView.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: self.webView.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
transformedFrame.origin = CGPoint()
let transform = CATransform3DScale(self.webView.layer.transform, transformedFrame.size.width / self.webView.layer.bounds.size.width, transformedFrame.size.height / self.webView.layer.bounds.size.height, 1.0)
self.webView.layer.animate(from: NSValue(caTransform3D: transform), to: NSValue(caTransform3D: self.webView.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25)
self.statusNodeContainer.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: self.statusNodeContainer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
self.statusNodeContainer.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
self.statusNodeContainer.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) {
var transformedFrame = node.0.view.convert(node.0.view.bounds, to: self.webView)
let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: self.webView.superview)
let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view)
let transformedCopyViewInitialFrame = self.webView.convert(self.webView.bounds, to: self.view)
var positionCompleted = false
var boundsCompleted = false
var copyCompleted = false
let (maybeCopyView, copyViewBackgrond) = node.2()
copyViewBackgrond?.alpha = 0.0
let copyView = maybeCopyView!
self.view.insertSubview(copyView, belowSubview: self.webView)
copyView.frame = transformedSelfFrame
let intermediateCompletion = { [weak copyView] in
if positionCompleted && boundsCompleted && copyCompleted {
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()
})
self.webView.layer.animatePosition(from: self.webView.layer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
positionCompleted = true
intermediateCompletion()
})
self.webView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
transformedFrame.origin = CGPoint()
let transform = CATransform3DScale(self.webView.layer.transform, transformedFrame.size.width / self.webView.layer.bounds.size.width, transformedFrame.size.height / self.webView.layer.bounds.size.height, 1.0)
self.webView.layer.animate(from: NSValue(caTransform3D: self.webView.layer.transform), to: NSValue(caTransform3D: transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in
boundsCompleted = true
intermediateCompletion()
})
self.statusNodeContainer.layer.animatePosition(from: self.statusNodeContainer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
self.statusNodeContainer.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue, removeOnCompletion: false)
}
override func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> {
return .single((self.footerContentNode, nil))
}
@objc func statusPressed() {
if let (context, fileReference) = self.contextAndFile, let status = self.status {
switch status {
case .Fetching:
context.account.postbox.mediaBox.cancelInteractiveResourceFetch(fileReference.media.resource)
case .Remote:
self.fetchDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: (self.message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, userContentType: .file, reference: fileReference.resourceReference(fileReference.media.resource)).start())
default:
break
}
}
}
}
@@ -0,0 +1,339 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Postbox
import Display
import SwiftSignalKit
import WebKit
import TelegramCore
import TelegramPresentationData
import AccountContext
import RadialStatusNode
import ShareController
class ChatExternalFileGalleryItem: GalleryItem {
var id: AnyHashable {
return self.message.stableId
}
let context: AccountContext
let presentationData: PresentationData
let message: Message
let location: MessageHistoryEntryLocation?
init(context: AccountContext, presentationData: PresentationData, message: Message, location: MessageHistoryEntryLocation?) {
self.context = context
self.presentationData = presentationData
self.message = message
self.location = location
}
func node(synchronous: Bool) -> GalleryItemNode {
let node = ChatExternalFileGalleryItemNode(context: self.context, presentationData: self.presentationData)
for media in self.message.media {
if let file = media as? TelegramMediaFile {
node.setFile(context: context, fileReference: .message(message: MessageReference(self.message), media: file))
break
} else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
if let file = content.file {
node.setFile(context: context, fileReference: .message(message: MessageReference(self.message), media: file))
break
}
}
}
if let location = self.location {
node._title.set(.single(self.presentationData.strings.Items_NOfM("\(location.index + 1)", "\(location.count)").string))
}
node.setMessage(self.message)
return node
}
func updateNode(node: GalleryItemNode, synchronous: Bool) {
if let node = node as? ChatExternalFileGalleryItemNode, let location = self.location {
node._title.set(.single(self.presentationData.strings.Items_NOfM("\(location.index + 1)", "\(location.count)").string))
node.setMessage(self.message)
}
}
func thumbnailItem() -> (Int64, GalleryThumbnailItem)? {
return nil
}
}
class ChatExternalFileGalleryItemNode: GalleryItemNode {
fileprivate let _title = Promise<String>()
private let statusNodeContainer: HighlightableButtonNode
private let statusNode: RadialStatusNode
private let containerNode: ASDisplayNode
private let fileNameNode: ImmediateTextNode
private let actionTitleNode: ImmediateTextNode
private let actionButtonNode: HighlightableButtonNode
private var contextAndFile: (AccountContext, FileMediaReference)?
private let dataDisposable = MetaDisposable()
private var itemIsVisible = false
private var message: Message?
private let footerContentNode: ChatItemGalleryFooterContentNode
private var fetchDisposable = MetaDisposable()
private let statusDisposable = MetaDisposable()
private var status: MediaResourceStatus?
init(context: AccountContext, presentationData: PresentationData) {
self.containerNode = ASDisplayNode()
self.containerNode.backgroundColor = .white
self.fileNameNode = ImmediateTextNode()
self.containerNode.addSubnode(self.fileNameNode)
self.actionTitleNode = ImmediateTextNode()
self.actionTitleNode.attributedText = NSAttributedString(string: presentationData.strings.Conversation_LinkDialogOpen, font: Font.regular(17.0), textColor: presentationData.theme.list.itemAccentColor)
self.containerNode.addSubnode(self.actionTitleNode)
self.actionButtonNode = HighlightableButtonNode()
self.containerNode.addSubnode(self.actionButtonNode)
self.footerContentNode = ChatItemGalleryFooterContentNode(context: context, presentationData: presentationData)
self.statusNodeContainer = HighlightableButtonNode()
self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.5))
self.statusNode.isHidden = true
super.init()
self.addSubnode(self.containerNode)
self.statusNodeContainer.addSubnode(self.statusNode)
self.addSubnode(self.statusNodeContainer)
self.statusNodeContainer.addTarget(self, action: #selector(self.statusPressed), forControlEvents: .touchUpInside)
self.statusNodeContainer.isUserInteractionEnabled = false
self.actionButtonNode.addTarget(self, action: #selector(self.actionButtonPressed), forControlEvents: .touchUpInside)
self.actionButtonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.actionTitleNode.layer.removeAnimation(forKey: "opacity")
strongSelf.actionTitleNode.alpha = 0.4
} else {
strongSelf.actionTitleNode.alpha = 1.0
strongSelf.actionTitleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
}
deinit {
self.dataDisposable.dispose()
self.fetchDisposable.dispose()
self.statusDisposable.dispose()
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: layout.size.height - navigationBarHeight - 44.0 - layout.insets(options: []).bottom))
self.containerNode.frame = containerFrame
let fileNameSize = self.fileNameNode.updateLayout(containerFrame.insetBy(dx: 10.0, dy: 0.0).size)
let actionTitleSize = self.actionTitleNode.updateLayout(containerFrame.insetBy(dx: 10.0, dy: 0.0).size)
let spacing: CGFloat = 4.0
let contentHeight: CGFloat = fileNameSize.height + spacing + actionTitleSize.height
let contentOrigin = floor((containerFrame.size.height - contentHeight) / 2.0)
let fileNameFrame = CGRect(origin: CGPoint(x: floor((containerFrame.width - fileNameSize.width) / 2.0), y: contentOrigin), size: fileNameSize)
transition.updateFrame(node: self.fileNameNode, frame: fileNameFrame)
let actionTitleFrame = CGRect(origin: CGPoint(x: floor((containerFrame.width - actionTitleSize.width) / 2.0), y: fileNameFrame.maxY + spacing), size: actionTitleSize)
transition.updateFrame(node: self.actionTitleNode, frame: actionTitleFrame)
transition.updateFrame(node: self.actionButtonNode, frame: actionTitleFrame.insetBy(dx: -8.0, dy: -8.0))
let statusSize = CGSize(width: 50.0, height: 50.0)
transition.updateFrame(node: self.statusNodeContainer, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - statusSize.width) / 2.0), y: floor((layout.size.height - statusSize.height) / 2.0)), size: statusSize))
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(), size: statusSize))
}
fileprivate func setMessage(_ message: Message) {
self.message = message
self.footerContentNode.setMessage(message)
}
override func navigationStyle() -> Signal<GalleryItemNodeNavigationStyle, NoError> {
return .single(.dark)
}
func setFile(context: AccountContext, fileReference: FileMediaReference) {
let updateFile = self.contextAndFile?.1.media != fileReference.media
self.contextAndFile = (context, fileReference)
if updateFile {
self.fileNameNode.attributedText = NSAttributedString(string: fileReference.media.fileName ?? " ", font: Font.regular(17.0), textColor: .black)
self.setupStatus(context: context, resource: fileReference.media.resource)
}
}
private func setupStatus(context: AccountContext, resource: MediaResource) {
self.statusDisposable.set((context.account.postbox.mediaBox.resourceStatus(resource)
|> deliverOnMainQueue).start(next: { [weak self] status in
if let strongSelf = self {
let previousStatus = strongSelf.status
strongSelf.status = status
switch status {
case .Remote, .Paused:
strongSelf.statusNode.isHidden = false
strongSelf.statusNode.alpha = 1.0
strongSelf.statusNodeContainer.isUserInteractionEnabled = true
strongSelf.statusNode.transitionToState(.download(.white), completion: {})
case let .Fetching(_, progress):
strongSelf.statusNode.isHidden = false
strongSelf.statusNode.alpha = 1.0
strongSelf.statusNodeContainer.isUserInteractionEnabled = true
let adjustedProgress = max(progress, 0.027)
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true, animateRotation: true), completion: {})
case .Local:
if let previousStatus = previousStatus, case .Fetching = previousStatus {
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: 1.0, cancelEnabled: true, animateRotation: true), completion: {
if let strongSelf = self {
strongSelf.statusNode.alpha = 0.0
strongSelf.statusNodeContainer.isUserInteractionEnabled = false
strongSelf.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { _ in
if let strongSelf = self {
strongSelf.statusNode.transitionToState(.none, animated: false, completion: {})
}
})
}
})
} else if !strongSelf.statusNode.isHidden && !strongSelf.statusNode.alpha.isZero {
strongSelf.statusNode.alpha = 0.0
strongSelf.statusNodeContainer.isUserInteractionEnabled = false
strongSelf.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { _ in
if let strongSelf = self {
strongSelf.statusNode.transitionToState(.none, animated: false, completion: {})
}
})
}
}
}
}))
}
override func visibilityUpdated(isVisible: Bool) {
super.visibilityUpdated(isVisible: isVisible)
if self.itemIsVisible != isVisible {
self.itemIsVisible = isVisible
if isVisible {
} else {
self.fetchDisposable.set(nil)
}
}
}
override func title() -> Signal<String, NoError> {
return self._title.get()
}
override func animateIn(from node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) {
var transformedFrame = node.0.view.convert(node.0.view.bounds, to: self.containerNode.view)
let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: self.containerNode.view.superview)
self.containerNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: self.containerNode.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
transformedFrame.origin = CGPoint()
let transform = CATransform3DScale(self.containerNode.layer.transform, transformedFrame.size.width / self.containerNode.layer.bounds.size.width, transformedFrame.size.height / self.containerNode.layer.bounds.size.height, 1.0)
self.containerNode.layer.animate(from: NSValue(caTransform3D: transform), to: NSValue(caTransform3D: self.containerNode.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25)
self.statusNodeContainer.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: self.statusNodeContainer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
self.statusNodeContainer.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
self.statusNodeContainer.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) {
var transformedFrame = node.0.view.convert(node.0.view.bounds, to: self.containerNode.view)
let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: self.containerNode.view.superview)
let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view)
let transformedCopyViewInitialFrame = self.containerNode.view.convert(self.containerNode.view.bounds, to: self.view)
var positionCompleted = false
var boundsCompleted = false
var copyCompleted = false
let (maybeCopyView, copyViewBackgrond) = node.2()
copyViewBackgrond?.alpha = 0.0
let copyView = maybeCopyView!
self.view.insertSubview(copyView, belowSubview: self.containerNode.view)
copyView.frame = transformedSelfFrame
let intermediateCompletion = { [weak copyView] in
if positionCompleted && boundsCompleted && copyCompleted {
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()
})
self.containerNode.layer.animatePosition(from: self.containerNode.layer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
positionCompleted = true
intermediateCompletion()
})
self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
transformedFrame.origin = CGPoint()
let transform = CATransform3DScale(self.containerNode.layer.transform, transformedFrame.size.width / self.containerNode.layer.bounds.size.width, transformedFrame.size.height / self.containerNode.layer.bounds.size.height, 1.0)
self.containerNode.layer.animate(from: NSValue(caTransform3D: self.containerNode.layer.transform), to: NSValue(caTransform3D: transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in
boundsCompleted = true
intermediateCompletion()
})
self.statusNodeContainer.layer.animatePosition(from: self.statusNodeContainer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
self.statusNodeContainer.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue, removeOnCompletion: false)
}
override func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> {
return .single((self.footerContentNode, nil))
}
@objc func statusPressed() {
if let (context, fileReference) = self.contextAndFile, let status = self.status {
switch status {
case .Fetching:
context.account.postbox.mediaBox.cancelInteractiveResourceFetch(fileReference.media.resource)
case .Remote:
self.fetchDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: (self.message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, userContentType: .file, reference: fileReference.resourceReference(fileReference.media.resource)).start())
default:
break
}
}
}
@objc func actionButtonPressed() {
if let (context, _) = self.contextAndFile, let message = self.message, let status = self.status, case .Local = status {
let baseNavigationController = self.baseNavigationController()
(baseNavigationController?.topViewController as? ViewController)?.present(ShareController(context: context, subject: .messages([message]), showInChat: nil, externalShare: true, immediateExternalShare: true), in: .window(.root))
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,307 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import ComponentFlow
import MultilineTextComponent
import MultilineTextWithEntitiesComponent
import Postbox
import TelegramCore
import TelegramPresentationData
import ContextUI
import PlainButtonComponent
import AvatarNode
import AccountContext
import PhotoResources
import TextFormat
final class VideoAdComponent: Component {
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let message: EngineMessage
let initialTimestamp: Int32
let action: (Bool) -> Void
let adAction: () -> Void
let moreAction: (ContextReferenceContentNode) -> Void
init(
context: AccountContext,
theme: PresentationTheme,
strings: PresentationStrings,
message: EngineMessage,
initialTimestamp: Int32,
action: @escaping (Bool) -> Void,
adAction: @escaping () -> Void,
moreAction: @escaping (ContextReferenceContentNode) -> Void
) {
self.context = context
self.theme = theme
self.strings = strings
self.message = message
self.initialTimestamp = initialTimestamp
self.action = action
self.adAction = adAction
self.moreAction = moreAction
}
static func ==(lhs: VideoAdComponent, rhs: VideoAdComponent) -> Bool {
if lhs.message != rhs.message {
return false
}
if lhs.initialTimestamp != rhs.initialTimestamp {
return false
}
return true
}
final class View: UIView {
private var component: VideoAdComponent?
private weak var componentState: EmptyComponentState?
private let wrapperView: UIView
private let backgroundView: UIView
private let imageNode: TransformImageNode
private let title = ComponentView<Empty>()
private let text = ComponentView<Empty>()
private let button = ComponentView<Empty>()
private let buttonNode: ContextReferenceContentNode
private let progress = ComponentView<Empty>()
private var adIcon: UIImage?
override init(frame: CGRect) {
self.wrapperView = UIView()
self.wrapperView.clipsToBounds = true
self.wrapperView.layer.cornerRadius = 14.0
if #available(iOS 13.0, *) {
self.wrapperView.layer.cornerCurve = .continuous
}
self.backgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
self.imageNode = TransformImageNode()
self.imageNode.isUserInteractionEnabled = false
self.buttonNode = ContextReferenceContentNode()
super.init(frame: frame)
self.addSubview(self.wrapperView)
self.wrapperView.addSubview(self.backgroundView)
self.wrapperView.addSubview(self.buttonNode.view)
self.wrapperView.addSubview(self.imageNode.view)
self.backgroundView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapped)))
}
required init(coder: NSCoder) {
preconditionFailure()
}
@objc private func tapped() {
if let component = self.component {
component.adAction()
}
}
func update(component: VideoAdComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let isFirstTime = self.component == nil
self.component = component
let titleString = component.message.author?.compactDisplayTitle ?? ""
var media: Media?
if let photo = component.message.media.first as? TelegramMediaImage {
media = photo
} else if let file = component.message.media.first as? TelegramMediaFile {
media = file
}
if isFirstTime {
let signal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>
if let photo = media as? TelegramMediaImage {
signal = mediaGridMessagePhoto(account: component.context.account, userLocation: .other, photoReference: .standalone(media: photo))
} else if let file = media as? TelegramMediaFile {
signal = mediaGridMessageVideo(postbox: component.context.account.postbox, userLocation: .other, videoReference: .standalone(media: file))
} else {
signal = .complete()
}
self.imageNode.setSignal(signal)
}
let color = UIColor(rgb: 0x64d2ff)
if self.adIcon == nil {
self.adIcon = generateAdIcon(color: color, strings: component.strings)
}
let leftInset: CGFloat = media != nil ? 51.0 : 16.0
let rightInset: CGFloat = 60.0
var titleRightInset: CGFloat = 0.0
if let adIcon = self.adIcon {
titleRightInset += adIcon.size.width + 16.0
}
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: titleString, font: Font.semibold(14.0), textColor: .white)))
),
environment: {},
containerSize: CGSize(width: availableSize.width - leftInset - rightInset - titleRightInset, height: availableSize.height)
)
let textColor = UIColor.white
var entities: [MessageTextEntity] = []
if let attribute = component.message.attributes.first(where: { $0 is TextEntitiesMessageAttribute }) as? TextEntitiesMessageAttribute {
entities = attribute.entities
}
let attributedText = stringWithAppliedEntities(component.message.text, entities: entities, baseColor: textColor, linkColor: textColor, baseFont: Font.regular(14.0), linkFont: Font.regular(14.0), boldFont: Font.semibold(14.0), italicFont: Font.italic(14.0), boldItalicFont: Font.semiboldItalic(14.0), fixedFont: Font.monospace(14.0), blockQuoteFont: Font.regular(14.0), message: nil)
let textSize = self.text.update(
transition: .immediate,
component: AnyComponent(
MultilineTextWithEntitiesComponent(
context: component.context,
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
placeholderColor: UIColor.white.withAlphaComponent(0.2),
text: .plain(attributedText),
maximumNumberOfLines: 0
)
),
environment: {},
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: availableSize.height)
)
let contentHeight = titleSize.height + 3.0 + textSize.height
let size = CGSize(width: availableSize.width, height: contentHeight + 24.0)
let imageSize = CGSize(width: 30.0, height: 30.0)
self.imageNode.frame = CGRect(origin: CGPoint(x: 10.0, y: floor((size.height - imageSize.height) / 2.0)), size: imageSize)
let makeLayout = self.imageNode.asyncLayout()
let apply = makeLayout(TransformImageArguments(corners: ImageCorners(radius: imageSize.width / 2.0), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: .zero))
apply()
let contentOriginY = floor((size.height - contentHeight) / 2.0)
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: contentOriginY), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.isUserInteractionEnabled = false
self.wrapperView.addSubview(titleView)
}
titleView.frame = titleFrame
}
let textFrame = CGRect(origin: CGPoint(x: leftInset, y: contentOriginY + contentHeight - textSize.height), size: textSize)
if let textView = self.text.view {
if textView.superview == nil {
textView.isUserInteractionEnabled = false
self.wrapperView.addSubview(textView)
}
textView.frame = textFrame
}
let buttonSize = self.button.update(
transition: .immediate,
component: AnyComponent(
PlainButtonComponent(
content: AnyComponent(
Image(image: self.adIcon, contentMode: .center)
),
effectAlignment: .center,
action: { [weak self] in
if let self {
component.moreAction(self.buttonNode)
}
},
animateScale: false
)
),
environment: {},
containerSize: availableSize
)
let buttonFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: floor(titleFrame.midY - buttonSize.height / 2.0) + 1.0), size: buttonSize)
if let buttonView = self.button.view {
if buttonView.superview == nil {
self.wrapperView.addSubview(buttonView)
}
buttonView.frame = buttonFrame
}
self.buttonNode.frame = buttonFrame
let progressSize = self.progress.update(
transition: .immediate,
component: AnyComponent(
AdRemainingProgressComponent(
initialTimestamp: component.initialTimestamp,
minDisplayDuration: 10,
maxDisplayDuration: 30,
action: { [weak self] available in
guard let self, let component = self.component else {
return
}
component.action(available)
}
)
),
environment: {},
containerSize: availableSize
)
let progressFrame = CGRect(origin: CGPoint(x: size.width - progressSize.width - 16.0, y: floor((size.height - progressSize.height) / 2.0)), size: progressSize)
if let progressView = self.progress.view {
if progressView.superview == nil {
self.wrapperView.addSubview(progressView)
}
progressView.frame = progressFrame
}
self.wrapperView.frame = CGRect(origin: .zero, size: size)
self.backgroundView.frame = CGRect(origin: .zero, size: size)
return size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private func generateAdIcon(color: UIColor, strings: PresentationStrings) -> UIImage? {
let titleString = NSAttributedString(string: strings.ChatList_Search_Ad, font: Font.regular(11.0), textColor: color, paragraphAlignment: .center)
let stringRect = titleString.boundingRect(with: CGSize(width: 200.0, height: 20.0), options: .usesLineFragmentOrigin, context: nil)
return generateImage(CGSize(width: floor(stringRect.width) + 18.0, height: 15.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
context.setFillColor(color.withMultipliedAlpha(0.1).cgColor)
context.addPath(UIBezierPath(roundedRect: bounds, cornerRadius: size.height / 2.0).cgPath)
context.fillPath()
context.setFillColor(color.cgColor)
let circleSize = CGSize(width: 2.0 - UIScreenPixel, height: 2.0 - UIScreenPixel)
context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - 8.0, y: 3.0 + UIScreenPixel), size: circleSize))
context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - 8.0, y: 7.0 - UIScreenPixel), size: circleSize))
context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - 8.0, y: 10.0), size: circleSize))
let textRect = CGRect(
x: 5.0,
y: (size.height - stringRect.height) / 2.0 - UIScreenPixel,
width: stringRect.width,
height: stringRect.height
)
UIGraphicsPushContext(context)
titleString.draw(in: textRect)
UIGraphicsPopContext()
})
}