import Foundation import UIKit import AsyncDisplayKit import Display import Postbox import TelegramCore import SyncCore import SwiftSignalKit import TelegramPresentationData import AccountContext import RadialStatusNode import ScreenCaptureDetection import AppBundle import LocalizedPeerData 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 messageId: MessageId private let _ready = Promise() override public var ready: Promise { 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.messageId = messageId self.presentationData = context.sharedContext.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() } } })) self.hiddenMediaManagerIndex = self.context.sharedContext.mediaManager.galleryHiddenMediaManager.addSource(self._hiddenMedia.get() |> map { messageIdAndMedia in if let (messageId, media) = messageIdAndMedia { return .chat(context.account.id, messageId, media) } else { return nil } }) } 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 { self.context.sharedContext.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 }, editMedia: { _ 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) if self.screenCaptureEventsDisposable == nil { self.screenCaptureEventsDisposable = (screenCaptureEvents() |> deliverOnMainQueue).start(next: { [weak self] _ in if let strongSelf = self, strongSelf.traceVisibility() { if strongSelf.messageId.peerId.namespace == Namespaces.Peer.CloudUser { let _ = enqueueMessages(account: strongSelf.context.account, peerId: strongSelf.messageId.peerId, messages: [.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaAction(action: TelegramMediaActionType.historyScreenshot)), replyToMessageId: nil, localGroupingKey: nil)]).start() } else if strongSelf.messageId.peerId.namespace == Namespaces.Peer.SecretChat { let _ = addSecretChatMessageScreenshot(account: strongSelf.context.account, peerId: strongSelf.messageId.peerId).start() } } }) } 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, completion: {}) } 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: MessageHistoryEntry(message: message, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false)), streamVideos: false, hideControls: true, tempFilePath: tempFilePath, playbackCompleted: { [weak self] in self?.dismiss(forceAway: false) }, present: { _, _ in }) 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) } }