2024-06-22 00:05:21 +04:00

560 lines
31 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramUIPreferences
import TelegramPresentationData
import AccountContext
import GridMessageSelectionNode
import ChatControllerInteraction
import ChatMessageDateAndStatusNode
import ChatMessageBubbleContentNode
import ChatMessageItemCommon
import ChatMessageInteractiveMediaNode
import ChatControllerInteraction
import InvisibleInkDustNode
public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
override public var supportsMosaic: Bool {
return true
}
private let interactiveImageNode: ChatMessageInteractiveMediaNode
private var selectionNode: GridMessageSelectionNode?
private var highlightedState: Bool = false
private var media: Media?
private var mediaIndex: Int?
private var automaticPlayback: Bool?
override public var visibility: ListViewItemNodeVisibility {
didSet {
self.interactiveImageNode.visibility = self.visibility != .none
}
}
required public init() {
self.interactiveImageNode = ChatMessageInteractiveMediaNode()
super.init()
self.addSubnode(self.interactiveImageNode)
self.interactiveImageNode.activateLocalContent = { [weak self] mode in
guard let self, let item = self.item else {
return
}
let openChatMessageMode: ChatControllerInteractionOpenMessageMode
switch mode {
case .default:
openChatMessageMode = .default
case .stream:
openChatMessageMode = .stream
case .automaticPlayback:
openChatMessageMode = .automaticPlayback
}
let _ = item.controllerInteraction.openMessage(item.message, OpenMessageParams(mode: openChatMessageMode, mediaIndex: self.mediaIndex))
}
self.interactiveImageNode.activateAgeRestrictedMedia = { [weak self] in
guard let self, let item = self.item else {
return
}
let _ = item.controllerInteraction.openAgeRestrictedMessageMedia(item.message, { [weak self] in
self?.interactiveImageNode.reveal()
})
}
self.interactiveImageNode.updateMessageReaction = { [weak self] message, value, force, sourceView in
guard let strongSelf = self, let item = strongSelf.item else {
return
}
item.controllerInteraction.updateMessageReaction(message, value, force, sourceView)
}
self.interactiveImageNode.activatePinch = { [weak self] sourceNode in
guard let strongSelf = self, let _ = strongSelf.item else {
return
}
strongSelf.item?.controllerInteraction.activateMessagePinch(sourceNode)
}
self.interactiveImageNode.playMessageEffect = { [weak self] message in
guard let strongSelf = self, let _ = strongSelf.item else {
return
}
strongSelf.item?.controllerInteraction.playMessageEffect(message)
}
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
let interactiveImageLayout = self.interactiveImageNode.asyncLayout()
return { item, layoutConstants, preparePosition, selection, constrainedSize, _ in
var selectedMedia: Media?
var selectedMediaIndex: Int?
var extendedMedia: TelegramExtendedMedia?
var automaticDownload: InteractiveMediaNodeAutodownloadMode = .none
var automaticPlayback: Bool = false
var contentMode: InteractiveMediaNodeContentMode = .aspectFit
if let updatingMedia = item.attributes.updatingMedia, case let .update(mediaReference) = updatingMedia.media {
selectedMedia = mediaReference.media
}
if selectedMedia == nil {
for media in item.message.media {
if let telegramImage = media as? TelegramMediaImage {
selectedMedia = telegramImage
if shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: telegramImage) {
automaticDownload = .full
}
} else if let telegramStory = media as? TelegramMediaStory {
selectedMedia = telegramStory
if let storyMedia = item.message.associatedStories[telegramStory.storyId], case let .item(storyItem) = storyMedia.get(Stories.StoredItem.self), let media = storyItem.media {
if let telegramImage = media as? TelegramMediaImage {
if shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: telegramImage) {
automaticDownload = .full
}
} else if let telegramFile = media as? TelegramMediaFile {
if shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: telegramFile) {
automaticDownload = .full
} else if shouldPredownloadMedia(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, media: telegramFile) {
automaticDownload = .prefetch
}
if !item.message.containsSecretMedia {
if telegramFile.isAnimated && item.context.sharedContext.energyUsageSettings.autoplayGif {
if case .full = automaticDownload {
automaticPlayback = true
} else {
automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil
}
} else if (telegramFile.isVideo && !telegramFile.isAnimated) && item.context.sharedContext.energyUsageSettings.autoplayVideo {
if case .full = automaticDownload {
automaticPlayback = true
} else {
automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil
}
}
}
contentMode = .aspectFill
}
}
} else if let telegramFile = media as? TelegramMediaFile {
selectedMedia = telegramFile
if shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: telegramFile) {
automaticDownload = .full
} else if shouldPredownloadMedia(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, media: telegramFile) {
automaticDownload = .prefetch
}
if !item.message.containsSecretMedia {
if telegramFile.isAnimated && item.context.sharedContext.energyUsageSettings.autoplayGif {
if case .full = automaticDownload {
automaticPlayback = true
} else {
automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil
}
} else if (telegramFile.isVideo && !telegramFile.isAnimated) && item.context.sharedContext.energyUsageSettings.autoplayVideo {
if case .full = automaticDownload {
automaticPlayback = true
} else {
automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil
}
}
}
contentMode = .aspectFill
} else if let invoice = media as? TelegramMediaInvoice {
selectedMedia = invoice
extendedMedia = invoice.extendedMedia
}
else if let paidContent = media as? TelegramMediaPaidContent {
selectedMedia = paidContent
if case let .mosaic(_, _, index) = preparePosition, let index {
extendedMedia = paidContent.extendedMedia[index]
selectedMediaIndex = index
} else {
extendedMedia = paidContent.extendedMedia.first
}
}
}
}
let _ = selectedMediaIndex
if let extendedMedia, case let .full(media) = extendedMedia {
if let telegramImage = media as? TelegramMediaImage {
if shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: telegramImage) {
automaticDownload = .full
}
} else if let telegramFile = media as? TelegramMediaFile {
if shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: telegramFile) {
automaticDownload = .full
} else if shouldPredownloadMedia(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, media: telegramFile) {
automaticDownload = .prefetch
}
if !item.message.containsSecretMedia {
if telegramFile.isAnimated && item.context.sharedContext.energyUsageSettings.autoplayGif {
if case .full = automaticDownload {
automaticPlayback = true
} else {
automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil
}
} else if (telegramFile.isVideo && !telegramFile.isAnimated) && item.context.sharedContext.energyUsageSettings.autoplayVideo {
if case .full = automaticDownload {
automaticPlayback = true
} else {
automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil
}
}
}
contentMode = .aspectFill
}
}
var hasReplyMarkup: Bool = false
for attribute in item.message.attributes {
if let attribute = attribute as? ReplyMarkupMessageAttribute, attribute.flags.contains(.inline), !attribute.rows.isEmpty {
var isExtendedMedia = false
for media in item.message.media {
if let invoice = media as? TelegramMediaInvoice, let _ = invoice.extendedMedia {
isExtendedMedia = true
break
}
}
if isExtendedMedia {
var updatedRows: [ReplyMarkupRow] = []
for row in attribute.rows {
let updatedButtons = row.buttons.filter { button in
if case .payment = button.action {
return false
} else {
return true
}
}
if !updatedButtons.isEmpty {
updatedRows.append(ReplyMarkupRow(buttons: updatedButtons))
}
}
if !updatedRows.isEmpty {
hasReplyMarkup = true
}
} else {
hasReplyMarkup = true
}
break
}
}
let bubbleInsets: UIEdgeInsets
let sizeCalculation: InteractiveMediaNodeSizeCalculation
switch preparePosition {
case .linear:
if case .color = item.presentationData.theme.wallpaper {
let colors: PresentationThemeBubbleColorComponents
if item.message.effectivelyIncoming(item.context.account.peerId) {
colors = item.presentationData.theme.theme.chat.message.incoming.bubble.withoutWallpaper
} else {
colors = item.presentationData.theme.theme.chat.message.outgoing.bubble.withoutWallpaper
}
if colors.fill[0] == colors.stroke || colors.stroke.alpha.isZero {
bubbleInsets = UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0)
} else {
bubbleInsets = layoutConstants.bubble.strokeInsets
}
} else {
bubbleInsets = layoutConstants.image.bubbleInsets
}
sizeCalculation = .constrained(CGSize(width: max(0.0, constrainedSize.width - bubbleInsets.left - bubbleInsets.right), height: constrainedSize.height))
case .mosaic:
bubbleInsets = UIEdgeInsets()
sizeCalculation = .unconstrained
}
var edited = false
if item.attributes.updatingMedia != nil {
edited = true
}
var viewCount: Int?
var dateReplies = 0
var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, 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 {
if case .mosaic = preparePosition {
} else {
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 dateFormat: MessageTimestampStatusFormat
if item.presentationData.isPreview {
dateFormat = .full
} else {
dateFormat = .regular
}
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: dateFormat, associatedData: item.associatedData)
let statusType: ChatMessageDateAndStatusType?
if case .customChatContents = item.associatedData.subject {
statusType = nil
} else if item.message.timestamp == 0 {
statusType = nil
} else {
switch preparePosition {
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
if item.message.effectivelyIncoming(item.context.account.peerId) {
statusType = .ImageIncoming
} else {
if item.message.flags.contains(.Failed) {
statusType = .ImageOutgoing(.Failed)
} else if (item.message.flags.isSending && !item.message.isSentOrAcknowledged) || item.attributes.updatingMedia != nil {
statusType = .ImageOutgoing(.Sending)
} else {
statusType = .ImageOutgoing(.Sent(read: item.read))
}
}
case .mosaic:
statusType = nil
default:
statusType = nil
}
}
var isReplyThread = false
if case .replyThread = item.chatLocation {
isReplyThread = true
}
let dateAndStatus = statusType.flatMap { statusType -> ChatMessageDateAndStatus in
ChatMessageDateAndStatus(
type: statusType,
edited: edited,
viewCount: viewCount,
dateReactions: dateReactionsAndPeers.reactions,
dateReactionPeers: dateReactionsAndPeers.peers,
dateReplies: dateReplies,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread,
dateText: dateText
)
}
let (unboundSize, initialWidth, refineLayout) = interactiveImageLayout(item.context, item.presentationData, item.presentationData.dateTimeFormat, item.message, item.associatedData, item.attributes, selectedMedia!, selectedMediaIndex, dateAndStatus, automaticDownload, item.associatedData.automaticDownloadPeerType, item.associatedData.automaticDownloadPeerId, sizeCalculation, layoutConstants, contentMode, item.controllerInteraction.presentationContext)
let forceFullCorners = false
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 7.0, hidesBackground: .emptyWallpaper, forceFullCorners: forceFullCorners, forceAlignment: .none)
return (contentProperties, unboundSize, initialWidth + bubbleInsets.left + bubbleInsets.right, { constrainedSize, position in
var wideLayout = true
if case let .mosaic(_, wide) = position {
wideLayout = wide
automaticPlayback = automaticPlayback && wide
}
var updatedPosition: ChatMessageBubbleContentPosition = position
if forceFullCorners, case .linear = updatedPosition {
updatedPosition = .linear(top: .None(.None(.None)), bottom: .None(.None(.None)))
} else if hasReplyMarkup, case let .linear(top, _) = updatedPosition {
updatedPosition = .linear(top: top, bottom: .BubbleNeighbour)
}
let imageCorners = chatMessageBubbleImageContentCorners(relativeContentPosition: updatedPosition, normalRadius: layoutConstants.image.defaultCornerRadius, mergedRadius: layoutConstants.image.mergedCornerRadius, mergedWithAnotherContentRadius: layoutConstants.image.contentMergedCornerRadius, layoutConstants: layoutConstants, chatPresentationData: item.presentationData)
let (refinedWidth, finishLayout) = refineLayout(CGSize(width: constrainedSize.width - bubbleInsets.left - bubbleInsets.right, height: constrainedSize.height), automaticPlayback, wideLayout, imageCorners)
return (refinedWidth + bubbleInsets.left + bubbleInsets.right, { boundingWidth in
let (imageSize, imageApply) = finishLayout(boundingWidth - bubbleInsets.left - bubbleInsets.right)
let imageLayoutSize = CGSize(width: imageSize.width + bubbleInsets.left + bubbleInsets.right, height: imageSize.height + bubbleInsets.top + bubbleInsets.bottom)
let layoutWidth = imageLayoutSize.width
let layoutSize = CGSize(width: layoutWidth, height: imageLayoutSize.height)
return (layoutSize, { [weak self] animation, synchronousLoads, _ in
if let strongSelf = self {
strongSelf.item = item
strongSelf.media = selectedMedia
strongSelf.mediaIndex = selectedMediaIndex
strongSelf.automaticPlayback = automaticPlayback
let imageFrame = CGRect(origin: CGPoint(x: bubbleInsets.left, y: bubbleInsets.top), size: imageSize)
animation.animator.updateFrame(layer: strongSelf.interactiveImageNode.layer, frame: imageFrame, completion: nil)
imageApply(animation, synchronousLoads)
if let selection = selection {
if let selectionNode = strongSelf.selectionNode {
selectionNode.frame = imageFrame
selectionNode.updateSelected(selection, animated: animation.isAnimated)
} else {
let selectionNode = GridMessageSelectionNode(theme: item.presentationData.theme.theme, toggle: { value in
item.controllerInteraction.toggleMessagesSelection([item.message.id], value)
})
strongSelf.selectionNode = selectionNode
strongSelf.addSubnode(selectionNode)
selectionNode.frame = imageFrame
selectionNode.updateSelected(selection, animated: false)
if animation.isAnimated {
selectionNode.animateIn()
}
}
} else if let selectionNode = strongSelf.selectionNode {
strongSelf.selectionNode = nil
if animation.isAnimated {
selectionNode.animateOut(completion: { [weak selectionNode] in
selectionNode?.removeFromSupernode()
})
} else {
selectionNode.removeFromSupernode()
}
}
if let forwardInfo = item.message.forwardInfo, forwardInfo.flags.contains(.isImported) {
strongSelf.interactiveImageNode.dateAndStatusNode.pressed = {
guard let strongSelf = self else {
return
}
item.controllerInteraction.displayImportedMessageTooltip(strongSelf.interactiveImageNode.dateAndStatusNode)
}
}
}
})
})
})
}
}
override public func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
if self.item?.message.id == messageId, var currentMedia = self.media {
if let invoice = currentMedia as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia {
currentMedia = fullMedia
}
if let paidContent = currentMedia as? TelegramMediaPaidContent, case let .full(fullMedia) = paidContent.extendedMedia[self.mediaIndex ?? 0] {
currentMedia = fullMedia
}
if currentMedia.isSemanticallyEqual(to: media) {
return self.interactiveImageNode.transitionNode(adjustRect: adjustRect)
}
}
return nil
}
override public func updateHiddenMedia(_ media: [Media]?) -> Bool {
var mediaHidden = false
var currentMedia = self.media
if let invoice = currentMedia as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia {
currentMedia = fullMedia
}
if let paidContent = currentMedia as? TelegramMediaPaidContent, case let .full(fullMedia) = paidContent.extendedMedia[self.mediaIndex ?? 0] {
currentMedia = fullMedia
}
if let currentMedia = currentMedia, let media = media {
for item in media {
if item.isSemanticallyEqual(to: currentMedia) {
mediaHidden = true
break
}
}
}
self.interactiveImageNode.isHidden = mediaHidden
self.interactiveImageNode.updateIsHidden(mediaHidden)
/*if let automaticPlayback = self.automaticPlayback {
if !automaticPlayback {
self.dateAndStatusNode.isHidden = false
} else if self.dateAndStatusNode.isHidden != mediaHidden {
if mediaHidden {
self.dateAndStatusNode.isHidden = true
} else {
self.dateAndStatusNode.isHidden = false
self.dateAndStatusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
}*/
return mediaHidden
}
override public func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? {
return self.interactiveImageNode.playMediaWithSound()
}
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
if self.interactiveImageNode.ignoreTapActionAtPoint(point) {
return ChatMessageBubbleContentTapAction(content: .ignore)
}
return ChatMessageBubbleContentTapAction(content: .none)
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
override public func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
}
override public func animateInsertionIntoBubble(_ duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
override public func updateHighlightedState(animated: Bool) -> Bool {
guard let item = self.item else {
return false
}
let highlighted = item.controllerInteraction.highlightedState?.messageStableId == item.message.stableId
if self.highlightedState != highlighted {
self.highlightedState = highlighted
if highlighted {
self.interactiveImageNode.setOverlayColor(item.presentationData.theme.theme.chat.message.mediaHighlightOverlayColor, animated: false)
} else {
self.interactiveImageNode.setOverlayColor(nil, animated: animated)
}
}
return false
}
override public func reactionTargetView(value: MessageReaction.Reaction) -> UIView? {
if !self.interactiveImageNode.dateAndStatusNode.isHidden {
return self.interactiveImageNode.dateAndStatusNode.reactionView(value: value)
}
return nil
}
override public func messageEffectTargetView() -> UIView? {
if !self.interactiveImageNode.dateAndStatusNode.isHidden {
return self.interactiveImageNode.dateAndStatusNode.messageEffectTargetView()
}
return nil
}
}