Swiftgram/TelegramUI/ChatExternalFileGalleryItem.swift
2019-01-03 21:44:27 +04:00

328 lines
18 KiB
Swift

import Foundation
import Postbox
import Display
import SwiftSignalKit
import WebKit
import TelegramCore
class ChatExternalFileGalleryItem: GalleryItem {
let account: Account
let presentationData: PresentationData
let message: Message
let location: MessageHistoryEntryLocation?
init(account: Account, presentationData: PresentationData, message: Message, location: MessageHistoryEntryLocation?) {
self.account = account
self.presentationData = presentationData
self.message = message
self.location = location
}
func node() -> GalleryItemNode {
let node = ChatExternalFileGalleryItemNode(account: self.account, presentationData: self.presentationData)
for media in self.message.media {
if let file = media as? TelegramMediaFile {
node.setFile(account: account, 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(account: account, fileReference: .message(message: MessageReference(self.message), media: file))
break
}
}
}
if let location = self.location {
node._title.set(.single("\(location.index + 1) \(self.presentationData.strings.Common_of) \(location.count)"))
}
node.setMessage(self.message)
return node
}
func updateNode(node: GalleryItemNode) {
if let node = node as? ChatExternalFileGalleryItemNode, let location = self.location {
node._title.set(.single("\(location.index + 1) \(self.presentationData.strings.Common_of) \(location.count)"))
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 accountAndFile: (Account, 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(account: Account, 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(account: account, 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(account: Account, fileReference: FileMediaReference) {
let updateFile = self.accountAndFile?.1.media != fileReference.media
self.accountAndFile = (account, fileReference)
if updateFile {
self.fileNameNode.attributedText = NSAttributedString(string: fileReference.media.fileName ?? " ", font: Font.regular(17.0), textColor: .black)
self.setupStatus(account: account, resource: fileReference.media.resource)
}
}
private func setupStatus(account: Account, resource: MediaResource) {
self.statusDisposable.set((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:
strongSelf.statusNode.isHidden = false
strongSelf.statusNode.alpha = 1.0
strongSelf.statusNodeContainer.isUserInteractionEnabled = true
strongSelf.statusNode.transitionToState(.download(.white), completion: {})
case let .Fetching(isActive, 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), completion: {})
case .Local:
if let previousStatus = previousStatus, case .Fetching = previousStatus {
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: 1.0, cancelEnabled: 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, () -> UIView?), addToTransitionSurface: (UIView) -> 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, () -> 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 copyView = node.1()!
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: kCAMediaTimingFunctionEaseIn, removeOnCompletion: false)
}
override func footerContent() -> Signal<GalleryFooterContentNode?, NoError> {
return .single(self.footerContentNode)
}
@objc func statusPressed() {
if let (account, fileReference) = self.accountAndFile, let status = self.status {
switch status {
case .Fetching:
account.postbox.mediaBox.cancelInteractiveResourceFetch(fileReference.media.resource)
case .Remote:
self.fetchDisposable.set(fetchedMediaResource(postbox: account.postbox, reference: fileReference.resourceReference(fileReference.media.resource)).start())
default:
break
}
}
}
@objc func actionButtonPressed() {
if let (account, _) = self.accountAndFile, let message = self.message, let status = self.status, case .Local = status {
let baseNavigationController = self.baseNavigationController()
(baseNavigationController?.topViewController as? ViewController)?.present(ShareController(account: account, subject: .messages([message]), showInChat: nil, externalShare: true, immediateExternalShare: true), in: .window(.root))
}
}
}