Swiftgram/TelegramUI/SecretMediaPreviewController.swift
2019-01-22 18:21:21 +04:00

487 lines
22 KiB
Swift

import Foundation
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import SwiftSignalKit
private func galleryMediaForMedia(media: Media) -> Media? {
if let media = media as? TelegramMediaImage {
return media
} else if let file = media as? TelegramMediaFile {
if file.mimeType.hasPrefix("audio/") {
return nil
} else if !file.isVideo && file.mimeType.hasPrefix("video/") {
return file
} else {
return file
}
}
return nil
}
private func mediaForMessage(message: Message) -> Media? {
for media in message.media {
if let result = galleryMediaForMedia(media: media) {
return result
} else if let webpage = media as? TelegramMediaWebpage {
switch webpage.content {
case let .Loaded(content):
if let embedUrl = content.embedUrl, !embedUrl.isEmpty {
return webpage
} else if let image = content.image {
if let result = galleryMediaForMedia(media: image) {
return result
}
} else if let file = content.file {
if let result = galleryMediaForMedia(media: file) {
return result
}
}
case .Pending:
break
}
}
}
return nil
}
private final class SecretMediaPreviewControllerNode: GalleryControllerNode {
private var timeoutNode: RadialStatusNode?
private var validLayout: (ContainerViewLayout, CGFloat)?
var beginTimeAndTimeout: (Double, Double)? {
didSet {
if let (beginTime, timeout) = self.beginTimeAndTimeout {
if self.timeoutNode == nil {
let timeoutNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.5))
self.timeoutNode = timeoutNode
var iconImage: UIImage?
if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/SecretMediaIcon"), color: .white) {
let factor: CGFloat = 0.48
iconImage = generateImage(CGSize(width: floor(image.size.width * factor), height: floor(image.size.height * factor)), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: size))
})
}
timeoutNode.transitionToState(.secretTimeout(color: .white, icon: iconImage, beginTime: beginTime, timeout: timeout, sparks: true), completion: {})
self.addSubnode(timeoutNode)
if let (layout, navigationHeight) = self.validLayout {
self.layoutTimeoutNode(layout, navigationBarHeight: navigationHeight, transition: .immediate)
}
}
} else if let timeoutNode = self.timeoutNode {
self.timeoutNode = nil
timeoutNode.removeFromSupernode()
}
}
}
override func animateIn(animateContent: Bool) {
super.animateIn(animateContent: animateContent)
self.timeoutNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override func animateOut(animateContent: Bool, completion: @escaping () -> Void) {
super.animateOut(animateContent: animateContent, completion: completion)
if let timeoutNode = self.timeoutNode {
timeoutNode.layer.animateAlpha(from: timeoutNode.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false)
}
}
override func updateDismissTransition(_ value: CGFloat) {
self.timeoutNode?.alpha = value
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
self.validLayout = (layout, navigationBarHeight)
self.layoutTimeoutNode(layout, navigationBarHeight: navigationBarHeight, transition: transition)
}
private func layoutTimeoutNode(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
if let timeoutNode = self.timeoutNode {
let diameter: CGFloat = 28.0
transition.updateFrame(node: timeoutNode, frame: CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.right - diameter - 9.0, y: navigationBarHeight - 9.0 - diameter), size: CGSize(width: diameter, height: diameter)))
}
}
}
public final class SecretMediaPreviewController: ViewController {
private let context: AccountContext
private let _ready = Promise<Bool>()
override public var ready: Promise<Bool> {
return self._ready
}
private var didSetReady = false
private let disposable = MetaDisposable()
private let markMessageAsConsumedDisposable = MetaDisposable()
private var controllerNode: SecretMediaPreviewControllerNode {
return self.displayNode as! SecretMediaPreviewControllerNode
}
private var messageView: MessageView?
private var currentNodeMessageId: MessageId?
private var currentNodeMessageIsVideo = false
private var tempFile: TempBoxFile?
private let _hiddenMedia = Promise<(MessageId, Media)?>(nil)
private var hiddenMediaManagerIndex: Int?
private let presentationData: PresentationData
private var screenCaptureEventsDisposable: Disposable?
public init(context: AccountContext, messageId: MessageId) {
self.context = context
self.presentationData = context.currentPresentationData.with { $0 }
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: GalleryController.darkNavigationTheme, strings: NavigationBarStrings(presentationStrings: self.presentationData.strings)))
let backItem = UIBarButtonItem(backButtonAppearanceWithTitle: presentationData.strings.Common_Back, target: self, action: #selector(self.donePressed))
self.navigationItem.leftBarButtonItem = backItem
self.statusBar.statusBarStyle = .White
self.disposable.set((context.account.postbox.messageView(messageId) |> deliverOnMainQueue).start(next: { [weak self] view in
if let strongSelf = self {
strongSelf.messageView = view
if strongSelf.isViewLoaded {
strongSelf.applyMessageView()
}
}
}))
let mediaManager = context.mediaManager
self.hiddenMediaManagerIndex = mediaManager.galleryHiddenMediaManager.addSource(self._hiddenMedia.get()
|> map { messageIdAndMedia in
if let (messageId, media) = messageIdAndMedia {
return .chat(messageId, media)
} else {
return nil
}
})
self.screenCaptureEventsDisposable = (screenCaptureEvents()
|> deliverOnMainQueue).start(next: { [weak self] _ in
if let strongSelf = self, strongSelf.traceVisibility() {
if messageId.peerId.namespace == Namespaces.Peer.CloudUser {
let _ = enqueueMessages(account: context.account, peerId: messageId.peerId, messages: [.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaAction(action: TelegramMediaActionType.historyScreenshot)), replyToMessageId: nil, localGroupingKey: nil)]).start()
} else if messageId.peerId.namespace == Namespaces.Peer.SecretChat {
let _ = addSecretChatMessageScreenshot(account: context.account, peerId: messageId.peerId).start()
}
}
})
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.disposable.dispose()
self.markMessageAsConsumedDisposable.dispose()
if let hiddenMediaManagerIndex = self.hiddenMediaManagerIndex {
let mediaManager = self.context.mediaManager
mediaManager.galleryHiddenMediaManager.removeSource(hiddenMediaManagerIndex)
}
self.screenCaptureEventsDisposable?.dispose()
if let tempFile = self.tempFile {
TempBox.shared.dispose(tempFile)
}
}
@objc func donePressed() {
self.dismiss(forceAway: false)
}
public 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)
}
}, dismissController: { [weak self] in
self?.dismiss(forceAway: true)
}, replaceRootController: { _, _ in
})
self.displayNode = SecretMediaPreviewControllerNode(controllerInteraction: controllerInteraction)
self.displayNodeDidLoad()
self.controllerNode.statusBar = self.statusBar
self.controllerNode.navigationBar = self.navigationBar
self.controllerNode.transitionDataForCentralItem = { [weak self] in
if let strongSelf = self {
if let _ = strongSelf.controllerNode.pager.centralItemNode(), let presentationArguments = strongSelf.presentationArguments as? GalleryControllerPresentationArguments {
if let message = strongSelf.messageView?.message {
if let media = mediaForMessage(message: message), let transitionArguments = presentationArguments.transitionArguments(message.id, media) {
return (transitionArguments.transitionNode, transitionArguments.addToTransitionSurface)
}
}
}
}
return nil
}
self.controllerNode.dismiss = { [weak self] in
self?._hiddenMedia.set(.single(nil))
self?.presentingViewController?.dismiss(animated: false, completion: nil)
}
self.controllerNode.beginCustomDismiss = { [weak self] in
if let strongSelf = self {
strongSelf._hiddenMedia.set(.single(nil))
let animatedOutNode = true
var animatedOutInterface = false
let completion = {
if animatedOutNode && animatedOutInterface {
//self?.presentingViewController?.dismiss(animated: false, completion: nil)
}
}
strongSelf.controllerNode.animateOut(animateContent: animatedOutNode, completion: {
animatedOutInterface = true
//completion()
})
}
}
self.controllerNode.completeCustomDismiss = { [weak self] in
self?._hiddenMedia.set(.single(nil))
self?.presentingViewController?.dismiss(animated: false, completion: nil)
}
self.controllerNode.pager.centralItemIndexUpdated = { [weak self] index in
if let strongSelf = self {
var hiddenItem: (MessageId, Media)?
if let _ = index {
if let message = strongSelf.messageView?.message, let media = mediaForMessage(message: message) {
var beginTimeAndTimeout: (Double, Double)?
var videoDuration: Int32?
for media in message.media {
if let file = media as? TelegramMediaFile {
videoDuration = file.duration
}
}
for attribute in message.attributes {
if let attribute = attribute as? AutoremoveTimeoutMessageAttribute {
if let countdownBeginTime = attribute.countdownBeginTime {
if let videoDuration = videoDuration {
beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, Double(videoDuration))
} else {
beginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout))
}
}
break
}
}
if let file = media as? TelegramMediaFile {
if file.isAnimated {
strongSelf.title = strongSelf.presentationData.strings.SecretGif_Title
} else {
strongSelf.title = strongSelf.presentationData.strings.SecretVideo_Title
}
} else {
strongSelf.title = strongSelf.presentationData.strings.SecretImage_Title
}
if let beginTimeAndTimeout = beginTimeAndTimeout {
strongSelf.controllerNode.beginTimeAndTimeout = beginTimeAndTimeout
}
if !message.flags.contains(.Incoming) {
if let _ = beginTimeAndTimeout {
strongSelf.controllerNode.updatePresentationState({
$0.withUpdatedFooterContentNode(nil)
}, transition: .immediate)
} else {
let contentNode = SecretMediaPreviewFooterContentNode()
let peerTitle = messageMainPeer(message)?.compactDisplayTitle ?? ""
let text: String
if let file = media as? TelegramMediaFile {
if file.isAnimated {
text = strongSelf.presentationData.strings.SecretGIF_NotViewedYet(peerTitle).0
} else {
text = strongSelf.presentationData.strings.SecretVideo_NotViewedYet(peerTitle).0
}
} else {
text = strongSelf.presentationData.strings.SecretImage_NotViewedYet(peerTitle).0
}
contentNode.setText(text)
strongSelf.controllerNode.updatePresentationState({
$0.withUpdatedFooterContentNode(contentNode)
}, transition: .immediate)
}
}
hiddenItem = (message.id, media)
}
}
if strongSelf.didSetReady {
strongSelf._hiddenMedia.set(.single(hiddenItem))
}
}
}
if let _ = self.messageView {
self.applyMessageView()
}
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
var nodeAnimatesItself = false
if let centralItemNode = self.controllerNode.pager.centralItemNode(), let message = self.messageView?.message {
if let media = mediaForMessage(message: message) {
if let presentationArguments = self.presentationArguments as? GalleryControllerPresentationArguments, let transitionArguments = presentationArguments.transitionArguments(message.id, media) {
nodeAnimatesItself = true
centralItemNode.activateAsInitial()
if presentationArguments.animated {
centralItemNode.animateIn(from: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface)
}
self._hiddenMedia.set(.single((message.id, media)))
} else if self.isPresentedInPreviewingContext() {
centralItemNode.activateAsInitial()
}
}
}
self.controllerNode.setControlsHidden(false, animated: false)
if let presentationArguments = self.presentationArguments as? GalleryControllerPresentationArguments {
if presentationArguments.animated {
self.controllerNode.animateIn(animateContent: !nodeAnimatesItself)
}
}
}
private func dismiss(forceAway: Bool) {
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 let centralItemNode = self.controllerNode.pager.centralItemNode(), let presentationArguments = self.presentationArguments as? GalleryControllerPresentationArguments, let message = self.messageView?.message {
if let media = mediaForMessage(message: message), let transitionArguments = presentationArguments.transitionArguments(message.id, media), !forceAway {
animatedOutNode = false
centralItemNode.animateOut(to: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface, completion: {
animatedOutNode = true
completion()
})
}
}
self.controllerNode.animateOut(animateContent: animatedOutNode, completion: {
animatedOutInterface = true
completion()
})
}
private func applyMessageView() {
var message: Message?
if let messageView = self.messageView, let m = messageView.message {
message = m
for media in m.media {
if media is TelegramMediaExpiredContent {
message = nil
break
}
}
}
if let message = message {
if self.currentNodeMessageId != message.id {
self.currentNodeMessageId = message.id
var tempFilePath: String?
for media in message.media {
if let file = media as? TelegramMediaFile {
if let path = self.context.account.postbox.mediaBox.completedResourcePath(file.resource) {
let tempFile = TempBox.shared.file(path: path, fileName: file.fileName ?? "file")
self.tempFile = tempFile
tempFilePath = tempFile.path
self.currentNodeMessageIsVideo = true
}
break
}
}
guard let item = galleryItemForEntry(context: self.context, presentationData: self.presentationData, entry: .MessageEntry(message, false, nil, nil), streamVideos: false, hideControls: true, tempFilePath: tempFilePath, playbackCompleted: { [weak self] in
self?.dismiss(forceAway: false)
}) else {
self._ready.set(.single(true))
return
}
self.controllerNode.pager.replaceItems([item], centralItemIndex: 0)
let ready = self.controllerNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak self] _ in
self?.didSetReady = true
}
self._ready.set(ready |> map { true })
self.markMessageAsConsumedDisposable.set(markMessageContentAsConsumedInteractively(postbox: self.context.account.postbox, messageId: message.id).start())
} else {
var beginTimeAndTimeout: (Double, Double)?
var videoDuration: Int32?
for media in message.media {
if let file = media as? TelegramMediaFile {
videoDuration = file.duration
}
}
for attribute in message.attributes {
if let attribute = attribute as? AutoremoveTimeoutMessageAttribute {
if let countdownBeginTime = attribute.countdownBeginTime {
if let videoDuration = videoDuration {
beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, Double(videoDuration))
} else {
beginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout))
}
}
break
}
}
if self.isNodeLoaded {
if let beginTimeAndTimeout = beginTimeAndTimeout {
self.controllerNode.beginTimeAndTimeout = beginTimeAndTimeout
}
}
}
} else {
if !self.didSetReady {
self._ready.set(.single(true))
}
if !self.currentNodeMessageIsVideo {
self.dismiss()
}
}
}
public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.frame = CGRect(origin: CGPoint(), size: layout.size)
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition)
}
override public func dismiss(completion: (() -> Void)? = nil) {
self.presentingViewController?.dismiss(animated: false, completion: completion)
}
}