import Foundation import UIKit import AsyncDisplayKit import TelegramCore import Postbox import SwiftSignalKit import Display import TelegramPresentationData import TelegramUIPreferences import ActivityIndicator import AccountContext import RadialStatusNode import PhotoResources import TelegramStringFormatting final class EditAccessoryPanelNode: AccessoryPanelNode { let dateTimeFormat: PresentationDateTimeFormat let messageId: MessageId let closeButton: HighlightableButtonNode let lineNode: ASImageNode let iconNode: ASImageNode let titleNode: ImmediateTextNode let textNode: ImmediateTextNode let imageNode: TransformImageNode let dimNode: ASDisplayNode let drawIconNode: ASImageNode private let actionArea: AccessibilityAreaNode private let activityIndicator: ActivityIndicator private let statusNode: RadialStatusNode private let tapNode: ASDisplayNode private let messageDisposable = MetaDisposable() private let editingMessageDisposable = MetaDisposable() private var isPhoto = false private var currentMessage: Message? private var currentEditMediaReference: AnyMediaReference? private var previousMediaReference: AnyMediaReference? override var interfaceInteraction: ChatPanelInterfaceInteraction? { didSet { if let statuses = self.interfaceInteraction?.statuses { self.editingMessageDisposable.set(statuses.editingMessage.start(next: { [weak self] value in if let strongSelf = self { if let value = value { if value.isZero { strongSelf.activityIndicator.isHidden = false strongSelf.statusNode.transitionToState(.none, completion: {}) } else { strongSelf.activityIndicator.isHidden = true strongSelf.statusNode.transitionToState(.progress(color: strongSelf.theme.chat.inputPanel.panelControlAccentColor, lineWidth: nil, value: CGFloat(value), cancelEnabled: false, animateRotation: true), completion: {}) } } else { strongSelf.activityIndicator.isHidden = true strongSelf.statusNode.transitionToState(.none, completion: {}) } } })) } } } private let context: AccountContext var theme: PresentationTheme var strings: PresentationStrings var nameDisplayOrder: PresentationPersonNameOrder private var validLayout: (size: CGSize, inset: CGFloat, interfaceState: ChatPresentationInterfaceState)? init(context: AccountContext, messageId: MessageId, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat) { self.context = context self.messageId = messageId self.theme = theme self.strings = strings self.nameDisplayOrder = nameDisplayOrder self.dateTimeFormat = dateTimeFormat self.closeButton = HighlightableButtonNode() self.closeButton.accessibilityLabel = strings.VoiceOver_DiscardPreparedContent self.closeButton.setImage(PresentationResourcesChat.chatInputPanelCloseIconImage(theme), for: []) self.closeButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0) self.closeButton.displaysAsynchronously = false self.lineNode = ASImageNode() self.lineNode.displayWithoutProcessing = true self.lineNode.displaysAsynchronously = false self.lineNode.image = PresentationResourcesChat.chatInputPanelVerticalSeparatorLineImage(theme) self.iconNode = ASImageNode() self.iconNode.displayWithoutProcessing = false self.iconNode.displaysAsynchronously = false self.iconNode.image = PresentationResourcesChat.chatInputPanelEditIconImage(theme) self.titleNode = ImmediateTextNode() self.titleNode.maximumNumberOfLines = 1 self.titleNode.displaysAsynchronously = false self.textNode = ImmediateTextNode() self.textNode.maximumNumberOfLines = 1 self.textNode.displaysAsynchronously = false self.textNode.isUserInteractionEnabled = true self.imageNode = TransformImageNode() self.imageNode.contentAnimations = [.subsequentUpdates] self.imageNode.isHidden = true self.imageNode.isUserInteractionEnabled = true self.dimNode = ASDisplayNode() self.dimNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.6) self.dimNode.cornerRadius = 2.0 self.dimNode.isHidden = true self.drawIconNode = ASImageNode() self.drawIconNode.contentMode = .center self.drawIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Draw"), color: .white) self.drawIconNode.isHidden = true self.activityIndicator = ActivityIndicator(type: .custom(theme.chat.inputPanel.panelControlAccentColor, 22.0, 2.0, false)) self.activityIndicator.isHidden = true self.statusNode = RadialStatusNode(backgroundNodeColor: .clear) self.tapNode = ASDisplayNode() self.actionArea = AccessibilityAreaNode() super.init() self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: [.touchUpInside]) self.addSubnode(self.closeButton) self.addSubnode(self.lineNode) self.addSubnode(self.iconNode) self.addSubnode(self.titleNode) self.addSubnode(self.textNode) self.addSubnode(self.imageNode) self.addSubnode(self.dimNode) self.addSubnode(self.drawIconNode) self.addSubnode(self.activityIndicator) self.addSubnode(self.statusNode) self.addSubnode(self.tapNode) self.addSubnode(self.actionArea) self.messageDisposable.set((context.account.postbox.messageAtId(messageId) |> deliverOnMainQueue).start(next: { [weak self] message in self?.updateMessage(message) })) } deinit { self.messageDisposable.dispose() self.editingMessageDisposable.dispose() } override func didLoad() { super.didLoad() self.tapNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.contentTap(_:)))) } private func updateMessage(_ message: Message?) { self.currentMessage = message var text = "" if let message = message { var effectiveMessage = message if let currentEditMediaReference = self.currentEditMediaReference { effectiveMessage = effectiveMessage.withUpdatedMedia([currentEditMediaReference.media]) } (text, _) = descriptionStringForMessage(contentSettings: context.currentContentSettings.with { $0 }, message: EngineMessage(effectiveMessage), strings: self.strings, nameDisplayOrder: self.nameDisplayOrder, dateTimeFormat: self.dateTimeFormat, accountPeerId: self.context.account.peerId) } var updatedMediaReference: AnyMediaReference? var imageDimensions: CGSize? if let message = message, !message.containsSecretMedia { var candidateMediaReference: AnyMediaReference? if let currentEditMedia = self.currentEditMediaReference { candidateMediaReference = currentEditMedia } else { for media in message.media { if media is TelegramMediaImage || media is TelegramMediaFile { candidateMediaReference = .message(message: MessageReference(message), media: media) break } } } if let imageReference = candidateMediaReference?.concrete(TelegramMediaImage.self) { updatedMediaReference = imageReference.abstract if let representation = largestRepresentationForPhoto(imageReference.media) { imageDimensions = representation.dimensions.cgSize } } else if let fileReference = candidateMediaReference?.concrete(TelegramMediaFile.self) { updatedMediaReference = fileReference.abstract if !fileReference.media.isInstantVideo, let representation = largestImageRepresentation(fileReference.media.previewRepresentations), !fileReference.media.isSticker { imageDimensions = representation.dimensions.cgSize } } } let imageNodeLayout = self.imageNode.asyncLayout() var applyImage: (() -> Void)? if let imageDimensions = imageDimensions { let boundingSize = CGSize(width: 35.0, height: 35.0) applyImage = imageNodeLayout(TransformImageArguments(corners: ImageCorners(radius: 2.0), imageSize: imageDimensions.aspectFilled(boundingSize), boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets())) } var mediaUpdated = false if let updatedMediaReference = updatedMediaReference, let previousMediaReference = self.previousMediaReference { mediaUpdated = !updatedMediaReference.media.isEqual(to: previousMediaReference.media) } else if (updatedMediaReference != nil) != (self.previousMediaReference != nil) { mediaUpdated = true } self.previousMediaReference = updatedMediaReference var isPhoto = false var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? if mediaUpdated { if let updatedMediaReference = updatedMediaReference, imageDimensions != nil { if let imageReference = updatedMediaReference.concrete(TelegramMediaImage.self) { updateImageSignal = chatMessagePhotoThumbnail(account: self.context.account, photoReference: imageReference) isPhoto = true } else if let fileReference = updatedMediaReference.concrete(TelegramMediaFile.self) { if fileReference.media.isVideo { updateImageSignal = chatMessageVideoThumbnail(account: self.context.account, fileReference: fileReference) } else if let iconImageRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) { updateImageSignal = chatWebpageSnippetFile(account: self.context.account, fileReference: fileReference, representation: iconImageRepresentation) } } } else { updateImageSignal = .single({ _ in return nil }) } } self.isPhoto = isPhoto let isMedia: Bool if let message = message { var effectiveMessage = message if let currentEditMediaReference = self.currentEditMediaReference { effectiveMessage = effectiveMessage.withUpdatedMedia([currentEditMediaReference.media]) } let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } switch messageContentKind(contentSettings: self.context.currentContentSettings.with { $0 }, message: EngineMessage(effectiveMessage), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat, accountPeerId: self.context.account.peerId) { case .text: isMedia = false default: isMedia = true } } else { isMedia = false } let canEditMedia: Bool if let message = message, !messageMediaEditingOptions(message: message).isEmpty { canEditMedia = true } else { canEditMedia = false } let titleString: String if canEditMedia { titleString = isPhoto ? self.strings.Conversation_EditingPhotoPanelTitle : self.strings.Conversation_EditingCaptionPanelTitle } else { titleString = self.strings.Conversation_EditingMessagePanelTitle } self.titleNode.attributedText = NSAttributedString(string: titleString, font: Font.medium(15.0), textColor: self.theme.chat.inputPanel.panelControlAccentColor) self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: isMedia ? self.theme.chat.inputPanel.secondaryTextColor : self.theme.chat.inputPanel.primaryTextColor) let headerString: String = titleString self.actionArea.accessibilityLabel = "\(headerString).\n\(text)" if let applyImage = applyImage { applyImage() self.imageNode.isHidden = false } else { self.imageNode.isHidden = true } if isPhoto && !self.imageNode.isHidden { self.dimNode.isHidden = false self.drawIconNode.isHidden = false } else { self.dimNode.isHidden = true self.drawIconNode.isHidden = true } if let updateImageSignal = updateImageSignal { self.imageNode.setSignal(.single({ arguments in /*let context = DrawingContext(size: arguments.boundingSize) context.withContext { c in c.setFillColor(UIColor.white.cgColor) c.fill(CGRect(origin: CGPoint(), size: context.size)) } return context*/ return nil }) |> then(updateImageSignal)) } if let (size, inset, interfaceState) = self.validLayout { self.updateState(size: size, inset: inset, interfaceState: interfaceState) } } override func animateIn() { self.iconNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2) } override func animateOut() { self.iconNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false) } override func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { if self.theme !== theme { self.theme = theme self.closeButton.setImage(PresentationResourcesChat.chatInputPanelCloseIconImage(theme), for: []) self.lineNode.image = PresentationResourcesChat.chatInputPanelVerticalSeparatorLineImage(theme) self.iconNode.image = PresentationResourcesChat.chatInputPanelEditIconImage(theme) if let text = self.titleNode.attributedText?.string { self.titleNode.attributedText = NSAttributedString(string: text, font: Font.medium(15.0), textColor: self.theme.chat.inputPanel.panelControlAccentColor) } if let text = self.textNode.attributedText?.string { self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: self.theme.chat.inputPanel.primaryTextColor) } if let (size, inset, interfaceState) = self.validLayout { self.updateState(size: size, inset: inset, interfaceState: interfaceState) } } } override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { return CGSize(width: constrainedSize.width, height: 45.0) } override func updateState(size: CGSize, inset: CGFloat, interfaceState: ChatPresentationInterfaceState) { self.validLayout = (size, inset, interfaceState) let editMediaReference = interfaceState.editMessageState?.mediaReference var updatedEditMedia = false if let currentEditMediaReference = self.currentEditMediaReference, let editMediaReference = editMediaReference { if !currentEditMediaReference.media.isEqual(to: editMediaReference.media) { updatedEditMedia = true } } else if (editMediaReference != nil) != (self.currentEditMediaReference != nil) { updatedEditMedia = true } if updatedEditMedia { if let editMediaReference = editMediaReference { self.currentEditMediaReference = editMediaReference } else { self.currentEditMediaReference = nil } self.updateMessage(self.currentMessage) } let bounds = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: 45.0)) let textLineInset: CGFloat = 10.0 let leftInset: CGFloat = 55.0 + inset let rightInset: CGFloat = 55.0 + inset let textRightInset: CGFloat = 20.0 let indicatorSize = CGSize(width: 22.0, height: 22.0) self.activityIndicator.frame = CGRect(origin: CGPoint(x: 18.0, y: 15.0), size: indicatorSize) self.statusNode.frame = CGRect(origin: CGPoint(x: 18.0, y: 15.0), size: indicatorSize).insetBy(dx: -2.0, dy: -2.0) let closeButtonSize = CGSize(width: 44.0, height: bounds.height) let closeButtonFrame = CGRect(origin: CGPoint(x: bounds.width - closeButtonSize.width - inset, y: 2.0), size: closeButtonSize) self.closeButton.frame = closeButtonFrame self.actionArea.frame = CGRect(origin: CGPoint(x: leftInset, y: 2.0), size: CGSize(width: closeButtonFrame.minX - leftInset, height: bounds.height)) self.lineNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 8.0), size: CGSize(width: 2.0, height: bounds.size.height - 10.0)) if let icon = self.iconNode.image { self.iconNode.frame = CGRect(origin: CGPoint(x: 7.0 + inset, y: 10.0), size: icon.size) } var imageTextInset: CGFloat = 0.0 if !self.imageNode.isHidden { imageTextInset = 9.0 + 35.0 } self.imageNode.frame = CGRect(origin: CGPoint(x: leftInset + 9.0, y: 8.0), size: CGSize(width: 35.0, height: 35.0)) self.dimNode.frame = self.imageNode.frame self.drawIconNode.frame = self.imageNode.frame let titleSize = self.titleNode.updateLayout(CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset - imageTextInset, height: bounds.size.height)) self.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset + imageTextInset, y: 7.0), size: titleSize) let textSize = self.textNode.updateLayout(CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset - imageTextInset, height: bounds.size.height)) self.textNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset + imageTextInset, y: 25.0), size: textSize) self.tapNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: bounds.width - leftInset - rightInset - closeButtonSize.width - 4.0, height: bounds.height)) } @objc func closePressed() { if let dismiss = self.dismiss { dismiss() } } @objc func contentTap(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state, let message = self.currentMessage { if self.isPhoto { self.interfaceInteraction?.editMessageMedia(message.id, true) } else { self.interfaceInteraction?.navigateToMessage(message.id, false, true, .generic) } } } }