Swiftgram/submodules/TelegramUI/Sources/ChatMessageMediaBubbleContentNode.swift
Ilya Laktyushin bf94ea75e9 Various fixes
2023-02-06 00:55:17 +04:00

476 lines
26 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
class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
override var supportsMosaic: Bool {
return true
}
private let interactiveImageNode: ChatMessageInteractiveMediaNode
private var selectionNode: GridMessageSelectionNode?
private var highlightedState: Bool = false
private var media: Media?
private var automaticPlayback: Bool?
override var visibility: ListViewItemNodeVisibility {
didSet {
self.interactiveImageNode.visibility = self.visibility != .none
}
}
required init() {
self.interactiveImageNode = ChatMessageInteractiveMediaNode()
super.init()
self.addSubnode(self.interactiveImageNode)
self.interactiveImageNode.activateLocalContent = { [weak self] mode in
if let strongSelf = self {
if let item = strongSelf.item {
let openChatMessageMode: ChatControllerInteractionOpenMessageMode
switch mode {
case .default:
openChatMessageMode = .default
case .stream:
openChatMessageMode = .stream
case .automaticPlayback:
openChatMessageMode = .automaticPlayback
}
let _ = item.controllerInteraction.openMessage(item.message, openChatMessageMode)
}
}
}
self.interactiveImageNode.updateMessageReaction = { [weak self] message, value in
guard let strongSelf = self, let item = strongSelf.item else {
return
}
item.controllerInteraction.updateMessageReaction(message, value)
}
self.interactiveImageNode.activatePinch = { [weak self] sourceNode in
guard let strongSelf = self, let _ = strongSelf.item else {
return
}
strongSelf.item?.controllerInteraction.activateMessagePinch(sourceNode)
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override 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 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 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.controllerInteraction.automaticMediaDownloadSettings.autoplayGifs {
if case .full = automaticDownload {
automaticPlayback = true
} else {
automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil
}
} else if (telegramFile.isVideo && !telegramFile.isAnimated) && item.controllerInteraction.automaticMediaDownloadSettings.autoplayVideos {
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
if let extendedMedia = invoice.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.controllerInteraction.automaticMediaDownloadSettings.autoplayGifs {
if case .full = automaticDownload {
automaticPlayback = true
} else {
automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil
}
} else if (telegramFile.isVideo && !telegramFile.isAnimated) && item.controllerInteraction.automaticMediaDownloadSettings.autoplayVideos {
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: 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(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 dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, associatedData: item.associatedData)
let statusType: ChatMessageDateAndStatusType?
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!, 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.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)
}
} else {
strongSelf.interactiveImageNode.dateAndStatusNode.pressed = nil
}
}
})
})
})
}
}
override func transitionNode(messageId: MessageId, media: Media) -> (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 currentMedia.isSemanticallyEqual(to: media) {
return self.interactiveImageNode.transitionNode()
}
}
return nil
}
override func peekPreviewContent(at point: CGPoint) -> (Message, ChatMessagePeekPreviewContent)? {
if let message = self.item?.message, let currentMedia = self.media, !message.containsSecretMedia {
if self.interactiveImageNode.frame.contains(point), self.interactiveImageNode.isReadyForInteractivePreview() {
return (message, .media(currentMedia))
}
}
return nil
}
override 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 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 func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? {
return self.interactiveImageNode.playMediaWithSound()
}
override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
return .none
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
}
override func animateInsertionIntoBubble(_ duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
override 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 func reactionTargetView(value: MessageReaction.Reaction) -> UIView? {
if !self.interactiveImageNode.dateAndStatusNode.isHidden {
return self.interactiveImageNode.dateAndStatusNode.reactionView(value: value)
}
return nil
}
}