import Foundation import UIKit import AsyncDisplayKit import Postbox import SwiftSignalKit import Display import TelegramCore private struct FetchControls { let fetch: (Bool) -> Void let cancel: () -> Void } enum InteractiveMediaNodeSizeCalculation { case constrained(CGSize) case unconstrained } enum InteractiveMediaNodeContentMode { case aspectFit case aspectFill } enum InteractiveMediaNodeActivateContent { case `default` case stream case automaticPlayback } enum InteractiveMediaNodeAutodownloadMode { case none case prefetch case full } enum InteractiveMediaNodePlayWithSoundMode { case single case loop } final class ChatMessageInteractiveMediaNode: ASDisplayNode { private let imageNode: TransformImageNode private var currentImageArguments: TransformImageArguments? private var videoNode: UniversalVideoNode? private var videoContent: NativeVideoContent? private var statusNode: RadialStatusNode? var videoNodeDecoration: ChatBubbleVideoDecoration? private var badgeNode: ChatMessageInteractiveMediaBadge? private var tapRecognizer: UITapGestureRecognizer? private var context: AccountContext? private var message: Message? private var media: Media? private var themeAndStrings: (PresentationTheme, PresentationStrings, String)? private var sizeCalculation: InteractiveMediaNodeSizeCalculation? private var wideLayout: Bool? private var automaticDownload: InteractiveMediaNodeAutodownloadMode? var automaticPlayback: Bool? private let statusDisposable = MetaDisposable() private let fetchControls = Atomic(value: nil) private var fetchStatus: MediaResourceStatus? private var actualFetchStatus: MediaResourceStatus? private let fetchDisposable = MetaDisposable() private let videoNodeReadyDisposable = MetaDisposable() private let playerStatusDisposable = MetaDisposable() private var playerUpdateTimer: SwiftSignalKit.Timer? private var playerStatus: MediaPlayerStatus? { didSet { if self.playerStatus != oldValue { if let playerStatus = playerStatus, case .playing = playerStatus.status { self.ensureHasTimer() } else { self.stopTimer() } self.updateStatus(animated: false) } } } private var secretTimer: SwiftSignalKit.Timer? var visibilityPromise = ValuePromise(false, ignoreRepeated: true) var visibility: ListViewItemNodeVisibility = .none { didSet { if let videoNode = self.videoNode { switch self.visibility { case .visible: if !videoNode.canAttachContent { videoNode.canAttachContent = true if videoNode.hasAttachedContext { videoNode.play() } } case .none: videoNode.canAttachContent = false } } var isVisible = false if case .visible = self.visibility { isVisible = true } self.visibilityPromise.set(isVisible) } } var activateLocalContent: (InteractiveMediaNodeActivateContent) -> Void = { _ in } override init() { self.imageNode = TransformImageNode() self.imageNode.contentAnimations = [.subsequentUpdates] super.init() self.imageNode.displaysAsynchronously = false self.addSubnode(self.imageNode) } deinit { self.statusDisposable.dispose() self.videoNodeReadyDisposable.dispose() self.playerStatusDisposable.dispose() self.fetchDisposable.dispose() self.secretTimer?.invalidate() } override func didLoad() { super.didLoad() let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.imageTap(_:))) self.imageNode.view.addGestureRecognizer(tapRecognizer) self.tapRecognizer = tapRecognizer } private func progressPressed(canActivate: Bool) { if let fetchStatus = self.fetchStatus { var activateContent = false if let state = self.statusNode?.state, case .play = state { activateContent = true } else if let message = self.message, !message.flags.isSending && (self.automaticPlayback ?? false) { activateContent = true } if canActivate, activateContent { switch fetchStatus { case .Remote, .Fetching: self.activateLocalContent(.stream) default: break } return } switch fetchStatus { case .Fetching: if let context = self.context, let message = self.message, message.flags.isSending { let _ = context.account.postbox.transaction({ transaction -> Void in deleteMessages(transaction: transaction, mediaBox: context.account.postbox.mediaBox, ids: [message.id]) }).start() } else if let media = media, let context = self.context, let message = message { if let media = media as? TelegramMediaFile { messageMediaFileCancelInteractiveFetch(context: context, messageId: message.id, file: media) } else if let media = media as? TelegramMediaImage, let resource = largestImageRepresentation(media.representations)?.resource { messageMediaImageCancelInteractiveFetch(context: context, messageId: message.id, image: media, resource: resource) } } if let cancel = self.fetchControls.with({ return $0?.cancel }) { cancel() } case .Remote: if let fetch = self.fetchControls.with({ return $0?.fetch }) { fetch(true) } case .Local: break } } } @objc func imageTap(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { let point = recognizer.location(in: self.imageNode.view) if let fetchStatus = self.fetchStatus, case .Local = fetchStatus { var videoContentMatch = true if let content = self.videoContent, case let .message(id, _, mediaId) = content.nativeId { videoContentMatch = self.message?.id == id && self.media?.id == mediaId } self.activateLocalContent((self.automaticPlayback ?? false) && videoContentMatch ? .automaticPlayback : .default) } else { if let message = self.message, message.flags.isSending { if let statusNode = self.statusNode, statusNode.frame.contains(point) { self.progressPressed(canActivate: true) } } else { self.progressPressed(canActivate: true) } } } } func asyncLayout() -> (_ context: AccountContext, _ theme: PresentationTheme, _ strings: PresentationStrings, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ media: Media, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> Void))) { let currentMessage = self.message let currentMedia = self.media let imageLayout = self.imageNode.asyncLayout() let currentVideoNode = self.videoNode let hasCurrentVideoNode = currentVideoNode != nil let currentAutomaticDownload = self.automaticDownload let currentAutomaticPlayback = self.automaticPlayback return { [weak self] context, theme, strings, dateTimeFormat, message, media, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode in var nativeSize: CGSize let isSecretMedia = message.containsSecretMedia var secretBeginTimeAndTimeout: (Double, Double)? if isSecretMedia { for attribute in message.attributes { if let attribute = attribute as? AutoremoveTimeoutMessageAttribute { if let countdownBeginTime = attribute.countdownBeginTime { secretBeginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout)) } break } } } var storeToDownloadsPeerType: MediaAutoDownloadPeerType? for media in message.media { if media is TelegramMediaImage { storeToDownloadsPeerType = peerType } } var isInlinePlayableVideo = false var maxDimensions = layoutConstants.image.maxDimensions var maxHeight = layoutConstants.image.maxDimensions.height var unboundSize: CGSize if let image = media as? TelegramMediaImage, let dimensions = largestImageRepresentation(image.representations)?.dimensions { unboundSize = CGSize(width: floor(dimensions.width * 0.5), height: floor(dimensions.height * 0.5)) } else if let file = media as? TelegramMediaFile, var dimensions = file.dimensions { if let thumbnail = file.previewRepresentations.first { let dimensionsVertical = dimensions.width < dimensions.height let thumbnailVertical = thumbnail.dimensions.width < thumbnail.dimensions.height if dimensionsVertical != thumbnailVertical { dimensions = CGSize(width: dimensions.height, height: dimensions.width) } } unboundSize = CGSize(width: floor(dimensions.width * 0.5), height: floor(dimensions.height * 0.5)) if file.isAnimated { unboundSize = unboundSize.aspectFilled(CGSize(width: 480.0, height: 480.0)) } else if file.isVideo && !file.isAnimated, case let .constrained(constrainedSize) = sizeCalculation { if unboundSize.width > unboundSize.height { maxDimensions = CGSize(width: constrainedSize.width, height: layoutConstants.video.maxHorizontalHeight) } else { maxDimensions = CGSize(width: constrainedSize.width, height: layoutConstants.video.maxVerticalHeight) } maxHeight = maxDimensions.height } else if file.isSticker { unboundSize = unboundSize.aspectFilled(CGSize(width: 162.0, height: 162.0)) } isInlinePlayableVideo = file.isVideo && !isSecretMedia } else if let image = media as? TelegramMediaWebFile, let dimensions = image.dimensions { unboundSize = CGSize(width: floor(dimensions.width * 0.5), height: floor(dimensions.height * 0.5)) } else if let wallpaper = media as? WallpaperPreviewMedia { switch wallpaper.content { case let .file(file, _): if let thumbnail = file.previewRepresentations.first, var dimensions = file.dimensions { let dimensionsVertical = dimensions.width < dimensions.height let thumbnailVertical = thumbnail.dimensions.width < thumbnail.dimensions.height if dimensionsVertical != thumbnailVertical { dimensions = CGSize(width: dimensions.height, height: dimensions.width) } unboundSize = CGSize(width: floor(dimensions.width * 0.5), height: floor(dimensions.height * 0.5)).fitted(CGSize(width: 240.0, height: 240.0)) } else { unboundSize = CGSize(width: 54.0, height: 54.0) } case .color: unboundSize = CGSize(width: 128.0, height: 128.0) } } else { unboundSize = CGSize(width: 54.0, height: 54.0) } switch sizeCalculation { case let .constrained(constrainedSize): if unboundSize.width > unboundSize.height { nativeSize = unboundSize.aspectFitted(constrainedSize) } else { nativeSize = unboundSize.aspectFitted(CGSize(width: constrainedSize.height, height: constrainedSize.width)) } case .unconstrained: nativeSize = unboundSize } let maxWidth: CGFloat if isSecretMedia { maxWidth = 180.0 } else { maxWidth = maxDimensions.width } if isSecretMedia { let _ = PresentationResourcesChat.chatBubbleSecretMediaIcon(theme) } return (nativeSize, maxWidth, { constrainedSize, automaticPlayback, wideLayout, corners in var resultWidth: CGFloat isInlinePlayableVideo = isInlinePlayableVideo && automaticPlayback switch sizeCalculation { case .constrained: if isSecretMedia { resultWidth = maxWidth } else { let maxFittedSize = nativeSize.aspectFitted(maxDimensions) resultWidth = min(nativeSize.width, min(maxFittedSize.width, min(constrainedSize.width, maxDimensions.width))) resultWidth = max(resultWidth, layoutConstants.image.minDimensions.width) } case .unconstrained: resultWidth = constrainedSize.width } return (resultWidth, { boundingWidth in var boundingSize: CGSize let drawingSize: CGSize switch sizeCalculation { case .constrained: if isSecretMedia { boundingSize = CGSize(width: maxWidth, height: maxWidth) drawingSize = nativeSize.aspectFilled(boundingSize) } else { let fittedSize = nativeSize.fittedToWidthOrSmaller(boundingWidth) let filledSize = fittedSize.aspectFilled(CGSize(width: boundingWidth, height: fittedSize.height)) boundingSize = CGSize(width: boundingWidth, height: filledSize.height).cropped(CGSize(width: CGFloat.greatestFiniteMagnitude, height: maxHeight)) boundingSize.height = max(boundingSize.height, layoutConstants.image.minDimensions.height) boundingSize.width = max(boundingSize.width, layoutConstants.image.minDimensions.width) switch contentMode { case .aspectFit: drawingSize = nativeSize.aspectFittedWithOverflow(boundingSize, leeway: 4.0) case .aspectFill: drawingSize = nativeSize.aspectFilled(boundingSize) } } case .unconstrained: boundingSize = constrainedSize drawingSize = nativeSize.aspectFilled(boundingSize) } var updateImageSignal: ((Bool) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError>)? var updatedStatusSignal: Signal<(MediaResourceStatus, MediaResourceStatus?), NoError>? var updatedFetchControls: FetchControls? var mediaUpdated = false if let currentMedia = currentMedia { mediaUpdated = !media.isSemanticallyEqual(to: currentMedia) } else { mediaUpdated = true } var isSendingUpdated = false if let currentMessage = currentMessage { isSendingUpdated = message.flags.isSending != currentMessage.flags.isSending } var automaticPlaybackUpdated = false if let currentAutomaticPlayback = currentAutomaticPlayback { automaticPlaybackUpdated = automaticPlayback != currentAutomaticPlayback } var statusUpdated = mediaUpdated if currentMessage?.id != message.id || currentMessage?.flags != message.flags { statusUpdated = true } var replaceVideoNode: Bool? var updateVideoFile: TelegramMediaFile? var onlyFullSizeVideoThumbnail: Bool? var emptyColor: UIColor = message.effectivelyIncoming(context.account.peerId) ? theme.chat.bubble.incomingMediaPlaceholderColor : theme.chat.bubble.outgoingMediaPlaceholderColor if let wallpaper = media as? WallpaperPreviewMedia, case let .file(_, patternColor) = wallpaper.content { emptyColor = patternColor ?? UIColor(rgb: 0xd6e2ee, alpha: 0.5) } if mediaUpdated || isSendingUpdated || automaticPlaybackUpdated { if let image = media as? TelegramMediaImage { if hasCurrentVideoNode { replaceVideoNode = true } if isSecretMedia { updateImageSignal = { synchronousLoad in return chatSecretPhoto(account: context.account, photoReference: .message(message: MessageReference(message), media: image)) } } else { updateImageSignal = { synchronousLoad in return chatMessagePhoto(postbox: context.account.postbox, photoReference: .message(message: MessageReference(message), media: image), synchronousLoad: synchronousLoad) } } updatedFetchControls = FetchControls(fetch: { manual in if let strongSelf = self { if !manual { strongSelf.fetchDisposable.set(chatMessagePhotoInteractiveFetched(context: context, photoReference: .message(message: MessageReference(message), media: image), storeToDownloadsPeerType: storeToDownloadsPeerType).start()) } else if let resource = largestRepresentationForPhoto(image)?.resource { strongSelf.fetchDisposable.set(messageMediaImageInteractiveFetched(context: context, message: message, image: image, resource: resource, storeToDownloadsPeerType: storeToDownloadsPeerType).start()) } } }, cancel: { chatMessagePhotoCancelInteractiveFetch(account: context.account, photoReference: .message(message: MessageReference(message), media: image)) if let resource = largestRepresentationForPhoto(image)?.resource { messageMediaImageCancelInteractiveFetch(context: context, messageId: message.id, image: image, resource: resource) } }) } else if let image = media as? TelegramMediaWebFile { if hasCurrentVideoNode { replaceVideoNode = true } updateImageSignal = { synchronousLoad in return chatWebFileImage(account: context.account, file: image) } updatedFetchControls = FetchControls(fetch: { _ in if let strongSelf = self { strongSelf.fetchDisposable.set(chatMessageWebFileInteractiveFetched(account: context.account, image: image).start()) } }, cancel: { chatMessageWebFileCancelInteractiveFetch(account: context.account, image: image) }) } else if let file = media as? TelegramMediaFile { if isSecretMedia { updateImageSignal = { synchronousLoad in return chatSecretMessageVideo(account: context.account, videoReference: .message(message: MessageReference(message), media: file)) } } else { if file.isSticker { updateImageSignal = { synchronousLoad in return chatMessageSticker(account: context.account, file: file, small: false) } } else { onlyFullSizeVideoThumbnail = isSendingUpdated updateImageSignal = { synchronousLoad in return mediaGridMessageVideo(postbox: context.account.postbox, videoReference: .message(message: MessageReference(message), media: file), onlyFullSize: currentMedia?.id?.namespace == Namespaces.Media.LocalFile, autoFetchFullSizeThumbnail: true) } } } var uploading = false if file.resource is VideoLibraryMediaResource { uploading = true } if file.isVideo && !isSecretMedia && automaticPlayback && !uploading { updateVideoFile = file if hasCurrentVideoNode { if let currentFile = currentMedia as? TelegramMediaFile, currentFile.resource is EmptyMediaResource { replaceVideoNode = true } } else if !(file.resource is LocalFileVideoMediaResource) { replaceVideoNode = true } } else { if hasCurrentVideoNode { replaceVideoNode = false } } updatedFetchControls = FetchControls(fetch: { manual in if let strongSelf = self { if file.isAnimated { strongSelf.fetchDisposable.set(fetchedMediaResource(postbox: context.account.postbox, reference: AnyMediaReference.message(message: MessageReference(message), media: file).resourceReference(file.resource), statsCategory: statsCategoryForFileWithAttributes(file.attributes)).start()) } else { strongSelf.fetchDisposable.set(messageMediaFileInteractiveFetched(context: context, message: message, file: file, userInitiated: manual).start()) } } }, cancel: { if file.isAnimated { context.account.postbox.mediaBox.cancelInteractiveResourceFetch(file.resource) } else { messageMediaFileCancelInteractiveFetch(context: context, messageId: message.id, file: file) } }) } else if let wallpaper = media as? WallpaperPreviewMedia { updateImageSignal = { synchronousLoad in switch wallpaper.content { case let .file(file, _): let representations: [ImageRepresentationWithReference] = file.previewRepresentations.map({ ImageRepresentationWithReference(representation: $0, reference: AnyMediaReference.message(message: MessageReference(message), media: file).resourceReference($0.resource)) }) if file.mimeType == "image/png" { return patternWallpaperImage(account: context.account, accountManager: context.sharedContext.accountManager, representations: representations, mode: .thumbnail) } else { return wallpaperImage(account: context.account, accountManager: context.sharedContext.accountManager, fileReference: FileMediaReference.message(message: MessageReference(message), media: file), representations: representations, alwaysShowThumbnailFirst: false, thumbnail: true, autoFetchFullSize: true) } case let .color(color): return solidColor(color) } } if case let .file(file, _) = wallpaper.content { updatedFetchControls = FetchControls(fetch: { manual in if let strongSelf = self { strongSelf.fetchDisposable.set(messageMediaFileInteractiveFetched(context: context, message: message, file: file, userInitiated: manual).start()) } }, cancel: { messageMediaFileCancelInteractiveFetch(context: context, messageId: message.id, file: file) }) } else { boundingSize = CGSize(width: boundingSize.width, height: boundingSize.width) } } } if statusUpdated { if let image = media as? TelegramMediaImage { if message.flags.isSending { updatedStatusSignal = combineLatest(chatMessagePhotoStatus(context: context, messageId: message.id, photoReference: .message(message: MessageReference(message), media: image)), context.account.pendingMessageManager.pendingMessageStatus(message.id)) |> map { resourceStatus, pendingStatus -> (MediaResourceStatus, MediaResourceStatus?) in if let pendingStatus = pendingStatus { let adjustedProgress = max(pendingStatus.progress, 0.027) return (.Fetching(isActive: pendingStatus.isRunning, progress: adjustedProgress), resourceStatus) } else { return (resourceStatus, nil) } } } else { updatedStatusSignal = chatMessagePhotoStatus(context: context, messageId: message.id, photoReference: .message(message: MessageReference(message), media: image)) |> map { resourceStatus -> (MediaResourceStatus, MediaResourceStatus?) in return (resourceStatus, nil) } } } else if let file = media as? TelegramMediaFile { updatedStatusSignal = combineLatest(messageMediaFileStatus(context: context, messageId: message.id, file: file), context.account.pendingMessageManager.pendingMessageStatus(message.id)) |> map { resourceStatus, pendingStatus -> (MediaResourceStatus, MediaResourceStatus?) in if let pendingStatus = pendingStatus { let adjustedProgress = max(pendingStatus.progress, 0.027) return (.Fetching(isActive: pendingStatus.isRunning, progress: adjustedProgress), resourceStatus) } else { return (resourceStatus, nil) } } } else if let wallpaper = media as? WallpaperPreviewMedia { switch wallpaper.content { case let .file(file, _): updatedStatusSignal = messageMediaFileStatus(context: context, messageId: message.id, file: file) |> map { resourceStatus -> (MediaResourceStatus, MediaResourceStatus?) in return (resourceStatus, nil) } case .color: updatedStatusSignal = .single((.Local, nil)) } } } let arguments = TransformImageArguments(corners: corners, imageSize: drawingSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets(), resizeMode: isInlinePlayableVideo ? .fill(.black) : .blurBackground, emptyColor: emptyColor) let imageFrame = CGRect(origin: CGPoint(x: -arguments.insets.left, y: -arguments.insets.top), size: arguments.drawingSize) let imageApply = imageLayout(arguments) return (boundingSize, { transition, synchronousLoads in if let strongSelf = self { strongSelf.context = context strongSelf.message = message strongSelf.media = media strongSelf.wideLayout = wideLayout strongSelf.themeAndStrings = (theme, strings, dateTimeFormat.decimalSeparator) strongSelf.sizeCalculation = sizeCalculation strongSelf.automaticPlayback = automaticPlayback strongSelf.automaticDownload = automaticDownload if let previousArguments = strongSelf.currentImageArguments { if previousArguments.imageSize == arguments.imageSize { strongSelf.imageNode.frame = imageFrame } else { transition.updateFrame(node: strongSelf.imageNode, frame: imageFrame) } } else { strongSelf.imageNode.frame = imageFrame } strongSelf.currentImageArguments = arguments imageApply() if let statusNode = strongSelf.statusNode { var statusFrame = statusNode.frame statusFrame.origin.x = floor(imageFrame.midX - statusFrame.width / 2.0) statusFrame.origin.y = floor(imageFrame.midY - statusFrame.height / 2.0) statusNode.frame = statusFrame } var updatedVideoNodeReadySignal: Signal? var updatedPlayerStatusSignal: Signal? if let currentReplaceVideoNode = replaceVideoNode { replaceVideoNode = nil if let videoNode = strongSelf.videoNode { videoNode.canAttachContent = false videoNode.removeFromSupernode() strongSelf.videoNode = nil } if currentReplaceVideoNode, let updatedVideoFile = updateVideoFile { let decoration = ChatBubbleVideoDecoration(corners: arguments.corners, nativeSize: nativeSize, contentMode: contentMode, backgroundColor: arguments.emptyColor ?? .black) strongSelf.videoNodeDecoration = decoration let mediaManager = context.sharedContext.mediaManager let streamVideo = isMediaStreamable(message: message, media: updatedVideoFile) let videoContent = NativeVideoContent(id: .message(message.id, message.stableId, updatedVideoFile.fileId), fileReference: .message(message: MessageReference(message), media: updatedVideoFile), streamVideo: streamVideo ? .conservative : .none, enableSound: false, fetchAutomatically: false, onlyFullSizeThumbnail: (onlyFullSizeVideoThumbnail ?? false), continuePlayingWithoutSoundOnLostAudioSession: isInlinePlayableVideo, placeholderColor: emptyColor) let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded) videoNode.isUserInteractionEnabled = false videoNode.ownsContentNodeUpdated = { [weak self] owns in if let strongSelf = self { strongSelf.videoNode?.isHidden = !owns } } strongSelf.videoContent = videoContent strongSelf.videoNode = videoNode updatedVideoNodeReadySignal = videoNode.ready updatedPlayerStatusSignal = videoNode.status |> mapToSignal { status -> Signal in if let status = status, case .buffering = status.status { return .single(status) |> delay(0.5, queue: Queue.mainQueue()) } else { return .single(status) } } } } if let videoNode = strongSelf.videoNode { if !(replaceVideoNode ?? false), let decoration = videoNode.decoration as? ChatBubbleVideoDecoration, decoration.corners != corners { decoration.updateCorners(corners) } videoNode.updateLayout(size: arguments.drawingSize, transition: .immediate) videoNode.frame = imageFrame if case .visible = strongSelf.visibility { if !videoNode.canAttachContent { videoNode.canAttachContent = true if videoNode.hasAttachedContext { videoNode.play() } } } else { videoNode.canAttachContent = false } } if let updateImageSignal = updateImageSignal { strongSelf.imageNode.setSignal(updateImageSignal(synchronousLoads), attemptSynchronously: synchronousLoads) } if let _ = secretBeginTimeAndTimeout { if updatedStatusSignal == nil, let fetchStatus = strongSelf.fetchStatus, case .Local = fetchStatus { if let statusNode = strongSelf.statusNode, case .secretTimeout = statusNode.state { } else { updatedStatusSignal = .single((fetchStatus, nil)) } } } if let updatedStatusSignal = updatedStatusSignal { strongSelf.statusDisposable.set((updatedStatusSignal |> deliverOnMainQueue).start(next: { [weak strongSelf] status, actualFetchStatus in displayLinkDispatcher.dispatch { if let strongSelf = strongSelf { strongSelf.fetchStatus = status strongSelf.actualFetchStatus = actualFetchStatus strongSelf.updateStatus(animated: synchronousLoads) } } })) } if let updatedVideoNodeReadySignal = updatedVideoNodeReadySignal { strongSelf.videoNodeReadyDisposable.set((updatedVideoNodeReadySignal |> deliverOnMainQueue).start(next: { [weak strongSelf] status in displayLinkDispatcher.dispatch { if let strongSelf = strongSelf, let videoNode = strongSelf.videoNode { strongSelf.insertSubnode(videoNode, aboveSubnode: strongSelf.imageNode) } } })) } if let updatedPlayerStatusSignal = updatedPlayerStatusSignal { strongSelf.playerStatusDisposable.set((updatedPlayerStatusSignal |> deliverOnMainQueue).start(next: { [weak strongSelf] status in displayLinkDispatcher.dispatch { if let strongSelf = strongSelf { strongSelf.playerStatus = status } } })) } if let updatedFetchControls = updatedFetchControls { let _ = strongSelf.fetchControls.swap(updatedFetchControls) if case .full = automaticDownload { if let _ = media as? TelegramMediaImage { updatedFetchControls.fetch(false) } else if let image = media as? TelegramMediaWebFile { strongSelf.fetchDisposable.set(chatMessageWebFileInteractiveFetched(account: context.account, image: image).start()) } else if let file = media as? TelegramMediaFile { let fetchSignal = messageMediaFileInteractiveFetched(context: context, message: message, file: file, userInitiated: false) let visibilityAwareFetchSignal = strongSelf.visibilityPromise.get() |> mapToSignal { visibility -> Signal in if visibility { return fetchSignal |> mapToSignal { _ -> Signal in return .complete() } } else { return .complete() } } strongSelf.fetchDisposable.set(visibilityAwareFetchSignal.start()) } } else if case .prefetch = automaticDownload, message.id.namespace != Namespaces.Message.SecretIncoming { if let file = media as? TelegramMediaFile { let fetchSignal = preloadVideoResource(postbox: context.account.postbox, resourceReference: AnyMediaReference.message(message: MessageReference(message), media: file).resourceReference(file.resource), duration: 4.0) let visibilityAwareFetchSignal = strongSelf.visibilityPromise.get() |> mapToSignal { visibility -> Signal in if visibility { return fetchSignal |> mapToSignal { _ -> Signal in return .complete() } } else { return .complete() } } strongSelf.fetchDisposable.set(visibilityAwareFetchSignal.start()) } } } else if currentAutomaticDownload != automaticDownload, case .full = automaticDownload { strongSelf.fetchControls.with({ $0 })?.fetch(false) } strongSelf.updateStatus(animated: synchronousLoads) } }) }) }) } } private func ensureHasTimer() { if self.playerUpdateTimer == nil { let timer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in self?.updateStatus(animated: false) }, queue: Queue.mainQueue()) self.playerUpdateTimer = timer timer.start() } } private func stopTimer() { self.playerUpdateTimer?.invalidate() self.playerUpdateTimer = nil } private func updateStatus(animated: Bool) { guard let (theme, strings, decimalSeparator) = self.themeAndStrings, let sizeCalculation = self.sizeCalculation, let message = self.message, var automaticPlayback = self.automaticPlayback, let wideLayout = self.wideLayout else { return } let automaticDownload: Bool if let autoDownload = self.automaticDownload, case .full = autoDownload { automaticDownload = true } else { automaticDownload = false } var secretBeginTimeAndTimeout: (Double?, Double)? let isSecretMedia = message.containsSecretMedia if isSecretMedia { for attribute in message.attributes { if let attribute = attribute as? AutoremoveTimeoutMessageAttribute { if let countdownBeginTime = attribute.countdownBeginTime { secretBeginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout)) } else { secretBeginTimeAndTimeout = (nil, Double(attribute.timeout)) } break } } } var game: TelegramMediaGame? var webpage: TelegramMediaWebpage? var invoice: TelegramMediaInvoice? for media in message.media { if let media = media as? TelegramMediaWebpage { webpage = media } else if let media = media as? TelegramMediaInvoice { invoice = media } else if let media = media as? TelegramMediaGame { game = media } } var progressRequired = false if secretBeginTimeAndTimeout?.0 != nil { progressRequired = true } else if let fetchStatus = self.fetchStatus { switch fetchStatus { case .Local: if let file = media as? TelegramMediaFile, file.isVideo { progressRequired = true } else if isSecretMedia { progressRequired = true } else if let webpage = webpage, case let .Loaded(content) = webpage.content { if content.embedUrl != nil { progressRequired = true } else if let file = content.file, file.isVideo, !file.isAnimated { progressRequired = true } } case .Remote, .Fetching: if let webpage = webpage, let automaticDownload = self.automaticDownload, case .full = automaticDownload, case let .Loaded(content) = webpage.content { if content.type == "telegram_background" { progressRequired = true } else if content.embedUrl != nil { progressRequired = true } else if let file = content.file, file.isVideo, !file.isAnimated { progressRequired = true } } else { progressRequired = true } } } let radialStatusSize: CGFloat = wideLayout ? 50.0 : 32.0 if progressRequired { if self.statusNode == nil { let statusNode = RadialStatusNode(backgroundNodeColor: theme.chat.bubble.mediaOverlayControlBackgroundColor) let imagePosition = self.imageNode.position statusNode.frame = CGRect(origin: CGPoint(x: floor(imagePosition.x - radialStatusSize / 2.0), y: floor(imagePosition.y - radialStatusSize / 2.0)), size: CGSize(width: radialStatusSize, height: radialStatusSize)) self.statusNode = statusNode self.addSubnode(statusNode) } } else { if let statusNode = self.statusNode { statusNode.transitionToState(.none, completion: { [weak statusNode] in statusNode?.removeFromSupernode() }) self.statusNode = nil } } var state: RadialStatusNodeState = .none var badgeContent: ChatMessageInteractiveMediaBadgeContent? var mediaDownloadState: ChatMessageInteractiveMediaDownloadState? let bubbleTheme = theme.chat.bubble if let invoice = invoice { let string = NSMutableAttributedString() if invoice.receiptMessageId != nil { var title = strings.Checkout_Receipt_Title.uppercased() if invoice.flags.contains(.isTest) { title += " (Test)" } string.append(NSAttributedString(string: title)) } else { string.append(NSAttributedString(string: "\(formatCurrencyAmount(invoice.totalAmount, currency: invoice.currency)) ", attributes: [ChatTextInputAttributes.bold: true as NSNumber])) var title = strings.Message_InvoiceLabel if invoice.flags.contains(.isTest) { title += " (Test)" } string.append(NSAttributedString(string: title)) } badgeContent = .text(inset: 0.0, backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, text: string) } var animated: Bool = animated if var fetchStatus = self.fetchStatus { var playerPosition: Int32? var playerDuration: Int32 = 0 var active = false var muted = automaticPlayback if let playerStatus = self.playerStatus { if !playerStatus.generationTimestamp.isZero, case .playing = playerStatus.status { playerPosition = Int32(playerStatus.timestamp + (CACurrentMediaTime() - playerStatus.generationTimestamp)) } else { playerPosition = Int32(playerStatus.timestamp) } playerDuration = Int32(playerStatus.duration) if case .buffering = playerStatus.status { active = true } if playerStatus.soundEnabled { muted = false } } else if case .Fetching = fetchStatus, !message.flags.contains(.Unsent) { active = true } if let file = self.media as? TelegramMediaFile, file.isAnimated { muted = false } if message.flags.contains(.Unsent) { automaticPlayback = false } if let actualFetchStatus = self.actualFetchStatus, automaticPlayback || message.forwardInfo != nil { fetchStatus = actualFetchStatus } let gifTitle = game != nil ? strings.Message_Game.uppercased() : strings.Message_Animation.uppercased() switch fetchStatus { case let .Fetching(_, progress): let adjustedProgress = max(progress, 0.027) var wasCheck = false if let statusNode = self.statusNode, case .check = statusNode.state { wasCheck = true } if adjustedProgress.isEqual(to: 1.0), case .unconstrained = sizeCalculation, (message.flags.contains(.Unsent) || wasCheck) { state = .check(bubbleTheme.mediaOverlayControlForegroundColor) } else { state = .progress(color: bubbleTheme.mediaOverlayControlForegroundColor, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true) } if let file = self.media as? TelegramMediaFile { if wideLayout { if let size = file.size { let sizeString = "\(dataSizeString(Int(Float(size) * progress), forceDecimal: true, decimalSeparator: decimalSeparator)) / \(dataSizeString(size, forceDecimal: true, decimalSeparator: decimalSeparator))" if file.isAnimated && (!automaticDownload || !automaticPlayback) { badgeContent = .mediaDownload(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, duration: "\(gifTitle) " + sizeString, size: nil, muted: false, active: false) } else if let duration = file.duration, !message.flags.contains(.Unsent) { let durationString = file.isAnimated ? gifTitle : stringForDuration(playerDuration > 0 ? playerDuration : duration, position: playerPosition) if isMediaStreamable(message: message, media: file) { badgeContent = .mediaDownload(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, duration: durationString, size: active ? sizeString : nil, muted: muted, active: active) mediaDownloadState = .fetching(progress: automaticPlayback ? nil : adjustedProgress) if self.playerStatus?.status == .playing { mediaDownloadState = nil } state = automaticPlayback ? .none : .play(bubbleTheme.mediaOverlayControlForegroundColor) } else { if automaticPlayback { mediaDownloadState = .fetching(progress: adjustedProgress) badgeContent = .mediaDownload(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, duration: durationString, size: active ? sizeString : nil, muted: muted, active: active) } else { badgeContent = .mediaDownload(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, duration: sizeString, size: nil, muted: false, active: false) } state = automaticPlayback ? .none : state } } else { badgeContent = .mediaDownload(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, duration: "\(dataSizeString(Int(Float(size) * progress), forceDecimal: true, decimalSeparator: decimalSeparator)) / \(dataSizeString(size, forceDecimal: true, decimalSeparator: decimalSeparator))", size: nil, muted: false, active: false) } } else if let _ = file.duration { badgeContent = .mediaDownload(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, duration: strings.Conversation_Processing, size: nil, muted: false, active: active) } } else { if isMediaStreamable(message: message, media: file), let size = file.size { let sizeString = "\(dataSizeString(Int(Float(size) * progress), forceDecimal: true, decimalSeparator: decimalSeparator)) / \(dataSizeString(size, forceDecimal: true, decimalSeparator: decimalSeparator))" if message.flags.contains(.Unsent), let duration = file.duration { let durationString = stringForDuration(playerDuration > 0 ? playerDuration : duration, position: playerPosition) badgeContent = .mediaDownload(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, duration: durationString, size: nil, muted: false, active: false) } else if automaticPlayback && !message.flags.contains(.Unsent), let duration = file.duration { let durationString = stringForDuration(playerDuration > 0 ? playerDuration : duration, position: playerPosition) badgeContent = .mediaDownload(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, duration: durationString, size: active ? sizeString : nil, muted: muted, active: active) mediaDownloadState = .fetching(progress: automaticPlayback ? nil : adjustedProgress) if self.playerStatus?.status == .playing { mediaDownloadState = nil } } else { let progressString = String(format: "%d%%", Int(progress * 100.0)) badgeContent = .text(inset: message.flags.contains(.Unsent) ? 0.0 : 12.0, backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, text: NSAttributedString(string: progressString)) mediaDownloadState = automaticPlayback ? .none : .compactFetching(progress: 0.0) } if !message.flags.contains(.Unsent) { state = automaticPlayback ? .none : .play(bubbleTheme.mediaOverlayControlForegroundColor) } } else { if let duration = file.duration, !file.isAnimated { let durationString = stringForDuration(playerDuration > 0 ? playerDuration : duration, position: playerPosition) if automaticPlayback, let size = file.size { let sizeString = "\(dataSizeString(Int(Float(size) * progress), forceDecimal: true, decimalSeparator: decimalSeparator)) / \(dataSizeString(size, forceDecimal: true, decimalSeparator: decimalSeparator))" mediaDownloadState = .fetching(progress: progress) badgeContent = .mediaDownload(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, duration: durationString, size: active ? sizeString : nil, muted: muted, active: active) } else { badgeContent = .mediaDownload(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, duration: durationString, size: nil, muted: false, active: false) } } state = automaticPlayback ? .none : state } } } else if let webpage = webpage, let automaticDownload = self.automaticDownload, case .full = automaticDownload, case let .Loaded(content) = webpage.content, content.type != "telegram_background" { state = .play(bubbleTheme.mediaOverlayControlForegroundColor) } case .Local: state = .none let secretProgressIcon: UIImage? if case .constrained = sizeCalculation { secretProgressIcon = PresentationResourcesChat.chatBubbleSecretMediaIcon(theme) } else { secretProgressIcon = PresentationResourcesChat.chatBubbleSecretMediaCompactIcon(theme) } if isSecretMedia, let (maybeBeginTime, timeout) = secretBeginTimeAndTimeout, let beginTime = maybeBeginTime { state = .secretTimeout(color: bubbleTheme.mediaOverlayControlForegroundColor, icon: secretProgressIcon, beginTime: beginTime, timeout: timeout, sparks: true) } else if isSecretMedia, let secretProgressIcon = secretProgressIcon { state = .customIcon(secretProgressIcon) } else if let file = media as? TelegramMediaFile { let isInlinePlayableVideo = file.isVideo && !isSecretMedia && (self.automaticPlayback ?? false) if !isInlinePlayableVideo && file.isVideo { state = .play(bubbleTheme.mediaOverlayControlForegroundColor) } else { state = .none } } else if let webpage = webpage, case let .Loaded(content) = webpage.content { if content.embedUrl != nil { state = .play(bubbleTheme.mediaOverlayControlForegroundColor) } else if let file = content.file, file.isVideo, !file.isAnimated { state = .play(bubbleTheme.mediaOverlayControlForegroundColor) } } if let file = media as? TelegramMediaFile, let duration = file.duration { let durationString = file.isAnimated ? gifTitle : stringForDuration(playerDuration > 0 ? playerDuration : duration, position: playerPosition) badgeContent = .mediaDownload(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, duration: durationString, size: nil, muted: muted, active: false) } case .Remote: state = .download(bubbleTheme.mediaOverlayControlForegroundColor) if let file = self.media as? TelegramMediaFile { if file.isAnimated && (!automaticDownload || !automaticPlayback) { let string = "\(gifTitle) " + dataSizeString(file.size ?? 0, decimalSeparator: decimalSeparator) badgeContent = .mediaDownload(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, duration: string, size: nil, muted: false, active: false) } else { let durationString = file.isAnimated ? gifTitle : stringForDuration(playerDuration > 0 ? playerDuration : (file.duration ?? 0), position: playerPosition) if wideLayout { if isMediaStreamable(message: message, media: file) { state = automaticPlayback ? .none : .play(bubbleTheme.mediaOverlayControlForegroundColor) badgeContent = .mediaDownload(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, duration: durationString, size: dataSizeString(file.size ?? 0, decimalSeparator: decimalSeparator), muted: muted, active: true) mediaDownloadState = .remote } else { state = automaticPlayback ? .none : state badgeContent = .mediaDownload(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, duration: durationString, size: nil, muted: muted, active: false) } } else { if isMediaStreamable(message: message, media: file) { state = automaticPlayback ? .none : .play(bubbleTheme.mediaOverlayControlForegroundColor) badgeContent = .text(inset: 12.0, backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, text: NSAttributedString(string: durationString)) mediaDownloadState = .compactRemote } else { state = automaticPlayback ? .none : state badgeContent = .text(inset: 0.0, backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, text: NSAttributedString(string: durationString)) } } } } else if let webpage = webpage, let automaticDownload = self.automaticDownload, case .full = automaticDownload, case let .Loaded(content) = webpage.content, content.type != "telegram_background" { state = .play(bubbleTheme.mediaOverlayControlForegroundColor) } } } if isSecretMedia, let (maybeBeginTime, timeout) = secretBeginTimeAndTimeout { let remainingTime: Int32 if let beginTime = maybeBeginTime { let elapsedTime = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - beginTime remainingTime = Int32(max(0.0, timeout - elapsedTime)) } else { remainingTime = Int32(timeout) } badgeContent = .text(inset: 0.0, backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, text: NSAttributedString(string: strings.MessageTimer_ShortSeconds(Int32(remainingTime)))) } if let statusNode = self.statusNode { var removeStatusNode = false if statusNode.state != .none && state == .none { self.statusNode = nil removeStatusNode = true } statusNode.transitionToState(state, animated: animated, completion: { [weak statusNode] in if removeStatusNode { statusNode?.removeFromSupernode() } }) } if let badgeContent = badgeContent { if self.badgeNode == nil { let badgeNode = ChatMessageInteractiveMediaBadge() badgeNode.frame = CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: radialStatusSize, height: radialStatusSize)) badgeNode.pressed = { [weak self] in guard let strongSelf = self, let fetchStatus = strongSelf.fetchStatus else { return } switch fetchStatus { case .Remote, .Fetching: strongSelf.progressPressed(canActivate: false) default: break } } self.badgeNode = badgeNode self.addSubnode(badgeNode) animated = false } self.badgeNode?.update(theme: theme, content: badgeContent, mediaDownloadState: mediaDownloadState, animated: animated) } else if let badgeNode = self.badgeNode { self.badgeNode = nil badgeNode.removeFromSupernode() } if isSecretMedia, secretBeginTimeAndTimeout?.0 != nil { if self.secretTimer == nil { self.secretTimer = SwiftSignalKit.Timer(timeout: 0.3, repeat: true, completion: { [weak self] in self?.updateStatus(animated: false) }, queue: Queue.mainQueue()) self.secretTimer?.start() } } else { if let secretTimer = self.secretTimer { self.secretTimer = nil secretTimer.invalidate() } } } static func asyncLayout(_ node: ChatMessageInteractiveMediaNode?) -> (_ context: AccountContext, _ theme: PresentationTheme, _ strings: PresentationStrings, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ media: Media, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> ChatMessageInteractiveMediaNode))) { let currentAsyncLayout = node?.asyncLayout() return { context, theme, strings, dateTimeFormat, message, media, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode in var imageNode: ChatMessageInteractiveMediaNode var imageLayout: (_ context: AccountContext, _ theme: PresentationTheme, _ strings: PresentationStrings, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ media: Media, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> Void))) if let node = node, let currentAsyncLayout = currentAsyncLayout { imageNode = node imageLayout = currentAsyncLayout } else { imageNode = ChatMessageInteractiveMediaNode() imageLayout = imageNode.asyncLayout() } let (unboundSize, initialWidth, continueLayout) = imageLayout(context, theme, strings, dateTimeFormat, message, media, automaticDownload, peerType, sizeCalculation, layoutConstants, contentMode) return (unboundSize, initialWidth, { constrainedSize, automaticPlayback, wideLayout, corners in let (finalWidth, finalLayout) = continueLayout(constrainedSize, automaticPlayback, wideLayout, corners) return (finalWidth, { boundingWidth in let (finalSize, apply) = finalLayout(boundingWidth) return (finalSize, { transition, synchronousLoads in apply(transition, synchronousLoads) return imageNode }) }) }) } } func setOverlayColor(_ color: UIColor?, animated: Bool) { self.imageNode.setOverlayColor(color, animated: animated) } func isReadyForInteractivePreview() -> Bool { if let fetchStatus = self.fetchStatus, case .Local = fetchStatus { return true } else { return false } } func updateIsHidden(_ isHidden: Bool) { if let badgeNode = self.badgeNode, badgeNode.isHidden != isHidden { if isHidden { badgeNode.isHidden = true } else { badgeNode.isHidden = false badgeNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } if let statusNode = self.statusNode, statusNode.isHidden != isHidden { if isHidden { statusNode.isHidden = true } else { statusNode.isHidden = false statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } } func transitionNode() -> (ASDisplayNode, () -> (UIView?, UIView?))? { return (self, { [weak self] in var badgeNodeHidden: Bool? if let badgeNode = self?.badgeNode { badgeNodeHidden = badgeNode.isHidden badgeNode.isHidden = true } var statusNodeHidden: Bool? if let statusNode = self?.statusNode { statusNodeHidden = statusNode.isHidden statusNode.isHidden = true } let view = self?.view.snapshotContentTree(unhide: true) if let badgeNode = self?.badgeNode, let badgeNodeHidden = badgeNodeHidden { badgeNode.isHidden = badgeNodeHidden } if let statusNode = self?.statusNode, let statusNodeHidden = statusNodeHidden { statusNode.isHidden = statusNodeHidden } return (view, nil) }) } func playMediaWithSound() -> (action: (Double?) -> Void, soundEnabled: Bool, isVideoMessage: Bool, isUnread: Bool, badgeNode: ASDisplayNode?)? { var isAnimated = false if let file = self.media as? TelegramMediaFile, file.isAnimated { isAnimated = true } var actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd = .loopDisablingSound if let message = self.message, message.id.peerId.namespace == Namespaces.Peer.CloudChannel { actionAtEnd = .loop } else { actionAtEnd = .repeatIfNeeded } if let videoNode = self.videoNode, let context = self.context, (self.automaticPlayback ?? false) && !isAnimated { return ({ timecode in if let timecode = timecode { context.sharedContext.mediaManager.playlistControl(.playback(.pause)) videoNode.playOnceWithSound(playAndRecord: false, seek: .timecode(timecode), actionAtEnd: actionAtEnd) } else { let _ = (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 { videoNode.playOnceWithSound(playAndRecord: false, seek: .none, actionAtEnd: actionAtEnd) } }) } }, (self.playerStatus?.soundEnabled ?? false), false, false, self.badgeNode) } else { return nil } } }