Swiftgram/submodules/TelegramUI/Sources/EditAccessoryPanelNode.swift
2021-09-20 23:07:38 +03:00

416 lines
20 KiB
Swift

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)
}
}
}
}