import Foundation import UIKit import AsyncDisplayKit import Display import SwiftSignalKit import Postbox import TelegramCore import TelegramPresentationData import TextFormat import AccountContext import StickerResources import ContextUI import Markdown import ShimmerEffect import WallpaperBackgroundNode private let nameFont = Font.medium(14.0) private let inlineBotPrefixFont = Font.regular(14.0) private let inlineBotNameFont = nameFont class ChatMessageStickerItemNode: ChatMessageItemView { let contextSourceNode: ContextExtractedContentContainingNode private let containerNode: ContextControllerSourceNode let imageNode: TransformImageNode private var backgroundNode: WallpaperBackgroundNode.BubbleBackgroundNode? private var placeholderNode: StickerShimmerEffectNode var textNode: TextNode? private var swipeToReplyNode: ChatMessageSwipeToReplyNode? private var swipeToReplyFeedback: HapticFeedback? private var selectionNode: ChatMessageSelectionNode? private var deliveryFailedNode: ChatMessageDeliveryFailedNode? private var shareButtonNode: ChatMessageShareButton? var telegramFile: TelegramMediaFile? private let fetchDisposable = MetaDisposable() private var forwardInfoNode: ChatMessageForwardInfoNode? private var forwardBackgroundNode: NavigationBackgroundNode? private var viaBotNode: TextNode? private let dateAndStatusNode: ChatMessageDateAndStatusNode private var replyInfoNode: ChatMessageReplyInfoNode? private var replyBackgroundNode: NavigationBackgroundNode? private var actionButtonsNode: ChatMessageActionButtonsNode? private let messageAccessibilityArea: AccessibilityAreaNode private var highlightedState: Bool = false private var currentSwipeToReplyTranslation: CGFloat = 0.0 private var currentSwipeAction: ChatControllerInteractionSwipeAction? private var appliedForwardInfo: (Peer?, String?)? private var enableSynchronousImageApply: Bool = false required init() { self.contextSourceNode = ContextExtractedContentContainingNode() self.containerNode = ContextControllerSourceNode() self.imageNode = TransformImageNode() self.placeholderNode = StickerShimmerEffectNode() self.placeholderNode.isUserInteractionEnabled = false self.dateAndStatusNode = ChatMessageDateAndStatusNode() self.messageAccessibilityArea = AccessibilityAreaNode() super.init(layerBacked: false) var firstTime = true self.imageNode.imageUpdated = { [weak self] image in guard let strongSelf = self else { return } if image != nil { if firstTime && !strongSelf.placeholderNode.isEmpty { if strongSelf.enableSynchronousImageApply { strongSelf.removePlaceholder(animated: false) } else { strongSelf.imageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { [weak self] _ in self?.removePlaceholder(animated: false) }) } } else { strongSelf.removePlaceholder(animated: true) } firstTime = false } } self.containerNode.shouldBegin = { [weak self] location in guard let strongSelf = self else { return false } if !strongSelf.imageNode.frame.contains(location) { return false } if let action = strongSelf.gestureRecognized(gesture: .tap, location: location, recognizer: nil) { if case .action = action { return false } } if let action = strongSelf.gestureRecognized(gesture: .longTap, location: location, recognizer: nil) { switch action { case .action, .optionalAction: return false case .openContextMenu: return true } } return true } self.containerNode.activated = { [weak self] gesture, location in guard let strongSelf = self, let item = strongSelf.item else { return } if let action = strongSelf.gestureRecognized(gesture: .longTap, location: location, recognizer: nil) { switch action { case .action, .optionalAction: break case let .openContextMenu(tapMessage, selectAll, subFrame): item.controllerInteraction.openMessageContextMenu(tapMessage, selectAll, strongSelf, subFrame, gesture) } } } self.imageNode.displaysAsynchronously = false self.containerNode.addSubnode(self.contextSourceNode) self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode self.addSubnode(self.containerNode) self.contextSourceNode.contentNode.addSubnode(self.placeholderNode) self.contextSourceNode.contentNode.addSubnode(self.imageNode) self.contextSourceNode.contentNode.addSubnode(self.dateAndStatusNode) self.addSubnode(self.messageAccessibilityArea) self.messageAccessibilityArea.focused = { [weak self] in self?.accessibilityElementDidBecomeFocused() } } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.fetchDisposable.dispose() } private func removePlaceholder(animated: Bool) { if !animated { self.placeholderNode.removeFromSupernode() } else { self.placeholderNode.alpha = 0.0 self.placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in self?.placeholderNode.removeFromSupernode() }) } } override func didLoad() { super.didLoad() let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) recognizer.tapActionAtPoint = { [weak self] point in if let strongSelf = self { if let shareButtonNode = strongSelf.shareButtonNode, shareButtonNode.frame.contains(point) { return .fail } if let item = strongSelf.item, item.presentationData.largeEmoji && messageIsElligibleForLargeEmoji(item.message) { if strongSelf.imageNode.frame.contains(point) { return .waitForDoubleTap } } } return .waitForSingleTap } recognizer.longTap = { [weak self] point, recognizer in guard let strongSelf = self else { return } if let action = strongSelf.gestureRecognized(gesture: .longTap, location: point, recognizer: recognizer) { switch action { case let .action(f): f() recognizer.cancel() case let .optionalAction(f): f() recognizer.cancel() case .openContextMenu: break } } } self.view.addGestureRecognizer(recognizer) let replyRecognizer = ChatSwipeToReplyRecognizer(target: self, action: #selector(self.swipeToReplyGesture(_:))) replyRecognizer.shouldBegin = { [weak self] in if let strongSelf = self, let item = strongSelf.item { if strongSelf.selectionNode != nil { return false } let action = item.controllerInteraction.canSetupReply(item.message) strongSelf.currentSwipeAction = action if case .none = action { return false } else { return true } } return false } self.view.addGestureRecognizer(replyRecognizer) } override func setupItem(_ item: ChatMessageItem, synchronousLoad: Bool) { super.setupItem(item, synchronousLoad: synchronousLoad) for media in item.message.media { if let telegramFile = media as? TelegramMediaFile { if self.telegramFile != telegramFile { let signal = chatMessageSticker(account: item.context.account, file: telegramFile, small: false, onlyFullSize: self.telegramFile != nil, synchronousLoad: synchronousLoad) self.telegramFile = telegramFile self.imageNode.setSignal(signal, attemptSynchronously: synchronousLoad) self.fetchDisposable.set(freeMediaFileInteractiveFetched(account: item.context.account, fileReference: .message(message: MessageReference(item.message), media: telegramFile)).start()) } break } } if self.telegramFile == nil && item.presentationData.largeEmoji && messageIsElligibleForLargeEmoji(item.message) { self.imageNode.setSignal(largeEmoji(postbox: item.context.account.postbox, emoji: item.message.text)) } } private var absoluteRect: (CGRect, CGSize)? override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { self.absoluteRect = (rect, containerSize) if !self.contextSourceNode.isExtractedToContextPreview { var rect = rect rect.origin.y = containerSize.height - rect.maxY + self.insets.top self.placeholderNode.updateAbsoluteRect(CGRect(origin: CGPoint(x: rect.minX + placeholderNode.frame.minX, y: rect.minY + placeholderNode.frame.minY), size: placeholderNode.frame.size), within: containerSize) if let backgroundNode = self.backgroundNode { backgroundNode.update(rect: CGRect(origin: CGPoint(x: rect.minX + self.placeholderNode.frame.minX, y: rect.minY + self.placeholderNode.frame.minY), size: self.placeholderNode.frame.size), within: containerSize) } } } override func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { if let backgroundNode = self.backgroundNode { backgroundNode.offset(value: value, animationCurve: animationCurve, duration: duration) } } override func updateAccessibilityData(_ accessibilityData: ChatMessageAccessibilityData) { super.updateAccessibilityData(accessibilityData) self.messageAccessibilityArea.accessibilityLabel = accessibilityData.label self.messageAccessibilityArea.accessibilityValue = accessibilityData.value self.messageAccessibilityArea.accessibilityHint = accessibilityData.hint self.messageAccessibilityArea.accessibilityTraits = accessibilityData.traits if let customActions = accessibilityData.customActions { self.messageAccessibilityArea.accessibilityCustomActions = customActions.map({ action -> UIAccessibilityCustomAction in return ChatMessageAccessibilityCustomAction(name: action.name, target: self, selector: #selector(self.performLocalAccessibilityCustomAction(_:)), action: action.action) }) } else { self.messageAccessibilityArea.accessibilityCustomActions = nil } } @objc private func performLocalAccessibilityCustomAction(_ action: UIAccessibilityCustomAction) { if let action = action as? ChatMessageAccessibilityCustomAction { switch action.action { case .reply: if let item = self.item { item.controllerInteraction.setupReply(item.message.id) } case .options: if let item = self.item { item.controllerInteraction.openMessageContextMenu(item.message, false, self, self.imageNode.frame, nil) } } } } override func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { let displaySize = CGSize(width: 184.0, height: 184.0) let telegramFile = self.telegramFile let layoutConstants = self.layoutConstants let imageLayout = self.imageNode.asyncLayout() let makeDateAndStatusLayout = self.dateAndStatusNode.asyncLayout() let actionButtonsLayout = ChatMessageActionButtonsNode.asyncLayout(self.actionButtonsNode) let textLayout = TextNode.asyncLayout(self.textNode) let makeForwardInfoLayout = ChatMessageForwardInfoNode.asyncLayout(self.forwardInfoNode) let viaBotLayout = TextNode.asyncLayout(self.viaBotNode) let makeReplyInfoLayout = ChatMessageReplyInfoNode.asyncLayout(self.replyInfoNode) let currentShareButtonNode = self.shareButtonNode let currentForwardInfo = self.appliedForwardInfo return { item, params, mergedTop, mergedBottom, dateHeaderAtBottom in let accessibilityData = ChatMessageAccessibilityData(item: item, isSelected: nil) let layoutConstants = chatMessageItemLayoutConstants(layoutConstants, params: params, presentationData: item.presentationData) let incoming = item.content.effectivelyIncoming(item.context.account.peerId, associatedData: item.associatedData) var imageSize: CGSize = CGSize(width: 100.0, height: 100.0) if let telegramFile = telegramFile { if let dimensions = telegramFile.dimensions { imageSize = dimensions.cgSize.aspectFitted(displaySize) } else if let thumbnailSize = telegramFile.previewRepresentations.first?.dimensions { imageSize = thumbnailSize.cgSize.aspectFitted(displaySize) } } var textLayoutAndApply: (TextNodeLayout, () -> TextNode)? var isEmoji = false if item.presentationData.largeEmoji && messageIsElligibleForLargeEmoji(item.message) { let attributedText = NSAttributedString(string: item.message.text, font: item.presentationData.messageEmojiFont, textColor: .black) textLayoutAndApply = textLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: 180.0, height: 90.0), alignment: .natural)) imageSize = CGSize(width: textLayoutAndApply!.0.size.width, height: textLayoutAndApply!.0.size.height) isEmoji = true } let avatarInset: CGFloat var hasAvatar = false switch item.chatLocation { case let .peer(peerId): if !peerId.isRepliesOrSavedMessages(accountPeerId: item.context.account.peerId) { if peerId.isGroupOrChannel && item.message.author != nil { var isBroadcastChannel = false if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { isBroadcastChannel = true } if !isBroadcastChannel { hasAvatar = true } } } else if incoming { hasAvatar = true } case let .replyThread(replyThreadMessage): if replyThreadMessage.messageId.peerId != item.context.account.peerId { if replyThreadMessage.messageId.peerId.isGroupOrChannel && item.message.author != nil { var isBroadcastChannel = false if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { isBroadcastChannel = true } if replyThreadMessage.isChannelPost, replyThreadMessage.effectiveTopId == item.message.id { isBroadcastChannel = true } if !isBroadcastChannel { hasAvatar = true } } } else if incoming { hasAvatar = true } } if hasAvatar { avatarInset = layoutConstants.avatarDiameter } else { avatarInset = 0.0 } let isFailed = item.content.firstMessage.effectivelyFailed(timestamp: item.context.account.network.getApproximateRemoteTimestamp()) var needShareButton = false if case .pinnedMessages = item.associatedData.subject { needShareButton = true } else if isFailed || Namespaces.Message.allScheduled.contains(item.message.id.namespace) { needShareButton = false } else if item.message.id.peerId == item.context.account.peerId { for attribute in item.content.firstMessage.attributes { if let _ = attribute as? SourceReferenceMessageAttribute { needShareButton = true break } } } else if item.message.effectivelyIncoming(item.context.account.peerId) { if let peer = item.message.peers[item.message.id.peerId] { if let channel = peer as? TelegramChannel { if case .broadcast = channel.info { needShareButton = true } } } if !needShareButton, let author = item.message.author as? TelegramUser, let _ = author.botInfo, !item.message.media.isEmpty { needShareButton = true } if !needShareButton { loop: for media in item.message.media { if media is TelegramMediaGame || media is TelegramMediaInvoice { needShareButton = true break loop } else if let media = media as? TelegramMediaWebpage, case .Loaded = media.content { needShareButton = true break loop } } } else { loop: for media in item.message.media { if media is TelegramMediaAction { needShareButton = false break loop } } } } var layoutInsets = UIEdgeInsets(top: mergedTop.merged ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, left: 0.0, bottom: mergedBottom.merged ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, right: 0.0) if dateHeaderAtBottom { layoutInsets.top += layoutConstants.timestampHeaderHeight } var deliveryFailedInset: CGFloat = 0.0 if isFailed { deliveryFailedInset += 24.0 } let displayLeftInset = params.leftInset + layoutConstants.bubble.edgeInset + avatarInset let innerImageInset: CGFloat = 10.0 let innerImageSize = CGSize(width: imageSize.width + innerImageInset * 2.0, height: imageSize.height + innerImageInset * 2.0) let imageFrame = CGRect(origin: CGPoint(x: 0.0 + (incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + avatarInset + layoutConstants.bubble.contentInsets.left) : (params.width - params.rightInset - innerImageSize.width - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left - deliveryFailedInset)), y: -innerImageInset), size: innerImageSize) let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(top: innerImageInset, left: innerImageInset, bottom: innerImageInset, right: innerImageInset)) let imageApply = imageLayout(arguments) let statusType: ChatMessageDateAndStatusType if item.message.effectivelyIncoming(item.context.account.peerId) { statusType = .FreeIncoming } else { if isFailed { statusType = .FreeOutgoing(.Failed) } else if item.message.flags.isSending && !item.message.isSentOrAcknowledged { statusType = .FreeOutgoing(.Sending) } else { statusType = .FreeOutgoing(.Sent(read: item.read)) } } var edited = false var viewCount: Int? = nil var dateReplies = 0 for attribute in item.message.attributes { if let _ = attribute as? EditedMessageAttribute, isEmoji { edited = true } 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) var isReplyThread = false if case .replyThread = item.chatLocation { isReplyThread = true } let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, item.message.isSelfExpiring) var viaBotApply: (TextNodeLayout, () -> TextNode)? var replyInfoApply: (CGSize, () -> ChatMessageReplyInfoNode)? var replyMarkup: ReplyMarkupMessageAttribute? var availableWidth = max(60.0, params.width - params.leftInset - params.rightInset - max(imageSize.width, 160.0) - 20.0 - layoutConstants.bubble.edgeInset * 2.0 - avatarInset - layoutConstants.bubble.contentInsets.left) if isEmoji { availableWidth -= 24.0 } var ignoreForward = false if let forwardInfo = item.message.forwardInfo { if item.message.id.peerId != 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 } break } } } } 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.username } 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())) } } if let replyAttribute = attribute as? ReplyMessageAttribute, let replyMessage = item.message.associatedMessages[replyAttribute.messageId] { if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.messageId == replyAttribute.messageId { } else { replyInfoApply = makeReplyInfoLayout(item.presentationData, item.presentationData.strings, item.context, .standalone, replyMessage, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude)) } } else if let attribute = attribute as? ReplyMarkupMessageAttribute, attribute.flags.contains(.inline), !attribute.rows.isEmpty { replyMarkup = attribute } } if item.message.id.peerId != item.context.account.peerId && !item.message.id.peerId.isReplies { 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 needsReplyBackground = false if replyInfoApply != nil || viaBotApply != nil { needsReplyBackground = true } var updatedShareButtonNode: ChatMessageShareButton? if needShareButton { if let currentShareButtonNode = currentShareButtonNode { updatedShareButtonNode = currentShareButtonNode } else { let buttonNode = ChatMessageShareButton() updatedShareButtonNode = buttonNode } } let contentHeight = max(imageSize.height, layoutConstants.image.minDimensions.height) var forwardSource: Peer? var forwardAuthorSignature: String? var forwardPsaType: String? var forwardInfoSizeApply: (CGSize, (CGFloat) -> ChatMessageForwardInfoNode)? var needsForwardBackground = false if !ignoreForward, let forwardInfo = item.message.forwardInfo { 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 availableForwardWidth = max(60.0, availableWidth + 6.0) forwardInfoSizeApply = makeForwardInfoLayout(item.presentationData, item.presentationData.strings, .standalone, forwardSource, forwardAuthorSignature, forwardPsaType, CGSize(width: availableForwardWidth, height: CGFloat.greatestFiniteMagnitude)) needsForwardBackground = true } var maxContentWidth = imageSize.width var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animated: Bool) -> ChatMessageActionButtonsNode))? if let replyMarkup = replyMarkup { let (minWidth, buttonsLayout) = actionButtonsLayout(item.context, item.presentationData.theme, item.presentationData.chatBubbleCorners, item.presentationData.strings, replyMarkup, item.message, maxContentWidth) maxContentWidth = max(maxContentWidth, minWidth) actionButtonsFinalize = buttonsLayout } var actionButtonsSizeAndApply: (CGSize, (Bool) -> ChatMessageActionButtonsNode)? if let actionButtonsFinalize = actionButtonsFinalize { actionButtonsSizeAndApply = actionButtonsFinalize(maxContentWidth) } var layoutSize = CGSize(width: params.width, height: contentHeight) if isEmoji && !incoming { layoutSize.height += dateAndStatusSize.height } if let actionButtonsSizeAndApply = actionButtonsSizeAndApply { layoutSize.height += actionButtonsSizeAndApply.0.height } var updatedImageFrame = imageFrame.offsetBy(dx: 0.0, dy: floor((contentHeight - imageSize.height) / 2.0)) var dateOffset = CGPoint(x: dateAndStatusSize.width + 4.0, y: dateAndStatusSize.height + 16.0) if isEmoji { if incoming { dateOffset.x = 12.0 } else { dateOffset.y = 12.0 } } var dateAndStatusFrame = CGRect(origin: CGPoint(x: min(layoutSize.width - dateAndStatusSize.width - 14.0, max(displayLeftInset, updatedImageFrame.maxX - dateOffset.x)), y: updatedImageFrame.maxY - dateOffset.y), size: dateAndStatusSize) let baseShareButtonSize = CGSize(width: 30.0, height: 60.0) var baseShareButtonFrame = CGRect(origin: CGPoint(x: updatedImageFrame.maxX + 6.0, y: updatedImageFrame.maxY - 10.0 - baseShareButtonSize.height - 4.0), size: baseShareButtonSize) if isEmoji && incoming { baseShareButtonFrame.origin.x = dateAndStatusFrame.maxX + 8.0 } var viaBotFrame: CGRect? if let (viaBotLayout, _) = viaBotApply { viaBotFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 15.0) : (params.width - params.rightInset - viaBotLayout.size.width - layoutConstants.bubble.edgeInset - 14.0)), y: 8.0), size: viaBotLayout.size) } var replyInfoFrame: CGRect? if let (replyInfoSize, _) = replyInfoApply { var viaBotSize = CGSize() if let viaBotFrame = viaBotFrame { viaBotSize = viaBotFrame.size } let replyInfoFrameValue = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 10.0) : (params.width - params.rightInset - max(replyInfoSize.width, viaBotSize.width) - layoutConstants.bubble.edgeInset - 10.0)), y: 8.0 + viaBotSize.height), size: replyInfoSize) replyInfoFrame = replyInfoFrameValue if let viaBotFrameValue = viaBotFrame { if replyInfoFrameValue.minX < replyInfoFrameValue.minX { viaBotFrame = viaBotFrameValue.offsetBy(dx: replyInfoFrameValue.minX - viaBotFrameValue.minX, dy: 0.0) } } } var replyBackgroundFrame: CGRect? if let replyInfoFrame = replyInfoFrame { var viaBotSize = CGSize() if let viaBotFrame = viaBotFrame { viaBotSize = viaBotFrame.size } replyBackgroundFrame = CGRect(origin: CGPoint(x: replyInfoFrame.minX - 4.0, y: replyInfoFrame.minY - viaBotSize.height - 2.0), size: CGSize(width: max(replyInfoFrame.size.width, viaBotSize.width) + 8.0, height: replyInfoFrame.size.height + viaBotSize.height + 5.0)) } if let replyBackgroundFrameValue = replyBackgroundFrame { if replyBackgroundFrameValue.insetBy(dx: -2.0, dy: -2.0).intersects(baseShareButtonFrame) { let offset: CGFloat = 25.0 layoutSize.height += offset updatedImageFrame.origin.y += offset dateAndStatusFrame.origin.y += offset baseShareButtonFrame.origin.y += offset } } return (ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets), { [weak self] animation, _, _ in if let strongSelf = self { var transition: ContainedViewLayoutTransition = .immediate if case let .System(duration) = animation { transition = .animated(duration: duration, curve: .spring) } strongSelf.appliedForwardInfo = (forwardSource, forwardAuthorSignature) strongSelf.updateAccessibilityData(accessibilityData) transition.updateFrame(node: strongSelf.imageNode, frame: updatedImageFrame) strongSelf.enableSynchronousImageApply = true imageApply() strongSelf.enableSynchronousImageApply = false if let immediateThumbnailData = telegramFile?.immediateThumbnailData { if strongSelf.backgroundNode == nil { if let backgroundNode = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { strongSelf.backgroundNode = backgroundNode strongSelf.placeholderNode.addBackdropNode(backgroundNode) if let (rect, size) = strongSelf.absoluteRect { strongSelf.updateAbsoluteRect(rect, within: size) } } } let foregroundColor = bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.stickerPlaceholderColor, wallpaper: item.presentationData.theme.wallpaper) let shimmeringColor = bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.stickerPlaceholderShimmerColor, wallpaper: item.presentationData.theme.wallpaper) let placeholderFrame = updatedImageFrame.insetBy(dx: innerImageInset, dy: innerImageInset) strongSelf.placeholderNode.update(backgroundColor: nil, foregroundColor: foregroundColor, shimmeringColor: shimmeringColor, data: immediateThumbnailData, size: placeholderFrame.size) strongSelf.placeholderNode.frame = placeholderFrame } strongSelf.messageAccessibilityArea.frame = CGRect(origin: CGPoint(), size: layoutSize) strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layoutSize) strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layoutSize) strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layoutSize) strongSelf.contextSourceNode.contentRect = strongSelf.imageNode.frame strongSelf.containerNode.targetNodeForActivationProgressContentRect = strongSelf.contextSourceNode.contentRect dateAndStatusApply(false) transition.updateFrame(node: strongSelf.dateAndStatusNode, frame: dateAndStatusFrame) if let updatedShareButtonNode = updatedShareButtonNode { if updatedShareButtonNode !== strongSelf.shareButtonNode { if let shareButtonNode = strongSelf.shareButtonNode { shareButtonNode.removeFromSupernode() } strongSelf.shareButtonNode = updatedShareButtonNode strongSelf.addSubnode(updatedShareButtonNode) updatedShareButtonNode.addTarget(strongSelf, action: #selector(strongSelf.shareButtonPressed), forControlEvents: .touchUpInside) } let buttonSize = updatedShareButtonNode.update(presentationData: item.presentationData, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account) let shareButtonFrame = CGRect(origin: CGPoint(x: baseShareButtonFrame.minX, y: baseShareButtonFrame.maxY - buttonSize.height), size: buttonSize) transition.updateFrame(node: updatedShareButtonNode, frame: shareButtonFrame) } else if let shareButtonNode = strongSelf.shareButtonNode { shareButtonNode.removeFromSupernode() strongSelf.shareButtonNode = nil } if needsReplyBackground { if let replyBackgroundNode = strongSelf.replyBackgroundNode { replyBackgroundNode.updateColor(color: selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), enableBlur: dateFillNeedsBlur(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), transition: .immediate) } else { let replyBackgroundNode = NavigationBackgroundNode(color: selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), enableBlur: dateFillNeedsBlur(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper)) strongSelf.replyBackgroundNode = replyBackgroundNode strongSelf.contextSourceNode.contentNode.addSubnode(replyBackgroundNode) } } else if let replyBackgroundNode = strongSelf.replyBackgroundNode { replyBackgroundNode.removeFromSupernode() strongSelf.replyBackgroundNode = nil } if let (_, viaBotApply) = viaBotApply, let viaBotFrame = viaBotFrame { let viaBotNode = viaBotApply() if strongSelf.viaBotNode == nil { strongSelf.viaBotNode = viaBotNode strongSelf.contextSourceNode.contentNode.addSubnode(viaBotNode) } viaBotNode.frame = viaBotFrame if let replyBackgroundNode = strongSelf.replyBackgroundNode { replyBackgroundNode.frame = CGRect(origin: CGPoint(x: viaBotFrame.minX - 6.0, y: viaBotFrame.minY - 2.0 - UIScreenPixel), size: CGSize(width: viaBotFrame.size.width + 11.0, height: viaBotFrame.size.height + 5.0)) replyBackgroundNode.update(size: replyBackgroundNode.bounds.size, cornerRadius: 8.0, transition: .immediate) } } else if let viaBotNode = strongSelf.viaBotNode { viaBotNode.removeFromSupernode() strongSelf.viaBotNode = nil } if let (_, replyInfoApply) = replyInfoApply, let replyInfoFrame = replyInfoFrame { let replyInfoNode = replyInfoApply() if strongSelf.replyInfoNode == nil { strongSelf.replyInfoNode = replyInfoNode strongSelf.contextSourceNode.contentNode.addSubnode(replyInfoNode) } replyInfoNode.frame = replyInfoFrame if let replyBackgroundNode = strongSelf.replyBackgroundNode, let replyBackgroundFrame = replyBackgroundFrame { replyBackgroundNode.frame = replyBackgroundFrame replyBackgroundNode.update(size: replyBackgroundNode.bounds.size, cornerRadius: 8.0, transition: .immediate) } if isEmoji && !incoming { if let _ = item.controllerInteraction.selectionState { replyInfoNode.alpha = 0.0 strongSelf.replyBackgroundNode?.alpha = 0.0 } else { replyInfoNode.alpha = 1.0 strongSelf.replyBackgroundNode?.alpha = 1.0 } } } else if let replyInfoNode = strongSelf.replyInfoNode { replyInfoNode.removeFromSupernode() strongSelf.replyInfoNode = nil } if isFailed { let deliveryFailedNode: ChatMessageDeliveryFailedNode var isAppearing = false if let current = strongSelf.deliveryFailedNode { deliveryFailedNode = current } else { isAppearing = true deliveryFailedNode = ChatMessageDeliveryFailedNode(tapped: { if let item = self?.item { item.controllerInteraction.requestRedeliveryOfFailedMessages(item.content.firstMessage.id) } }) strongSelf.deliveryFailedNode = deliveryFailedNode strongSelf.addSubnode(deliveryFailedNode) } let deliveryFailedSize = deliveryFailedNode.updateLayout(theme: item.presentationData.theme.theme) let deliveryFailedFrame = CGRect(origin: CGPoint(x: imageFrame.maxX + deliveryFailedInset - deliveryFailedSize.width, y: imageFrame.maxY - deliveryFailedSize.height - innerImageInset), size: deliveryFailedSize) if isAppearing { deliveryFailedNode.frame = deliveryFailedFrame transition.animatePositionAdditive(node: deliveryFailedNode, offset: CGPoint(x: deliveryFailedInset, y: 0.0)) } else { transition.updateFrame(node: deliveryFailedNode, frame: deliveryFailedFrame) } } else if let deliveryFailedNode = strongSelf.deliveryFailedNode { strongSelf.deliveryFailedNode = nil transition.updateAlpha(node: deliveryFailedNode, alpha: 0.0) transition.updateFrame(node: deliveryFailedNode, frame: deliveryFailedNode.frame.offsetBy(dx: 24.0, dy: 0.0), completion: { [weak deliveryFailedNode] _ in deliveryFailedNode?.removeFromSupernode() }) } if needsForwardBackground { if let forwardBackgroundNode = strongSelf.forwardBackgroundNode { forwardBackgroundNode.updateColor(color: selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), enableBlur: dateFillNeedsBlur(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), transition: .immediate) } else { let forwardBackgroundNode = NavigationBackgroundNode(color: selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), enableBlur: dateFillNeedsBlur(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper)) strongSelf.forwardBackgroundNode = forwardBackgroundNode strongSelf.contextSourceNode.contentNode.addSubnode(forwardBackgroundNode) if animation.isAnimated { forwardBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } } else if let forwardBackgroundNode = strongSelf.forwardBackgroundNode { if animation.isAnimated { strongSelf.forwardBackgroundNode = nil forwardBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false, completion: { [weak forwardBackgroundNode] _ in forwardBackgroundNode?.removeFromSupernode() }) } else { forwardBackgroundNode.removeFromSupernode() strongSelf.forwardBackgroundNode = nil } } if let (forwardInfoSize, forwardInfoApply) = forwardInfoSizeApply { let forwardInfoNode = forwardInfoApply(forwardInfoSize.width) if strongSelf.forwardInfoNode == nil { strongSelf.forwardInfoNode = forwardInfoNode strongSelf.contextSourceNode.contentNode.addSubnode(forwardInfoNode) if animation.isAnimated { forwardInfoNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } let forwardInfoFrame = CGRect(origin: CGPoint(x: (!incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + 12.0) : (params.width - params.rightInset - forwardInfoSize.width - layoutConstants.bubble.edgeInset - 12.0)), y: 8.0), size: forwardInfoSize) forwardInfoNode.frame = forwardInfoFrame if let forwardBackgroundNode = strongSelf.forwardBackgroundNode { forwardBackgroundNode.frame = CGRect(origin: CGPoint(x: forwardInfoFrame.minX - 6.0, y: forwardInfoFrame.minY - 2.0), size: CGSize(width: forwardInfoFrame.size.width + 10.0, height: forwardInfoFrame.size.height + 4.0)) forwardBackgroundNode.update(size: forwardBackgroundNode.bounds.size, cornerRadius: 8.0, transition: .immediate) } } 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 actionButtonsSizeAndApply = actionButtonsSizeAndApply { var animated = false if let _ = strongSelf.actionButtonsNode { if case .System = animation { animated = true } } let actionButtonsNode = actionButtonsSizeAndApply.1(animated) let previousFrame = actionButtonsNode.frame let actionButtonsFrame = CGRect(origin: CGPoint(x: imageFrame.minX, y: imageFrame.maxY - 10.0), size: actionButtonsSizeAndApply.0) actionButtonsNode.frame = actionButtonsFrame if actionButtonsNode !== strongSelf.actionButtonsNode { strongSelf.actionButtonsNode = actionButtonsNode actionButtonsNode.buttonPressed = { button in if let strongSelf = self { strongSelf.performMessageButtonAction(button: button) } } actionButtonsNode.buttonLongTapped = { button in if let strongSelf = self { strongSelf.presentMessageButtonContextMenu(button: button) } } strongSelf.addSubnode(actionButtonsNode) } else { if case let .System(duration) = animation { actionButtonsNode.layer.animateFrame(from: previousFrame, to: actionButtonsFrame, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) } } } else if let actionButtonsNode = strongSelf.actionButtonsNode { actionButtonsNode.removeFromSupernode() strongSelf.actionButtonsNode = nil } 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 } } }) } } @objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { switch recognizer.state { case .ended: if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { if case .doubleTap = gesture { self.containerNode.cancelGesture() } if let action = self.gestureRecognized(gesture: gesture, location: location, recognizer: nil) { if case .doubleTap = gesture { self.containerNode.cancelGesture() } switch action { case let .action(f): f() case let .optionalAction(f): f() case let .openContextMenu(tapMessage, selectAll, subFrame): self.item?.controllerInteraction.openMessageContextMenu(tapMessage, selectAll, self, subFrame, nil) } } else if case .tap = gesture { self.item?.controllerInteraction.clickThroughMessage() } } default: break } } private func gestureRecognized(gesture: TapLongTapOrDoubleTapGesture, location: CGPoint, recognizer: TapLongTapOrDoubleTapGestureRecognizer?) -> InternalBubbleTapAction? { switch gesture { case .tap: if let avatarNode = self.accessoryItemNode as? ChatMessageAvatarAccessoryItemNode, avatarNode.frame.contains(location) { if let item = self.item, let author = item.content.firstMessage.author { return .optionalAction({ var openPeerId = item.effectiveAuthorId ?? author.id var navigate: ChatControllerInteractionNavigateToPeer if item.content.firstMessage.id.peerId == item.context.account.peerId { navigate = .chat(textInputState: nil, subject: nil, peekData: nil) } else { navigate = .info } for attribute in item.content.firstMessage.attributes { if let attribute = attribute as? SourceReferenceMessageAttribute { openPeerId = attribute.messageId.peerId navigate = .chat(textInputState: nil, subject: .message(id: attribute.messageId, highlight: true, timecode: nil), peekData: nil) } } if item.effectiveAuthorId?.namespace == Namespaces.Peer.Empty { item.controllerInteraction.displayMessageTooltip(item.content.firstMessage.id, item.presentationData.strings.Conversation_ForwardAuthorHiddenTooltip, self, avatarNode.frame) } else if let forwardInfo = item.content.firstMessage.forwardInfo, forwardInfo.flags.contains(.isImported), forwardInfo.author == nil { item.controllerInteraction.displayImportedMessageTooltip(avatarNode) } else { if !item.message.id.peerId.isReplies, let channel = item.content.firstMessage.forwardInfo?.author as? TelegramChannel, channel.username == nil { if case .member = channel.participationStatus { } else { item.controllerInteraction.displayMessageTooltip(item.message.id, item.presentationData.strings.Conversation_PrivateChannelTooltip, self, avatarNode.frame) } } item.controllerInteraction.openPeer(openPeerId, navigate, item.message) } }) } } 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 { return .optionalAction({ item.controllerInteraction.updateInputState { textInputState in return ChatTextInputState(inputText: NSAttributedString(string: "@" + botAddressName + " ")) } item.controllerInteraction.updateInputMode { _ in return .text } }) } } } } } 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 { return .optionalAction({ item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId) }) } } } } if let item = self.item, self.imageNode.frame.contains(location) { return .optionalAction({ let _ = item.controllerInteraction.openMessage(item.message, .default) }) } return nil case .longTap, .doubleTap: if let item = self.item, self.imageNode.frame.contains(location) { return .openContextMenu(tapMessage: item.message, selectAll: false, subFrame: self.imageNode.frame) } case .hold: break } return nil } @objc func shareButtonPressed() { if let item = self.item { if case .pinnedMessages = item.associatedData.subject { item.controllerInteraction.navigateToMessageStandalone(item.content.firstMessage.id) return } if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = channel.info { for attribute in item.message.attributes { if let _ = attribute as? ReplyThreadMessageAttribute { item.controllerInteraction.openMessageReplies(item.message.id, true, false) return } } } if item.content.firstMessage.id.peerId.isReplies { item.controllerInteraction.openReplyThreadOriginalMessage(item.content.firstMessage) } else if item.content.firstMessage.id.peerId.isRepliesOrSavedMessages(accountPeerId: item.context.account.peerId) { for attribute in item.content.firstMessage.attributes { if let attribute = attribute as? SourceReferenceMessageAttribute { item.controllerInteraction.navigateToMessage(item.content.firstMessage.id, attribute.messageId) break } } } else { item.controllerInteraction.openMessageShareMenu(item.message.id) } } } @objc func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) { switch recognizer.state { case .began: self.currentSwipeToReplyTranslation = 0.0 if self.swipeToReplyFeedback == nil { self.swipeToReplyFeedback = HapticFeedback() self.swipeToReplyFeedback?.prepareImpact() } (self.view.window as? WindowHost)?.cancelInteractiveKeyboardGestures() case .changed: var translation = recognizer.translation(in: self.view) translation.x = max(-80.0, min(0.0, translation.x)) var animateReplyNodeIn = false if (translation.x < -45.0) != (self.currentSwipeToReplyTranslation < -45.0) { if translation.x < -45.0, self.swipeToReplyNode == nil, let item = self.item { self.swipeToReplyFeedback?.impact() let swipeToReplyNode = ChatMessageSwipeToReplyNode(fillColor: selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), enableBlur: dateFillNeedsBlur(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), foregroundColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.shareButtonForegroundColor, wallpaper: item.presentationData.theme.wallpaper), action: ChatMessageSwipeToReplyNode.Action(self.currentSwipeAction)) self.swipeToReplyNode = swipeToReplyNode self.addSubnode(swipeToReplyNode) animateReplyNodeIn = true } } self.currentSwipeToReplyTranslation = translation.x var bounds = self.bounds bounds.origin.x = -translation.x self.bounds = bounds self.updateAttachedAvatarNodeOffset(offset: translation.x, transition: .immediate) if let swipeToReplyNode = self.swipeToReplyNode { swipeToReplyNode.frame = CGRect(origin: CGPoint(x: bounds.size.width, y: floor((self.contentSize.height - 33.0) / 2.0)), size: CGSize(width: 33.0, height: 33.0)) if animateReplyNodeIn { swipeToReplyNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12) swipeToReplyNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4) } else { swipeToReplyNode.alpha = min(1.0, abs(translation.x / 45.0)) } } case .cancelled, .ended: self.swipeToReplyFeedback = nil let translation = recognizer.translation(in: self.view) if case .ended = recognizer.state, translation.x < -45.0 { if let item = self.item { if let currentSwipeAction = currentSwipeAction { switch currentSwipeAction { case .none: break case .reply: item.controllerInteraction.setupReply(item.message.id) } } } } var bounds = self.bounds let previousBounds = bounds bounds.origin.x = 0.0 self.bounds = bounds self.layer.animateBounds(from: previousBounds, to: bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) self.updateAttachedAvatarNodeOffset(offset: 0.0, transition: .animated(duration: 0.3, curve: .spring)) if let swipeToReplyNode = self.swipeToReplyNode { self.swipeToReplyNode = nil swipeToReplyNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak swipeToReplyNode] _ in swipeToReplyNode?.removeFromSupernode() }) swipeToReplyNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) } default: break } } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if let shareButtonNode = self.shareButtonNode, shareButtonNode.frame.contains(point) { return shareButtonNode.view } return super.hitTest(point, with: event) } override func updateSelectionState(animated: Bool) { guard let item = self.item else { return } if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.effectiveTopId == item.message.id { return } let incoming = item.content.effectivelyIncoming(item.context.account.peerId, associatedData: item.associatedData) var isEmoji = false if let item = self.item, item.presentationData.largeEmoji && messageIsElligibleForLargeEmoji(item.message) { isEmoji = true } let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate let replyAlpha: CGFloat = item.controllerInteraction.selectionState == nil ? 1.0 : 0.0 if let replyInfoNode = self.replyInfoNode { transition.updateAlpha(node: replyInfoNode, alpha: replyAlpha) } if let replyBackgroundNode = self.replyBackgroundNode { transition.updateAlpha(node: replyBackgroundNode, alpha: replyAlpha) } if let selectionState = item.controllerInteraction.selectionState { let selected = selectionState.selectedIds.contains(item.message.id) let offset: CGFloat = incoming ? 42.0 : 0.0 if let selectionNode = self.selectionNode { let selectionFrame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: self.contentBounds.size.width, height: self.contentBounds.size.height)) selectionNode.frame = selectionFrame selectionNode.updateLayout(size: selectionFrame.size, leftInset: self.safeInsets.left) selectionNode.updateSelected(selected, animated: animated) self.subnodeTransform = CATransform3DMakeTranslation(offset, 0.0, 0.0); } else { let selectionNode = ChatMessageSelectionNode(wallpaper: item.presentationData.theme.wallpaper, theme: item.presentationData.theme.theme, toggle: { [weak self] value in if let strongSelf = self, let item = strongSelf.item { item.controllerInteraction.toggleMessagesSelection([item.message.id], value) } }) let selectionFrame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: self.contentBounds.size.width, height: self.contentBounds.size.height)) selectionNode.frame = selectionFrame selectionNode.updateLayout(size: selectionFrame.size, leftInset: self.safeInsets.left) self.addSubnode(selectionNode) self.selectionNode = selectionNode selectionNode.updateSelected(selected, animated: false) let previousSubnodeTransform = self.subnodeTransform self.subnodeTransform = CATransform3DMakeTranslation(offset, 0.0, 0.0); if animated { selectionNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.layer.animate(from: NSValue(caTransform3D: previousSubnodeTransform), to: NSValue(caTransform3D: self.subnodeTransform), keyPath: "sublayerTransform", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.2) if !incoming { let position = selectionNode.layer.position selectionNode.layer.animatePosition(from: CGPoint(x: position.x - 42.0, y: position.y), to: position, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) } } } if let replyInfoNode = self.replyInfoNode, isEmoji && !incoming { let alpha: CGFloat = 0.0 let previousAlpha = replyInfoNode.alpha replyInfoNode.alpha = alpha self.replyBackgroundNode?.alpha = alpha if animated { replyInfoNode.layer.animateAlpha(from: previousAlpha, to: alpha, duration: 0.3) self.replyBackgroundNode?.layer.animateAlpha(from: previousAlpha, to: alpha, duration: 0.3) } } } else { if let selectionNode = self.selectionNode { self.selectionNode = nil let previousSubnodeTransform = self.subnodeTransform self.subnodeTransform = CATransform3DIdentity if animated { self.layer.animate(from: NSValue(caTransform3D: previousSubnodeTransform), to: NSValue(caTransform3D: self.subnodeTransform), keyPath: "sublayerTransform", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.2, completion: { [weak selectionNode]_ in selectionNode?.removeFromSupernode() }) selectionNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) if CGFloat(0.0).isLessThanOrEqualTo(selectionNode.frame.origin.x) { let position = selectionNode.layer.position selectionNode.layer.animatePosition(from: position, to: CGPoint(x: position.x - 42.0, y: position.y), duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false) } } else { selectionNode.removeFromSupernode() } } if let replyInfoNode = self.replyInfoNode, isEmoji && !incoming { let alpha: CGFloat = 1.0 let previousAlpha = replyInfoNode.alpha replyInfoNode.alpha = alpha self.replyBackgroundNode?.alpha = alpha if animated { replyInfoNode.layer.animateAlpha(from: previousAlpha, to: alpha, duration: 0.3) self.replyBackgroundNode?.layer.animateAlpha(from: previousAlpha, to: alpha, duration: 0.3) } } } } override func updateHighlightedState(animated: Bool) { super.updateHighlightedState(animated: animated) if let item = self.item { var highlighted = false if let highlightedState = item.controllerInteraction.highlightedState { if highlightedState.messageStableId == item.message.stableId { highlighted = true } } if self.highlightedState != highlighted { self.highlightedState = highlighted if highlighted { self.imageNode.setOverlayColor(item.presentationData.theme.theme.chat.message.mediaHighlightOverlayColor, animated: false) } else { self.imageNode.setOverlayColor(nil, animated: animated) } } } } override func cancelInsertionAnimations() { self.layer.removeAllAnimations() } override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { super.animateInsertion(currentTimestamp, duration: duration, short: short) self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } override func animateRemoved(_ currentTimestamp: Double, duration: Double) { super.animateRemoved(currentTimestamp, duration: duration) self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) } override func animateAdded(_ currentTimestamp: Double, duration: Double) { super.animateAdded(currentTimestamp, duration: duration) self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } override func getMessageContextSourceNode(stableId: UInt32?) -> ContextExtractedContentContainingNode? { return self.contextSourceNode } override func addAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode) { self.contextSourceNode.contentNode.addSubnode(accessoryItemNode) } func animateContentFromTextInputField(textInput: ChatMessageTransitionNode.Source.TextInput, transition: CombinedTransition) { guard let _ = self.item else { return } let localSourceContentFrame = self.contextSourceNode.contentNode.view.convert(textInput.contentView.frame.offsetBy(dx: self.contextSourceNode.contentRect.minX, dy: self.contextSourceNode.contentRect.minY), to: self.contextSourceNode.contentNode.view) textInput.contentView.frame = localSourceContentFrame self.contextSourceNode.contentNode.view.addSubview(textInput.contentView) let sourceCenter = CGPoint( x: localSourceContentFrame.minX + 11.2, y: localSourceContentFrame.midY - 1.8 ) let localSourceCenter = CGPoint( x: sourceCenter.x - localSourceContentFrame.minX, y: sourceCenter.y - localSourceContentFrame.minY ) let localSourceOffset = CGPoint( x: localSourceCenter.x - localSourceContentFrame.width / 2.0, y: localSourceCenter.y - localSourceContentFrame.height / 2.0 ) let sourceScale: CGFloat = 28.0 / self.imageNode.frame.height let offset = CGPoint( x: sourceCenter.x - self.imageNode.frame.midX, y: sourceCenter.y - self.imageNode.frame.midY ) transition.animatePositionAdditive(layer: self.imageNode.layer, offset: offset) transition.horizontal.animateTransformScale(node: self.imageNode, from: sourceScale) transition.animatePositionAdditive(layer: self.placeholderNode.layer, offset: offset) transition.horizontal.animateTransformScale(node: self.placeholderNode, from: sourceScale) let inverseScale = 1.0 / sourceScale transition.animatePositionAdditive(layer: textInput.contentView.layer, offset: CGPoint(), to: CGPoint( x: -offset.x - localSourceOffset.x * (inverseScale - 1.0), y: -offset.y - localSourceOffset.y * (inverseScale - 1.0) ), removeOnCompletion: false) transition.horizontal.updateTransformScale(layer: textInput.contentView.layer, scale: 1.0 / sourceScale) textInput.contentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { _ in textInput.contentView.removeFromSuperview() }) self.imageNode.layer.animateAlpha(from: 0.0, to: self.imageNode.alpha, duration: 0.1) self.placeholderNode.layer.animateAlpha(from: 0.0, to: self.placeholderNode.alpha, duration: 0.1) self.dateAndStatusNode.layer.animateAlpha(from: 0.0, to: self.dateAndStatusNode.alpha, duration: 0.15, delay: 0.16) } func animateContentFromStickerGridItem(stickerSource: ChatMessageTransitionNode.Sticker, transition: CombinedTransition) { guard let _ = self.item else { return } let localSourceContentFrame = CGRect( origin: CGPoint( x: self.imageNode.frame.minX + self.imageNode.frame.size.width / 2.0 - stickerSource.imageNode.frame.size.width / 2.0, y: self.imageNode.frame.minY + self.imageNode.frame.size.height / 2.0 - stickerSource.imageNode.frame.size.height / 2.0 ), size: stickerSource.imageNode.frame.size ) var snapshotView: UIView? if let animationNode = stickerSource.animationNode { snapshotView = animationNode.view.snapshotContentTree() } else { snapshotView = stickerSource.imageNode.view.snapshotContentTree() } snapshotView?.frame = localSourceContentFrame if let snapshotView = snapshotView { self.contextSourceNode.contentNode.view.addSubview(snapshotView) } let sourceCenter = CGPoint( x: localSourceContentFrame.midX, y: localSourceContentFrame.midY ) let localSourceCenter = CGPoint( x: sourceCenter.x - localSourceContentFrame.minX, y: sourceCenter.y - localSourceContentFrame.minY ) let localSourceOffset = CGPoint( x: localSourceCenter.x - localSourceContentFrame.width / 2.0, y: localSourceCenter.y - localSourceContentFrame.height / 2.0 ) let sourceScale: CGFloat = stickerSource.imageNode.frame.height / self.imageNode.frame.height let offset = CGPoint( x: sourceCenter.x - self.imageNode.frame.midX, y: sourceCenter.y - self.imageNode.frame.midY ) transition.animatePositionAdditive(layer: self.imageNode.layer, offset: offset) transition.horizontal.animateTransformScale(node: self.imageNode, from: sourceScale) transition.animatePositionAdditive(layer: self.placeholderNode.layer, offset: offset) transition.horizontal.animateTransformScale(node: self.placeholderNode, from: sourceScale) let inverseScale = 1.0 / sourceScale if let snapshotView = snapshotView { transition.animatePositionAdditive(layer: snapshotView.layer, offset: CGPoint(), to: CGPoint( x: -offset.x - localSourceOffset.x * (inverseScale - 1.0), y: -offset.y - localSourceOffset.y * (inverseScale - 1.0) ), removeOnCompletion: false) transition.horizontal.updateTransformScale(layer: snapshotView.layer, scale: 1.0 / sourceScale) snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.06, removeOnCompletion: false, completion: { [weak snapshotView] _ in snapshotView?.removeFromSuperview() }) self.imageNode.layer.animateAlpha(from: 0.0, to: self.imageNode.alpha, duration: 0.03) self.placeholderNode.layer.animateAlpha(from: 0.0, to: self.placeholderNode.alpha, duration: 0.03) } self.dateAndStatusNode.layer.animateAlpha(from: 0.0, to: self.dateAndStatusNode.alpha, duration: 0.15, delay: 0.16) if let animationNode = stickerSource.animationNode { animationNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) animationNode.layer.animateAlpha(from: 0.0, to: animationNode.alpha, duration: 0.4) } stickerSource.imageNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) stickerSource.imageNode.layer.animateAlpha(from: 0.0, to: stickerSource.imageNode.alpha, duration: 0.4) if let placeholderNode = stickerSource.placeholderNode { placeholderNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) placeholderNode.layer.animateAlpha(from: 0.0, to: placeholderNode.alpha, duration: 0.4) } } func animateReplyPanel(sourceReplyPanel: ChatMessageTransitionNode.ReplyPanel, transition: CombinedTransition) { if let replyInfoNode = self.replyInfoNode { let localRect = self.contextSourceNode.contentNode.view.convert(sourceReplyPanel.relativeSourceRect, to: replyInfoNode.view) let offset = replyInfoNode.animateFromInputPanel(sourceReplyPanel: sourceReplyPanel, localRect: localRect, transition: transition) if let replyBackgroundNode = self.replyBackgroundNode { transition.animatePositionAdditive(layer: replyBackgroundNode.layer, offset: offset) replyBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) } } } }