import Foundation import UIKit import AsyncDisplayKit import TelegramCore import Postbox import SwiftSignalKit import Display import TelegramPresentationData import TelegramUIPreferences import AccountContext import LocalizedPeerData import AlertUI import PresentationDataUtils import TextFormat import Markdown import TelegramNotices func textStringForForwardedMessage(_ message: Message, strings: PresentationStrings) -> (String, Bool) { for media in message.media { switch media { case _ as TelegramMediaImage: return (strings.Message_Photo, true) case let file as TelegramMediaFile: var fileName: String = strings.Message_File for attribute in file.attributes { switch attribute { case .Sticker: return (strings.Message_Sticker, true) case let .FileName(name): fileName = name case let .Audio(isVoice, _, title, performer, _): if isVoice { return (strings.Message_Audio, true) } else { if let title = title, let performer = performer, !title.isEmpty, !performer.isEmpty { return (title + " — " + performer, true) } else if let title = title, !title.isEmpty { return (title, true) } else if let performer = performer, !performer.isEmpty { return (performer, true) } else { return (strings.Message_Audio, true) } } case .Video: if file.isAnimated { return (strings.Message_Animation, true) } else { return (strings.Message_Video, true) } default: break } } if file.isAnimatedSticker { return (strings.Message_Sticker, true) } return (fileName, true) case _ as TelegramMediaContact: return (strings.Message_Contact, true) case let game as TelegramMediaGame: return (game.title, true) case _ as TelegramMediaMap: return (strings.Message_Location, true) case _ as TelegramMediaAction: return ("", true) case _ as TelegramMediaPoll: return (strings.ForwardedPolls(1), true) case let dice as TelegramMediaDice: return (dice.emoji, true) case let invoice as TelegramMediaInvoice: return (invoice.title, true) default: break } } return (message.text, false) } final class ForwardAccessoryPanelNode: AccessoryPanelNode { private let messageDisposable = MetaDisposable() let messageIds: [MessageId] private var messages: [Message] = [] private var authors: String? private var sourcePeer: (isPersonal: Bool, displayTitle: String)? let closeButton: HighlightableButtonNode let lineNode: ASImageNode let iconNode: ASImageNode let titleNode: ImmediateTextNode let textNode: ImmediateTextNode private let actionArea: AccessibilityAreaNode let context: AccountContext var theme: PresentationTheme var strings: PresentationStrings var fontSize: PresentationFontSize var nameDisplayOrder: PresentationPersonNameOrder var forwardOptionsState: ChatInterfaceForwardOptionsState? private var validLayout: (size: CGSize, inset: CGFloat, interfaceState: ChatPresentationInterfaceState)? init(context: AccountContext, messageIds: [MessageId], theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, nameDisplayOrder: PresentationPersonNameOrder, forwardOptionsState: ChatInterfaceForwardOptionsState?) { self.context = context self.messageIds = messageIds self.theme = theme self.strings = strings self.fontSize = fontSize self.nameDisplayOrder = nameDisplayOrder self.forwardOptionsState = forwardOptionsState 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 = false 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.chatInputPanelForwardIconImage(theme) self.titleNode = ImmediateTextNode() self.titleNode.maximumNumberOfLines = 1 self.titleNode.displaysAsynchronously = false self.textNode = ImmediateTextNode() self.textNode.maximumNumberOfLines = 1 self.textNode.displaysAsynchronously = false 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.actionArea) self.messageDisposable.set((context.account.postbox.messagesAtIds(messageIds) |> deliverOnMainQueue).start(next: { [weak self] messages in if let strongSelf = self { var authors = "" var uniquePeerIds = Set() var title = "" var text = "" var sourcePeer: (Bool, String)? for message in messages { if let author = message.forwardInfo?.author ?? message.effectiveAuthor, !uniquePeerIds.contains(author.id) { uniquePeerIds.insert(author.id) if !authors.isEmpty { authors.append(", ") } if author.id == context.account.peerId { authors.append(strongSelf.strings.DialogList_You) } else { authors.append(EnginePeer(author).compactDisplayTitle) } } if let peer = message.peers[message.id.peerId] { sourcePeer = (peer.id.namespace == Namespaces.Peer.CloudUser, EnginePeer(peer).displayTitle(strings: strongSelf.strings, displayOrder: strongSelf.nameDisplayOrder)) } } if messages.count == 1 { title = strongSelf.strings.Conversation_ForwardOptions_ForwardTitleSingle let (string, _) = textStringForForwardedMessage(messages[0], strings: strings) text = "\(authors): \(string)" } else { title = strongSelf.strings.Conversation_ForwardOptions_ForwardTitle(Int32(messages.count)) text = strongSelf.strings.Conversation_ForwardFrom(authors).string } strongSelf.messages = messages strongSelf.sourcePeer = sourcePeer strongSelf.authors = authors strongSelf.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(15.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor) strongSelf.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: strongSelf.theme.chat.inputPanel.secondaryTextColor) let headerString: String if messages.count == 1 { headerString = "Forward message" } else { headerString = "Forward messages" } strongSelf.actionArea.accessibilityLabel = "\(headerString). From: \(authors).\n\(text)" if let (size, inset, interfaceState) = strongSelf.validLayout { strongSelf.updateState(size: size, inset: inset, interfaceState: interfaceState) } let _ = (ApplicationSpecificNotice.getChatForwardOptionsTip(accountManager: strongSelf.context.sharedContext.accountManager) |> deliverOnMainQueue).start(next: { [weak self] count in if let strongSelf = self, count < 3 { Queue.mainQueue().after(3.0) { if let snapshotView = strongSelf.textNode.view.snapshotContentTree() { let text: String if let (size, _, _) = strongSelf.validLayout, size.width > 320.0 { text = strongSelf.strings.Conversation_ForwardOptions_TapForOptions } else { text = strongSelf.strings.Conversation_ForwardOptions_TapForOptionsShort } strongSelf.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: strongSelf.theme.chat.inputPanel.secondaryTextColor) strongSelf.view.addSubview(snapshotView) if let (size, inset, interfaceState) = strongSelf.validLayout { strongSelf.updateState(size: size, inset: inset, interfaceState: interfaceState) } strongSelf.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in snapshotView?.removeFromSuperview() }) } let _ = ApplicationSpecificNotice.incrementChatForwardOptionsTip(accountManager: strongSelf.context.sharedContext.accountManager).start() } } }) } })) } deinit { self.messageDisposable.dispose() } override func didLoad() { super.didLoad() self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } 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) { self.updateThemeAndStrings(theme: theme, strings: strings, forwardOptionsState: self.forwardOptionsState) } func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings, forwardOptionsState: ChatInterfaceForwardOptionsState?, force: Bool = false) { if force || self.theme !== theme || self.strings !== strings || self.forwardOptionsState != forwardOptionsState { self.theme = theme self.strings = strings self.forwardOptionsState = forwardOptionsState self.closeButton.setImage(PresentationResourcesChat.chatInputPanelCloseIconImage(theme), for: []) self.lineNode.image = PresentationResourcesChat.chatInputPanelVerticalSeparatorLineImage(theme) self.iconNode.image = PresentationResourcesChat.chatInputPanelForwardIconImage(theme) let filteredMessages = self.messages var authors = self.authors ?? "" if forwardOptionsState?.hideNames == true { authors = self.strings.DialogList_You } var title = "" var text = "" if filteredMessages.count == 1, let message = filteredMessages.first { title = self.strings.Conversation_ForwardOptions_ForwardTitleSingle let (string, _) = textStringForForwardedMessage(message, strings: strings) text = "\(authors): \(string)" } else { title = self.strings.Conversation_ForwardOptions_ForwardTitle(Int32(filteredMessages.count)) text = self.strings.Conversation_ForwardFrom(authors).string } self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(15.0), textColor: self.theme.chat.inputPanel.panelControlAccentColor) self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: self.theme.chat.inputPanel.secondaryTextColor) 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 bounds = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: 45.0)) let leftInset: CGFloat = 55.0 + inset let rightInset: CGFloat = 55.0 + inset let textLineInset: CGFloat = 10.0 let textRightInset: CGFloat = 20.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.closeButton.isHidden = interfaceState.renderedPeer == nil 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) } let titleSize = self.titleNode.updateLayout(CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset, height: bounds.size.height)) self.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset, y: 7.0), size: titleSize) let textSize = self.textNode.updateLayout(CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset, height: bounds.size.height)) self.textNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset, y: 25.0), size: textSize) } @objc func closePressed() { guard let (isPersonal, peerDisplayTitle) = self.sourcePeer else { return } let messageCount = Int32(self.messageIds.count) let messages = self.strings.Conversation_ForwardOptions_Messages(messageCount) let string = isPersonal ? self.strings.Conversation_ForwardOptions_TextPersonal(messages, peerDisplayTitle) : self.strings.Conversation_ForwardOptions_Text(messages, peerDisplayTitle) let font = Font.regular(floor(self.fontSize.baseDisplaySize * 15.0 / 17.0)) let boldFont = Font.semibold(floor(self.fontSize.baseDisplaySize * 15.0 / 17.0)) let body = MarkdownAttributeSet(font: font, textColor: self.theme.actionSheet.secondaryTextColor) let bold = MarkdownAttributeSet(font: boldFont, textColor: self.theme.actionSheet.secondaryTextColor) let title = NSAttributedString(string: self.strings.Conversation_ForwardOptions_Title(messageCount), font: Font.semibold(floor(self.fontSize.baseDisplaySize)), textColor: self.theme.actionSheet.primaryTextColor, paragraphAlignment: .center) let text = addAttributesToStringWithRanges(string._tuple, body: body, argumentAttributes: [0: bold, 1: bold], textAlignment: .center) let alertController = richTextAlertController(context: self.context, title: title, text: text, actions: [TextAlertAction(type: .genericAction, title: self.strings.Conversation_ForwardOptions_ShowOptions, action: { [weak self] in if let strongSelf = self { strongSelf.interfaceInteraction?.presentForwardOptions(strongSelf) Queue.mainQueue().after(0.5) { strongSelf.updateThemeAndStrings(theme: strongSelf.theme, strings: strongSelf.strings, forwardOptionsState: strongSelf.forwardOptionsState, force: true) } let _ = ApplicationSpecificNotice.incrementChatForwardOptionsTip(accountManager: strongSelf.context.sharedContext.accountManager, count: 3).start() } }), TextAlertAction(type: .destructiveAction, title: self.strings.Conversation_ForwardOptions_CancelForwarding, action: { [weak self] in self?.dismiss?() })], actionLayout: .vertical) self.interfaceInteraction?.presentController(alertController, nil) } private var previousTapTimestamp: Double? @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { let timestamp = CFAbsoluteTimeGetCurrent() if let previousTapTimestamp = self.previousTapTimestamp, previousTapTimestamp + 1.0 > timestamp { return } self.previousTapTimestamp = CFAbsoluteTimeGetCurrent() self.interfaceInteraction?.presentForwardOptions(self) Queue.mainQueue().after(1.5) { self.updateThemeAndStrings(theme: self.theme, strings: self.strings, forwardOptionsState: self.forwardOptionsState, force: true) } let _ = ApplicationSpecificNotice.incrementChatForwardOptionsTip(accountManager: self.context.sharedContext.accountManager, count: 3).start() } } }