Swiftgram/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift
2021-09-24 11:59:21 +03:00

945 lines
48 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import UniversalMediaPlayer
import TelegramPresentationData
import AccountContext
import RadialStatusNode
import PhotoResources
import TelegramUniversalVideoContent
import FileMediaResourceStatus
struct ChatMessageInstantVideoItemLayoutResult {
let contentSize: CGSize
let overflowLeft: CGFloat
let overflowRight: CGFloat
}
enum ChatMessageInstantVideoItemLayoutData {
case unconstrained(width: CGFloat)
case constrained(left: CGFloat, right: CGFloat)
}
private let textFont = Font.regular(11.0)
enum ChatMessageInteractiveInstantVideoNodeStatusType {
case free
case bubble
}
class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
private var videoNode: UniversalVideoNode?
private let secretVideoPlaceholderBackground: ASImageNode
private let secretVideoPlaceholder: TransformImageNode
private var statusNode: RadialStatusNode?
private var disappearingStatusNode: RadialStatusNode?
private var playbackStatusNode: InstantVideoRadialStatusNode?
private(set) var videoFrame: CGRect?
private var item: ChatMessageBubbleContentItem?
private var automaticDownload: Bool?
var media: TelegramMediaFile?
private var secretProgressIcon: UIImage?
private let fetchDisposable = MetaDisposable()
private var durationBackgroundNode: NavigationBackgroundNode?
private var durationNode: ChatInstantVideoMessageDurationNode?
private let dateAndStatusNode: ChatMessageDateAndStatusNode
private let infoBackgroundNode: ASImageNode
private let muteIconNode: ASImageNode
private var status: FileMediaResourceStatus?
private var playerStatus: MediaPlayerStatus? {
didSet {
if self.playerStatus != oldValue {
self.updateStatus()
}
}
}
private let playbackStatusDisposable = MetaDisposable()
private let playerStatusDisposable = MetaDisposable()
private let fetchedThumbnailDisposable = MetaDisposable()
private var shouldAcquireVideoContext: Bool {
if self.visibility {
return true
} else {
return false
}
}
var visibility: Bool = false {
didSet {
if self.visibility != oldValue {
self.videoNode?.canAttachContent = self.shouldAcquireVideoContext
}
}
}
var shouldOpen: () -> Bool = { return true }
override init() {
self.secretVideoPlaceholderBackground = ASImageNode()
self.secretVideoPlaceholderBackground.isLayerBacked = true
self.secretVideoPlaceholderBackground.displaysAsynchronously = false
self.secretVideoPlaceholderBackground.displayWithoutProcessing = true
self.secretVideoPlaceholder = TransformImageNode()
self.infoBackgroundNode = ASImageNode()
self.infoBackgroundNode.isLayerBacked = true
self.infoBackgroundNode.displayWithoutProcessing = true
self.infoBackgroundNode.displaysAsynchronously = false
self.dateAndStatusNode = ChatMessageDateAndStatusNode()
self.muteIconNode = ASImageNode()
self.muteIconNode.isLayerBacked = true
self.muteIconNode.displayWithoutProcessing = true
self.muteIconNode.displaysAsynchronously = false
super.init()
self.addSubnode(self.dateAndStatusNode)
self.addSubnode(self.infoBackgroundNode)
self.infoBackgroundNode.addSubnode(self.muteIconNode)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.fetchDisposable.dispose()
self.playbackStatusDisposable.dispose()
self.playerStatusDisposable.dispose()
self.fetchedThumbnailDisposable.dispose()
}
override func didLoad() {
super.didLoad()
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
recognizer.tapActionAtPoint = { _ in
return .waitForSingleTap
}
self.view.addGestureRecognizer(recognizer)
}
func asyncLayout() -> (_ item: ChatMessageBubbleContentItem, _ width: CGFloat, _ displaySize: CGSize, _ maximumDisplaySize: CGSize, _ scaleProgress: CGFloat, _ statusType: ChatMessageInteractiveInstantVideoNodeStatusType, _ automaticDownload: Bool) -> (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ContainedViewLayoutTransition) -> Void) {
let previousFile = self.media
let currentItem = self.item
let previousAutomaticDownload = self.automaticDownload
let makeDateAndStatusLayout = self.dateAndStatusNode.asyncLayout()
return { item, width, displaySize, maximumDisplaySize, scaleProgress, statusDisplayType, automaticDownload in
var secretVideoPlaceholderBackgroundImage: UIImage?
var updatedInfoBackgroundImage: UIImage?
var updatedMuteIconImage: UIImage?
var updatedInstantVideoBackgroundImage: UIImage?
let instantVideoBackgroundImage: UIImage?
switch statusDisplayType {
case .free:
instantVideoBackgroundImage = PresentationResourcesChat.chatInstantVideoBackgroundImage(item.presentationData.theme.theme, wallpaper: !item.presentationData.theme.wallpaper.isEmpty)
case .bubble:
instantVideoBackgroundImage = nil
}
if item.presentationData.theme != currentItem?.presentationData.theme {
updatedInstantVideoBackgroundImage = instantVideoBackgroundImage
updatedInfoBackgroundImage = PresentationResourcesChat.chatInstantMessageInfoBackgroundImage(item.presentationData.theme.theme)
updatedMuteIconImage = PresentationResourcesChat.chatInstantMessageMuteIconImage(item.presentationData.theme.theme)
}
let theme = item.presentationData.theme
let isSecretMedia = item.message.containsSecretMedia
var secretProgressIcon: UIImage?
if isSecretMedia {
secretProgressIcon = PresentationResourcesChat.chatBubbleSecretMediaIcon(theme.theme)
secretVideoPlaceholderBackgroundImage = PresentationResourcesChat.chatInstantVideoBackgroundImage(theme.theme, wallpaper: !theme.wallpaper.isEmpty)
}
let imageSize = maximumDisplaySize
let imageScale = displaySize.width / maximumDisplaySize.width
let updatedMessageId = item.message.id != currentItem?.message.id
var updatedFile: TelegramMediaFile?
var updatedMedia = false
for media in item.message.media {
if let file = media as? TelegramMediaFile {
updatedFile = file
if let previousFile = previousFile {
updatedMedia = previousFile.resource.id != file.resource.id
} else if previousFile == nil {
updatedMedia = true
}
} else if let webPage = media as? TelegramMediaWebpage, case let .Loaded(content) = webPage.content, let file = content.file {
updatedFile = file
if let previousFile = previousFile {
updatedMedia = previousFile.resource.id != file.resource.id
} else if previousFile == nil {
updatedMedia = true
}
}
}
var notConsumed = false
for attribute in item.message.attributes {
if let attribute = attribute as? ConsumableContentMessageAttribute {
if !attribute.consumed {
notConsumed = true
}
break
}
}
if item.message.id.namespace == Namespaces.Message.Local {
notConsumed = true
}
var updatedPlaybackStatus: Signal<FileMediaResourceStatus, NoError>?
if let updatedFile = updatedFile, updatedMedia || updatedMessageId {
updatedPlaybackStatus = combineLatest(messageFileMediaResourceStatus(context: item.context, file: updatedFile, message: item.message, isRecentActions: item.associatedData.isRecentActions), item.context.account.pendingMessageManager.pendingMessageStatus(item.message.id) |> map { $0.0 })
|> map { resourceStatus, pendingStatus -> FileMediaResourceStatus in
if let pendingStatus = pendingStatus {
var progress = pendingStatus.progress
if pendingStatus.isRunning {
progress = max(progress, 0.27)
}
return FileMediaResourceStatus(mediaStatus: .fetchStatus(.Fetching(isActive: pendingStatus.isRunning, progress: progress)), fetchStatus: resourceStatus.fetchStatus)
} else {
return resourceStatus
}
}
}
let videoFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: imageSize)
let arguments = TransformImageArguments(corners: ImageCorners(radius: videoFrame.size.width / 2.0), imageSize: videoFrame.size, boundingSize: videoFrame.size, intrinsicInsets: UIEdgeInsets())
let statusType: ChatMessageDateAndStatusType
if item.message.effectivelyIncoming(item.context.account.peerId) {
switch statusDisplayType {
case .free:
statusType = .FreeIncoming
case .bubble:
statusType = .BubbleIncoming
}
} else {
switch statusDisplayType {
case .free:
if item.message.flags.contains(.Failed) {
statusType = .FreeOutgoing(.Failed)
} else if item.message.flags.isSending && !item.message.isSentOrAcknowledged {
statusType = .FreeOutgoing(.Sending)
} else {
statusType = .FreeOutgoing(.Sent(read: item.read))
}
case .bubble:
if item.message.flags.contains(.Failed) {
statusType = .BubbleOutgoing(.Failed)
} else if (item.message.flags.isSending && !item.message.isSentOrAcknowledged) || item.attributes.updatingMedia != nil {
statusType = .BubbleOutgoing(.Sending)
} else {
statusType = .BubbleOutgoing(.Sent(read: item.read))
}
}
}
var edited = false
if item.attributes.updatingMedia != nil {
edited = true
}
let sentViaBot = false
var viewCount: Int? = nil
var dateReplies = 0
for attribute in item.message.attributes {
if let attribute = attribute as? EditedMessageAttribute {
edited = !attribute.isHidden
} else if let attribute = attribute as? ViewCountMessageAttribute {
viewCount = attribute.count
} else if let attribute = attribute as? ReplyThreadMessageAttribute, case .peer = item.chatLocation {
if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info {
dateReplies = Int(attribute.count)
}
}
}
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: .regular)
let maxDateAndStatusWidth: CGFloat
if case .bubble = statusDisplayType {
maxDateAndStatusWidth = width
} else {
maxDateAndStatusWidth = width - videoFrame.midX - 85.0
}
var isReplyThread = false
if case .replyThread = item.chatLocation {
isReplyThread = true
}
let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, edited && !sentViaBot, viewCount, dateText, statusType, CGSize(width: max(1.0, maxDateAndStatusWidth), height: CGFloat.greatestFiniteMagnitude), dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, item.message.isSelfExpiring)
var displayVideoFrame = videoFrame
displayVideoFrame.size.width *= imageScale
displayVideoFrame.size.height *= imageScale
var contentSize = displayVideoFrame.size
var dateAndStatusOverflow = false
if case .bubble = statusDisplayType, displayVideoFrame.maxX + dateAndStatusSize.width > width {
contentSize.height += dateAndStatusSize.height + 2.0
contentSize.width = max(contentSize.width, dateAndStatusSize.width)
dateAndStatusOverflow = true
}
let result = ChatMessageInstantVideoItemLayoutResult(contentSize: contentSize, overflowLeft: 0.0, overflowRight: dateAndStatusOverflow ? 0.0 : (max(0.0, floorToScreenPixels(videoFrame.midX) + 55.0 + dateAndStatusSize.width - videoFrame.width)))
return (result, { [weak self] layoutData, transition in
if let strongSelf = self {
strongSelf.item = item
strongSelf.videoFrame = displayVideoFrame
strongSelf.secretProgressIcon = secretProgressIcon
strongSelf.automaticDownload = automaticDownload
if let updatedInfoBackgroundImage = updatedInfoBackgroundImage {
strongSelf.infoBackgroundNode.image = updatedInfoBackgroundImage
}
if let updatedMuteIconImage = updatedMuteIconImage {
strongSelf.muteIconNode.image = updatedMuteIconImage
}
if let secretVideoPlaceholderBackgroundImage = secretVideoPlaceholderBackgroundImage {
strongSelf.secretVideoPlaceholderBackground.image = secretVideoPlaceholderBackgroundImage
}
if let updatedInstantVideoBackgroundImage = updatedInstantVideoBackgroundImage, let decoration = strongSelf.videoNode?.decoration as? ChatBubbleInstantVideoDecoration, let decorationBackgroundNode = decoration.backgroundNode as? ASImageNode {
decorationBackgroundNode.image = updatedInstantVideoBackgroundImage
}
strongSelf.media = updatedFile
if let infoBackgroundImage = strongSelf.infoBackgroundNode.image, let muteImage = strongSelf.muteIconNode.image {
let infoWidth = muteImage.size.width
let infoBackgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(displayVideoFrame.minX + (displayVideoFrame.size.width - infoWidth) / 2.0), y: displayVideoFrame.maxY - infoBackgroundImage.size.height - 8.0), size: CGSize(width: infoWidth, height: infoBackgroundImage.size.height))
strongSelf.infoBackgroundNode.frame = infoBackgroundFrame
let muteIconFrame = CGRect(origin: CGPoint(x: infoBackgroundFrame.width - muteImage.size.width, y: 0.0), size: muteImage.size)
strongSelf.muteIconNode.frame = muteIconFrame
}
if let updatedFile = updatedFile, updatedMedia {
if let resource = updatedFile.previewRepresentations.first?.resource {
strongSelf.fetchedThumbnailDisposable.set(fetchedMediaResource(mediaBox: item.context.account.postbox.mediaBox, reference: FileMediaReference.message(message: MessageReference(item.message), media: updatedFile).resourceReference(resource)).start())
} else {
strongSelf.fetchedThumbnailDisposable.set(nil)
}
}
dateAndStatusApply(false)
switch layoutData {
case let .unconstrained(width):
let dateAndStatusOrigin: CGPoint
if dateAndStatusOverflow {
dateAndStatusOrigin = CGPoint(x: displayVideoFrame.minX - 4.0, y: displayVideoFrame.maxY + 2.0)
} else {
dateAndStatusOrigin = CGPoint(x: min(floorToScreenPixels(displayVideoFrame.midX) + 55.0 + 25.0 * scaleProgress, width - dateAndStatusSize.width - 4.0), y: displayVideoFrame.height - dateAndStatusSize.height)
}
strongSelf.dateAndStatusNode.frame = CGRect(origin: dateAndStatusOrigin, size: dateAndStatusSize)
case let .constrained(_, right):
strongSelf.dateAndStatusNode.frame = CGRect(origin: CGPoint(x: min(floorToScreenPixels(displayVideoFrame.midX) + 55.0 + 25.0 * scaleProgress, displayVideoFrame.maxX + right - dateAndStatusSize.width - 4.0), y: displayVideoFrame.maxY - dateAndStatusSize.height), size: dateAndStatusSize)
}
var updatedPlayerStatusSignal: Signal<MediaPlayerStatus?, NoError>?
if let telegramFile = updatedFile {
if updatedMedia {
let durationTextColor: UIColor
let durationBlurColor: (UIColor, Bool)?
switch statusDisplayType {
case .free:
let serviceColor = serviceMessageColorComponents(theme: theme.theme, wallpaper: theme.wallpaper)
durationTextColor = serviceColor.primaryText
durationBlurColor = (selectDateFillStaticColor(theme: theme.theme, wallpaper: theme.wallpaper), dateFillNeedsBlur(theme: theme.theme, wallpaper: theme.wallpaper))
case .bubble:
durationBlurColor = nil
if item.message.effectivelyIncoming(item.context.account.peerId) {
durationTextColor = theme.theme.chat.message.incoming.secondaryTextColor
} else {
durationTextColor = theme.theme.chat.message.outgoing.secondaryTextColor
}
}
if let durationBlurColor = durationBlurColor {
if let durationBackgroundNode = strongSelf.durationBackgroundNode {
durationBackgroundNode.updateColor(color: durationBlurColor.0, enableBlur: durationBlurColor.1, transition: .immediate)
} else {
let durationBackgroundNode = NavigationBackgroundNode(color: durationBlurColor.0, enableBlur: durationBlurColor.1)
strongSelf.durationBackgroundNode = durationBackgroundNode
strongSelf.addSubnode(durationBackgroundNode)
}
} else if let durationBackgroundNode = strongSelf.durationBackgroundNode {
strongSelf.durationBackgroundNode = nil
durationBackgroundNode.removeFromSupernode()
}
let durationNode: ChatInstantVideoMessageDurationNode
if let current = strongSelf.durationNode {
durationNode = current
current.updateTheme(textColor: durationTextColor)
} else {
durationNode = ChatInstantVideoMessageDurationNode(textColor: durationTextColor)
strongSelf.durationNode = durationNode
strongSelf.addSubnode(durationNode)
durationNode.sizeUpdated = { [weak strongSelf] size in
guard let strongSelf = strongSelf else {
return
}
if let durationBackgroundNode = strongSelf.durationBackgroundNode, let durationNode = strongSelf.durationNode {
durationBackgroundNode.frame = CGRect(origin: CGPoint(x: durationNode.frame.maxX - size.width, y: durationNode.frame.minY), size: size)
durationBackgroundNode.update(size: size, cornerRadius: size.height / 2.0, transition: .immediate)
}
}
}
durationNode.defaultDuration = telegramFile.duration.flatMap(Double.init)
let streamVideo = automaticDownload && isMediaStreamable(message: item.message, media: telegramFile) && telegramFile.id?.namespace != Namespaces.Media.LocalFile
if let videoNode = strongSelf.videoNode {
videoNode.layer.allowsGroupOpacity = true
videoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.5, delay: 0.2, removeOnCompletion: false, completion: { [weak videoNode] _ in
videoNode?.removeFromSupernode()
})
}
let mediaManager = item.context.sharedContext.mediaManager
let videoNode = UniversalVideoNode(postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: ChatBubbleInstantVideoDecoration(inset: 2.0, backgroundImage: instantVideoBackgroundImage, tapped: {
if let strongSelf = self {
if let item = strongSelf.item {
if strongSelf.infoBackgroundNode.alpha.isZero {
item.context.sharedContext.mediaManager.playlistControl(.playback(.togglePlayPause), type: .voice)
}
}
}
}), content: NativeVideoContent(id: .message(item.message.stableId, telegramFile.fileId), fileReference: .message(message: MessageReference(item.message), media: telegramFile), streamVideo: streamVideo ? .conservative : .none, enableSound: false, fetchAutomatically: false), priority: .embedded, autoplay: true)
let previousVideoNode = strongSelf.videoNode
strongSelf.videoNode = videoNode
strongSelf.insertSubnode(videoNode, belowSubnode: previousVideoNode ?? strongSelf.dateAndStatusNode)
videoNode.canAttachContent = strongSelf.shouldAcquireVideoContext
if isSecretMedia {
let updatedSecretPlaceholderSignal = chatSecretMessageVideo(account: item.context.account, videoReference: .message(message: MessageReference(item.message), media: telegramFile))
strongSelf.secretVideoPlaceholder.setSignal(updatedSecretPlaceholderSignal)
if strongSelf.secretVideoPlaceholder.supernode == nil {
strongSelf.insertSubnode(strongSelf.secretVideoPlaceholderBackground, belowSubnode: videoNode)
strongSelf.insertSubnode(strongSelf.secretVideoPlaceholder, belowSubnode: videoNode)
}
}
updatedPlayerStatusSignal = videoNode.status
|> mapToSignal { status -> Signal<MediaPlayerStatus?, NoError> in
if let status = status, case .buffering = status.status {
return .single(status) |> delay(0.75, queue: Queue.mainQueue())
} else {
return .single(status)
}
}
}
}
if let updatedPlaybackStatus = updatedPlaybackStatus {
strongSelf.playbackStatusDisposable.set((updatedPlaybackStatus
|> deliverOnMainQueue).start(next: { status in
if let strongSelf = self {
strongSelf.status = status
strongSelf.updateStatus()
}
}))
}
if let updatedPlayerStatusSignal = updatedPlayerStatusSignal {
strongSelf.playerStatusDisposable.set((updatedPlayerStatusSignal
|> deliverOnMainQueue).start(next: { [weak self] status in
displayLinkDispatcher.dispatch {
if let strongSelf = self {
strongSelf.playerStatus = status
}
}
}))
}
if let durationNode = strongSelf.durationNode {
durationNode.frame = CGRect(origin: CGPoint(x: displayVideoFrame.midX - 56.0 - 25.0 * scaleProgress, y: displayVideoFrame.maxY - 18.0), size: CGSize(width: 1.0, height: 1.0))
durationNode.isSeen = !notConsumed
let size = durationNode.size
if let durationBackgroundNode = strongSelf.durationBackgroundNode, size.width > 1.0 {
durationBackgroundNode.frame = CGRect(origin: CGPoint(x: durationNode.frame.maxX - size.width, y: durationNode.frame.minY), size: size)
durationBackgroundNode.update(size: size, cornerRadius: size.height / 2.0, transition: .immediate)
}
}
if let videoNode = strongSelf.videoNode {
videoNode.bounds = CGRect(origin: CGPoint(), size: videoFrame.size)
videoNode.transform = CATransform3DMakeScale(imageScale, imageScale, 1.0)
videoNode.position = displayVideoFrame.center
videoNode.updateLayout(size: arguments.boundingSize, transition: .immediate)
}
strongSelf.secretVideoPlaceholderBackground.frame = displayVideoFrame
let placeholderFrame = videoFrame.insetBy(dx: 2.0, dy: 2.0)
strongSelf.secretVideoPlaceholder.bounds = CGRect(origin: CGPoint(), size: videoFrame.size)
strongSelf.secretVideoPlaceholder.transform = CATransform3DMakeScale(imageScale, imageScale, 1.0)
strongSelf.secretVideoPlaceholder.position = displayVideoFrame.center
let makeSecretPlaceholderLayout = strongSelf.secretVideoPlaceholder.asyncLayout()
let arguments = TransformImageArguments(corners: ImageCorners(radius: placeholderFrame.size.width / 2.0), imageSize: placeholderFrame.size, boundingSize: placeholderFrame.size, intrinsicInsets: UIEdgeInsets())
let applySecretPlaceholder = makeSecretPlaceholderLayout(arguments)
applySecretPlaceholder()
strongSelf.updateStatus()
if let telegramFile = updatedFile, previousAutomaticDownload != automaticDownload, automaticDownload {
strongSelf.fetchDisposable.set(messageMediaFileInteractiveFetched(context: item.context, message: item.message, file: telegramFile, userInitiated: false).start())
}
if let forwardInfo = item.message.forwardInfo, forwardInfo.flags.contains(.isImported) {
strongSelf.dateAndStatusNode.pressed = {
guard let strongSelf = self else {
return
}
item.controllerInteraction.displayImportedMessageTooltip(strongSelf.dateAndStatusNode)
}
} else {
strongSelf.dateAndStatusNode.pressed = nil
}
}
})
}
}
private func updateStatus() {
guard let item = self.item, let status = self.status, let videoFrame = self.videoFrame else {
return
}
let messageTheme = item.presentationData.theme.theme.chat.message
let isSecretMedia = item.message.containsSecretMedia
var secretBeginTimeAndTimeout: (Double, Double)?
if isSecretMedia {
if let attribute = item.message.autoclearAttribute {
if let countdownBeginTime = attribute.countdownBeginTime {
secretBeginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout))
}
} else if let attribute = item.message.autoremoveAttribute {
if let countdownBeginTime = attribute.countdownBeginTime {
secretBeginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout))
}
}
}
var selectedMedia: TelegramMediaFile?
for media in item.message.media {
if let file = media as? TelegramMediaFile {
selectedMedia = file
} else if let webPage = media as? TelegramMediaWebpage, case let .Loaded(content) = webPage.content, let file = content.file {
selectedMedia = file
}
}
guard let file = selectedMedia else {
return
}
let displayMute: Bool
switch status.mediaStatus {
case let .fetchStatus(fetchStatus):
switch fetchStatus {
case .Local:
displayMute = true
default:
displayMute = self.automaticDownload ?? false
}
case .playbackStatus:
displayMute = false
}
if displayMute != (!self.infoBackgroundNode.alpha.isZero) {
if displayMute {
self.infoBackgroundNode.alpha = 1.0
self.infoBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
self.infoBackgroundNode.layer.animateScale(from: 0.4, to: 1.0, duration: 0.15)
} else {
self.infoBackgroundNode.alpha = 0.0
self.infoBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15)
self.infoBackgroundNode.layer.animateScale(from: 1.0, to: 0.4, duration: 0.15)
}
}
var isBuffering: Bool?
if let message = self.item?.message, let media = self.media, isMediaStreamable(message: message, media: media) && (self.automaticDownload ?? false) {
if let playerStatus = self.playerStatus, case .buffering = playerStatus.status {
isBuffering = true
} else {
isBuffering = false
}
}
var progressRequired = false
if case let .fetchStatus(fetchStatus) = status.mediaStatus {
if case .Local = fetchStatus {
if file.isVideo {
progressRequired = true
} else if isSecretMedia {
progressRequired = true
}
} else {
progressRequired = true
}
} else if isBuffering ?? false {
progressRequired = true
}
if progressRequired {
if self.statusNode == nil {
let statusNode = RadialStatusNode(backgroundNodeColor: item.presentationData.theme.theme.chat.message.mediaOverlayControlColors.fillColor)
self.isUserInteractionEnabled = false
self.statusNode = statusNode
self.addSubnode(statusNode)
}
} else {
if let statusNode = self.statusNode {
self.disappearingStatusNode = statusNode
statusNode.transitionToState(.none, completion: { [weak statusNode, weak self] in
statusNode?.removeFromSupernode()
if self?.disappearingStatusNode === statusNode {
self?.disappearingStatusNode = nil
}
})
self.statusNode = nil
}
}
let statusFrame = CGRect(origin: CGPoint(x: videoFrame.origin.x + floorToScreenPixels((videoFrame.size.width - 50.0) / 2.0), y: videoFrame.origin.y + floorToScreenPixels((videoFrame.size.height - 50.0) / 2.0)), size: CGSize(width: 50.0, height: 50.0))
self.statusNode?.frame = statusFrame
self.disappearingStatusNode?.frame = statusFrame
var state: RadialStatusNodeState
switch status.mediaStatus {
case var .fetchStatus(fetchStatus):
if item.message.forwardInfo != nil {
fetchStatus = status.fetchStatus
}
switch fetchStatus {
case let .Fetching(_, progress):
if let isBuffering = isBuffering {
if isBuffering {
state = .progress(color: messageTheme.mediaOverlayControlColors.foregroundColor, lineWidth: nil, value: nil, cancelEnabled: true, animateRotation: true)
} else {
state = .none
}
} else {
let adjustedProgress = max(progress, 0.027)
state = .progress(color: messageTheme.mediaOverlayControlColors.foregroundColor, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true, animateRotation: true)
}
case .Local:
if isSecretMedia && self.secretProgressIcon != nil {
if let (beginTime, timeout) = secretBeginTimeAndTimeout {
state = .secretTimeout(color: messageTheme.mediaOverlayControlColors.foregroundColor, icon: secretProgressIcon, beginTime: beginTime, timeout: timeout, sparks: true)
} else {
state = .customIcon(secretProgressIcon!)
}
} else {
state = .none
}
case .Remote:
state = .download(messageTheme.mediaOverlayControlColors.foregroundColor)
}
default:
var isLocal = false
if case .Local = status.fetchStatus {
isLocal = true
}
if (isBuffering ?? false) && !isLocal {
state = .progress(color: messageTheme.mediaOverlayControlColors.foregroundColor, lineWidth: nil, value: nil, cancelEnabled: true, animateRotation: true)
} else {
state = .none
}
}
if let statusNode = self.statusNode {
if state == .none {
self.statusNode = nil
}
statusNode.transitionToState(state, completion: { [weak statusNode] in
if state == .none {
statusNode?.removeFromSupernode()
}
})
}
if case .playbackStatus = status.mediaStatus {
let playbackStatusNode: InstantVideoRadialStatusNode
if let current = self.playbackStatusNode {
playbackStatusNode = current
} else {
playbackStatusNode = InstantVideoRadialStatusNode(color: UIColor(white: 1.0, alpha: 0.6), hasSeek: true)
playbackStatusNode.seekTo = { [weak self] position, play in
guard let strongSelf = self else {
return
}
strongSelf.seekTo(position)
if play {
strongSelf.play()
}
}
self.addSubnode(playbackStatusNode)
self.playbackStatusNode = playbackStatusNode
}
playbackStatusNode.frame = videoFrame.insetBy(dx: 1.5, dy: 1.5)
let status = messageFileMediaPlaybackStatus(context: item.context, file: file, message: item.message, isRecentActions: item.associatedData.isRecentActions, isGlobalSearch: false)
playbackStatusNode.status = status
self.durationNode?.status = status
|> map(Optional.init)
self.videoNode?.isHidden = false
self.secretVideoPlaceholderBackground.isHidden = true
self.secretVideoPlaceholder.isHidden = true
} else {
if let playbackStatusNode = self.playbackStatusNode {
self.playbackStatusNode = nil
playbackStatusNode.removeFromSupernode()
}
self.durationNode?.status = .single(nil)
self.videoNode?.isHidden = isSecretMedia
self.secretVideoPlaceholderBackground.isHidden = !isSecretMedia
self.secretVideoPlaceholder.isHidden = !isSecretMedia
}
}
@objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
switch recognizer.state {
case .ended:
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
switch gesture {
case .tap:
if let statusNode = self.statusNode, statusNode.supernode != nil, !statusNode.isHidden, statusNode.frame.contains(location) {
self.progressPressed()
return
}
if let _ = self.item, let videoNode = self.videoNode, videoNode.frame.contains(location) {
self.activateVideoPlayback()
return
}
self.item?.controllerInteraction.clickThroughMessage()
case .longTap, .doubleTap:
break
case .hold:
break
}
}
default:
break
}
}
private func activateVideoPlayback() {
guard let item = self.item, self.shouldOpen() else {
return
}
if self.infoBackgroundNode.alpha.isZero {
if let status = self.status, case let .fetchStatus(fetchStatus) = status.mediaStatus, case .Remote = fetchStatus {
item.context.sharedContext.mediaManager.playlistControl(.playback(.pause), type: .voice)
self.videoNode?.fetchControl(.fetch)
} else {
item.context.sharedContext.mediaManager.playlistControl(.playback(.togglePlayPause), type: .voice)
}
} else {
let _ = item.controllerInteraction.openMessage(item.message, .default)
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.bounds.contains(point) {
return nil
}
if let playbackNode = self.playbackStatusNode, !self.isPlaying, !playbackNode.frame.insetBy(dx: 0.2 * playbackNode.frame.width, dy: 0.2 * playbackNode.frame.height).contains(point) {
let distanceFromCenter = point.distanceTo(playbackNode.position)
if distanceFromCenter < 0.2 * playbackNode.frame.width {
return self.view
} else {
return playbackNode.view
}
}
if let statusNode = self.statusNode, statusNode.supernode != nil, !statusNode.isHidden, statusNode.frame.contains(point) {
return self.view
}
if let videoNode = self.videoNode, videoNode.frame.contains(point) {
return self.view
}
return super.hitTest(point, with: event)
}
private func progressPressed() {
guard let item = self.item, let file = self.media else {
return
}
if let status = self.status {
switch status.mediaStatus {
case let .fetchStatus(fetchStatus):
switch fetchStatus {
case .Fetching:
if item.message.flags.isSending {
let messageId = item.message.id
let _ = item.context.account.postbox.transaction({ transaction -> Void in
item.context.engine.messages.deleteMessages(transaction: transaction, ids: [messageId])
}).start()
} else {
messageMediaFileCancelInteractiveFetch(context: item.context, messageId: item.message.id, file: file)
}
case .Remote:
self.videoNode?.fetchControl(.fetch)
case .Local:
self.activateVideoPlayback()
}
default:
break
}
}
}
override func accessibilityActivate() -> Bool {
self.progressPressed()
return true
}
func videoContentNode(at point: CGPoint) -> ASDisplayNode? {
if let videoFrame = self.videoFrame {
if videoFrame.contains(point) {
return self.videoNode
}
}
return nil
}
static func asyncLayout(_ node: ChatMessageInteractiveInstantVideoNode?) -> (_ item: ChatMessageBubbleContentItem, _ width: CGFloat, _ displaySize: CGSize, _ maximumDisplaySize: CGSize, _ scaleProgress: CGFloat, _ statusType: ChatMessageInteractiveInstantVideoNodeStatusType, _ automaticDownload: Bool) -> (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ContainedViewLayoutTransition) -> ChatMessageInteractiveInstantVideoNode) {
let makeLayout = node?.asyncLayout()
return { item, width, displaySize, maximumDisplaySize, scaleProgress, statusType, automaticDownload in
var createdNode: ChatMessageInteractiveInstantVideoNode?
let sizeAndApplyLayout: (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ContainedViewLayoutTransition) -> Void)
if let makeLayout = makeLayout {
sizeAndApplyLayout = makeLayout(item, width, displaySize, maximumDisplaySize, scaleProgress, statusType, automaticDownload)
} else {
let node = ChatMessageInteractiveInstantVideoNode()
sizeAndApplyLayout = node.asyncLayout()(item, width, displaySize, maximumDisplaySize, scaleProgress, statusType, automaticDownload)
createdNode = node
}
return (sizeAndApplyLayout.0, { [weak node] layoutData, transition in
sizeAndApplyLayout.1(layoutData, transition)
if let createdNode = createdNode {
return createdNode
} else {
return node!
}
})
}
}
var isPlaying: Bool {
if let status = self.status, case let .playbackStatus(playbackStatus) = status.mediaStatus, case .playing = playbackStatus {
return true
} else {
return false
}
}
func seekTo(_ position: Double) {
if let duration = self.playbackStatusNode?.duration {
self.videoNode?.seek(position * duration)
}
}
func play() {
self.videoNode?.play()
}
func pause() {
self.videoNode?.pause()
}
func playMediaWithSound() -> (action: (Double?) -> Void, soundEnabled: Bool, isVideoMessage: Bool, isUnread: Bool, badgeNode: ASDisplayNode?)? {
if let item = self.item {
var isUnconsumed = false
for attribute in item.message.attributes {
if let attribute = attribute as? ConsumableContentMessageAttribute {
if !attribute.consumed {
isUnconsumed = true
}
break
}
}
return ({ _ in
if !self.infoBackgroundNode.alpha.isZero {
let _ = (item.context.sharedContext.mediaManager.globalMediaPlayerState
|> take(1)
|> deliverOnMainQueue).start(next: { playlistStateAndType in
var canPlay = true
if let (_, state, _) = playlistStateAndType {
switch state {
case let .state(state):
if case .playing = state.status.status {
canPlay = false
}
case .loading:
break
}
}
if canPlay {
let _ = item.controllerInteraction.openMessage(item.message, .default)
}
})
}
}, false, true, isUnconsumed, nil)
} else {
return nil
}
}
private var animatedFadeIn = false
func animateFromSnapshot(snapshotView: UIView, transition: CombinedTransition) {
guard let videoFrame = self.videoFrame else {
return
}
let scale = videoFrame.height / snapshotView.frame.height
snapshotView.transform = CGAffineTransform(scaleX: scale, y: scale)
snapshotView.center = CGPoint(x: videoFrame.midX, y: videoFrame.midY)
self.view.addSubview(snapshotView)
transition.horizontal.updateAlpha(layer: snapshotView.layer, alpha: 0.0, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
transition.horizontal.animateTransformScale(node: self, from: 1.0 / scale)
if !self.animatedFadeIn {
self.animatedFadeIn = true
self.dateAndStatusNode.layer.animateAlpha(from: 0.0, to: self.dateAndStatusNode.alpha, duration: 0.15, delay: 0.18)
if let durationNode = self.durationNode {
durationNode.layer.animateAlpha(from: 0.0, to: durationNode.alpha, duration: 0.15, delay: 0.18)
}
}
}
}