import Foundation import UIKit import AsyncDisplayKit import Display import SwiftSignalKit import Postbox import TelegramCore import SyncCore import AccountContext import TelegramPresentationData import TelegramUIPreferences import TextFormat import LocalizedPeerData import UrlEscaping import PhotoResources import TelegramStringFormatting import UniversalMediaPlayer import TelegramUniversalVideoContent import GalleryUI private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, message: Message, accountPeerId: PeerId) -> NSAttributedString? { return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, message: message, accountPeerId: accountPeerId) } class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { let labelNode: TextNode let filledBackgroundNode: LinkHighlightingNode var linkHighlightingNode: LinkHighlightingNode? fileprivate var imageNode: TransformImageNode? fileprivate var videoNode: UniversalVideoNode? private var videoContent: NativeVideoContent? private var videoStartTimestamp: Double? private let fetchDisposable = MetaDisposable() required init() { self.labelNode = TextNode() self.labelNode.isUserInteractionEnabled = false self.labelNode.displaysAsynchronously = true self.filledBackgroundNode = LinkHighlightingNode(color: .clear) super.init() self.addSubnode(self.filledBackgroundNode) self.addSubnode(self.labelNode) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.fetchDisposable.dispose() } override func didLoad() { super.didLoad() } override func transitionNode(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { if let imageNode = self.imageNode, self.item?.message.id == messageId { return (imageNode, imageNode.bounds, { [weak imageNode] in return (imageNode?.view.snapshotContentTree(unhide: true), nil) }) } else { return nil } } override func updateHiddenMedia(_ media: [Media]?) -> Bool { var mediaHidden = false var currentMedia: Media? if let item = item { mediaLoop: for media in item.message.media { if let media = media as? TelegramMediaAction { switch media.action { case let .photoUpdated(image): currentMedia = image break mediaLoop default: break } } } } if let currentMedia = currentMedia, let media = media { for item in media { if item.isSemanticallyEqual(to: currentMedia) { mediaHidden = true break } } } self.imageNode?.isHidden = mediaHidden return mediaHidden } override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) { let makeLabelLayout = TextNode.asyncLayout(self.labelNode) let backgroundLayout = self.filledBackgroundNode.asyncLayout() return { item, layoutConstants, _, _, _ in let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center) return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in let attributedString = attributedServiceMessageString(theme: item.presentationData.theme, strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, message: item.message, accountPeerId: item.context.account.peerId) var image: TelegramMediaImage? for media in item.message.media { if let action = media as? TelegramMediaAction { switch action.action { case let .photoUpdated(img): image = img default: break } } } let imageSize = layoutConstants.instantVideo.dimensions let (labelLayout, apply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: attributedString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) var labelRects = labelLayout.linesRects() if labelRects.count > 1 { let sortedIndices = (0 ..< labelRects.count).sorted(by: { labelRects[$0].width > labelRects[$1].width }) for i in 0 ..< sortedIndices.count { let index = sortedIndices[i] for j in -1 ... 1 { if j != 0 && index + j >= 0 && index + j < sortedIndices.count { if abs(labelRects[index + j].width - labelRects[index].width) < 40.0 { labelRects[index + j].size.width = max(labelRects[index + j].width, labelRects[index].width) labelRects[index].size.width = labelRects[index + j].size.width } } } } } for i in 0 ..< labelRects.count { labelRects[i] = labelRects[i].insetBy(dx: -6.0, dy: floor((labelRects[i].height - 20.0) / 2.0)) labelRects[i].size.height = 20.0 labelRects[i].origin.x = floor((labelLayout.size.width - labelRects[i].width) / 2.0) } let serviceColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper) let backgroundApply = backgroundLayout(serviceColor.fill, labelRects, 10.0, 10.0, 0.0) var backgroundSize = CGSize(width: labelLayout.size.width + 8.0 + 8.0, height: labelLayout.size.height + 4.0) let layoutInsets = UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0) if let _ = image { backgroundSize.height += imageSize.height + 10 } return (backgroundSize.width, { boundingWidth in return (backgroundSize, { [weak self] animation, _ in if let strongSelf = self { strongSelf.item = item let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - imageSize.width) / 2.0), y: labelLayout.size.height + 10 + 2), size: imageSize) if let image = image { let imageNode: TransformImageNode if let current = strongSelf.imageNode { imageNode = current } else { imageNode = TransformImageNode() strongSelf.imageNode = imageNode strongSelf.insertSubnode(imageNode, at: 0) let arguments = TransformImageArguments(corners: ImageCorners(radius: imageSize.width / 2), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()) let apply = imageNode.asyncLayout()(arguments) apply() } strongSelf.fetchDisposable.set(chatMessagePhotoInteractiveFetched(context: item.context, photoReference: .message(message: MessageReference(item.message), media: image), storeToDownloadsPeerType: nil).start()) let updateImageSignal = chatMessagePhoto(postbox: item.context.account.postbox, photoReference: .message(message: MessageReference(item.message), media: image)) imageNode.setSignal(updateImageSignal) imageNode.frame = imageFrame } else if let imageNode = strongSelf.imageNode { imageNode.removeFromSupernode() strongSelf.imageNode = nil } if let image = image, let video = image.videoRepresentations.last, let id = image.id?.id { let videoFileReference = FileMediaReference.standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: image.representations, videoThumbnails: [], immediateThumbnailData: image.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [])])) let videoContent = NativeVideoContent(id: .profileVideo(id), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, autoFetchFullSizeThumbnail: true, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear) if videoContent.id != strongSelf.videoContent?.id { let mediaManager = item.context.sharedContext.mediaManager let videoNode = UniversalVideoNode(postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .secondaryOverlay) videoNode.isUserInteractionEnabled = false videoNode.ownsContentNodeUpdated = { [weak self] owns in if let strongSelf = self { strongSelf.videoNode?.isHidden = !owns } } strongSelf.videoContent = videoContent strongSelf.videoNode = videoNode videoNode.updateLayout(size: imageSize, transition: .immediate) videoNode.frame = imageFrame let maskPath = UIBezierPath(ovalIn: CGRect(origin: CGPoint(), size: imageSize)) let shape = CAShapeLayer() shape.path = maskPath.cgPath videoNode.layer.mask = shape strongSelf.addSubnode(videoNode) videoNode.canAttachContent = true if let videoStartTimestamp = video.startTimestamp { videoNode.seek(videoStartTimestamp) } else { videoNode.seek(0.0) } videoNode.play() } } else if let videoNode = strongSelf.videoNode { strongSelf.videoContent = nil strongSelf.videoNode = nil videoNode.removeFromSupernode() } let _ = apply() let _ = backgroundApply() let labelFrame = CGRect(origin: CGPoint(x: 8.0, y: image != nil ? 2 : floorToScreenPixels((backgroundSize.height - labelLayout.size.height) / 2.0) - 1.0), size: labelLayout.size) strongSelf.labelNode.frame = labelFrame strongSelf.filledBackgroundNode.frame = labelFrame.offsetBy(dx: 0.0, dy: -11.0) } }) }) }) } } override func updateTouchesAtPoint(_ point: CGPoint?) { if let item = self.item { var rects: [(CGRect, CGRect)]? let textNodeFrame = self.labelNode.frame if let point = point { if let (index, attributes) = self.labelNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY - 10.0)) { let possibleNames: [String] = [ TelegramTextAttributes.URL, TelegramTextAttributes.PeerMention, TelegramTextAttributes.PeerTextMention, TelegramTextAttributes.BotCommand, TelegramTextAttributes.Hashtag ] for name in possibleNames { if let _ = attributes[NSAttributedString.Key(rawValue: name)] { rects = self.labelNode.lineAndAttributeRects(name: name, at: index) break } } } } if let rects = rects { var mappedRects: [CGRect] = [] for i in 0 ..< rects.count { let lineRect = rects[i].0 var itemRect = rects[i].1 itemRect.origin.x = floor((textNodeFrame.size.width - lineRect.width) / 2.0) + itemRect.origin.x mappedRects.append(itemRect) } let linkHighlightingNode: LinkHighlightingNode if let current = self.linkHighlightingNode { linkHighlightingNode = current } else { let serviceColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper) linkHighlightingNode = LinkHighlightingNode(color: serviceColor.linkHighlight) linkHighlightingNode.inset = 2.5 self.linkHighlightingNode = linkHighlightingNode self.insertSubnode(linkHighlightingNode, belowSubnode: self.labelNode) } linkHighlightingNode.frame = self.labelNode.frame.offsetBy(dx: 0.0, dy: 1.5) linkHighlightingNode.updateRects(mappedRects) } else if let linkHighlightingNode = self.linkHighlightingNode { self.linkHighlightingNode = nil linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in linkHighlightingNode?.removeFromSupernode() }) } } } override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { let textNodeFrame = self.labelNode.frame if let (index, attributes) = self.labelNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY - 10.0)), gesture == .tap { if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { var concealed = true if let (attributeText, fullText) = self.labelNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText) } return .url(url: url, concealed: concealed) } else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { return .peerMention(peerMention.peerId, peerMention.mention) } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { return .textMention(peerName) } else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String { return .botCommand(botCommand) } else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { return .hashtag(hashtag.peerName, hashtag.hashtag) } } if let imageNode = imageNode, imageNode.frame.contains(point) { return .openMessage } if self.filledBackgroundNode.frame.contains(point.offsetBy(dx: 0.0, dy: -10.0)) { return .openMessage } else { return .none } } }