Swiftgram/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift
2023-09-08 21:58:28 +04:00

656 lines
31 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import AccountContext
import RadialStatusNode
import ScreenCaptureDetection
import AppBundle
import LocalizedPeerData
import TooltipUI
import TelegramNotices
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 {
fileprivate var timeoutNode: RadialStatusNode?
private var validLayout: (ContainerViewLayout, CGFloat)?
var beginTimeAndTimeout: (Double, Double, Bool)? {
didSet {
if let (beginTime, timeout, isOutgoing) = self.beginTimeAndTimeout {
var beginTime = beginTime
if self.timeoutNode == nil {
let timeoutNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.5))
self.timeoutNode = timeoutNode
let icon: RadialStatusNodeState.SecretTimeoutIcon
let timeoutValue = Int32(timeout)
let state: RadialStatusNodeState
if timeoutValue == 0 && isOutgoing {
state = .staticTimeout
} else if timeoutValue == viewOnceTimeout {
beginTime = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970
if let image = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/ViewOnce"), color: .white) {
icon = .image(image)
} else {
icon = .flame
}
state = .secretTimeout(color: .white, icon: icon, beginTime: beginTime, timeout: timeout, sparks: isOutgoing ? false : true)
} else {
state = .secretTimeout(color: .white, icon: .flame, beginTime: beginTime, timeout: timeout, sparks: true)
}
timeoutNode.transitionToState(state, completion: {})
self.addSubnode(timeoutNode)
timeoutNode.addTarget(self, action: #selector(self.statusTapGesture), forControlEvents: .touchUpInside)
if let (layout, navigationHeight) = self.validLayout {
self.layoutTimeoutNode(layout, navigationBarHeight: navigationHeight, transition: .immediate)
}
}
} else if let timeoutNode = self.timeoutNode {
self.timeoutNode = nil
timeoutNode.removeFromSupernode()
}
}
}
var statusPressed: (UIView) -> Void = { _ in }
@objc private func statusTapGesture() {
if let sourceView = self.timeoutNode?.view {
self.statusPressed(sourceView)
}
}
var onDismissTransitionUpdate: (CGFloat) -> Void = { _ in }
override func animateIn(animateContent: Bool, useSimpleAnimation: Bool) {
super.animateIn(animateContent: animateContent, useSimpleAnimation: useSimpleAnimation)
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
self.onDismissTransitionUpdate(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<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 currentNodeMessageIsViewOnce = false
private var currentMessageIsDismissed = false
private var tempFile: TempBoxFile?
private let centralItemAttributesDisposable = DisposableSet();
private let footerContentNode = Promise<(GalleryFooterContentNode?, GalleryOverlayContentNode?)>()
private let _hiddenMedia = Promise<(MessageId, Media)?>(nil)
private var hiddenMediaManagerIndex: Int?
private let presentationData: PresentationData
private var screenCaptureEventsDisposable: Disposable?
private weak var tooltipController: TooltipScreen?
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
}
})
self.centralItemAttributesDisposable.add(self.footerContentNode.get().start(next: { [weak self] footerContentNode, _ in
guard let self else {
return
}
self.controllerNode.updatePresentationState({
$0.withUpdatedFooterContentNode(footerContentNode)
}, transition: .immediate)
}))
}
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)
}
self.centralItemAttributesDisposable.dispose()
}
@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)
}
}, pushController: { _ in
}, dismissController: { [weak self] in
self?.dismiss(forceAway: true)
}, replaceRootController: { _, _ in
}, editMedia: { _ in
})
self.displayNode = SecretMediaPreviewControllerNode(controllerInteraction: controllerInteraction)
self.displayNodeDidLoad()
self.controllerNode.statusPressed = { [weak self] _ in
if let self {
self.presentViewOnceTooltip()
}
}
self.controllerNode.onDismissTransitionUpdate = { [weak self] _ in
if let self {
self.dismissAllTooltips()
}
}
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)
if let tooltipController = self?.tooltipController {
self?.tooltipController = nil
tooltipController.dismiss()
}
}
self.controllerNode.beginCustomDismiss = { [weak self] _ in
if let strongSelf = self {
strongSelf._hiddenMedia.set(.single(nil))
let animatedOutNode = true
strongSelf.controllerNode.animateOut(animateContent: animatedOutNode, 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, Bool)?
var videoDuration: Double?
for media in message.media {
if let file = media as? TelegramMediaFile {
videoDuration = file.duration
}
}
let isOutgoing = !message.flags.contains(.Incoming)
if let attribute = message.autoclearAttribute {
strongSelf.currentNodeMessageIsViewOnce = attribute.timeout == viewOnceTimeout
if let countdownBeginTime = attribute.countdownBeginTime {
if let videoDuration = videoDuration, attribute.timeout != viewOnceTimeout {
beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, max(videoDuration, Double(attribute.timeout)), isOutgoing)
} else {
beginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout), isOutgoing)
}
} else if isOutgoing {
beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, attribute.timeout != viewOnceTimeout ? 0.0 : Double(viewOnceTimeout), isOutgoing)
}
} else if let attribute = message.autoremoveAttribute {
strongSelf.currentNodeMessageIsViewOnce = attribute.timeout == viewOnceTimeout
if let countdownBeginTime = attribute.countdownBeginTime {
if let videoDuration = videoDuration, attribute.timeout != viewOnceTimeout {
beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, max(videoDuration, Double(attribute.timeout)), isOutgoing)
} else {
beginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout), isOutgoing)
}
} else if isOutgoing {
beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, attribute.timeout != viewOnceTimeout ? 0.0 : Double(viewOnceTimeout), isOutgoing)
}
}
if let file = media as? TelegramMediaFile {
if file.isAnimated {
strongSelf.title = strongSelf.presentationData.strings.SecretGif_Title
} else {
if strongSelf.currentNodeMessageIsViewOnce {
strongSelf.title = strongSelf.presentationData.strings.SecretVideo_ViewOnce_Title
} else {
strongSelf.title = strongSelf.presentationData.strings.SecretVideo_Title
}
}
} else {
if strongSelf.currentNodeMessageIsViewOnce {
strongSelf.title = strongSelf.presentationData.strings.SecretImage_ViewOnce_Title
} else {
strongSelf.title = strongSelf.presentationData.strings.SecretImage_Title
}
}
if let beginTimeAndTimeout = beginTimeAndTimeout {
strongSelf.controllerNode.beginTimeAndTimeout = beginTimeAndTimeout
}
if strongSelf.currentNodeMessageIsVideo {
if let node = strongSelf.controllerNode.pager.centralItemNode() {
strongSelf.footerContentNode.set(node.footerContent())
}
} else if !message.flags.contains(.Incoming) {
if let _ = beginTimeAndTimeout {
strongSelf.controllerNode.updatePresentationState({
$0.withUpdatedFooterContentNode(nil)
}, transition: .immediate)
} else {
let contentNode = SecretMediaPreviewFooterContentNode()
let peerTitle = messageMainPeer(EngineMessage(message))?.compactDisplayTitle ?? ""
let text: String
if let file = media as? TelegramMediaFile {
if file.isAnimated {
text = strongSelf.presentationData.strings.SecretGIF_NotViewedYet(peerTitle).string
} else {
text = strongSelf.presentationData.strings.SecretVideo_NotViewedYet(peerTitle).string
}
} else {
text = strongSelf.presentationData.strings.SecretImage_NotViewedYet(peerTitle).string
}
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: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaAction(action: TelegramMediaActionType.historyScreenshot)), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).start()
} else if strongSelf.messageId.peerId.namespace == Namespaces.Peer.SecretChat {
let _ = strongSelf.context.engine.messages.addSecretChatMessageScreenshot(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, useSimpleAnimation: false)
}
}
if self.currentNodeMessageIsViewOnce {
let _ = (ApplicationSpecificNotice.incrementViewOnceTooltip(accountManager: self.context.sharedContext.accountManager)
|> deliverOnMainQueue).start(next: { [weak self] count in
guard let self else {
return
}
if count < 2 {
self.presentViewOnceTooltip()
}
})
}
}
private func dismiss(forceAway: Bool) {
self.dismissAllTooltips()
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?
var duration: Double = 0.0
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
}
duration = file.duration ?? 0.0
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, isSecret: true, playbackRate: { nil }, tempFilePath: tempFilePath, playbackCompleted: { [weak self] in
if let self {
if self.currentNodeMessageIsViewOnce || (duration < 30.0 && !self.currentMessageIsDismissed) {
if let node = self.controllerNode.pager.centralItemNode() as? UniversalVideoGalleryItemNode {
node.seekToStart()
}
} else {
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(self.context.engine.messages.markMessageContentAsConsumedInteractively(messageId: message.id).start())
} else {
var beginTimeAndTimeout: (Double, Double, Bool)?
var videoDuration: Double?
for media in message.media {
if let file = media as? TelegramMediaFile {
videoDuration = file.duration
}
}
let isOutgoing = !message.flags.contains(.Incoming)
if let attribute = message.autoclearAttribute {
if let countdownBeginTime = attribute.countdownBeginTime {
if let videoDuration = videoDuration, attribute.timeout != viewOnceTimeout {
beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, max(videoDuration, Double(attribute.timeout)), isOutgoing)
} else {
beginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout), isOutgoing)
}
} else if isOutgoing {
beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, attribute.timeout != viewOnceTimeout ? 0.0 : Double(viewOnceTimeout), isOutgoing)
}
} else if let attribute = message.autoremoveAttribute {
if let countdownBeginTime = attribute.countdownBeginTime {
if let videoDuration = videoDuration, attribute.timeout != viewOnceTimeout {
beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, max(videoDuration, Double(attribute.timeout)), isOutgoing)
} else {
beginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout), isOutgoing)
}
} else if isOutgoing {
beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, attribute.timeout != viewOnceTimeout ? 0.0 : Double(viewOnceTimeout), isOutgoing)
}
}
if self.isNodeLoaded {
if let beginTimeAndTimeout = beginTimeAndTimeout {
self.controllerNode.beginTimeAndTimeout = beginTimeAndTimeout
}
}
}
} else {
if !self.didSetReady {
self._ready.set(.single(true))
}
if !(self.currentNodeMessageIsVideo || self.currentNodeMessageIsViewOnce) {
self.dismiss()
}
self.currentMessageIsDismissed = true
}
}
private func dismissAllTooltips() {
if let tooltipController = self.tooltipController {
self.tooltipController = nil
tooltipController.dismiss()
}
}
private func presentViewOnceTooltip() {
guard self.currentNodeMessageIsViewOnce, let sourceView = self.controllerNode.timeoutNode?.view else {
return
}
self.dismissAllTooltips()
let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil)
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.maxY + 2.0), size: CGSize())
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let iconName = "anim_autoremove_on"
let text: String
if self.currentNodeMessageIsVideo {
text = presentationData.strings.Gallery_ViewOnceVideoTooltip
} else {
text = presentationData.strings.Gallery_ViewOncePhotoTooltip
}
let tooltipController = TooltipScreen(
account: self.context.account,
sharedContext: self.context.sharedContext,
text: .plain(text: text),
balancedTextLayout: true,
constrainWidth: 210.0,
style: .customBlur(UIColor(rgb: 0x18181a), 0.0),
arrowStyle: .small,
icon: .animation(name: iconName, delay: 0.1, tintColor: nil),
location: .point(location, .top),
displayDuration: .default,
inset: 8.0,
cornerRadius: 8.0,
shouldDismissOnTouch: { _, _ in
return .ignore
}
)
self.tooltipController = tooltipController
self.present(tooltipController, in: .window(.root))
}
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.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
override public func dismiss(completion: (() -> Void)? = nil) {
self.presentingViewController?.dismiss(animated: false, completion: completion)
}
}