mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
1845 lines
105 KiB
Swift
1845 lines
105 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
|
|
import HierarchyTrackingLayer
|
|
import ComponentFlow
|
|
import AudioTranscriptionButtonComponent
|
|
import UndoUI
|
|
import TelegramNotices
|
|
import Markdown
|
|
import TextFormat
|
|
import ChatMessageForwardInfoNode
|
|
|
|
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)
|
|
private let nameFont = Font.medium(14.0)
|
|
private let inlineBotPrefixFont = Font.regular(14.0)
|
|
private let inlineBotNameFont = nameFont
|
|
|
|
enum ChatMessageInteractiveInstantVideoNodeStatusType {
|
|
case free
|
|
case bubble
|
|
}
|
|
|
|
class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
|
private var hierarchyTrackingLayer: HierarchyTrackingLayer?
|
|
private var trackingIsInHierarchy: Bool = false {
|
|
didSet {
|
|
if self.trackingIsInHierarchy != oldValue {
|
|
Queue.mainQueue().justDispatch {
|
|
self.videoNode?.canAttachContent = self.shouldAcquireVideoContext
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var canAttachContent: Bool = false {
|
|
didSet {
|
|
if self.canAttachContent != oldValue {
|
|
Queue.mainQueue().justDispatch {
|
|
self.videoNode?.canAttachContent = self.shouldAcquireVideoContext
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private var videoNode: UniversalVideoNode?
|
|
private let secretVideoPlaceholderBackground: ASImageNode
|
|
private let secretVideoPlaceholder: TransformImageNode
|
|
|
|
var audioTranscriptionButton: ComponentHostView<Empty>?
|
|
|
|
private var statusNode: RadialStatusNode?
|
|
private var disappearingStatusNode: RadialStatusNode?
|
|
private var playbackStatusNode: InstantVideoRadialStatusNode?
|
|
private(set) var videoFrame: CGRect?
|
|
private var imageScale: CGFloat = 1.0
|
|
|
|
private var item: ChatMessageBubbleContentItem?
|
|
private var automaticDownload: Bool?
|
|
var media: TelegramMediaFile?
|
|
var appliedForwardInfo: (Peer?, String?)?
|
|
|
|
private let fetchDisposable = MetaDisposable()
|
|
|
|
private var durationBackgroundNode: NavigationBackgroundNode?
|
|
private var durationNode: ChatInstantVideoMessageDurationNode?
|
|
let dateAndStatusNode: ChatMessageDateAndStatusNode
|
|
|
|
private let infoBackgroundNode: ASImageNode
|
|
private let muteIconNode: ASImageNode
|
|
|
|
var viaBotNode: TextNode?
|
|
var replyInfoNode: ChatMessageReplyInfoNode?
|
|
var replyBackgroundNode: NavigationBackgroundNode?
|
|
var forwardInfoNode: ChatMessageForwardInfoNode?
|
|
|
|
private var status: FileMediaResourceStatus?
|
|
private var playerStatus: MediaPlayerStatus? {
|
|
didSet {
|
|
if self.playerStatus != oldValue {
|
|
self.updateStatus(animator: nil)
|
|
}
|
|
}
|
|
}
|
|
private let playbackStatusDisposable = MetaDisposable()
|
|
private let playerStatusDisposable = MetaDisposable()
|
|
private let fetchedThumbnailDisposable = MetaDisposable()
|
|
|
|
private var shouldAcquireVideoContext: Bool {
|
|
if self.visibility && self.trackingIsInHierarchy && !self.canAttachContent {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
var visibility: Bool = false {
|
|
didSet {
|
|
if self.visibility != oldValue {
|
|
self.videoNode?.canAttachContent = self.shouldAcquireVideoContext
|
|
}
|
|
}
|
|
}
|
|
|
|
var shouldOpen: () -> Bool = { return true }
|
|
|
|
var updateTranscriptionExpanded: ((AudioTranscriptionButtonComponent.TranscriptionState) -> Void)?
|
|
var updateTranscriptionText: ((TranscribedText?) -> Void)?
|
|
|
|
var audioTranscriptionState: AudioTranscriptionButtonComponent.TranscriptionState = .collapsed
|
|
var audioTranscriptionText: TranscribedText?
|
|
private var transcribeDisposable: Disposable?
|
|
var hasExpandedAudioTranscription: Bool {
|
|
if case .expanded = audioTranscriptionState {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
private var isWaitingForCollapse: Bool = false
|
|
|
|
private var hapticFeedback: HapticFeedback?
|
|
|
|
var requestUpdateLayout: (Bool) -> Void = { _ in }
|
|
|
|
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 = { point in
|
|
return .waitForSingleTap
|
|
}
|
|
self.view.addGestureRecognizer(recognizer)
|
|
|
|
let hierarchyTrackingLayer = HierarchyTrackingLayer()
|
|
hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.trackingIsInHierarchy = true
|
|
}
|
|
|
|
hierarchyTrackingLayer.didExitHierarchy = { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.trackingIsInHierarchy = false
|
|
}
|
|
self.hierarchyTrackingLayer = hierarchyTrackingLayer
|
|
self.layer.addSublayer(hierarchyTrackingLayer)
|
|
}
|
|
|
|
func asyncLayout() -> (_ item: ChatMessageBubbleContentItem, _ width: CGFloat, _ displaySize: CGSize, _ maximumDisplaySize: CGSize, _ scaleProgress: CGFloat, _ statusType: ChatMessageInteractiveInstantVideoNodeStatusType, _ automaticDownload: Bool, _ avatarInset: CGFloat) -> (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ListViewItemUpdateAnimation) -> Void) {
|
|
let previousFile = self.media
|
|
|
|
let currentItem = self.item
|
|
let currentForwardInfo = self.appliedForwardInfo
|
|
let previousAutomaticDownload = self.automaticDownload
|
|
|
|
let makeDateAndStatusLayout = self.dateAndStatusNode.asyncLayout()
|
|
|
|
let audioTranscriptionState = self.audioTranscriptionState
|
|
let audioTranscriptionText = self.audioTranscriptionText
|
|
|
|
let viaBotLayout = TextNode.asyncLayout(self.viaBotNode)
|
|
let makeReplyInfoLayout = ChatMessageReplyInfoNode.asyncLayout(self.replyInfoNode)
|
|
let makeForwardInfoLayout = ChatMessageForwardInfoNode.asyncLayout(self.forwardInfoNode)
|
|
let currentReplyBackgroundNode = self.replyBackgroundNode
|
|
|
|
return { item, width, displaySize, maximumDisplaySize, scaleProgress, statusDisplayType, automaticDownload, avatarInset in
|
|
var secretVideoPlaceholderBackgroundImage: UIImage?
|
|
var updatedInfoBackgroundImage: UIImage?
|
|
var updatedMuteIconImage: UIImage?
|
|
|
|
var incoming = item.message.effectivelyIncoming(item.context.account.peerId)
|
|
if case .forwardedMessages = item.associatedData.subject {
|
|
incoming = false
|
|
}
|
|
|
|
var viaBotApply: (TextNodeLayout, () -> TextNode)?
|
|
var replyInfoApply: (CGSize, (Bool) -> ChatMessageReplyInfoNode)?
|
|
|
|
var updatedInstantVideoBackgroundImage: UIImage?
|
|
let instantVideoBackgroundImage: UIImage?
|
|
var ignoreHeaders = false
|
|
switch statusDisplayType {
|
|
case .free:
|
|
instantVideoBackgroundImage = nil
|
|
case .bubble:
|
|
instantVideoBackgroundImage = nil
|
|
ignoreHeaders = true
|
|
}
|
|
|
|
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
|
|
if isSecretMedia {
|
|
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 ignoreForward = false
|
|
var ignoreSource = false
|
|
|
|
if let forwardInfo = item.message.forwardInfo {
|
|
if !item.message.id.peerId.isRepliesOrSavedMessages(accountPeerId: item.context.account.peerId) {
|
|
for attribute in item.message.attributes {
|
|
if let attribute = attribute as? SourceReferenceMessageAttribute {
|
|
if attribute.messageId.peerId == forwardInfo.author?.id {
|
|
ignoreForward = true
|
|
} else {
|
|
ignoreSource = true
|
|
}
|
|
break
|
|
}
|
|
}
|
|
} else {
|
|
ignoreForward = true
|
|
}
|
|
}
|
|
|
|
let bubbleEdgeInset: CGFloat = 4.0
|
|
let bubbleContentInsetsLeft: CGFloat = 6.0
|
|
let availableWidth = max(60.0, width - 210.0 - bubbleEdgeInset * 2.0 - bubbleContentInsetsLeft - 20.0)
|
|
let availableContentWidth = width - bubbleEdgeInset * 2.0 - bubbleContentInsetsLeft - 20.0
|
|
|
|
if !ignoreHeaders {
|
|
var replyMessage: Message?
|
|
var replyStory: StoryId?
|
|
|
|
for attribute in item.message.attributes {
|
|
if let attribute = attribute as? InlineBotMessageAttribute {
|
|
var inlineBotNameString: String?
|
|
if let peerId = attribute.peerId, let bot = item.message.peers[peerId] as? TelegramUser {
|
|
inlineBotNameString = bot.addressName
|
|
} else {
|
|
inlineBotNameString = attribute.title
|
|
}
|
|
|
|
if let inlineBotNameString = inlineBotNameString {
|
|
let inlineBotNameColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText
|
|
|
|
let bodyAttributes = MarkdownAttributeSet(font: nameFont, textColor: inlineBotNameColor)
|
|
let boldAttributes = MarkdownAttributeSet(font: inlineBotPrefixFont, textColor: inlineBotNameColor)
|
|
let botString = addAttributesToStringWithRanges(item.presentationData.strings.Conversation_MessageViaUser("@\(inlineBotNameString)")._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes])
|
|
|
|
viaBotApply = viaBotLayout(TextNodeLayoutArguments(attributedString: botString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0, availableWidth), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
|
|
|
ignoreForward = true
|
|
}
|
|
}
|
|
|
|
if let replyAttribute = attribute as? ReplyMessageAttribute {
|
|
if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.messageId == replyAttribute.messageId {
|
|
} else {
|
|
replyMessage = item.message.associatedMessages[replyAttribute.messageId]
|
|
}
|
|
} else if let attribute = attribute as? ReplyStoryAttribute {
|
|
replyStory = attribute.storyId
|
|
}
|
|
}
|
|
|
|
if replyMessage != nil || replyStory != nil {
|
|
if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.messageId == replyMessage?.id {
|
|
} else {
|
|
replyInfoApply = makeReplyInfoLayout(ChatMessageReplyInfoNode.Arguments(
|
|
presentationData: item.presentationData,
|
|
strings: item.presentationData.strings,
|
|
context: item.context,
|
|
type: .standalone,
|
|
message: replyMessage,
|
|
story: replyStory,
|
|
parentMessage: item.message,
|
|
constrainedSize: CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude),
|
|
animationCache: item.controllerInteraction.presentationContext.animationCache,
|
|
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer,
|
|
associatedData: item.associatedData
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
if !ignoreSource && !ignoreHeaders, !item.message.id.peerId.isRepliesOrSavedMessages(accountPeerId: item.context.account.peerId) {
|
|
for attribute in item.message.attributes {
|
|
if let attribute = attribute as? SourceReferenceMessageAttribute {
|
|
if let sourcePeer = item.message.peers[attribute.messageId.peerId] {
|
|
let inlineBotNameColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText
|
|
|
|
let nameString = NSAttributedString(string: EnginePeer(sourcePeer).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder), font: inlineBotPrefixFont, textColor: inlineBotNameColor)
|
|
|
|
viaBotApply = viaBotLayout(TextNodeLayoutArguments(attributedString: nameString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0, availableWidth), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var forwardSource: Peer?
|
|
var forwardAuthorSignature: String?
|
|
|
|
var forwardInfoSizeApply: (CGSize, (CGFloat) -> ChatMessageForwardInfoNode)?
|
|
|
|
if !ignoreForward && !ignoreHeaders, let forwardInfo = item.message.forwardInfo {
|
|
let forwardPsaType = forwardInfo.psaType
|
|
|
|
if let source = forwardInfo.source {
|
|
forwardSource = source
|
|
if let authorSignature = forwardInfo.authorSignature {
|
|
forwardAuthorSignature = authorSignature
|
|
} else if let forwardInfoAuthor = forwardInfo.author, forwardInfoAuthor.id != source.id {
|
|
forwardAuthorSignature = EnginePeer(forwardInfoAuthor).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
|
|
} else {
|
|
forwardAuthorSignature = nil
|
|
}
|
|
} else {
|
|
if let currentForwardInfo = currentForwardInfo, forwardInfo.author == nil && currentForwardInfo.0 != nil {
|
|
forwardSource = nil
|
|
forwardAuthorSignature = currentForwardInfo.0.flatMap(EnginePeer.init)?.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
|
|
} else {
|
|
forwardSource = forwardInfo.author
|
|
forwardAuthorSignature = forwardInfo.authorSignature
|
|
}
|
|
}
|
|
let availableWidth = max(60.0, availableContentWidth - 210.0 + 6.0)
|
|
forwardInfoSizeApply = makeForwardInfoLayout(item.presentationData, item.presentationData.strings, .standalone, forwardSource, forwardAuthorSignature, forwardPsaType, nil, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude))
|
|
}
|
|
|
|
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 || item.message.id.namespace == Namespaces.Message.ScheduledLocal {
|
|
notConsumed = true
|
|
}
|
|
|
|
var updatedPlaybackStatus: Signal<FileMediaResourceStatus, NoError>?
|
|
if let updatedFile = updatedFile, updatedMedia || updatedMessageId {
|
|
updatedPlaybackStatus = combineLatest(messageFileMediaResourceStatus(context: item.context, file: updatedFile, message: EngineMessage(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 incoming {
|
|
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
|
|
var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeer: item.associatedData.accountPeer, message: item.message)
|
|
if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) {
|
|
dateReactionsAndPeers = ([], [])
|
|
}
|
|
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, associatedData: item.associatedData)
|
|
|
|
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 statusSuggestedWidthAndContinue = makeDateAndStatusLayout(ChatMessageDateAndStatusNode.Arguments(
|
|
context: item.context,
|
|
presentationData: item.presentationData,
|
|
edited: edited && !sentViaBot,
|
|
impressionCount: viewCount,
|
|
dateText: dateText,
|
|
type: statusType,
|
|
layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium, forceInline: item.associatedData.forceInlineReactions) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil),
|
|
constrainedSize: CGSize(width: max(1.0, maxDateAndStatusWidth), height: CGFloat.greatestFiniteMagnitude),
|
|
availableReactions: item.associatedData.availableReactions,
|
|
reactions: dateReactionsAndPeers.reactions,
|
|
reactionPeers: dateReactionsAndPeers.peers,
|
|
displayAllReactionPeers: item.message.id.peerId.namespace == Namespaces.Peer.CloudUser,
|
|
replyCount: dateReplies,
|
|
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread,
|
|
hasAutoremove: item.message.isSelfExpiring,
|
|
canViewReactionList: canViewMessageReactionList(message: item.message),
|
|
animationCache: item.controllerInteraction.presentationContext.animationCache,
|
|
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
|
|
))
|
|
|
|
let (dateAndStatusSize, dateAndStatusApply) = statusSuggestedWidthAndContinue.1(statusSuggestedWidthAndContinue.0)
|
|
|
|
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)))
|
|
|
|
var updatedAudioTranscriptionState: AudioTranscriptionButtonComponent.TranscriptionState?
|
|
let transcribedText = transcribedText(message: item.message)
|
|
|
|
switch audioTranscriptionState {
|
|
case .inProgress:
|
|
if transcribedText != nil {
|
|
updatedAudioTranscriptionState = .expanded
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
|
|
var updatedTranscriptionText: TranscribedText?
|
|
if audioTranscriptionText != transcribedText {
|
|
updatedTranscriptionText = transcribedText
|
|
}
|
|
|
|
let effectiveAudioTranscriptionState = updatedAudioTranscriptionState ?? audioTranscriptionState
|
|
|
|
return (result, { [weak self] layoutData, animation in
|
|
if let strongSelf = self {
|
|
strongSelf.item = item
|
|
strongSelf.videoFrame = displayVideoFrame
|
|
strongSelf.appliedForwardInfo = (forwardSource, forwardAuthorSignature)
|
|
|
|
strongSelf.automaticDownload = automaticDownload
|
|
|
|
var updatedReplyBackgroundNode: NavigationBackgroundNode?
|
|
if replyInfoApply != nil || viaBotApply != nil || forwardInfoSizeApply != nil {
|
|
if let currentReplyBackgroundNode = currentReplyBackgroundNode {
|
|
updatedReplyBackgroundNode = currentReplyBackgroundNode
|
|
} else {
|
|
updatedReplyBackgroundNode = NavigationBackgroundNode(color: selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), enableBlur: item.controllerInteraction.enableFullTranslucency && dateFillNeedsBlur(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper))
|
|
}
|
|
|
|
updatedReplyBackgroundNode?.updateColor(color: selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), enableBlur: item.controllerInteraction.enableFullTranslucency && dateFillNeedsBlur(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), transition: .immediate)
|
|
}
|
|
|
|
if let updatedAudioTranscriptionState = updatedAudioTranscriptionState {
|
|
strongSelf.audioTranscriptionState = updatedAudioTranscriptionState
|
|
strongSelf.updateTranscriptionExpanded?(strongSelf.audioTranscriptionState)
|
|
}
|
|
if let updatedTranscriptionText = updatedTranscriptionText {
|
|
strongSelf.audioTranscriptionText = updatedTranscriptionText
|
|
strongSelf.updateTranscriptionText?(strongSelf.audioTranscriptionText)
|
|
}
|
|
|
|
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))
|
|
animation.animator.updateFrame(layer: strongSelf.infoBackgroundNode.layer, frame: infoBackgroundFrame, completion: nil)
|
|
|
|
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, userLocation: .peer(item.message.id.peerId), userContentType: .video, reference: FileMediaReference.message(message: MessageReference(item.message), media: updatedFile).resourceReference(resource)).start())
|
|
} else {
|
|
strongSelf.fetchedThumbnailDisposable.set(nil)
|
|
}
|
|
}
|
|
|
|
var durationBlurColor: (UIColor, Bool)?
|
|
let durationTextColor: UIColor
|
|
switch statusDisplayType {
|
|
case .free:
|
|
let serviceColor = serviceMessageColorComponents(theme: theme.theme, wallpaper: theme.wallpaper)
|
|
durationTextColor = serviceColor.primaryText
|
|
durationBlurColor = (selectDateFillStaticColor(theme: theme.theme, wallpaper: theme.wallpaper), item.controllerInteraction.enableFullTranslucency && dateFillNeedsBlur(theme: theme.theme, wallpaper: theme.wallpaper))
|
|
case .bubble:
|
|
durationBlurColor = nil
|
|
if incoming {
|
|
durationTextColor = theme.theme.chat.message.incoming.secondaryTextColor
|
|
} else {
|
|
durationTextColor = theme.theme.chat.message.outgoing.secondaryTextColor
|
|
}
|
|
}
|
|
|
|
var previousVideoNode: UniversalVideoNode?
|
|
var updatedPlayerStatusSignal: Signal<MediaPlayerStatus?, NoError>?
|
|
if let telegramFile = updatedFile {
|
|
if updatedMedia {
|
|
previousVideoNode = strongSelf.videoNode
|
|
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), userLocation: .peer(item.message.id.peerId), fileReference: .message(message: MessageReference(item.message), media: telegramFile), streamVideo: streamVideo ? .conservative : .none, enableSound: false, fetchAutomatically: false, isAudioVideoMessage: true, captureProtected: item.message.isCopyProtected(), storeAfterDownload: nil), priority: .embedded, autoplay: item.context.sharedContext.energyUsageSettings.autoplayVideo)
|
|
if let previousVideoNode = previousVideoNode {
|
|
videoNode.bounds = previousVideoNode.bounds
|
|
videoNode.position = previousVideoNode.position
|
|
videoNode.transform = previousVideoNode.transform
|
|
}
|
|
strongSelf.videoNode = videoNode
|
|
strongSelf.insertSubnode(videoNode, belowSubnode: previousVideoNode ?? strongSelf.dateAndStatusNode)
|
|
videoNode.canAttachContent = strongSelf.shouldAcquireVideoContext
|
|
|
|
if isSecretMedia {
|
|
let updatedSecretPlaceholderSignal = chatSecretMessageVideo(account: item.context.account, userLocation: .peer(item.message.id.peerId), 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(animator: nil)
|
|
}
|
|
}))
|
|
}
|
|
|
|
if let updatedPlayerStatusSignal = updatedPlayerStatusSignal {
|
|
strongSelf.playerStatusDisposable.set((updatedPlayerStatusSignal
|
|
|> deliverOnMainQueue).start(next: { [weak self] status in
|
|
displayLinkDispatcher.dispatch {
|
|
if let strongSelf = self {
|
|
strongSelf.playerStatus = status
|
|
}
|
|
}
|
|
}))
|
|
}
|
|
|
|
var displayTranscribe: Bool
|
|
if item.message.id.peerId.namespace != Namespaces.Peer.SecretChat && statusDisplayType == .free {
|
|
if item.associatedData.isPremium {
|
|
displayTranscribe = true
|
|
} else if item.associatedData.alwaysDisplayTranscribeButton.canBeDisplayed {
|
|
if incoming && notConsumed && item.associatedData.alwaysDisplayTranscribeButton.displayForNotConsumed {
|
|
displayTranscribe = true
|
|
} else {
|
|
displayTranscribe = false
|
|
}
|
|
} else {
|
|
displayTranscribe = false
|
|
}
|
|
} else {
|
|
displayTranscribe = false
|
|
}
|
|
|
|
if displayTranscribe, let durationBlurColor = durationBlurColor {
|
|
var added = false
|
|
let audioTranscriptionButton: ComponentHostView<Empty>
|
|
if let current = strongSelf.audioTranscriptionButton {
|
|
audioTranscriptionButton = current
|
|
} else {
|
|
audioTranscriptionButton = ComponentHostView<Empty>()
|
|
strongSelf.audioTranscriptionButton = audioTranscriptionButton
|
|
strongSelf.view.addSubview(audioTranscriptionButton)
|
|
added = true
|
|
}
|
|
let audioTranscriptionButtonSize = audioTranscriptionButton.update(
|
|
transition: animation.isAnimated ? .easeInOut(duration: 0.3) : .immediate,
|
|
component: AnyComponent(AudioTranscriptionButtonComponent(
|
|
theme: .freeform(durationBlurColor, durationTextColor),
|
|
transcriptionState: effectiveAudioTranscriptionState,
|
|
pressed: {
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.transcribe()
|
|
}
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: 30.0, height: 30.0)
|
|
)
|
|
|
|
var audioTranscriptionButtonFrame: CGRect
|
|
if incoming {
|
|
audioTranscriptionButtonFrame = CGRect(origin: CGPoint(x: displayVideoFrame.maxX - 30.0, y: displayVideoFrame.maxY - 30.0), size: audioTranscriptionButtonSize)
|
|
if !scaleProgress.isZero {
|
|
audioTranscriptionButtonFrame.origin.x = displayVideoFrame.midX + 43.0
|
|
}
|
|
} else {
|
|
audioTranscriptionButtonFrame = CGRect(origin: CGPoint(x: displayVideoFrame.minX, y: displayVideoFrame.maxY - 30.0), size: audioTranscriptionButtonSize)
|
|
if !scaleProgress.isZero {
|
|
audioTranscriptionButtonFrame.origin.x = displayVideoFrame.midX - 74.0
|
|
}
|
|
}
|
|
|
|
if animation.isAnimated && added {
|
|
audioTranscriptionButton.layer.frame = audioTranscriptionButtonFrame
|
|
audioTranscriptionButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
} else {
|
|
animation.animator.updateFrame(layer: audioTranscriptionButton.layer, frame: audioTranscriptionButtonFrame, completion: nil)
|
|
}
|
|
animation.animator.updateAlpha(layer: audioTranscriptionButton.layer, alpha: scaleProgress.isZero ? 1.0 : 0.0, completion: nil)
|
|
if !scaleProgress.isZero {
|
|
displayTranscribe = false
|
|
}
|
|
} else {
|
|
if let audioTranscriptionButton = strongSelf.audioTranscriptionButton {
|
|
strongSelf.audioTranscriptionButton = nil
|
|
audioTranscriptionButton.removeFromSuperview()
|
|
}
|
|
}
|
|
|
|
if let durationNode = strongSelf.durationNode {
|
|
var durationFrame = 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.update(size: size, cornerRadius: size.height / 2.0, transition: .immediate)
|
|
|
|
if !incoming, let audioTranscriptionButton = strongSelf.audioTranscriptionButton, displayTranscribe {
|
|
durationFrame.origin.x = audioTranscriptionButton.frame.minX - 7.0
|
|
}
|
|
animation.animator.updateFrame(layer: durationNode.layer, frame: durationFrame, completion: nil)
|
|
animation.animator.updateFrame(layer: durationBackgroundNode.layer, frame: CGRect(origin: CGPoint(x: durationNode.frame.maxX - size.width, y: durationNode.frame.minY), size: size), completion: nil)
|
|
} else {
|
|
animation.animator.updateFrame(layer: durationNode.layer, frame: durationFrame, completion: nil)
|
|
}
|
|
}
|
|
|
|
dateAndStatusApply(animation)
|
|
switch layoutData {
|
|
case let .unconstrained(width):
|
|
var 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)
|
|
if !incoming, let audioTranscriptionButton = strongSelf.audioTranscriptionButton, displayTranscribe {
|
|
dateAndStatusOrigin.x = audioTranscriptionButton.frame.maxX + 7.0
|
|
}
|
|
}
|
|
animation.animator.updateFrame(layer: strongSelf.dateAndStatusNode.layer, frame: CGRect(origin: dateAndStatusOrigin, size: dateAndStatusSize), completion: nil)
|
|
case let .constrained(_, right):
|
|
var dateAndStatusFrame = 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)
|
|
if incoming, let audioTranscriptionButton = strongSelf.audioTranscriptionButton, displayTranscribe {
|
|
dateAndStatusFrame.origin.x = audioTranscriptionButton.frame.maxX + 7.0
|
|
}
|
|
animation.animator.updateFrame(layer: strongSelf.dateAndStatusNode.layer, frame: dateAndStatusFrame, completion: nil)
|
|
}
|
|
|
|
if let videoNode = strongSelf.videoNode {
|
|
videoNode.bounds = CGRect(origin: CGPoint(), size: videoFrame.size)
|
|
if strongSelf.imageScale != imageScale {
|
|
strongSelf.imageScale = imageScale
|
|
animation.animator.updateScale(layer: videoNode.layer, scale: imageScale, completion: nil)
|
|
}
|
|
animation.animator.updatePosition(layer: videoNode.layer, position: displayVideoFrame.center, completion: nil)
|
|
videoNode.updateLayout(size: arguments.boundingSize, transition: animation.transition)
|
|
|
|
if let previousVideoNode = previousVideoNode {
|
|
animation.animator.updateScale(layer: previousVideoNode.layer, scale: imageScale, completion: nil)
|
|
animation.animator.updatePosition(layer: previousVideoNode.layer, position: displayVideoFrame.center, completion: nil)
|
|
}
|
|
}
|
|
animation.animator.updateFrame(layer: strongSelf.secretVideoPlaceholderBackground.layer, frame: displayVideoFrame, completion: nil)
|
|
|
|
let placeholderFrame = videoFrame.insetBy(dx: 2.0, dy: 2.0)
|
|
strongSelf.secretVideoPlaceholder.bounds = CGRect(origin: CGPoint(), size: videoFrame.size)
|
|
animation.animator.updateScale(layer: strongSelf.secretVideoPlaceholder.layer, scale: imageScale, completion: nil)
|
|
animation.animator.updatePosition(layer: strongSelf.secretVideoPlaceholder.layer, position: displayVideoFrame.center, completion: nil)
|
|
|
|
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(animator: animation.animator)
|
|
|
|
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
|
|
}
|
|
|
|
if let updatedReplyBackgroundNode = updatedReplyBackgroundNode {
|
|
if strongSelf.replyBackgroundNode == nil {
|
|
strongSelf.replyBackgroundNode = updatedReplyBackgroundNode
|
|
strongSelf.addSubnode(updatedReplyBackgroundNode)
|
|
}
|
|
} else if let replyBackgroundNode = strongSelf.replyBackgroundNode {
|
|
replyBackgroundNode.removeFromSupernode()
|
|
strongSelf.replyBackgroundNode = nil
|
|
}
|
|
|
|
var messageInfoSize = CGSize()
|
|
if let (viaBotLayout, _) = viaBotApply, forwardInfoSizeApply == nil {
|
|
messageInfoSize = CGSize(width: viaBotLayout.size.width + 1.0, height: 0.0)
|
|
}
|
|
if let (forwardInfoSize, _) = forwardInfoSizeApply {
|
|
messageInfoSize = CGSize(width: max(messageInfoSize.width, forwardInfoSize.width + 2.0), height: 0.0)
|
|
}
|
|
if let (replyInfoSize, _) = replyInfoApply {
|
|
messageInfoSize = CGSize(width: max(messageInfoSize.width, replyInfoSize.width), height: 0.0)
|
|
}
|
|
|
|
var width = width
|
|
if !scaleProgress.isZero {
|
|
width += avatarInset
|
|
}
|
|
if let (viaBotLayout, viaBotApply) = viaBotApply, forwardInfoSizeApply == nil {
|
|
let viaBotNode = viaBotApply()
|
|
if strongSelf.viaBotNode == nil {
|
|
strongSelf.viaBotNode = viaBotNode
|
|
strongSelf.addSubnode(viaBotNode)
|
|
}
|
|
|
|
let viaBotFrame = CGRect(origin: CGPoint(x: (!incoming ? (displayVideoFrame.maxX - width + 6.0) : (width - messageInfoSize.width - bubbleEdgeInset - 9.0 + 10.0)), y: 8.0), size: viaBotLayout.size)
|
|
animation.animator.updateFrame(layer: viaBotNode.layer, frame: viaBotFrame, completion: nil)
|
|
|
|
messageInfoSize = CGSize(width: messageInfoSize.width, height: viaBotLayout.size.height)
|
|
} else if let viaBotNode = strongSelf.viaBotNode {
|
|
viaBotNode.removeFromSupernode()
|
|
strongSelf.viaBotNode = nil
|
|
}
|
|
|
|
if let (forwardInfoSize, forwardInfoApply) = forwardInfoSizeApply {
|
|
let forwardInfoNode = forwardInfoApply(forwardInfoSize.width)
|
|
if strongSelf.forwardInfoNode == nil {
|
|
strongSelf.forwardInfoNode = forwardInfoNode
|
|
strongSelf.addSubnode(forwardInfoNode)
|
|
|
|
if animation.isAnimated {
|
|
forwardInfoNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
}
|
|
}
|
|
let forwardInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (displayVideoFrame.maxX - width + 6.0) : (width - messageInfoSize.width - bubbleEdgeInset - 8.0 + 10.0)), y: 8.0 + messageInfoSize.height), size: forwardInfoSize)
|
|
animation.animator.updateFrame(layer: forwardInfoNode.layer, frame: forwardInfoFrame, completion: nil)
|
|
|
|
messageInfoSize = CGSize(width: messageInfoSize.width, height: messageInfoSize.height + forwardInfoSize.height - 1.0)
|
|
} else if let forwardInfoNode = strongSelf.forwardInfoNode {
|
|
if animation.isAnimated {
|
|
if let forwardInfoNode = strongSelf.forwardInfoNode {
|
|
strongSelf.forwardInfoNode = nil
|
|
forwardInfoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false, completion: { [weak forwardInfoNode] _ in
|
|
forwardInfoNode?.removeFromSupernode()
|
|
})
|
|
}
|
|
} else {
|
|
forwardInfoNode.removeFromSupernode()
|
|
strongSelf.forwardInfoNode = nil
|
|
}
|
|
}
|
|
|
|
if let (replyInfoSize, replyInfoApply) = replyInfoApply {
|
|
let replyInfoNode = replyInfoApply(false)
|
|
if strongSelf.replyInfoNode == nil {
|
|
strongSelf.replyInfoNode = replyInfoNode
|
|
strongSelf.addSubnode(replyInfoNode)
|
|
}
|
|
let replyInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (displayVideoFrame.maxX - width + 5.0) : (width - messageInfoSize.width - bubbleEdgeInset - 9.0 + 10.0)), y: 8.0 + messageInfoSize.height), size: replyInfoSize)
|
|
animation.animator.updateFrame(layer: replyInfoNode.layer, frame: replyInfoFrame, completion: nil)
|
|
|
|
messageInfoSize = CGSize(width: max(messageInfoSize.width, replyInfoSize.width), height: messageInfoSize.height + replyInfoSize.height)
|
|
} else if let replyInfoNode = strongSelf.replyInfoNode {
|
|
replyInfoNode.removeFromSupernode()
|
|
strongSelf.replyInfoNode = nil
|
|
}
|
|
|
|
if let replyBackgroundNode = strongSelf.replyBackgroundNode {
|
|
let replyBackgroundFrame = CGRect(origin: CGPoint(x: (!incoming ? (displayVideoFrame.maxX - width + 4.0) : (width - messageInfoSize.width - bubbleEdgeInset)) - 4.0, y: 6.0), size: CGSize(width: messageInfoSize.width + 8.0, height: messageInfoSize.height + 5.0))
|
|
animation.animator.updateFrame(layer: replyBackgroundNode.layer, frame: replyBackgroundFrame, completion: nil)
|
|
|
|
let cornerRadius = replyBackgroundNode.frame.height <= 22.0 ? replyBackgroundNode.frame.height / 2.0 : 8.0
|
|
replyBackgroundNode.update(size: replyBackgroundNode.bounds.size, cornerRadius: cornerRadius, transition: .immediate)
|
|
}
|
|
|
|
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
|
|
if let viaBotNode = strongSelf.viaBotNode {
|
|
transition.updateAlpha(node: viaBotNode, alpha: strongSelf.isPlaying ? 0.0 : 1.0)
|
|
}
|
|
if let replyBackgroundNode = strongSelf.replyBackgroundNode {
|
|
transition.updateAlpha(node: replyBackgroundNode, alpha: strongSelf.isPlaying ? 0.0 : 1.0)
|
|
}
|
|
if let forwardInfoNode = strongSelf.forwardInfoNode {
|
|
transition.updateAlpha(node: forwardInfoNode, alpha: strongSelf.isPlaying ? 0.0 : 1.0)
|
|
}
|
|
if let replyInfoNode = strongSelf.replyInfoNode {
|
|
transition.updateAlpha(node: replyInfoNode, alpha: strongSelf.isPlaying ? 0.0 : 1.0)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
private func updateStatus(animator: ControlledTransitionAnimator? = nil) {
|
|
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))
|
|
if let animator = animator {
|
|
if let statusNode = self.statusNode {
|
|
animator.updateFrame(layer: statusNode.layer, frame: statusFrame, completion: nil)
|
|
}
|
|
if let disappearingStatusNode = self.disappearingStatusNode {
|
|
animator.updateFrame(layer: disappearingStatusNode.layer, frame: statusFrame, completion: nil)
|
|
}
|
|
} else {
|
|
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 {
|
|
if let (beginTime, timeout) = secretBeginTimeAndTimeout {
|
|
state = .secretTimeout(color: messageTheme.mediaOverlayControlColors.foregroundColor, icon: .flame, beginTime: beginTime, timeout: timeout, sparks: true)
|
|
} else {
|
|
state = .staticTimeout
|
|
}
|
|
} else {
|
|
state = .none
|
|
}
|
|
case .Remote, .Paused:
|
|
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)
|
|
|
|
if let audioTranscriptionButton = self.audioTranscriptionButton {
|
|
audioTranscriptionButton.superview?.bringSubviewToFront(audioTranscriptionButton)
|
|
}
|
|
|
|
self.playbackStatusNode = playbackStatusNode
|
|
}
|
|
playbackStatusNode.frame = videoFrame.insetBy(dx: 1.5, dy: 1.5)
|
|
|
|
let status = messageFileMediaPlaybackStatus(context: item.context, file: file, message: EngineMessage(item.message), isRecentActions: item.associatedData.isRecentActions, isGlobalSearch: false, isDownloadList: 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 viaBotNode = self.viaBotNode, viaBotNode.frame.contains(location) {
|
|
if let item = self.item {
|
|
for attribute in item.message.attributes {
|
|
if let attribute = attribute as? InlineBotMessageAttribute {
|
|
var botAddressName: String?
|
|
if let peerId = attribute.peerId, let botPeer = item.message.peers[peerId], let addressName = botPeer.addressName {
|
|
botAddressName = addressName
|
|
} else {
|
|
botAddressName = attribute.title
|
|
}
|
|
|
|
if let botAddressName = botAddressName {
|
|
item.controllerInteraction.updateInputState { textInputState in
|
|
return ChatTextInputState(inputText: NSAttributedString(string: "@" + botAddressName + " "))
|
|
}
|
|
item.controllerInteraction.updateInputMode { _ in
|
|
return .text
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if let replyInfoNode = self.replyInfoNode, replyInfoNode.frame.contains(location) {
|
|
if let item = self.item {
|
|
for attribute in item.message.attributes {
|
|
if let attribute = attribute as? ReplyMessageAttribute {
|
|
item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId)
|
|
return
|
|
} else if let attribute = attribute as? ReplyStoryAttribute {
|
|
item.controllerInteraction.navigateToStory(item.message, attribute.storyId)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let forwardInfoNode = self.forwardInfoNode, forwardInfoNode.frame.contains(location) {
|
|
if let item = self.item, let forwardInfo = item.message.forwardInfo {
|
|
if let sourceMessageId = forwardInfo.sourceMessageId {
|
|
if !item.message.id.peerId.isReplies, let channel = forwardInfo.author as? TelegramChannel, channel.addressName == nil {
|
|
if case let .broadcast(info) = channel.info, info.flags.contains(.hasDiscussionGroup) {
|
|
} else if case .member = channel.participationStatus {
|
|
} else {
|
|
item.controllerInteraction.displayMessageTooltip(item.message.id, item.presentationData.strings.Conversation_PrivateChannelTooltip, forwardInfoNode, nil)
|
|
return
|
|
}
|
|
}
|
|
item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId)
|
|
return
|
|
} else if let peer = forwardInfo.source ?? forwardInfo.author {
|
|
item.controllerInteraction.openPeer(EnginePeer(peer), peer is TelegramUser ? .info : .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default)
|
|
return
|
|
} else if let _ = forwardInfo.authorSignature {
|
|
item.controllerInteraction.displayMessageTooltip(item.message.id, item.presentationData.strings.Conversation_ForwardAuthorHiddenTooltip, forwardInfoNode, nil)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
if let audioTranscriptionButton = self.audioTranscriptionButton, !audioTranscriptionButton.isHidden, audioTranscriptionButton.frame.contains(location) {
|
|
self.transcribe()
|
|
return
|
|
}
|
|
|
|
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, .secondaryTap:
|
|
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 let audioTranscriptionButton = self.audioTranscriptionButton, !audioTranscriptionButton.isHidden, audioTranscriptionButton.frame.contains(point) {
|
|
return audioTranscriptionButton
|
|
}
|
|
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 viaBotNode = self.viaBotNode, viaBotNode.frame.contains(point), !viaBotNode.alpha.isZero {
|
|
return self.view
|
|
}
|
|
if let forwardInfoNode = self.forwardInfoNode, forwardInfoNode.frame.contains(point), !forwardInfoNode.alpha.isZero {
|
|
return self.view
|
|
}
|
|
if let replyInfoNode = self.replyInfoNode, replyInfoNode.frame.contains(point), !replyInfoNode.alpha.isZero {
|
|
return self.view
|
|
}
|
|
if let statusNode = self.statusNode, statusNode.supernode != nil, !statusNode.isHidden, statusNode.frame.contains(point) {
|
|
return self.view
|
|
}
|
|
|
|
if let videoNode = self.videoNode, videoNode.view.frame.contains(point) {
|
|
return self.view
|
|
}
|
|
if !self.bounds.contains(point) {
|
|
return nil
|
|
}
|
|
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 _ = item.context.engine.messages.deleteMessagesInteractively(messageIds: [item.message.id], type: .forEveryone).start()
|
|
} else {
|
|
messageMediaFileCancelInteractiveFetch(context: item.context, messageId: item.message.id, file: file)
|
|
}
|
|
case .Remote, .Paused:
|
|
if let file = self.media {
|
|
self.fetchDisposable.set(messageMediaFileInteractiveFetched(context: item.context, message: item.message, file: file, userInitiated: true).start())
|
|
}
|
|
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, _ avatarInset: CGFloat) -> (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ListViewItemUpdateAnimation) -> ChatMessageInteractiveInstantVideoNode) {
|
|
let makeLayout = node?.asyncLayout()
|
|
return { item, width, displaySize, maximumDisplaySize, scaleProgress, statusType, automaticDownload, avatarInset in
|
|
var createdNode: ChatMessageInteractiveInstantVideoNode?
|
|
let sizeAndApplyLayout: (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ListViewItemUpdateAnimation) -> Void)
|
|
if let makeLayout = makeLayout {
|
|
sizeAndApplyLayout = makeLayout(item, width, displaySize, maximumDisplaySize, scaleProgress, statusType, automaticDownload, avatarInset)
|
|
} else {
|
|
let node = ChatMessageInteractiveInstantVideoNode()
|
|
sizeAndApplyLayout = node.asyncLayout()(item, width, displaySize, maximumDisplaySize, scaleProgress, statusType, automaticDownload, avatarInset)
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func transcribe() {
|
|
guard let item = self.item, item.message.id.namespace == Namespaces.Message.Cloud else {
|
|
return
|
|
}
|
|
|
|
guard item.associatedData.isPremium else {
|
|
if self.hapticFeedback == nil {
|
|
self.hapticFeedback = HapticFeedback()
|
|
}
|
|
self.hapticFeedback?.impact(.medium)
|
|
|
|
let presentationData = item.context.sharedContext.currentPresentationData.with { $0 }
|
|
let tipController = UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_voiceToText", scale: 0.065, colors: [:], title: nil, text: presentationData.strings.Message_AudioTranscription_SubscribeToPremium, customUndoText: presentationData.strings.Message_AudioTranscription_SubscribeToPremiumAction, timeout: nil), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { action in
|
|
if case .undo = action {
|
|
let context = item.context
|
|
var replaceImpl: ((ViewController) -> Void)?
|
|
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .voiceToText, action: {
|
|
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil)
|
|
replaceImpl?(controller)
|
|
})
|
|
replaceImpl = { [weak controller] c in
|
|
controller?.replace(with: c)
|
|
}
|
|
item.controllerInteraction.navigationController()?.pushViewController(controller, animated: true)
|
|
|
|
let _ = ApplicationSpecificNotice.incrementAudioTranscriptionSuggestion(accountManager: item.context.sharedContext.accountManager).start()
|
|
}
|
|
return false })
|
|
item.controllerInteraction.presentControllerInCurrent(tipController, nil)
|
|
return
|
|
}
|
|
|
|
var shouldBeginTranscription = false
|
|
var shouldExpandNow = false
|
|
|
|
if case .expanded = self.audioTranscriptionState {
|
|
shouldExpandNow = true
|
|
} else {
|
|
if let result = transcribedText(message: item.message) {
|
|
shouldExpandNow = true
|
|
|
|
if case let .success(_, isPending) = result {
|
|
shouldBeginTranscription = isPending
|
|
} else {
|
|
shouldBeginTranscription = true
|
|
}
|
|
} else {
|
|
shouldBeginTranscription = true
|
|
}
|
|
}
|
|
|
|
if shouldBeginTranscription {
|
|
if self.transcribeDisposable == nil {
|
|
self.audioTranscriptionState = .inProgress
|
|
self.requestUpdateLayout(true)
|
|
|
|
self.transcribeDisposable = (item.context.engine.messages.transcribeAudio(messageId: item.message.id)
|
|
|> deliverOnMainQueue).start(next: { [weak self] result in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.transcribeDisposable = nil
|
|
})
|
|
}
|
|
}
|
|
|
|
if shouldExpandNow {
|
|
switch self.audioTranscriptionState {
|
|
case .expanded:
|
|
self.audioTranscriptionState = .collapsed
|
|
self.isWaitingForCollapse = true
|
|
self.requestUpdateLayout(true)
|
|
case .collapsed:
|
|
self.audioTranscriptionState = .inProgress
|
|
self.requestUpdateLayout(true)
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
self.updateTranscriptionExpanded?(self.audioTranscriptionState)
|
|
}
|
|
|
|
func animateTo(_ node: ChatMessageInteractiveFileNode, animator: ControlledTransitionAnimator) {
|
|
let duration: Double = 0.2
|
|
|
|
node.alpha = 1.0
|
|
if node.supernode == nil {
|
|
self.supernode?.insertSubnode(node, belowSubnode: self)
|
|
}
|
|
|
|
self.alpha = 0.0
|
|
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration)
|
|
|
|
node.waveformView?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.1)
|
|
|
|
if let videoNode = self.videoNode, let targetNode = node.statusNode, let videoSnapshotView = videoNode.view.snapshotView(afterScreenUpdates: false) {
|
|
videoSnapshotView.frame = videoNode.bounds
|
|
videoNode.view.insertSubview(videoSnapshotView, at: 1)
|
|
videoSnapshotView.alpha = 0.0
|
|
videoSnapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, completion: { [weak videoSnapshotView] _ in
|
|
videoSnapshotView?.removeFromSuperview()
|
|
})
|
|
|
|
let targetFrame = targetNode.view.convert(targetNode.bounds, to: self.view)
|
|
animator.animatePosition(layer: videoNode.layer, from: videoNode.position, to: targetFrame.center, completion: { _ in
|
|
self.isHidden = true
|
|
self.canAttachContent = true
|
|
})
|
|
let targetScale = targetNode.frame.width / videoNode.bounds.width
|
|
animator.animateScale(layer: videoNode.layer, from: self.imageScale, to: targetScale, completion: nil)
|
|
|
|
animator.animatePosition(layer: self.infoBackgroundNode.layer, from: self.infoBackgroundNode.position, to: targetFrame.center.offsetBy(dx: 0.0, dy: 19.0), completion: nil)
|
|
animator.animateScale(layer: self.infoBackgroundNode.layer, from: 1.0, to: targetScale / self.imageScale, completion: nil)
|
|
self.infoBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration)
|
|
|
|
if let playbackStatusNode = self.playbackStatusNode {
|
|
animator.animatePosition(layer: playbackStatusNode.layer, from: playbackStatusNode.position, to: targetFrame.center, completion: nil)
|
|
animator.animateScale(layer: playbackStatusNode.layer, from: 1.0, to: targetScale / self.imageScale, completion: nil)
|
|
playbackStatusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration)
|
|
}
|
|
|
|
let sourceFrame = self.view.convert(videoNode.frame, to: node.view)
|
|
animator.animatePosition(layer: targetNode.layer, from: sourceFrame.center, to: targetNode.position, completion: nil)
|
|
let sourceScale = (videoNode.bounds.width * self.imageScale) / targetNode.frame.width
|
|
animator.animateScale(layer: targetNode.layer, from: sourceScale, to: 1.0, completion: nil)
|
|
targetNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
|
|
|
|
let verticalDelta = (videoNode.position.y - targetFrame.center.y) * 2.0
|
|
animator.animatePosition(layer: node.textClippingNode.layer, from: node.textClippingNode.position.offsetBy(dx: 0.0, dy: verticalDelta), to: node.textClippingNode.position, completion: nil)
|
|
node.textClippingNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
|
|
|
|
if let maskImage = generateGradientImage(size: CGSize(width: 8.0, height: 10.0), colors: [UIColor.black, UIColor.black, UIColor.clear], locations: [0.0, 0.1, 1.0], direction: .vertical) {
|
|
let textClippingFrame = node.textClippingNode.frame
|
|
|
|
let maskView = UIImageView(image: maskImage.stretchableImage(withLeftCapWidth: 0, topCapHeight: 1))
|
|
node.textClippingNode.view.mask = maskView
|
|
|
|
maskView.frame = CGRect(origin: CGPoint(), size: CGSize(width: textClippingFrame.width, height: maskImage.size.height))
|
|
animator.updateFrame(layer: maskView.layer, frame: CGRect(origin: CGPoint(), size: textClippingFrame.size), completion: { [weak maskView, weak node] _ in
|
|
maskView?.removeFromSuperview()
|
|
node?.textClippingNode.view.mask = nil
|
|
})
|
|
}
|
|
}
|
|
|
|
if let audioTranscriptionButton = self.audioTranscriptionButton, let targetAudioTranscriptionButton = node.audioTranscriptionButton {
|
|
let sourceFrame = audioTranscriptionButton.convert(audioTranscriptionButton.bounds, to: node.view)
|
|
|
|
animator.animatePosition(layer: targetAudioTranscriptionButton.layer, from: sourceFrame.center, to: targetAudioTranscriptionButton.center, completion: nil)
|
|
targetAudioTranscriptionButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
|
|
|
|
let targetFrame = targetAudioTranscriptionButton.convert(targetAudioTranscriptionButton.bounds, to: self.view)
|
|
animator.animatePosition(layer: audioTranscriptionButton.layer, from: audioTranscriptionButton.center, to: targetFrame.center, completion: nil)
|
|
audioTranscriptionButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration)
|
|
}
|
|
|
|
let sourceDateFrame = self.dateAndStatusNode.view.convert(self.dateAndStatusNode.view.bounds, to: node.view)
|
|
let targetDateFrame = node.dateAndStatusNode.view.convert(node.dateAndStatusNode.view.bounds, to: self.view)
|
|
|
|
animator.animatePosition(layer: self.dateAndStatusNode.layer, from: self.dateAndStatusNode.position, to: CGPoint(x: targetDateFrame.maxX - self.dateAndStatusNode.frame.width / 2.0 + 2.0, y: targetDateFrame.midY - 7.0), completion: nil)
|
|
animator.animatePosition(layer: node.dateAndStatusNode.layer, from: CGPoint(x: sourceDateFrame.maxX - node.dateAndStatusNode.frame.width / 2.0, y: sourceDateFrame.midY + 7.0), to: node.dateAndStatusNode.position, completion: nil)
|
|
|
|
self.dateAndStatusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration)
|
|
node.dateAndStatusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration - 0.05, delay: 0.05)
|
|
|
|
if let durationNode = self.durationNode, let durationBackgroundNode = self.durationBackgroundNode {
|
|
let sourceDurationFrame = durationNode.view.convert(durationNode.view.bounds, to: node.view)
|
|
let targetDurationFrame = node.fetchingTextNode.view.convert(node.fetchingTextNode.view.bounds, to: self.view)
|
|
|
|
let delta = CGPoint(x: targetDurationFrame.center.x - durationNode.position.x, y: targetDurationFrame.center.y - durationNode.position.y)
|
|
animator.animatePosition(layer: durationNode.layer, from: durationNode.position, to: targetDurationFrame.center, completion: nil)
|
|
animator.animatePosition(layer: durationBackgroundNode.layer, from: durationBackgroundNode.position, to: durationBackgroundNode.position.offsetBy(dx: delta.x, dy: delta.y), completion: nil)
|
|
animator.animatePosition(layer: node.fetchingTextNode.layer, from: sourceDurationFrame.center, to: node.fetchingTextNode.position, completion: nil)
|
|
|
|
durationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration)
|
|
self.durationBackgroundNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration)
|
|
|
|
node.fetchingTextNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration - 0.05, delay: 0.05)
|
|
}
|
|
|
|
if let viaBotNode = self.viaBotNode {
|
|
viaBotNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration)
|
|
}
|
|
if let replyBackgroundNode = self.replyBackgroundNode {
|
|
replyBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration)
|
|
}
|
|
if let forwardInfoNode = self.forwardInfoNode {
|
|
forwardInfoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration)
|
|
}
|
|
if let replyInfoNode = self.replyInfoNode {
|
|
replyInfoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration)
|
|
}
|
|
}
|
|
|
|
func animateFrom(_ node: ChatMessageInteractiveFileNode, animator: ControlledTransitionAnimator) {
|
|
let duration: Double = 0.2
|
|
|
|
self.alpha = 1.0
|
|
self.isHidden = false
|
|
|
|
node.alpha = 0.0
|
|
node.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, completion: { _ in
|
|
node.removeFromSupernode()
|
|
})
|
|
node.waveformView?.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration)
|
|
|
|
if let videoNode = self.videoNode, let sourceNode = node.statusNode {
|
|
videoNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
|
|
|
|
let sourceFrame = sourceNode.view.convert(sourceNode.bounds, to: self.view)
|
|
animator.animatePosition(layer: videoNode.layer, from: sourceFrame.center, to: videoNode.position, completion: nil)
|
|
let sourceScale = sourceNode.frame.width / videoNode.bounds.width
|
|
animator.animateScale(layer: videoNode.layer, from: sourceScale, to: self.imageScale, completion: nil)
|
|
|
|
animator.animatePosition(layer: self.infoBackgroundNode.layer, from: sourceFrame.center.offsetBy(dx: 0.0, dy: 19.0), to: self.infoBackgroundNode.position, completion: nil)
|
|
animator.animateScale(layer: self.infoBackgroundNode.layer, from: sourceScale / self.imageScale, to: 1.0, completion: nil)
|
|
self.infoBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
|
|
|
|
if let playbackStatusNode = self.playbackStatusNode {
|
|
animator.animatePosition(layer: playbackStatusNode.layer, from: sourceFrame.center, to: playbackStatusNode.position, completion: nil)
|
|
animator.animateScale(layer: playbackStatusNode.layer, from: sourceScale / self.imageScale, to: 1.0, completion: nil)
|
|
playbackStatusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
|
|
}
|
|
|
|
let targetFrame = self.view.convert(videoNode.frame, to: node.view)
|
|
animator.animatePosition(layer: sourceNode.layer, from: sourceNode.position, to: targetFrame.center, completion: nil)
|
|
let targetScale = (videoNode.bounds.width * self.imageScale) / sourceNode.frame.width
|
|
animator.animateScale(layer: sourceNode.layer, from: 1.0, to: targetScale, completion: nil)
|
|
sourceNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration)
|
|
|
|
let verticalDelta = (videoNode.position.y - sourceFrame.center.y) * 2.0
|
|
animator.animatePosition(layer: node.textClippingNode.layer, from: node.textClippingNode.position, to: node.textClippingNode.position.offsetBy(dx: 0.0, dy: verticalDelta), completion: nil)
|
|
node.textClippingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration)
|
|
|
|
if let maskImage = generateGradientImage(size: CGSize(width: 8.0, height: 10.0), colors: [UIColor.black, UIColor.black, UIColor.clear], locations: [0.0, 0.1, 1.0], direction: .vertical) {
|
|
let textClippingFrame = node.textClippingNode.frame
|
|
|
|
let maskView = UIImageView(image: maskImage.stretchableImage(withLeftCapWidth: 0, topCapHeight: 1))
|
|
node.textClippingNode.view.mask = maskView
|
|
|
|
maskView.frame = CGRect(origin: CGPoint(), size: textClippingFrame.size)
|
|
animator.updateFrame(layer: maskView.layer, frame: CGRect(origin: CGPoint(), size: CGSize(width: textClippingFrame.width, height: maskImage.size.height)), completion: { [weak maskView, weak node] _ in
|
|
maskView?.removeFromSuperview()
|
|
node?.textClippingNode.view.mask = nil
|
|
})
|
|
}
|
|
}
|
|
|
|
if let audioTranscriptionButton = self.audioTranscriptionButton, let sourceAudioTranscriptionButton = node.audioTranscriptionButton {
|
|
audioTranscriptionButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
|
|
|
|
let targetFrame = audioTranscriptionButton.convert(audioTranscriptionButton.bounds, to: node.view)
|
|
animator.animatePosition(layer: sourceAudioTranscriptionButton.layer, from: sourceAudioTranscriptionButton.center, to: targetFrame.center, completion: nil)
|
|
sourceAudioTranscriptionButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration)
|
|
|
|
let sourceFrame = sourceAudioTranscriptionButton.convert(sourceAudioTranscriptionButton.bounds, to: self.view)
|
|
animator.animatePosition(layer: audioTranscriptionButton.layer, from: sourceFrame.center, to: audioTranscriptionButton.center, completion: nil)
|
|
}
|
|
|
|
let sourceDateFrame = node.dateAndStatusNode.view.convert(node.dateAndStatusNode.view.bounds, to: self.view)
|
|
let targetDateFrame = self.dateAndStatusNode.view.convert(self.dateAndStatusNode.view.bounds, to: node.view)
|
|
|
|
animator.animatePosition(layer: self.dateAndStatusNode.layer, from: CGPoint(x: sourceDateFrame.maxX - self.dateAndStatusNode.frame.width / 2.0 + 2.0, y: sourceDateFrame.midY - 7.0), to: self.dateAndStatusNode.position, completion: nil)
|
|
animator.animatePosition(layer: node.dateAndStatusNode.layer, from: node.dateAndStatusNode.position, to: CGPoint(x: targetDateFrame.maxX - node.dateAndStatusNode.frame.width / 2.0, y: targetDateFrame.midY + 7.0), completion: nil)
|
|
|
|
self.dateAndStatusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
|
|
node.dateAndStatusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration)
|
|
|
|
if let durationNode = self.durationNode, let durationBackgroundNode = self.durationBackgroundNode {
|
|
let sourceDurationFrame = node.fetchingTextNode.view.convert(node.fetchingTextNode.view.bounds, to: self.view)
|
|
let targetDurationFrame = durationNode.view.convert(durationNode.view.bounds, to: node.view)
|
|
|
|
let delta = CGPoint(x: sourceDurationFrame.center.x - durationNode.position.x, y: sourceDurationFrame.center.y - durationNode.position.y)
|
|
animator.animatePosition(layer: durationNode.layer, from: sourceDurationFrame.center, to: durationNode.position, completion: nil)
|
|
animator.animatePosition(layer: durationBackgroundNode.layer, from: durationBackgroundNode.position.offsetBy(dx: delta.x, dy: delta.y), to: durationBackgroundNode.position, completion: nil)
|
|
animator.animatePosition(layer: node.fetchingTextNode.layer, from: node.fetchingTextNode.position, to: targetDurationFrame.center, completion: nil)
|
|
|
|
durationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
|
|
self.durationBackgroundNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration - 0.05, delay: 0.05)
|
|
|
|
node.fetchingTextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration)
|
|
}
|
|
|
|
if let viaBotNode = self.viaBotNode {
|
|
viaBotNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
|
|
}
|
|
if let replyBackgroundNode = self.replyBackgroundNode {
|
|
replyBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
|
|
}
|
|
if let forwardInfoNode = self.forwardInfoNode {
|
|
forwardInfoNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
|
|
}
|
|
if let replyInfoNode = self.replyInfoNode {
|
|
replyInfoNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
|
|
}
|
|
|
|
self.canAttachContent = false
|
|
}
|
|
|
|
func targetForStoryTransition(id: StoryId) -> UIView? {
|
|
guard let item = self.item else {
|
|
return nil
|
|
}
|
|
for attribute in item.message.attributes {
|
|
if let attribute = attribute as? ReplyStoryAttribute {
|
|
if attribute.storyId == id {
|
|
if let replyInfoNode = self.replyInfoNode {
|
|
return replyInfoNode.mediaTransitionView()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
}
|