import Foundation import UIKit import AsyncDisplayKit import Display import SwiftSignalKit import Postbox import TelegramCore import AccountContext import TelegramPresentationData import TelegramUIPreferences import TextFormat import LocalizedPeerData import TelegramStringFormatting import WallpaperBackgroundNode import ReactionSelectionNode import PhotoResources import UniversalMediaPlayer import TelegramUniversalVideoContent import GalleryUI import Markdown import ChatMessageBubbleContentNode import ChatMessageItemCommon import ChatControllerInteraction public class ChatMessageProfilePhotoSuggestionContentNode: ChatMessageBubbleContentNode { private var mediaBackgroundContent: WallpaperBubbleBackgroundNode? private let mediaBackgroundNode: NavigationBackgroundNode private let subtitleNode: TextNode private let imageNode: TransformImageNode fileprivate var videoNode: UniversalVideoNode? private var videoContent: NativeVideoContent? private var videoStartTimestamp: Double? private let buttonNode: HighlightTrackingButtonNode private let buttonTitleNode: TextNode private var absoluteRect: (CGRect, CGSize)? private let fetchDisposable = MetaDisposable() required public init() { self.mediaBackgroundNode = NavigationBackgroundNode(color: .clear) self.mediaBackgroundNode.clipsToBounds = true self.mediaBackgroundNode.cornerRadius = 24.0 self.subtitleNode = TextNode() self.subtitleNode.isUserInteractionEnabled = false self.subtitleNode.displaysAsynchronously = false self.imageNode = TransformImageNode() self.imageNode.contentAnimations = [.subsequentUpdates] self.buttonNode = HighlightTrackingButtonNode() self.buttonNode.clipsToBounds = true self.buttonNode.cornerRadius = 17.0 self.buttonTitleNode = TextNode() self.buttonTitleNode.isUserInteractionEnabled = false self.buttonTitleNode.displaysAsynchronously = false super.init() self.addSubnode(self.mediaBackgroundNode) self.addSubnode(self.subtitleNode) self.addSubnode(self.imageNode) self.addSubnode(self.buttonNode) self.addSubnode(self.buttonTitleNode) self.buttonNode.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { strongSelf.buttonNode.layer.removeAnimation(forKey: "opacity") strongSelf.buttonNode.alpha = 0.4 strongSelf.buttonTitleNode.layer.removeAnimation(forKey: "opacity") strongSelf.buttonTitleNode.alpha = 0.4 } else { strongSelf.buttonNode.alpha = 1.0 strongSelf.buttonNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) strongSelf.buttonTitleNode.alpha = 1.0 strongSelf.buttonTitleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) } } } self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) } required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.fetchDisposable.dispose() } override public func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { if self.item?.message.id == messageId { return (self.imageNode, self.imageNode.bounds, { [weak self] in guard let strongSelf = self else { return (nil, nil) } let resultView = strongSelf.imageNode.view.snapshotContentTree(unhide: true) return (resultView, nil) }) } else { return nil } } override public 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 .suggestedProfilePhoto(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 } @objc private func buttonPressed() { guard let item = self.item else { return } let _ = item.controllerInteraction.openMessage(item.message, OpenMessageParams(mode: .default)) } override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { let makeImageLayout = self.imageNode.asyncLayout() let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode) let makeButtonTitleLayout = TextNode.asyncLayout(self.buttonTitleNode) let currentItem = self.item 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 width: CGFloat = 220.0 let imageSize = CGSize(width: 100.0, height: 100.0) let primaryTextColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText var photo: TelegramMediaImage? if let media = item.message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .suggestedProfilePhoto(image) = media.action { photo = image } var mediaUpdated = true if let photo = photo, let media = currentItem?.message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .suggestedProfilePhoto(maybeCurrentPhoto) = media.action, let currentPhoto = maybeCurrentPhoto { mediaUpdated = !photo.isSemanticallyEqual(to: currentPhoto) } let isVideo = !(photo?.videoRepresentations.isEmpty ?? true) let fromYou = item.message.author?.id == item.context.account.peerId let peerName = item.message.peers[item.message.id.peerId].flatMap { EnginePeer($0).compactDisplayTitle } ?? "" let text: String if fromYou { text = isVideo ? item.presentationData.strings.Conversation_SuggestedVideoTextYou(peerName).string : item.presentationData.strings.Conversation_SuggestedPhotoTextYou(peerName).string } else { text = isVideo ? item.presentationData.strings.Conversation_SuggestedVideoText(peerName).string : item.presentationData.strings.Conversation_SuggestedPhotoText(peerName).string } let body = MarkdownAttributeSet(font: Font.regular(13.0), textColor: primaryTextColor) let bold = MarkdownAttributeSet(font: Font.semibold(13.0), textColor: primaryTextColor) let subtitle = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center) let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: subtitle, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) let (buttonTitleLayout, buttonTitleApply) = makeButtonTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: isVideo ? item.presentationData.strings.Conversation_SuggestedVideoView : item.presentationData.strings.Conversation_SuggestedPhotoView, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) let backgroundSize = CGSize(width: width, height: subtitleLayout.size.height + 182.0) return (backgroundSize.width, { boundingWidth in return (backgroundSize, { [weak self] animation, synchronousLoads, _ in if let strongSelf = self { strongSelf.item = item let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - imageSize.width) / 2.0), y: 13.0), size: imageSize) if let photo = photo { if mediaUpdated { strongSelf.fetchDisposable.set(chatMessagePhotoInteractiveFetched(context: item.context, userLocation: .peer(item.message.id.peerId), photoReference: .message(message: MessageReference(item.message), media: photo), displayAtSize: nil, storeToDownloadsPeerId: nil).startStrict()) } let updateImageSignal = chatMessagePhoto(postbox: item.context.account.postbox, userLocation: .peer(item.message.id.peerId), photoReference: .message(message: MessageReference(item.message), media: photo), synchronousLoad: synchronousLoads) strongSelf.imageNode.setSignal(updateImageSignal, attemptSynchronously: synchronousLoads) let arguments = TransformImageArguments(corners: ImageCorners(radius: imageSize.width / 2.0), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()) let apply = makeImageLayout(arguments) apply() strongSelf.imageNode.frame = imageFrame } if let photo = photo, let video = photo.videoRepresentations.last, let id = photo.id?.id { let videoFileReference = FileMediaReference.message(message: MessageReference(item.message), media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: photo.representations, videoThumbnails: [], immediateThumbnailData: photo.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [], preloadSize: nil)])) let videoContent = NativeVideoContent(id: .profileVideo(id, "action"), userLocation: .peer(item.message.id.peerId), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, storeAfterDownload: nil) 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 videoNode.clipsToBounds = true videoNode.cornerRadius = imageFrame.width / 2.0 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 mediaBackgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - width) / 2.0), y: 0.0), size: backgroundSize) strongSelf.mediaBackgroundNode.frame = mediaBackgroundFrame strongSelf.mediaBackgroundNode.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) strongSelf.mediaBackgroundNode.update(size: mediaBackgroundFrame.size, transition: .immediate) strongSelf.buttonNode.backgroundColor = item.presentationData.theme.theme.overallDarkAppearance ? UIColor(rgb: 0xffffff, alpha: 0.12) : UIColor(rgb: 0x000000, alpha: 0.12) let _ = subtitleApply() let _ = buttonTitleApply() let subtitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: mediaBackgroundFrame.minY + 127.0), size: subtitleLayout.size) strongSelf.subtitleNode.frame = subtitleFrame let buttonTitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonTitleLayout.size.width) / 2.0), y: subtitleFrame.maxY + 18.0), size: buttonTitleLayout.size) strongSelf.buttonTitleNode.frame = buttonTitleFrame let buttonSize = CGSize(width: buttonTitleLayout.size.width + 38.0, height: 34.0) strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonSize.width) / 2.0), y: subtitleFrame.maxY + 10.0), size: buttonSize) if item.controllerInteraction.presentationContext.backgroundNode?.hasExtraBubbleBackground() == true { if strongSelf.mediaBackgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { strongSelf.mediaBackgroundNode.isHidden = true backgroundContent.clipsToBounds = true backgroundContent.allowsGroupOpacity = true backgroundContent.cornerRadius = 24.0 strongSelf.mediaBackgroundContent = backgroundContent strongSelf.insertSubnode(backgroundContent, at: 0) } strongSelf.mediaBackgroundContent?.frame = mediaBackgroundFrame } else { strongSelf.mediaBackgroundNode.isHidden = false strongSelf.mediaBackgroundContent?.removeFromSupernode() strongSelf.mediaBackgroundContent = nil } if let (rect, size) = strongSelf.absoluteRect { strongSelf.updateAbsoluteRect(rect, within: size) } } }) }) }) } } override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { self.absoluteRect = (rect, containerSize) if let mediaBackgroundContent = self.mediaBackgroundContent { var backgroundFrame = mediaBackgroundContent.frame backgroundFrame.origin.x += rect.minX backgroundFrame.origin.y += rect.minY mediaBackgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate) } } override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { if self.mediaBackgroundNode.frame.contains(point) { return ChatMessageBubbleContentTapAction(content: .openMessage) } else { return ChatMessageBubbleContentTapAction(content: .none) } } }