diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 65d1e9bb7a..d8fd813274 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12298,3 +12298,13 @@ Sorry for the inconvenience."; "Stars.Transfer.Balance" = "Balance"; "Settings.Stars" = "Your Stars"; + +"Chat.MessageEffectMenu.TitleAddEffect" = "Add an animated effect"; +"Chat.MessageEffectMenu.SectionMessageEffects" = "Message Effects"; +"Chat.SendMessageMenu.MoveCaptionUp" = "Move Caption Up"; +"Chat.SendMessageMenu.MoveCaptionDown" = "Move Caption Down"; +"Chat.SendMessageMenu.ToastPremiumRequired.Text" = "Subscribe to [Telegram Premium]() to add this animated effect."; + +"BusinessLink.AlertTextLimitText" = "The message text limit is 4096 characters"; + +"Chat.SendMessageMenu.EditMessage" = "Edit Message"; diff --git a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputActionButtonsNode.swift b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputActionButtonsNode.swift index 032008d056..045e0a6459 100644 --- a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputActionButtonsNode.swift +++ b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputActionButtonsNode.swift @@ -57,7 +57,7 @@ final class AttachmentTextInputActionButtonsNode: ASDisplayNode, ChatSendMessage self.sendButton.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { - if strongSelf.sendButtonHasApplyIcon || !strongSelf.sendButtonLongPressEnabled { + if !strongSelf.sendButtonLongPressEnabled { if highlighted { strongSelf.sendContainerNode.layer.removeAnimation(forKey: "opacity") strongSelf.sendContainerNode.alpha = 0.4 diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index 714a90e2e0..37c42cf21f 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -1002,21 +1002,24 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peerId: strongSelf.presentationInterfaceState.chatLocation.peerId, - forwardMessageIds: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds, + params: .sendMessage(SendMessageActionSheetControllerParams.SendMessage( + isScheduledMessages: false, + mediaPreview: mediaPreview, + mediaCaptionIsAbove: (captionIsAboveMedia, { [weak strongSelf] value in + guard let strongSelf, let controller = strongSelf.controller, let mediaPickerContext = controller.mediaPickerContext else { + return + } + mediaPickerContext.setCaptionIsAboveMedia(value) + }), + attachment: true, + canSendWhenOnline: sendWhenOnlineAvailable, + forwardMessageIds: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds ?? [] + )), hasEntityKeyboard: hasEntityKeyboard, gesture: gesture, sourceSendButton: node, textInputView: textInputNode.textView, - mediaPreview: mediaPreview, - mediaCaptionIsAbove: (captionIsAboveMedia, { [weak strongSelf] value in - guard let strongSelf, let controller = strongSelf.controller, let mediaPickerContext = controller.mediaPickerContext else { - return - } - mediaPickerContext.setCaptionIsAboveMedia(value) - }), emojiViewProvider: textInputPanelNode.emojiViewProvider, - attachment: true, - canSendWhenOnline: sendWhenOnlineAvailable, completion: { }, sendMessage: { [weak textInputPanelNode] mode, messageEffect in diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift index 0e8bd258e0..68a87ead3b 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift @@ -11,169 +11,57 @@ import TextFormat import ReactionSelectionNode import WallpaperBackgroundNode -private final class ChatSendMessageActionSheetControllerImpl: ViewController, ChatSendMessageActionSheetController { - private var controllerNode: ChatSendMessageActionSheetControllerNode { - return self.displayNode as! ChatSendMessageActionSheetControllerNode - } - - private let context: AccountContext - - private let peerId: EnginePeer.Id? - private let isScheduledMessages: Bool - private let forwardMessageIds: [EngineMessage.Id]? - private let hasEntityKeyboard: Bool - - private let gesture: ContextGesture - private let sourceSendButton: ASDisplayNode - private let textInputView: UITextView - private let attachment: Bool - private let canSendWhenOnline: Bool - private let completion: () -> Void - private let sendMessage: (SendMode, SendParameters?) -> Void - private let schedule: (SendParameters?) -> Void - private let reactionItems: [ReactionItem]? - - private var presentationData: PresentationData - private var presentationDataDisposable: Disposable? - - private var didPlayPresentationAnimation = false - - private var validLayout: ContainerViewLayout? - - private let hapticFeedback = HapticFeedback() - - private let emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)? - - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: EnginePeer.Id?, isScheduledMessages: Bool = false, forwardMessageIds: [EngineMessage.Id]?, hasEntityKeyboard: Bool, gesture: ContextGesture, sourceSendButton: ASDisplayNode, textInputView: UITextView, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?, attachment: Bool = false, canSendWhenOnline: Bool, completion: @escaping () -> Void, sendMessage: @escaping (SendMode, SendParameters?) -> Void, schedule: @escaping (SendParameters?) -> Void, reactionItems: [ReactionItem]? = nil) { - self.context = context - self.peerId = peerId - self.isScheduledMessages = isScheduledMessages - self.forwardMessageIds = forwardMessageIds - self.hasEntityKeyboard = hasEntityKeyboard - self.gesture = gesture - self.sourceSendButton = sourceSendButton - self.textInputView = textInputView - self.emojiViewProvider = emojiViewProvider - self.attachment = attachment - self.canSendWhenOnline = canSendWhenOnline - self.completion = completion - self.sendMessage = sendMessage - self.schedule = schedule - self.reactionItems = reactionItems +public enum SendMessageActionSheetControllerParams { + public final class SendMessage { + public let isScheduledMessages: Bool + public let mediaPreview: ChatSendMessageContextScreenMediaPreview? + public let mediaCaptionIsAbove: (Bool, (Bool) -> Void)? + public let attachment: Bool + public let canSendWhenOnline: Bool + public let forwardMessageIds: [EngineMessage.Id] - self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } - - super.init(navigationBarPresentationData: nil) - - self.blocksBackgroundWhenInOverlay = true - - self.presentationDataDisposable = ((updatedPresentationData?.signal ?? context.sharedContext.presentationData) - |> deliverOnMainQueue).startStrict(next: { [weak self] presentationData in - if let strongSelf = self { - strongSelf.presentationData = presentationData - if strongSelf.isNodeLoaded { - strongSelf.controllerNode.updatePresentationData(presentationData) - } - } - }).strict() - - self.statusBar.statusBarStyle = .Hide - self.statusBar.ignoreInCall = true - } - - required init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - self.presentationDataDisposable?.dispose() - } - - override public func loadDisplayNode() { - var forwardedCount: Int? - if let forwardMessageIds = self.forwardMessageIds, forwardMessageIds.count > 0 { - forwardedCount = forwardMessageIds.count - } - - var reminders = false - var isSecret = false - var canSchedule = false - if let peerId = self.peerId { - reminders = peerId == context.account.peerId - isSecret = peerId.namespace == Namespaces.Peer.SecretChat - canSchedule = !isSecret - } - if self.isScheduledMessages { - canSchedule = false - } - - self.displayNode = ChatSendMessageActionSheetControllerNode(context: self.context, presentationData: self.presentationData, reminders: reminders, gesture: gesture, sourceSendButton: self.sourceSendButton, textInputView: self.textInputView, attachment: self.attachment, canSendWhenOnline: self.canSendWhenOnline, forwardedCount: forwardedCount, hasEntityKeyboard: self.hasEntityKeyboard, emojiViewProvider: self.emojiViewProvider, send: { [weak self] in - self?.sendMessage(.generic, nil) - self?.dismiss(cancel: false) - }, sendSilently: { [weak self] in - self?.sendMessage(.silently, nil) - self?.dismiss(cancel: false) - }, sendWhenOnline: { [weak self] in - self?.sendMessage(.whenOnline, nil) - self?.dismiss(cancel: false) - }, schedule: !canSchedule ? nil : { [weak self] in - self?.schedule(nil) - self?.dismiss(cancel: false) - }, cancel: { [weak self] in - self?.dismiss(cancel: true) - }, reactionItems: self.reactionItems) - self.displayNodeDidLoad() - } - - override public func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - if !self.didPlayPresentationAnimation { - self.didPlayPresentationAnimation = true - - self.hapticFeedback.impact() - self.controllerNode.animateIn() + public init( + isScheduledMessages: Bool, + mediaPreview: ChatSendMessageContextScreenMediaPreview?, + mediaCaptionIsAbove: (Bool, (Bool) -> Void)?, + attachment: Bool, + canSendWhenOnline: Bool, + forwardMessageIds: [EngineMessage.Id] + ) { + self.isScheduledMessages = isScheduledMessages + self.mediaPreview = mediaPreview + self.mediaCaptionIsAbove = mediaCaptionIsAbove + self.attachment = attachment + self.canSendWhenOnline = canSendWhenOnline + self.forwardMessageIds = forwardMessageIds } } - override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - self.validLayout = layout + public final class EditMessage { + public let messages: [EngineMessage] + public let mediaPreview: ChatSendMessageContextScreenMediaPreview? - super.containerLayoutUpdated(layout, transition: transition) - - self.controllerNode.containerLayoutUpdated(layout, transition: transition) + public init(messages: [EngineMessage], mediaPreview: ChatSendMessageContextScreenMediaPreview?) { + self.messages = messages + self.mediaPreview = mediaPreview + } } - override public func dismiss(completion: (() -> Void)? = nil) { - self.dismiss(cancel: true) - } - - private func dismiss(cancel: Bool) { - self.statusBar.statusBarStyle = .Ignore - self.controllerNode.animateOut(cancel: cancel, completion: { [weak self] in - self?.completion() - self?.didPlayPresentationAnimation = false - self?.presentingViewController?.dismiss(animated: false, completion: nil) - }) - } + case sendMessage(SendMessage) + case editMessage(EditMessage) } public func makeChatSendMessageActionSheetController( context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: EnginePeer.Id?, - isScheduledMessages: Bool = false, - forwardMessageIds: [EngineMessage.Id]?, + params: SendMessageActionSheetControllerParams, hasEntityKeyboard: Bool, gesture: ContextGesture, sourceSendButton: ASDisplayNode, textInputView: UITextView, - mediaPreview: ChatSendMessageContextScreenMediaPreview? = nil, - mediaCaptionIsAbove: (Bool, (Bool) -> Void)? = nil, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?, wallpaperBackgroundNode: WallpaperBackgroundNode? = nil, - attachment: Bool = false, - canSendWhenOnline: Bool, completion: @escaping () -> Void, sendMessage: @escaping (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.SendParameters?) -> Void, schedule: @escaping (ChatSendMessageActionSheetController.SendParameters?) -> Void, @@ -182,43 +70,17 @@ public func makeChatSendMessageActionSheetController( availableMessageEffects: AvailableMessageEffects? = nil, isPremium: Bool = false ) -> ChatSendMessageActionSheetController { - if textInputView.text.isEmpty && !"".isEmpty { - return ChatSendMessageActionSheetControllerImpl( - context: context, - updatedPresentationData: updatedPresentationData, - peerId: peerId, - isScheduledMessages: isScheduledMessages, - forwardMessageIds: forwardMessageIds, - hasEntityKeyboard: hasEntityKeyboard, - gesture: gesture, - sourceSendButton: sourceSendButton, - textInputView: textInputView, - emojiViewProvider: emojiViewProvider, - attachment: attachment, - canSendWhenOnline: canSendWhenOnline, - completion: completion, - sendMessage: sendMessage, - schedule: schedule, - reactionItems: nil - ) - } - return ChatSendMessageContextScreen( context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, - isScheduledMessages: isScheduledMessages, - forwardMessageIds: forwardMessageIds, + params: params, hasEntityKeyboard: hasEntityKeyboard, gesture: gesture, sourceSendButton: sourceSendButton, textInputView: textInputView, - mediaPreview: mediaPreview, - mediaCaptionIsAbove: mediaCaptionIsAbove, emojiViewProvider: emojiViewProvider, wallpaperBackgroundNode: wallpaperBackgroundNode, - attachment: attachment, - canSendWhenOnline: canSendWhenOnline, completion: completion, sendMessage: sendMessage, schedule: schedule, diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift index e369006a21..8b13789179 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift @@ -1,1204 +1 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import SwiftSignalKit -import Display -import TelegramCore -import Postbox -import TelegramPresentationData -import AccountContext -import AppBundle -import ContextUI -import TextFormat -import EmojiTextAttachmentView -import ChatInputTextNode -import ReactionSelectionNode -import EntityKeyboard -import ReactionButtonListComponent -private let leftInset: CGFloat = 16.0 -private let rightInset: CGFloat = 16.0 - -private enum ChatSendMessageActionIcon { - case sendWithoutSound - case sendWhenOnline - case schedule - - func image(theme: PresentationTheme) -> UIImage? { - let imageName: String - switch self { - case .sendWithoutSound: - imageName = "Chat/Input/Menu/SilentIcon" - case .sendWhenOnline: - imageName = "Chat/Input/Menu/WhenOnlineIcon" - case .schedule: - imageName = "Chat/Input/Menu/ScheduleIcon" - } - return generateTintedImage(image: UIImage(bundleImageName: imageName), color: theme.contextMenu.primaryColor) - } -} - -private final class ActionSheetItemNode: ASDisplayNode { - private let title: String - private let icon: ChatSendMessageActionIcon - let action: () -> Void - - private let separatorNode: ASDisplayNode - private let backgroundNode: ASDisplayNode - private let highlightedBackgroundNode: ASDisplayNode - private let buttonNode: HighlightTrackingButtonNode - private let iconNode: ASImageNode - private let titleNode: ImmediateTextNode - - private var maxWidth: CGFloat? - - init(theme: PresentationTheme, title: String, icon: ChatSendMessageActionIcon, hasSeparator: Bool, action: @escaping () -> Void) { - self.title = title - self.icon = icon - self.action = action - - self.separatorNode = ASDisplayNode() - self.separatorNode.backgroundColor = theme.contextMenu.itemSeparatorColor - - self.backgroundNode = ASDisplayNode() - self.backgroundNode.isAccessibilityElement = false - self.backgroundNode.backgroundColor = theme.contextMenu.itemBackgroundColor - - self.highlightedBackgroundNode = ASDisplayNode() - self.highlightedBackgroundNode.isAccessibilityElement = false - self.highlightedBackgroundNode.backgroundColor = theme.contextMenu.itemHighlightedBackgroundColor - self.highlightedBackgroundNode.alpha = 0.0 - - self.buttonNode = HighlightTrackingButtonNode() - self.buttonNode.isAccessibilityElement = true - self.buttonNode.accessibilityLabel = title - - self.titleNode = ImmediateTextNode() - self.titleNode.isAccessibilityElement = false - self.titleNode.maximumNumberOfLines = 1 - self.titleNode.attributedText = NSAttributedString(string: title, font: Font.regular(17.0), textColor: theme.contextMenu.primaryColor) - self.titleNode.isUserInteractionEnabled = false - self.titleNode.displaysAsynchronously = false - - self.iconNode = ASImageNode() - self.iconNode.image = icon.image(theme: theme) - self.iconNode.contentMode = .center - self.iconNode.isAccessibilityElement = false - self.iconNode.displaysAsynchronously = false - self.iconNode.displayWithoutProcessing = true - self.iconNode.isUserInteractionEnabled = false - - super.init() - - self.addSubnode(self.highlightedBackgroundNode) - self.addSubnode(self.titleNode) - self.addSubnode(self.iconNode) - self.addSubnode(self.buttonNode) - if hasSeparator { - self.addSubnode(self.separatorNode) - } - - self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) - self.buttonNode.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - strongSelf.setHighlighted(highlighted, animated: true) - } - } - } - - func setHighlighted(_ highlighted: Bool, animated: Bool) { - if highlighted == (self.highlightedBackgroundNode.alpha == 1.0) { - return - } - - if highlighted { - self.highlightedBackgroundNode.layer.removeAnimation(forKey: "opacity") - self.highlightedBackgroundNode.alpha = 1.0 - } else { - self.highlightedBackgroundNode.alpha = 0.0 - if animated { - self.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) - } - } - } - - func updateTheme(_ theme: PresentationTheme) { - self.separatorNode.backgroundColor = theme.contextMenu.itemSeparatorColor - self.backgroundNode.backgroundColor = theme.contextMenu.itemBackgroundColor - self.highlightedBackgroundNode.backgroundColor = theme.contextMenu.itemHighlightedBackgroundColor - self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.regular(17.0), textColor: theme.contextMenu.primaryColor) - self.iconNode.image = self.icon.image(theme: theme) - - if let maxWidth = self.maxWidth { - let _ = self.titleNode.updateLayout(CGSize(width: maxWidth - leftInset - rightInset, height: .greatestFiniteMagnitude)) - } - } - - func updateLayout(maxWidth: CGFloat) -> (CGFloat, CGFloat, (CGFloat) -> Void) { - self.maxWidth = maxWidth - - let titleSize = self.titleNode.updateLayout(CGSize(width: maxWidth - leftInset - rightInset, height: .greatestFiniteMagnitude)) - let height: CGFloat = 44.0 - - return (titleSize.width + leftInset + rightInset, height, { width in - self.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - titleSize.height) / 2.0)), size: titleSize) - - if let image = self.iconNode.image { - self.iconNode.frame = CGRect(origin: CGPoint(x: width - image.size.width - 12.0, y: floor((height - image.size.height) / 2.0)), size: image.size) - } - - self.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: height - UIScreenPixel), size: CGSize(width: width, height: UIScreenPixel)) - self.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: height)) - self.buttonNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: height)) - }) - } - - @objc private func buttonPressed() { - self.buttonNode.isUserInteractionEnabled = false - self.action() - } -} - -final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, ASScrollViewDelegate { - private let context: AccountContext - private var presentationData: PresentationData - private let sourceSendButton: ASDisplayNode - private let textFieldFrame: CGRect - private let textInputView: UITextView - private let attachment: Bool - private let forwardedCount: Int? - private let hasEntityKeyboard: Bool - - private let send: (() -> Void)? - private let cancel: (() -> Void)? - - private let effectView: UIVisualEffectView - private let dimNode: ASDisplayNode - - private let contentContainerNode: ASDisplayNode - private let contentNodes: [ActionSheetItemNode] - private let sendButtonNode: HighlightableButtonNode - - private let messageClipNode: ASDisplayNode - private let messageBackgroundNode: ASImageNode - private let messageEffectAnchorView: UIView - private let fromMessageTextScrollView: UIScrollView - private let fromMessageTextNode: ChatInputTextNode - private let toMessageTextScrollView: UIScrollView - private let toMessageTextNode: ChatInputTextNode - private var messageEffectReactionIcon: ReactionIconView? - private var messageEffectReactionText: ImmediateTextView? - private let scrollNode: ASScrollNode - - private(set) var selectedMessageEffect: (id: Int64, effect: AvailableMessageEffects.MessageEffect)? - private var reactionContextNode: ReactionContextNode? - - private var fromCustomEmojiContainerView: CustomEmojiContainerView? - private var toCustomEmojiContainerView: CustomEmojiContainerView? - - private var validLayout: ContainerViewLayout? - - private var sendButtonFrame: CGRect { - return self.sourceSendButton.view.convert(self.sourceSendButton.bounds, to: nil) - } - - private var animateInputField = false - - private var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)? - - private let messageEffectDisposable = MetaDisposable() - private var standaloneReactionAnimation: StandaloneReactionAnimation? - - init(context: AccountContext, presentationData: PresentationData, reminders: Bool, gesture: ContextGesture, sourceSendButton: ASDisplayNode, textInputView: UITextView, attachment: Bool, canSendWhenOnline: Bool, forwardedCount: Int?, hasEntityKeyboard: Bool, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?, send: (() -> Void)?, sendSilently: (() -> Void)?, sendWhenOnline: (() -> Void)?, schedule: (() -> Void)?, cancel: (() -> Void)?, reactionItems: [ReactionItem]?) { - self.context = context - self.presentationData = presentationData - self.sourceSendButton = sourceSendButton - self.textFieldFrame = textInputView.convert(textInputView.bounds, to: nil) - self.textInputView = textInputView - self.attachment = attachment - self.forwardedCount = forwardedCount - self.hasEntityKeyboard = hasEntityKeyboard - self.emojiViewProvider = emojiViewProvider - - self.send = send - self.cancel = cancel - - self.effectView = UIVisualEffectView() - - self.dimNode = ASDisplayNode() - self.dimNode.alpha = 1.0 - self.dimNode.backgroundColor = self.presentationData.theme.contextMenu.dimColor - - self.sendButtonNode = HighlightableButtonNode() - self.sendButtonNode.imageNode.displayWithoutProcessing = false - self.sendButtonNode.imageNode.displaysAsynchronously = false - self.sendButtonNode.accessibilityLabel = self.presentationData.strings.MediaPicker_Send - - self.messageClipNode = ASDisplayNode() - self.messageClipNode.clipsToBounds = true - self.messageClipNode.transform = CATransform3DMakeScale(1.0, -1.0, 1.0) - self.messageBackgroundNode = ASImageNode() - self.messageBackgroundNode.isUserInteractionEnabled = true - self.messageEffectAnchorView = UIView() - self.fromMessageTextNode = ChatInputTextNode(disableTiling: true) - self.fromMessageTextNode.textView.isScrollEnabled = false - self.fromMessageTextNode.isUserInteractionEnabled = false - self.fromMessageTextScrollView = UIScrollView() - self.fromMessageTextScrollView.isUserInteractionEnabled = false - self.toMessageTextNode = ChatInputTextNode(disableTiling: true) - self.toMessageTextNode.textView.isScrollEnabled = false - self.toMessageTextNode.isUserInteractionEnabled = false - self.toMessageTextScrollView = UIScrollView() - self.toMessageTextScrollView.alpha = 0.0 - self.toMessageTextScrollView.isUserInteractionEnabled = false - self.toMessageTextScrollView.clipsToBounds = false - - self.scrollNode = ASScrollNode() - self.scrollNode.transform = CATransform3DMakeScale(1.0, -1.0, 1.0) - - self.contentContainerNode = ASDisplayNode() - self.contentContainerNode.backgroundColor = self.presentationData.theme.contextMenu.backgroundColor - self.contentContainerNode.cornerRadius = 14.0 - self.contentContainerNode.clipsToBounds = true - - var contentNodes: [ActionSheetItemNode] = [] - if !reminders { - contentNodes.append(ActionSheetItemNode(theme: self.presentationData.theme, title: self.presentationData.strings.Conversation_SendMessage_SendSilently, icon: .sendWithoutSound, hasSeparator: true, action: { - sendSilently?() - })) - if canSendWhenOnline && schedule != nil { - contentNodes.append(ActionSheetItemNode(theme: self.presentationData.theme, title: self.presentationData.strings.Conversation_SendMessage_SendWhenOnline, icon: .sendWhenOnline, hasSeparator: true, action: { - sendWhenOnline?() - })) - } - } - if let _ = schedule { - contentNodes.append(ActionSheetItemNode(theme: self.presentationData.theme, title: reminders ? self.presentationData.strings.Conversation_SendMessage_SetReminder: self.presentationData.strings.Conversation_SendMessage_ScheduleMessage, icon: .schedule, hasSeparator: false, action: { - schedule?() - })) - } - self.contentNodes = contentNodes - - super.init() - - self.sendButtonNode.addTarget(self, action: #selector(self.sendButtonPressed), forControlEvents: .touchUpInside) - - if let attributedText = textInputView.attributedText, !attributedText.string.isEmpty { - self.animateInputField = true - if let textInputView = self.textInputView as? ChatInputTextView { - if let textTheme = textInputView.theme { - self.fromMessageTextNode.textView.theme = textTheme - - let mainColor = presentationData.theme.chat.message.outgoing.accentControlColor - let mappedLineStyle: ChatInputTextView.Theme.Quote.LineStyle - switch textTheme.quote.lineStyle { - case .solid: - mappedLineStyle = .solid(color: mainColor) - case .doubleDashed: - mappedLineStyle = .doubleDashed(mainColor: mainColor, secondaryColor: .clear) - case .tripleDashed: - mappedLineStyle = .tripleDashed(mainColor: mainColor, secondaryColor: .clear, tertiaryColor: .clear) - } - - self.toMessageTextNode.textView.theme = ChatInputTextView.Theme( - quote: ChatInputTextView.Theme.Quote( - background: mainColor.withMultipliedAlpha(0.1), - foreground: mainColor, - lineStyle: mappedLineStyle, - codeBackground: mainColor.withMultipliedAlpha(0.1), - codeForeground: mainColor - ) - ) - } - } - self.fromMessageTextNode.attributedText = attributedText - - if let toAttributedText = self.fromMessageTextNode.attributedText?.mutableCopy() as? NSMutableAttributedString { - toAttributedText.addAttribute(NSAttributedString.Key.foregroundColor, value: self.presentationData.theme.chat.message.outgoing.primaryTextColor, range: NSMakeRange(0, (toAttributedText.string as NSString).length)) - self.toMessageTextNode.attributedText = toAttributedText - } - } else { - if let _ = forwardedCount { - self.animateInputField = true - } - self.fromMessageTextNode.attributedText = NSAttributedString(string: self.attachment ? self.presentationData.strings.MediaPicker_AddCaption : self.presentationData.strings.Conversation_InputTextPlaceholder, attributes: [NSAttributedString.Key.foregroundColor: self.presentationData.theme.chat.inputPanel.inputPlaceholderColor, NSAttributedString.Key.font: Font.regular(self.presentationData.chatFontSize.baseDisplaySize)]) - - self.toMessageTextNode.attributedText = NSAttributedString(string: self.presentationData.strings.ForwardedMessages(Int32(forwardedCount ?? 0)), attributes: [NSAttributedString.Key.foregroundColor: self.presentationData.theme.chat.message.outgoing.primaryTextColor, NSAttributedString.Key.font: Font.regular(self.presentationData.chatFontSize.baseDisplaySize)]) - } - self.messageBackgroundNode.contentMode = .scaleToFill - - let outgoing: PresentationThemeBubbleColorComponents = self.presentationData.chatWallpaper.isEmpty ? self.presentationData.theme.chat.message.outgoing.bubble.withoutWallpaper : self.presentationData.theme.chat.message.outgoing.bubble.withWallpaper - - let maxCornerRadius = self.presentationData.chatBubbleCorners.mainRadius - self.messageBackgroundNode.image = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: maxCornerRadius, incoming: false, fillColor: outgoing.fill.last ?? outgoing.fill[0], strokeColor: outgoing.fill.count > 1 ? outgoing.stroke : .clear, neighbors: .none, theme: self.presentationData.theme.chat, wallpaper: self.presentationData.chatWallpaper, knockout: false) - - self.view.addSubview(self.effectView) - self.addSubnode(self.dimNode) - - self.addSubnode(self.contentContainerNode) - self.addSubnode(self.scrollNode) - - self.addSubnode(self.sendButtonNode) - self.scrollNode.addSubnode(self.messageClipNode) - self.messageClipNode.addSubnode(self.messageBackgroundNode) - self.messageClipNode.view.addSubview(self.messageEffectAnchorView) - self.messageClipNode.view.addSubview(self.fromMessageTextScrollView) - self.fromMessageTextScrollView.addSubview(self.fromMessageTextNode.view) - self.messageClipNode.view.addSubview(self.toMessageTextScrollView) - self.toMessageTextScrollView.addSubview(self.toMessageTextNode.view) - - self.contentNodes.forEach(self.contentContainerNode.addSubnode) - - gesture.externalUpdated = { [weak self] view, location in - guard let strongSelf = self else { - return - } - for contentNode in strongSelf.contentNodes { - let localPoint = contentNode.view.convert(location, from: view) - if contentNode.bounds.contains(localPoint) { - contentNode.setHighlighted(true, animated: false) - } else { - contentNode.setHighlighted(false, animated: false) - } - } - } - - gesture.externalEnded = { [weak self] viewAndLocation in - guard let strongSelf = self else { - return - } - for contentNode in strongSelf.contentNodes { - if let (view, location) = viewAndLocation { - let localPoint = contentNode.view.convert(location, from: view) - if contentNode.bounds.contains(localPoint) { - contentNode.action() - } else { - contentNode.setHighlighted(false, animated: false) - } - } else { - contentNode.setHighlighted(false, animated: false) - } - } - } - - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - if let reactionItems, !reactionItems.isEmpty { - //TODO:localize - let reactionContextNode = ReactionContextNode( - context: context, - animationCache: context.animationCache, - presentationData: presentationData, - items: reactionItems.map { ReactionContextItem.reaction(item: $0, icon: .none) }, - selectedItems: Set(), - title: "Add an animated effect", - reactionsLocked: false, - alwaysAllowPremiumReactions: false, - allPresetReactionsAreAvailable: true, - getEmojiContent: { animationCache, animationRenderer in - return EmojiPagerContentComponent.messageEffectsInputData( - context: context, - animationCache: animationCache, - animationRenderer: animationRenderer, - hasSearch: true, - hideBackground: false - ) - }, - isExpandedUpdated: { [weak self] transition in - guard let self else { - return - } - self.update(transition: transition) - }, - requestLayout: { [weak self] transition in - guard let self else { - return - } - self.update(transition: transition) - }, - requestUpdateOverlayWantsToBeBelowKeyboard: { [weak self] transition in - guard let self else { - return - } - self.update(transition: transition) - } - ) - reactionContextNode.reactionSelected = { [weak self] updateReaction, _ in - guard let self, let reactionContextNode = self.reactionContextNode else { - return - } - - guard case let .custom(sourceEffectId, _) = updateReaction else { - return - } - - let messageEffect: Signal - messageEffect = context.engine.stickers.availableMessageEffects() - |> take(1) - |> map { availableMessageEffects -> AvailableMessageEffects.MessageEffect? in - guard let availableMessageEffects else { - return nil - } - for messageEffect in availableMessageEffects.messageEffects { - if messageEffect.id == sourceEffectId || messageEffect.effectSticker.fileId.id == sourceEffectId { - return messageEffect - } - } - return nil - } - - self.messageEffectDisposable.set((combineLatest( - messageEffect, - ReactionContextNode.randomGenericReactionEffect(context: context) - ) - |> deliverOnMainQueue).startStrict(next: { [weak self] messageEffect, path in - guard let self else { - return - } - guard let messageEffect else { - return - } - let effectId = messageEffect.id - - let reactionItem = ReactionItem( - reaction: ReactionItem.Reaction(rawValue: updateReaction.reaction), - appearAnimation: messageEffect.effectSticker, - stillAnimation: messageEffect.effectSticker, - listAnimation: messageEffect.effectSticker, - largeListAnimation: messageEffect.effectSticker, - applicationAnimation: nil, - largeApplicationAnimation: nil, - isCustom: true - ) - - if let selectedMessageEffect = self.selectedMessageEffect { - if selectedMessageEffect.id == effectId { - self.selectedMessageEffect = nil - reactionContextNode.selectedItems = Set([]) - - if let standaloneReactionAnimation = self.standaloneReactionAnimation { - self.standaloneReactionAnimation = nil - standaloneReactionAnimation.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { [weak standaloneReactionAnimation] _ in - standaloneReactionAnimation?.removeFromSupernode() - }) - } - - self.update(transition: .animated(duration: 0.2, curve: .easeInOut)) - return - } else { - self.selectedMessageEffect = (id: effectId, effect: messageEffect) - reactionContextNode.selectedItems = Set([AnyHashable(updateReaction.reaction)]) - self.update(transition: .animated(duration: 0.2, curve: .easeInOut)) - } - } else { - self.selectedMessageEffect = (id: effectId, effect: messageEffect) - reactionContextNode.selectedItems = Set([AnyHashable(updateReaction.reaction)]) - self.update(transition: .animated(duration: 0.2, curve: .easeInOut)) - } - - guard let targetView = self.messageEffectReactionIcon ?? self.messageEffectReactionText else { - return - } - - if let standaloneReactionAnimation = self.standaloneReactionAnimation { - self.standaloneReactionAnimation = nil - standaloneReactionAnimation.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { [weak standaloneReactionAnimation] _ in - standaloneReactionAnimation?.removeFromSupernode() - }) - } - - let genericReactionEffect = path - - let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: genericReactionEffect) - standaloneReactionAnimation.frame = self.bounds - self.standaloneReactionAnimation = standaloneReactionAnimation - self.addSubnode(standaloneReactionAnimation) - - var customEffectResource: MediaResource? - if let effectAnimation = messageEffect.effectAnimation { - customEffectResource = effectAnimation.resource - } else { - let effectSticker = messageEffect.effectSticker - if let effectFile = effectSticker.videoThumbnails.first { - customEffectResource = effectFile.resource - } - } - - standaloneReactionAnimation.animateReactionSelection( - context: context, - theme: self.presentationData.theme, - animationCache: context.animationCache, - reaction: reactionItem, - customEffectResource: customEffectResource, - avatarPeers: [], - playHaptic: true, - isLarge: true, - playCenterReaction: false, - targetView: targetView, - addStandaloneReactionAnimation: { standaloneReactionAnimation in - /*guard let strongSelf = self else { - return - } - strongSelf.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation) - standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds - strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation)*/ - }, - completion: { [weak standaloneReactionAnimation] in - standaloneReactionAnimation?.removeFromSupernode() - } - ) - })) - } - reactionContextNode.displayTail = true - reactionContextNode.forceTailToRight = false - reactionContextNode.forceDark = false - self.reactionContextNode = reactionContextNode - self.addSubnode(reactionContextNode) - } - - self.update(transition: .immediate) - } - - deinit { - self.messageEffectDisposable.dispose() - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - let result = super.hitTest(point, with: event) - if result != self.scrollNode.view { - return result - } else { - return self.dimNode.view - } - } - - override func didLoad() { - super.didLoad() - - self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) - - self.scrollNode.view.showsVerticalScrollIndicator = false - self.scrollNode.view.delaysContentTouches = false - self.scrollNode.view.delegate = self.wrappedScrollViewDelegate - self.scrollNode.view.alwaysBounceVertical = true - if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { - self.scrollNode.view.contentInsetAdjustmentBehavior = .never - } - - self.effectView.effect = makeCustomZoomBlurEffect(isLight: self.presentationData.theme.rootController.keyboardColor == .light) - - if let snapshotView = self.sourceSendButton.view.snapshotView(afterScreenUpdates: false) { - self.sendButtonNode.view.addSubview(snapshotView) - } - } - - func updateTextContents() { - var customEmojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)] = [] - - let textInputNode = self.fromMessageTextNode - if let attributedText = textInputNode.attributedText { - let beginning = textInputNode.textView.beginningOfDocument - attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, range, _ in - if let value = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute { - if let start = textInputNode.textView.position(from: beginning, offset: range.location), let end = textInputNode.textView.position(from: start, offset: range.length), let textRange = textInputNode.textView.textRange(from: start, to: end) { - let textRects = textInputNode.textView.selectionRects(for: textRange) - for textRect in textRects { - customEmojiRects.append((textRect.rect, value)) - break - } - } - } - }) - } - - self.updateTextContents(rects: customEmojiRects, textInputNode: self.fromMessageTextNode, from: true) - self.updateTextContents(rects: customEmojiRects, textInputNode: self.toMessageTextNode, from: false) - } - - func updateTextContents(rects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)], textInputNode: ChatInputTextNode, from: Bool) { - if !rects.isEmpty { - let customEmojiContainerView: CustomEmojiContainerView - if from, let current = self.fromCustomEmojiContainerView { - customEmojiContainerView = current - } else if !from, let current = self.toCustomEmojiContainerView { - customEmojiContainerView = current - } else { - customEmojiContainerView = CustomEmojiContainerView(emojiViewProvider: { [weak self] emoji in - guard let strongSelf = self, let emojiViewProvider = strongSelf.emojiViewProvider else { - return nil - } - return emojiViewProvider(emoji) - }) - customEmojiContainerView.isUserInteractionEnabled = false - textInputNode.textView.addSubview(customEmojiContainerView) - if from { - self.fromCustomEmojiContainerView = customEmojiContainerView - } else { - self.toCustomEmojiContainerView = customEmojiContainerView - } - } - - customEmojiContainerView.update(emojiRects: rects) - } else { - if from, let customEmojiContainerView = self.fromCustomEmojiContainerView { - customEmojiContainerView.removeFromSuperview() - self.fromCustomEmojiContainerView = nil - } else if !from, let customEmojiContainerView = self.toCustomEmojiContainerView { - customEmojiContainerView.removeFromSuperview() - self.fromCustomEmojiContainerView = nil - } - } - } - - func updatePresentationData(_ presentationData: PresentationData) { - guard presentationData.theme !== self.presentationData.theme else { - return - } - self.presentationData = presentationData - - self.effectView.effect = makeCustomZoomBlurEffect(isLight: self.presentationData.theme.rootController.keyboardColor == .light) - - self.dimNode.backgroundColor = presentationData.theme.contextMenu.dimColor - - self.contentContainerNode.backgroundColor = self.presentationData.theme.contextMenu.backgroundColor - - if let toAttributedText = self.textInputView.attributedText?.mutableCopy() as? NSMutableAttributedString { - toAttributedText.addAttribute(NSAttributedString.Key.foregroundColor, value: self.presentationData.theme.chat.message.outgoing.primaryTextColor, range: NSMakeRange(0, (toAttributedText.string as NSString).length)) - self.toMessageTextNode.attributedText = toAttributedText - } - - let outgoing: PresentationThemeBubbleColorComponents = self.presentationData.chatWallpaper.isEmpty ? self.presentationData.theme.chat.message.outgoing.bubble.withoutWallpaper : self.presentationData.theme.chat.message.outgoing.bubble.withWallpaper - let maxCornerRadius = self.presentationData.chatBubbleCorners.mainRadius - self.messageBackgroundNode.image = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: maxCornerRadius, incoming: false, fillColor: outgoing.fill.last ?? outgoing.fill[0], strokeColor: outgoing.fill.count > 1 ? outgoing.stroke : .clear, neighbors: .none, theme: self.presentationData.theme.chat, wallpaper: self.presentationData.chatWallpaper, knockout: false) - - for node in self.contentNodes { - node.updateTheme(presentationData.theme) - } - } - - func animateIn() { - guard let layout = self.validLayout else { - return - } - - self.textInputView.setContentOffset(self.textInputView.contentOffset, animated: false) - - self.effectView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - self.contentContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - self.messageBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - - self.sourceSendButton.isHidden = true - if self.animateInputField { - self.fromMessageTextScrollView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) - self.toMessageTextScrollView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, removeOnCompletion: false) - } else { - self.messageBackgroundNode.isHidden = true - self.fromMessageTextScrollView.isHidden = true - self.toMessageTextScrollView.isHidden = true - } - - let duration = 0.4 - self.sendButtonNode.layer.animateScale(from: 0.75, to: 1.0, duration: 0.2, timingFunction: CAMediaTimingFunctionName.linear.rawValue) - self.sendButtonNode.layer.animatePosition(from: self.sendButtonFrame.center, to: self.sendButtonNode.position, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) - - var initialWidth = self.textFieldFrame.width + 32.0 - if self.textInputView.attributedText.string.isEmpty { - initialWidth = ceil(layout.size.width - self.textFieldFrame.origin.x - self.sendButtonFrame.width - layout.safeInsets.left - layout.safeInsets.right + 21.0) - } - - let fromFrame = CGRect(origin: CGPoint(), size: CGSize(width: initialWidth, height: self.textFieldFrame.height + 2.0)) - let delta = (fromFrame.height - self.messageClipNode.bounds.height) / 2.0 - - var inputHeight = layout.inputHeight ?? 0.0 - if self.hasEntityKeyboard { - inputHeight = layout.standardInputHeight - } - - var clipDelta = delta - if inputHeight < 70.0 || layout.isNonExclusive { - clipDelta -= self.contentContainerNode.frame.height + 16.0 - } - - self.messageClipNode.layer.animateBounds(from: fromFrame, to: self.messageClipNode.bounds, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) - self.messageClipNode.layer.animatePosition(from: CGPoint(x: (self.messageClipNode.bounds.width - initialWidth) / 2.0, y: clipDelta), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true, completion: { [weak self] _ in - if let strongSelf = self { - strongSelf.insertSubnode(strongSelf.contentContainerNode, aboveSubnode: strongSelf.scrollNode) - } - }) - - self.messageBackgroundNode.layer.animateBounds(from: fromFrame, to: self.messageBackgroundNode.bounds, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) - self.messageBackgroundNode.layer.animatePosition(from: CGPoint(x: (initialWidth - self.messageClipNode.bounds.width) / 2.0, y: delta), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - - var textXOffset: CGFloat = 0.0 - let textYOffset = self.textInputView.contentSize.height - self.textInputView.contentOffset.y - self.textInputView.frame.height - if self.textInputView.numberOfLines == 1 && self.textInputView.isRTL { - textXOffset = initialWidth - self.messageClipNode.bounds.width - } - self.fromMessageTextScrollView.layer.animatePosition(from: CGPoint(x: textXOffset, y: delta * 2.0 + textYOffset), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - self.toMessageTextScrollView.layer.animatePosition(from: CGPoint(x: textXOffset, y: delta * 2.0 + textYOffset), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - - let contentOffset = CGPoint(x: self.sendButtonFrame.midX - self.contentContainerNode.frame.midX, y: self.sendButtonFrame.midY - self.contentContainerNode.frame.midY) - - let springDuration: Double = 0.42 - let springDamping: CGFloat = 104.0 - self.contentContainerNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping) - self.contentContainerNode.layer.animateSpring(from: NSValue(cgPoint: contentOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true) - - if let reactionContextNode = self.reactionContextNode { - reactionContextNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: 0.0, y: -clipDelta)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true) - } - - Queue.mainQueue().after(0.01, { - if self.animateInputField { - self.textInputView.isHidden = true - } - self.updateTextContents() - }) - } - - func animateOut(cancel: Bool, completion: @escaping () -> Void) { - guard let layout = self.validLayout else { - return - } - - self.isUserInteractionEnabled = false - - self.scrollNode.view.setContentOffset(self.scrollNode.view.contentOffset, animated: false) - - var completedEffect = false - var completedButton = false - var completedBubble = false - var completedAlpha = false - - var completed = false - let intermediateCompletion: () -> Void = { [weak self] in - if completedEffect && completedButton && completedBubble && completedAlpha && !completed { - completed = true - self?.textInputView.isHidden = false - self?.sourceSendButton.isHidden = false - completion() - } - } - - self.effectView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in - completedEffect = true - intermediateCompletion() - }) - self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) - self.contentContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in }) - - Queue.mainQueue().after(0.45) { - if !completed { - completed = true - self.textInputView.isHidden = false - self.sourceSendButton.isHidden = false - completion() - } - } - - if self.animateInputField { - if cancel { - self.fromMessageTextScrollView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, delay: 0.15, removeOnCompletion: false) - self.toMessageTextScrollView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, delay: 0.15, removeOnCompletion: false) - self.messageBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, delay: 0.15, removeOnCompletion: false, completion: { _ in - completedAlpha = true - intermediateCompletion() - }) - } else { - self.textInputView.isHidden = false - self.messageClipNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in - completedAlpha = true - intermediateCompletion() - }) - } - } else { - completedAlpha = true - } - - let duration = 0.4 - self.sendButtonNode.layer.animatePosition(from: self.sendButtonNode.position, to: self.sendButtonFrame.center, duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in - completedButton = true - intermediateCompletion() - }) - - if !cancel { - self.sourceSendButton.isHidden = false - self.sendButtonNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.2, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false) - self.sendButtonNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false) - } - - var initialWidth = self.textFieldFrame.width + 32.0 - if self.textInputView.attributedText.string.isEmpty { - initialWidth = ceil(layout.size.width - self.textFieldFrame.origin.x - self.sendButtonFrame.width - layout.safeInsets.left - layout.safeInsets.right + 21.0) - } - - let toFrame = CGRect(origin: CGPoint(x: 0.0, y: -1.0), size: CGSize(width: initialWidth, height: self.textFieldFrame.height + 2.0)) - let delta = (toFrame.height - self.messageClipNode.bounds.height) / 2.0 - - if cancel && self.animateInputField { - var inputHeight = layout.inputHeight ?? 0.0 - if self.hasEntityKeyboard { - inputHeight = layout.standardInputHeight - } - - var clipDelta = delta - if inputHeight < 70.0 || layout.isNonExclusive { - clipDelta -= self.contentContainerNode.frame.height + 16.0 - } - - self.messageClipNode.layer.animateBounds(from: self.messageClipNode.bounds, to: toFrame.offsetBy(dx: 0.0, dy: 1.0), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in - completedBubble = true - intermediateCompletion() - }) - self.messageClipNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: (self.messageClipNode.bounds.width - initialWidth) / 2.0, y: clipDelta + self.scrollNode.view.contentOffset.y), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) - - self.messageBackgroundNode.layer.animateBounds(from: self.messageBackgroundNode.bounds, to: toFrame, duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) - self.messageBackgroundNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: (initialWidth - self.messageClipNode.bounds.width) / 2.0, y: delta), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) - - var textXOffset: CGFloat = 0.0 - let textYOffset = self.textInputView.contentSize.height - self.textInputView.contentOffset.y - self.textInputView.frame.height - if self.textInputView.numberOfLines == 1 && self.textInputView.isRTL { - textXOffset = initialWidth - self.messageClipNode.bounds.width - } - self.fromMessageTextScrollView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: textXOffset, y: delta * 2.0 + textYOffset), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) - self.toMessageTextScrollView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: textXOffset, y: delta * 2.0 + textYOffset), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) - - if let reactionContextNode = self.reactionContextNode { - reactionContextNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -clipDelta), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) - reactionContextNode.animateOut(to: nil, animatingOutToReaction: false) - } - - if let standaloneReactionAnimation = self.standaloneReactionAnimation { - self.standaloneReactionAnimation = nil - standaloneReactionAnimation.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -clipDelta), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) - standaloneReactionAnimation.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { [weak standaloneReactionAnimation] _ in - standaloneReactionAnimation?.removeFromSupernode() - }) - } - } else { - completedBubble = true - if let reactionContextNode = self.reactionContextNode { - reactionContextNode.animateOut(to: nil, animatingOutToReaction: false) - } - } - - let contentOffset = CGPoint(x: self.sendButtonFrame.midX - self.contentContainerNode.frame.midX, y: self.sendButtonFrame.midY - self.contentContainerNode.frame.midY) - - self.contentContainerNode.layer.animatePosition(from: CGPoint(), to: contentOffset, duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) - self.contentContainerNode.layer.animateScale(from: 1.0, to: 0.1, duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) - } - - func scrollViewDidScroll(_ scrollView: UIScrollView) { - if let layout = self.validLayout { - self.containerLayoutUpdated(layout, transition: .immediate) - } - } - - private func update(transition: ContainedViewLayoutTransition) { - if let validLayout = self.validLayout { - self.containerLayoutUpdated(validLayout, transition: transition) - } - } - - func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - self.validLayout = layout - - transition.updateFrame(view: self.effectView, frame: CGRect(origin: CGPoint(), size: layout.size)) - transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - - let sideInset: CGFloat = self.sendButtonFrame.width - 1.0 - - var contentSize = CGSize() - contentSize.width = min(layout.size.width - 40.0, 250.0) - var applyNodes: [(ASDisplayNode, CGFloat, (CGFloat) -> Void)] = [] - for itemNode in self.contentNodes { - let (width, height, apply) = itemNode.updateLayout(maxWidth: layout.size.width - 16.0 * 2.0) - applyNodes.append((itemNode, height, apply)) - contentSize.width = max(contentSize.width, width) - contentSize.height += height - } - - let menuHeightWithInset = contentSize.height + 16.0 - - var insets = layout.insets(options: [.statusBar, .input]) - var inputHeight = layout.inputHeight ?? 0.0 - if self.hasEntityKeyboard { - insets.bottom = max(insets.bottom, layout.standardInputHeight) - inputHeight = layout.standardInputHeight - } - - let contentOffset = self.scrollNode.view.contentOffset.y - let initialSendButtonFrame = self.sendButtonFrame - - var contentOrigin: CGPoint - if initialSendButtonFrame.width > initialSendButtonFrame.height * 1.2 { - contentOrigin = CGPoint(x: layout.size.width - contentSize.width - layout.safeInsets.right - 5.0, y: initialSendButtonFrame.minY - contentSize.height) - } else { - contentOrigin = CGPoint(x: layout.size.width - sideInset - contentSize.width - layout.safeInsets.right, y: layout.size.height - 6.0 - insets.bottom - contentSize.height - 6.0) - } - if inputHeight > 70.0 && !layout.isNonExclusive && self.animateInputField { - contentOrigin.y += menuHeightWithInset - } - contentOrigin.y = min(contentOrigin.y + contentOffset, layout.size.height - 6.0 - layout.intrinsicInsets.bottom - contentSize.height - 6.0) - - transition.updateFrame(node: self.contentContainerNode, frame: CGRect(origin: contentOrigin, size: contentSize)) - var nextY: CGFloat = 0.0 - for (itemNode, height, apply) in applyNodes { - transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: nextY), size: CGSize(width: contentSize.width, height: height))) - apply(contentSize.width) - nextY += height - } - - var sendButtonFrame = CGRect(origin: CGPoint(x: layout.size.width - initialSendButtonFrame.width + 1.0 - UIScreenPixel - layout.safeInsets.right, y: layout.size.height - insets.bottom - initialSendButtonFrame.height), size: initialSendButtonFrame.size) - if (inputHeight < 70.0 || layout.isNonExclusive) && self.animateInputField { - sendButtonFrame.origin.y -= menuHeightWithInset - } - sendButtonFrame.origin.y = min(sendButtonFrame.origin.y + contentOffset, layout.size.height - layout.intrinsicInsets.bottom - initialSendButtonFrame.height) - transition.updateFrameAsPositionAndBounds(node: self.sendButtonNode, frame: sendButtonFrame) - - var messageFrame = self.textFieldFrame - messageFrame.size.width += 32.0 - messageFrame.origin.x -= 13.0 - messageFrame.origin.y = layout.size.height - messageFrame.origin.y - messageFrame.size.height - 1.0 - - let messageHeightAddition: CGFloat = max(0.0, 35.0 - messageFrame.size.height) - - if inputHeight < 70.0 || layout.isNonExclusive { - messageFrame.origin.y += menuHeightWithInset - } - - if self.textInputView.attributedText.string.isEmpty { - messageFrame.size.width = ceil(layout.size.width - messageFrame.origin.x - sendButtonFrame.width - layout.safeInsets.left - layout.safeInsets.right + 8.0) - } - - var messageOriginDelta: CGFloat = 0.0 - if self.textInputView.numberOfLines == 1 || self.textInputView.attributedText.string.isEmpty { - let textWidth = min(self.toMessageTextNode.textView.sizeThatFits(layout.size).width + 36.0, messageFrame.width) - messageOriginDelta = messageFrame.width - textWidth - messageFrame.origin.x += messageOriginDelta - messageFrame.size.width = textWidth - } - - let messageHeight = max(messageFrame.size.height, self.textInputView.contentSize.height + 2.0) - messageFrame.size.height = messageHeight - - transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - - var scrollContentSize = CGSize(width: layout.size.width, height: messageHeight + max(0.0, messageFrame.origin.y)) - if messageHeight > layout.size.height - messageFrame.origin.y { - scrollContentSize.height += insets.top + 16.0 - } - self.scrollNode.view.contentSize = scrollContentSize - - let clipFrame = messageFrame - transition.updateFrame(node: self.messageClipNode, frame: clipFrame) - - var backgroundFrame = CGRect(origin: CGPoint(), size: messageFrame.size) - backgroundFrame.origin.y -= messageHeightAddition * 0.5 - backgroundFrame.size.height += messageHeightAddition - transition.updateFrame(node: self.messageBackgroundNode, frame: backgroundFrame) - - transition.updateFrame(view: self.messageEffectAnchorView, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX - 20.0, y: backgroundFrame.maxY - 10.0), size: CGSize(width: 1.0, height: 1.0))) - - var textFrame = self.textFieldFrame - textFrame.origin = CGPoint(x: 13.0, y: 6.0 - UIScreenPixel) - textFrame.size.height = self.textInputView.contentSize.height - - if let textInputView = self.textInputView as? ChatInputTextView { - textFrame.origin.y -= 5.0 - - self.fromMessageTextNode.textView.defaultTextContainerInset = textInputView.defaultTextContainerInset - self.toMessageTextNode.textView.defaultTextContainerInset = textInputView.defaultTextContainerInset - } - /*if let textInputView = self.textInputView as? ChatInputTextView { - textFrame.size.width -= textInputView.defaultTextContainerInset.right - } else { - textFrame.size.width -= self.textInputView.textContainerInset.right - }*/ - - if self.textInputView.isRTL { - textFrame.origin.x -= messageOriginDelta - } - - self.fromMessageTextScrollView.frame = textFrame - self.fromMessageTextNode.frame = CGRect(origin: CGPoint(), size: textFrame.size) - self.fromMessageTextNode.updateLayout(size: textFrame.size) - - self.toMessageTextScrollView.frame = textFrame - self.toMessageTextNode.frame = CGRect(origin: CGPoint(), size: textFrame.size) - self.toMessageTextNode.updateLayout(size: textFrame.size) - - if let selectedMessageEffect = self.selectedMessageEffect { - if let iconFile = selectedMessageEffect.effect.staticIcon { - let messageEffectReactionIcon: ReactionIconView - var iconTransition = transition - var animateIn = false - if let current = self.messageEffectReactionIcon { - messageEffectReactionIcon = current - } else { - iconTransition = .immediate - animateIn = true - messageEffectReactionIcon = ReactionIconView(frame: CGRect()) - self.messageEffectReactionIcon = messageEffectReactionIcon - self.toMessageTextScrollView.addSubview(messageEffectReactionIcon) - } - - let iconSize = CGSize(width: 10.0, height: 10.0) - messageEffectReactionIcon.update(size: iconSize, context: self.context, file: iconFile, fileId: iconFile.fileId.id, animationCache: self.context.animationCache, animationRenderer: self.context.animationRenderer, tintColor: nil, placeholderColor: self.presentationData.theme.chat.message.stickerPlaceholderColor.withWallpaper, animateIdle: false, reaction: .custom(selectedMessageEffect.id), transition: iconTransition) - - let iconFrame = CGRect(origin: CGPoint(x: self.toMessageTextNode.frame.minX + messageFrame.width - 30.0 + 2.0 - iconSize.width, y: self.toMessageTextNode.frame.maxY - 6.0 - iconSize.height), size: iconSize) - iconTransition.updateFrame(view: messageEffectReactionIcon, frame: iconFrame) - if animateIn && transition.isAnimated { - transition.animateTransformScale(view: messageEffectReactionIcon, from: 0.001) - messageEffectReactionIcon.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) - } - - if let messageEffectReactionText = self.messageEffectReactionText { - self.messageEffectReactionText = nil - - transition.updateTransformScale(layer: messageEffectReactionText.layer, scale: 0.001) - transition.updateAlpha(layer: messageEffectReactionText.layer, alpha: 0.0, completion: { [weak messageEffectReactionText] _ in - messageEffectReactionText?.removeFromSuperview() - }) - } - } else { - let messageEffectReactionText: ImmediateTextView - var iconTransition = transition - var animateIn = false - if let current = self.messageEffectReactionText { - messageEffectReactionText = current - } else { - iconTransition = .immediate - animateIn = true - messageEffectReactionText = ImmediateTextView() - self.messageEffectReactionText = messageEffectReactionText - self.toMessageTextScrollView.addSubview(messageEffectReactionText) - } - - let iconSize = CGSize(width: 10.0, height: 10.0) - messageEffectReactionText.attributedText = NSAttributedString(string: selectedMessageEffect.effect.emoticon, font: Font.regular(9.0), textColor: .black) - let textSize = messageEffectReactionText.updateLayout(CGSize(width: 100.0, height: 100.0)) - - let iconFrame = CGRect(origin: CGPoint(x: self.toMessageTextNode.frame.minX + messageFrame.width - 30.0 + 2.0 - iconSize.width + floorToScreenPixels((iconSize.width - textSize.width) * 0.5), y: self.toMessageTextNode.frame.maxY - 6.0 - iconSize.height + floorToScreenPixels((iconSize.height - textSize.height) * 0.5)), size: textSize) - iconTransition.updateFrame(view: messageEffectReactionText, frame: iconFrame) - if animateIn && transition.isAnimated { - transition.animateTransformScale(view: messageEffectReactionText, from: 0.001) - messageEffectReactionText.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) - } - - if let messageEffectReactionIcon = self.messageEffectReactionIcon { - self.messageEffectReactionIcon = nil - - transition.updateTransformScale(layer: messageEffectReactionIcon.layer, scale: 0.001) - transition.updateAlpha(layer: messageEffectReactionIcon.layer, alpha: 0.0, completion: { [weak messageEffectReactionIcon] _ in - messageEffectReactionIcon?.removeFromSuperview() - }) - } - } - } else { - if let messageEffectReactionIcon = self.messageEffectReactionIcon { - self.messageEffectReactionIcon = nil - - transition.updateTransformScale(layer: messageEffectReactionIcon.layer, scale: 0.001) - transition.updateAlpha(layer: messageEffectReactionIcon.layer, alpha: 0.0, completion: { [weak messageEffectReactionIcon] _ in - messageEffectReactionIcon?.removeFromSuperview() - }) - } - if let messageEffectReactionText = self.messageEffectReactionText { - self.messageEffectReactionText = nil - - transition.updateTransformScale(layer: messageEffectReactionText.layer, scale: 0.001) - transition.updateAlpha(layer: messageEffectReactionText.layer, alpha: 0.0, completion: { [weak messageEffectReactionText] _ in - messageEffectReactionText?.removeFromSuperview() - }) - } - } - - if let reactionContextNode = self.reactionContextNode { - let isFirstTime = reactionContextNode.bounds.isEmpty - - let size = layout.size - var reactionsAnchorRect = messageFrame - reactionsAnchorRect.origin.y = layout.size.height - reactionsAnchorRect.maxY - reactionsAnchorRect.origin.y -= 1.0 - transition.updateFrame(node: reactionContextNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)) - reactionContextNode.updateLayout(size: size, insets: UIEdgeInsets(), anchorRect: reactionsAnchorRect, centerAligned: false, isCoveredByInput: false, isAnimatingOut: false, transition: transition) - reactionContextNode.updateIsIntersectingContent(isIntersectingContent: false, transition: .immediate) - if isFirstTime { - reactionContextNode.animateIn(from: reactionsAnchorRect) - } - } - } - - @objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - self.cancel?() - } - } - - @objc private func sendButtonPressed() { - self.sendButtonNode.isUserInteractionEnabled = false - self.send?() - } -} - -final class CustomEmojiContainerView: UIView { - private let emojiViewProvider: (ChatTextInputTextCustomEmojiAttribute) -> UIView? - - private var emojiLayers: [InlineStickerItemLayer.Key: UIView] = [:] - - init(emojiViewProvider: @escaping (ChatTextInputTextCustomEmojiAttribute) -> UIView?) { - self.emojiViewProvider = emojiViewProvider - - super.init(frame: CGRect()) - } - - required init(coder: NSCoder) { - preconditionFailure() - } - - func update(emojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)]) { - var nextIndexById: [Int64: Int] = [:] - - var validKeys = Set() - for (rect, emoji) in emojiRects { - let index: Int - if let nextIndex = nextIndexById[emoji.fileId] { - index = nextIndex - } else { - index = 0 - } - nextIndexById[emoji.fileId] = index + 1 - - let key = InlineStickerItemLayer.Key(id: emoji.fileId, index: index) - - let view: UIView - if let current = self.emojiLayers[key] { - view = current - } else if let newView = self.emojiViewProvider(emoji) { - view = newView - self.addSubview(newView) - self.emojiLayers[key] = view - } else { - continue - } - - let size = CGSize(width: 24.0, height: 24.0) - - view.frame = CGRect(origin: CGPoint(x: floor(rect.midX - size.width / 2.0), y: floor(rect.midY - size.height / 2.0)), size: size) - - validKeys.insert(key) - } - - var removeKeys: [InlineStickerItemLayer.Key] = [] - for (key, view) in self.emojiLayers { - if !validKeys.contains(key) { - removeKeys.append(key) - view.removeFromSuperview() - } - } - for key in removeKeys { - self.emojiLayers.removeValue(forKey: key) - } - } -} diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift index 1bbad11d98..d2502c5ab8 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift @@ -56,18 +56,13 @@ final class ChatSendMessageContextScreenComponent: Component { let context: AccountContext let updatedPresentationData: (initial: PresentationData, signal: Signal)? let peerId: EnginePeer.Id? - let isScheduledMessages: Bool - let forwardMessageIds: [EngineMessage.Id]? + let params: SendMessageActionSheetControllerParams let hasEntityKeyboard: Bool let gesture: ContextGesture let sourceSendButton: ASDisplayNode let textInputView: UITextView - let mediaPreview: ChatSendMessageContextScreenMediaPreview? - let mediaCaptionIsAbove: (Bool, (Bool) -> Void)? let emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)? let wallpaperBackgroundNode: WallpaperBackgroundNode? - let attachment: Bool - let canSendWhenOnline: Bool let completion: () -> Void let sendMessage: (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.SendParameters?) -> Void let schedule: (ChatSendMessageActionSheetController.SendParameters?) -> Void @@ -80,18 +75,13 @@ final class ChatSendMessageContextScreenComponent: Component { context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: EnginePeer.Id?, - isScheduledMessages: Bool, - forwardMessageIds: [EngineMessage.Id]?, + params: SendMessageActionSheetControllerParams, hasEntityKeyboard: Bool, gesture: ContextGesture, sourceSendButton: ASDisplayNode, textInputView: UITextView, - mediaPreview: ChatSendMessageContextScreenMediaPreview?, - mediaCaptionIsAbove: (Bool, (Bool) -> Void)?, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?, wallpaperBackgroundNode: WallpaperBackgroundNode?, - attachment: Bool, - canSendWhenOnline: Bool, completion: @escaping () -> Void, sendMessage: @escaping (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.SendParameters?) -> Void, schedule: @escaping (ChatSendMessageActionSheetController.SendParameters?) -> Void, @@ -103,18 +93,13 @@ final class ChatSendMessageContextScreenComponent: Component { self.context = context self.updatedPresentationData = updatedPresentationData self.peerId = peerId - self.isScheduledMessages = isScheduledMessages - self.forwardMessageIds = forwardMessageIds + self.params = params self.hasEntityKeyboard = hasEntityKeyboard self.gesture = gesture self.sourceSendButton = sourceSendButton self.textInputView = textInputView - self.mediaPreview = mediaPreview - self.mediaCaptionIsAbove = mediaCaptionIsAbove self.emojiViewProvider = emojiViewProvider self.wallpaperBackgroundNode = wallpaperBackgroundNode - self.attachment = attachment - self.canSendWhenOnline = canSendWhenOnline self.completion = completion self.sendMessage = sendMessage self.schedule = schedule @@ -331,7 +316,14 @@ final class ChatSendMessageContextScreenComponent: Component { let themeUpdated = environment.theme !== self.environment?.theme if self.component == nil { - self.mediaCaptionIsAbove = component.mediaCaptionIsAbove?.0 ?? false + switch component.params { + case let .sendMessage(sendMessage): + self.mediaCaptionIsAbove = sendMessage.mediaCaptionIsAbove?.0 ?? false + case let .editMessage(editMessage): + self.mediaCaptionIsAbove = editMessage.messages.contains(where: { + return $0.attributes.contains(where: { $0 is InvertMediaMessageAttribute }) + }) + } component.gesture.externalUpdated = { [weak self] view, location in guard let self, let actionsStackNode = self.actionsStackNode else { @@ -375,7 +367,15 @@ final class ChatSendMessageContextScreenComponent: Component { ) } - var isMessageVisible = component.mediaPreview != nil + var mediaPreview: ChatSendMessageContextScreenMediaPreview? + switch component.params { + case let .sendMessage(sendMessage): + mediaPreview = sendMessage.mediaPreview + case let .editMessage(editMessage): + mediaPreview = editMessage.mediaPreview + } + + var isMessageVisible: Bool = mediaPreview != nil let textString: NSAttributedString if let attributedText = component.textInputView.attributedText { @@ -391,7 +391,14 @@ final class ChatSendMessageContextScreenComponent: Component { if let current = self.sendButton { sendButton = current } else { - sendButton = SendButton() + let sendButtonKind: SendButton.Kind + switch component.params { + case .sendMessage: + sendButtonKind = .send + case .editMessage: + sendButtonKind = .edit + } + sendButton = SendButton(kind: sendButtonKind) sendButton.accessibilityLabel = environment.strings.MediaPicker_Send sendButton.addTarget(self, action: #selector(self.onSendButtonPressed), for: .touchUpInside) /*if let snapshotView = component.sourceSendButton.view.snapshotView(afterScreenUpdates: false) { @@ -427,22 +434,43 @@ final class ChatSendMessageContextScreenComponent: Component { var reminders = false var isSecret = false var canSchedule = false - if let peerId = component.peerId { - reminders = peerId == component.context.account.peerId - isSecret = peerId.namespace == Namespaces.Peer.SecretChat - canSchedule = !isSecret - } - if component.isScheduledMessages { - canSchedule = false + switch component.params { + case let .sendMessage(sendMessage): + if let peerId = component.peerId { + reminders = peerId == component.context.account.peerId + isSecret = peerId.namespace == Namespaces.Peer.SecretChat + canSchedule = !isSecret + } + if sendMessage.isScheduledMessages { + canSchedule = false + } + case .editMessage: + break } var items: [ContextMenuItem] = [] - if component.mediaCaptionIsAbove != nil, textString.length != 0, case .media = component.mediaPreview?.layoutType { - //TODO:localize + + let canAdjustMediaCaptionPosition: Bool + switch component.params { + case let .sendMessage(sendMessage): + if case .media = mediaPreview?.layoutType { + canAdjustMediaCaptionPosition = sendMessage.mediaCaptionIsAbove != nil + } else { + canAdjustMediaCaptionPosition = false + } + case .editMessage: + if case .media = mediaPreview?.layoutType { + canAdjustMediaCaptionPosition = textString.length != 0 + } else { + canAdjustMediaCaptionPosition = false + } + } + + if canAdjustMediaCaptionPosition, textString.length != 0 { let mediaCaptionIsAbove = self.mediaCaptionIsAbove items.append(.action(ContextMenuActionItem( id: AnyHashable("captionPosition"), - text: mediaCaptionIsAbove ? "Move Caption Down" : "Move Caption Up", + text: mediaCaptionIsAbove ? presentationData.strings.Chat_SendMessageMenu_MoveCaptionDown : presentationData.strings.Chat_SendMessageMenu_MoveCaptionUp, icon: { _ in return nil }, iconAnimation: ContextMenuActionItem.IconAnimation( @@ -452,7 +480,12 @@ final class ChatSendMessageContextScreenComponent: Component { return } self.mediaCaptionIsAbove = !self.mediaCaptionIsAbove - component.mediaCaptionIsAbove?.1(self.mediaCaptionIsAbove) + switch component.params { + case let .sendMessage(sendMessage): + sendMessage.mediaCaptionIsAbove?.1(self.mediaCaptionIsAbove) + case .editMessage: + break + } if !self.isUpdating { self.state?.updated(transition: .spring(duration: 0.35)) } @@ -461,34 +494,14 @@ final class ChatSendMessageContextScreenComponent: Component { items.append(.separator) } - if !reminders { - items.append(.action(ContextMenuActionItem( - id: AnyHashable("silent"), - text: environment.strings.Conversation_SendMessage_SendSilently, - icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, _ in - guard let self, let component = self.component else { - return - } - self.animateOutToEmpty = true - - let sendParameters = ChatSendMessageActionSheetController.SendParameters( - effect: self.selectedMessageEffect.flatMap({ ChatSendMessageActionSheetController.SendParameters.Effect(id: $0.id) }), - textIsAboveMedia: self.mediaCaptionIsAbove - ) - - component.sendMessage(.silently, sendParameters) - self.environment?.controller()?.dismiss() - } - ))) - - if component.canSendWhenOnline && canSchedule { + switch component.params { + case let.sendMessage(sendMessage): + if !reminders { items.append(.action(ContextMenuActionItem( - id: AnyHashable("whenOnline"), - text: environment.strings.Conversation_SendMessage_SendWhenOnline, + id: AnyHashable("silent"), + text: environment.strings.Conversation_SendMessage_SendSilently, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/WhenOnlineIcon"), color: theme.contextMenu.primaryColor) + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, _ in guard let self, let component = self.component else { return @@ -500,18 +513,62 @@ final class ChatSendMessageContextScreenComponent: Component { textIsAboveMedia: self.mediaCaptionIsAbove ) - component.sendMessage(.whenOnline, sendParameters) + component.sendMessage(.silently, sendParameters) + self.environment?.controller()?.dismiss() + } + ))) + + if sendMessage.canSendWhenOnline && canSchedule { + items.append(.action(ContextMenuActionItem( + id: AnyHashable("whenOnline"), + text: environment.strings.Conversation_SendMessage_SendWhenOnline, + icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/WhenOnlineIcon"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, _ in + guard let self, let component = self.component else { + return + } + self.animateOutToEmpty = true + + let sendParameters = ChatSendMessageActionSheetController.SendParameters( + effect: self.selectedMessageEffect.flatMap({ ChatSendMessageActionSheetController.SendParameters.Effect(id: $0.id) }), + textIsAboveMedia: self.mediaCaptionIsAbove + ) + + component.sendMessage(.whenOnline, sendParameters) + self.environment?.controller()?.dismiss() + } + ))) + } + } + if canSchedule { + items.append(.action(ContextMenuActionItem( + id: AnyHashable("schedule"), + text: reminders ? environment.strings.Conversation_SendMessage_SetReminder: environment.strings.Conversation_SendMessage_ScheduleMessage, + icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, _ in + guard let self, let component = self.component else { + return + } + self.animateOutToEmpty = true + + let sendParameters = ChatSendMessageActionSheetController.SendParameters( + effect: self.selectedMessageEffect.flatMap({ ChatSendMessageActionSheetController.SendParameters.Effect(id: $0.id) }), + textIsAboveMedia: self.mediaCaptionIsAbove + ) + + component.schedule(sendParameters) self.environment?.controller()?.dismiss() } ))) } - } - if canSchedule { + case .editMessage: items.append(.action(ContextMenuActionItem( - id: AnyHashable("schedule"), - text: reminders ? environment.strings.Conversation_SendMessage_SetReminder: environment.strings.Conversation_SendMessage_ScheduleMessage, + id: AnyHashable("silent"), + text: environment.strings.Chat_SendMessageMenu_EditMessage, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.contextMenu.primaryColor) + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, _ in guard let self, let component = self.component else { return @@ -519,11 +576,11 @@ final class ChatSendMessageContextScreenComponent: Component { self.animateOutToEmpty = true let sendParameters = ChatSendMessageActionSheetController.SendParameters( - effect: self.selectedMessageEffect.flatMap({ ChatSendMessageActionSheetController.SendParameters.Effect(id: $0.id) }), + effect: nil, textIsAboveMedia: self.mediaCaptionIsAbove ) - component.schedule(sendParameters) + component.sendMessage(.generic, sendParameters) self.environment?.controller()?.dismiss() } ))) @@ -636,7 +693,7 @@ final class ChatSendMessageContextScreenComponent: Component { let messageTextInsets = sourceMessageTextInsets let messageItemViewContainerSize: CGSize - if let mediaPreview = component.mediaPreview { + if let mediaPreview { switch mediaPreview.layoutType { case .message, .media: messageItemViewContainerSize = CGSize(width: availableSize.width - 16.0 - 40.0, height: availableSize.height) @@ -654,7 +711,7 @@ final class ChatSendMessageContextScreenComponent: Component { textString: textString, sourceTextInputView: component.textInputView as? ChatInputTextView, emojiViewProvider: component.emojiViewProvider, - sourceMediaPreview: component.mediaPreview, + sourceMediaPreview: mediaPreview, mediaCaptionIsAbove: self.mediaCaptionIsAbove, textInsets: messageTextInsets, explicitBackgroundSize: explicitMessageBackgroundSize, @@ -671,7 +728,6 @@ final class ChatSendMessageContextScreenComponent: Component { if let current = self.reactionContextNode { reactionContextNode = current } else { - //TODO:localize reactionContextNode = ReactionContextNode( context: component.context, animationCache: component.context.animationCache, @@ -692,7 +748,7 @@ final class ChatSendMessageContextScreenComponent: Component { return ReactionContextItem.reaction(item: item, icon: icon) }, selectedItems: Set(), - title: "Add an animated effect", + title: presentationData.strings.Chat_MessageEffectMenu_TitleAddEffect, reactionsLocked: false, alwaysAllowPremiumReactions: false, allPresetReactionsAreAvailable: true, @@ -937,13 +993,12 @@ final class ChatSendMessageContextScreenComponent: Component { } } - //TODO:localize let presentationData = component.updatedPresentationData?.initial ?? component.context.sharedContext.currentPresentationData.with({ $0 }) self.environment?.controller()?.present(UndoOverlayController( presentationData: presentationData, content: .premiumPaywall( title: nil, - text: "Subscribe to [Telegram Premium]() to add this animated effect.", + text: presentationData.strings.Chat_SendMessageMenu_ToastPremiumRequired_Text, customUndoText: nil, timeout: nil, linkAction: nil @@ -984,7 +1039,7 @@ final class ChatSendMessageContextScreenComponent: Component { } var readyMessageItemFrame = CGRect(origin: CGPoint(x: readySendButtonFrame.minX + 8.0 - messageItemSize.width, y: readySendButtonFrame.maxY - 6.0 - messageItemSize.height), size: messageItemSize) - if let mediaPreview = component.mediaPreview { + if let mediaPreview { switch mediaPreview.layoutType { case .message, .media: break @@ -1012,7 +1067,7 @@ final class ChatSendMessageContextScreenComponent: Component { readySendButtonFrame.origin.y -= inputCoverOverflow } - if let mediaPreview = component.mediaPreview { + if let mediaPreview { switch mediaPreview.layoutType { case .message, .media: break @@ -1034,7 +1089,7 @@ final class ChatSendMessageContextScreenComponent: Component { let sendButtonFrame: CGRect switch self.presentationAnimationState { case .initial: - if component.mediaPreview != nil { + if mediaPreview != nil { messageItemFrame = readyMessageItemFrame actionsStackFrame = readyActionsStackFrame } else { @@ -1048,7 +1103,7 @@ final class ChatSendMessageContextScreenComponent: Component { actionsStackFrame = readyActionsStackFrame sendButtonFrame = readySendButtonFrame } else { - if component.mediaPreview != nil { + if mediaPreview != nil { messageItemFrame = readyMessageItemFrame actionsStackFrame = readyActionsStackFrame } else { @@ -1066,7 +1121,7 @@ final class ChatSendMessageContextScreenComponent: Component { transition.setFrame(view: messageItemView, frame: messageItemFrame) transition.setAlpha(view: messageItemView, alpha: isMessageVisible ? 1.0 : 0.0) messageItemView.updateClippingRect( - sourceMediaPreview: component.mediaPreview, + sourceMediaPreview: mediaPreview, isAnimatedIn: self.presentationAnimationState.key == .animatedIn, localFrame: messageItemFrame, containerSize: availableSize, @@ -1120,7 +1175,7 @@ final class ChatSendMessageContextScreenComponent: Component { let reactionContextY = environment.statusBarHeight let size = availableSize var reactionsAnchorRect = messageItemFrame - if let mediaPreview = component.mediaPreview { + if let mediaPreview { switch mediaPreview.layoutType { case .message, .media: reactionsAnchorRect.size.width += 100.0 @@ -1193,14 +1248,14 @@ final class ChatSendMessageContextScreenComponent: Component { guard let component = self.component else { return } - if component.mediaPreview == nil { + if mediaPreview == nil { component.textInputView.isHidden = true } component.sourceSendButton.isHidden = true }) } } else { - if component.mediaPreview == nil { + if mediaPreview == nil { component.textInputView.isHidden = true } component.sourceSendButton.isHidden = true @@ -1212,7 +1267,7 @@ final class ChatSendMessageContextScreenComponent: Component { backgroundAlpha = 0.0 if self.animateOutToEmpty { - if component.mediaPreview == nil { + if mediaPreview == nil { component.textInputView.isHidden = false } component.sourceSendButton.isHidden = false @@ -1234,7 +1289,7 @@ final class ChatSendMessageContextScreenComponent: Component { if !self.performedActionsOnAnimateOut { self.performedActionsOnAnimateOut = true if let component = self.component, !self.animateOutToEmpty { - if component.mediaPreview == nil { + if mediaPreview == nil { component.textInputView.isHidden = false } component.sourceSendButton.isHidden = false @@ -1277,18 +1332,13 @@ public class ChatSendMessageContextScreen: ViewControllerComponentContainer, Cha context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: EnginePeer.Id?, - isScheduledMessages: Bool, - forwardMessageIds: [EngineMessage.Id]?, + params: SendMessageActionSheetControllerParams, hasEntityKeyboard: Bool, gesture: ContextGesture, sourceSendButton: ASDisplayNode, textInputView: UITextView, - mediaPreview: ChatSendMessageContextScreenMediaPreview?, - mediaCaptionIsAbove: (Bool, (Bool) -> Void)?, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?, wallpaperBackgroundNode: WallpaperBackgroundNode?, - attachment: Bool, - canSendWhenOnline: Bool, completion: @escaping () -> Void, sendMessage: @escaping (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.SendParameters?) -> Void, schedule: @escaping (ChatSendMessageActionSheetController.SendParameters?) -> Void, @@ -1305,18 +1355,13 @@ public class ChatSendMessageContextScreen: ViewControllerComponentContainer, Cha context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, - isScheduledMessages: isScheduledMessages, - forwardMessageIds: forwardMessageIds, + params: params, hasEntityKeyboard: hasEntityKeyboard, gesture: gesture, sourceSendButton: sourceSendButton, textInputView: textInputView, - mediaPreview: mediaPreview, - mediaCaptionIsAbove: mediaCaptionIsAbove, emojiViewProvider: emojiViewProvider, wallpaperBackgroundNode: wallpaperBackgroundNode, - attachment: attachment, - canSendWhenOnline: canSendWhenOnline, completion: completion, sendMessage: sendMessage, schedule: schedule, diff --git a/submodules/ChatSendMessageActionUI/Sources/MessageItemView.swift b/submodules/ChatSendMessageActionUI/Sources/MessageItemView.swift index d4c296ee61..60b5fcade2 100644 --- a/submodules/ChatSendMessageActionUI/Sources/MessageItemView.swift +++ b/submodules/ChatSendMessageActionUI/Sources/MessageItemView.swift @@ -18,6 +18,7 @@ import MultilineTextWithEntitiesComponent import ReactionButtonListComponent import MultilineTextComponent import ChatInputTextNode +import EmojiTextAttachmentView private final class EffectIcon: Component { enum Content: Equatable { @@ -134,6 +135,68 @@ private final class EffectIcon: Component { } } +final class CustomEmojiContainerView: UIView { + private let emojiViewProvider: (ChatTextInputTextCustomEmojiAttribute) -> UIView? + + private var emojiLayers: [InlineStickerItemLayer.Key: UIView] = [:] + + init(emojiViewProvider: @escaping (ChatTextInputTextCustomEmojiAttribute) -> UIView?) { + self.emojiViewProvider = emojiViewProvider + + super.init(frame: CGRect()) + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + func update(emojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)]) { + var nextIndexById: [Int64: Int] = [:] + + var validKeys = Set() + for (rect, emoji) in emojiRects { + let index: Int + if let nextIndex = nextIndexById[emoji.fileId] { + index = nextIndex + } else { + index = 0 + } + nextIndexById[emoji.fileId] = index + 1 + + let key = InlineStickerItemLayer.Key(id: emoji.fileId, index: index) + + let view: UIView + if let current = self.emojiLayers[key] { + view = current + } else if let newView = self.emojiViewProvider(emoji) { + view = newView + self.addSubview(newView) + self.emojiLayers[key] = view + } else { + continue + } + + let size = CGSize(width: 24.0, height: 24.0) + + view.frame = CGRect(origin: CGPoint(x: floor(rect.midX - size.width / 2.0), y: floor(rect.midY - size.height / 2.0)), size: size) + + validKeys.insert(key) + } + + var removeKeys: [InlineStickerItemLayer.Key] = [] + for (key, view) in self.emojiLayers { + if !validKeys.contains(key) { + removeKeys.append(key) + view.removeFromSuperview() + } + } + for key in removeKeys { + self.emojiLayers.removeValue(forKey: key) + } + } +} + + final class MessageItemView: UIView { private let backgroundWallpaperNode: ChatMessageBubbleBackdrop private let backgroundNode: ChatMessageBackground diff --git a/submodules/ChatSendMessageActionUI/Sources/SendButton.swift b/submodules/ChatSendMessageActionUI/Sources/SendButton.swift index 2dbd9c68ee..6d637852d1 100644 --- a/submodules/ChatSendMessageActionUI/Sources/SendButton.swift +++ b/submodules/ChatSendMessageActionUI/Sources/SendButton.swift @@ -19,6 +19,13 @@ import ActivityIndicator import RadialStatusNode final class SendButton: HighlightTrackingButton { + enum Kind { + case send + case edit + } + + private let kind: Kind + private let containerView: UIView private var backgroundContent: WallpaperBubbleBackgroundNode? private let backgroundLayer: SimpleLayer @@ -28,7 +35,9 @@ final class SendButton: HighlightTrackingButton { private var didProcessSourceCustomContent: Bool = false private var sourceCustomContentView: UIView? - override init(frame: CGRect) { + init(kind: Kind) { + self.kind = kind + self.containerView = UIView() self.containerView.isUserInteractionEnabled = false @@ -37,7 +46,7 @@ final class SendButton: HighlightTrackingButton { self.iconView = UIImageView() self.iconView.isUserInteractionEnabled = false - super.init(frame: frame) + super.init(frame: CGRect()) self.containerView.clipsToBounds = true self.addSubview(self.containerView) @@ -100,7 +109,12 @@ final class SendButton: HighlightTrackingButton { } if self.iconView.image == nil { - self.iconView.image = PresentationResourcesChat.chatInputPanelSendIconImage(presentationData.theme) + switch self.kind { + case .send: + self.iconView.image = PresentationResourcesChat.chatInputPanelSendIconImage(presentationData.theme) + case .edit: + self.iconView.image = PresentationResourcesChat.chatInputPanelApplyIconImage(presentationData.theme) + } } if let sourceCustomContentView = self.sourceCustomContentView { diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index b086ebde0d..4513f5623c 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -2502,8 +2502,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { mediaCaptionIsAbove = interaction.captionIsAboveMedia } - //TODO:localize - items.append(.action(ContextMenuActionItem(text: mediaCaptionIsAbove ? "Move Caption Down" : "Move Caption Up", icon: { _ in return nil }, iconAnimation: ContextMenuActionItem.IconAnimation( + items.append(.action(ContextMenuActionItem(text: mediaCaptionIsAbove ? strings.Chat_SendMessageMenu_MoveCaptionDown : strings.Chat_SendMessageMenu_MoveCaptionUp, icon: { _ in return nil }, iconAnimation: ContextMenuActionItem.IconAnimation( name: !mediaCaptionIsAbove ? "message_preview_sort_above" : "message_preview_sort_below" ), action: { [weak self] _, f in f(.default) diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index d51ed1b00d..3183090123 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -1796,6 +1796,9 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { return } + let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 }) + let strings = presentationData.strings + switch query { case .none: self.emojiSearchDisposable.set(nil) @@ -1929,7 +1932,7 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { } else { resultGroupIndexById[groupId] = resultGroups.count //TODO:localize - resultGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: i == 0 ? nil : "Message Effects", subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, hasEdit: false, headerItem: nil, items: [resultItem])) + resultGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: i == 0 ? nil : strings.Chat_MessageEffectMenu_SectionMessageEffects, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, hasEdit: false, headerItem: nil, items: [resultItem])) } } } @@ -2281,7 +2284,7 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { } else { resultGroupIndexById[groupId] = resultGroups.count //TODO:localize - resultGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: i == 0 ? nil : "Message Effects", subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, hasEdit: false, headerItem: nil, items: [resultItem])) + resultGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: i == 0 ? nil : strings.Chat_MessageEffectMenu_SectionMessageEffects, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, hasEdit: false, headerItem: nil, items: [resultItem])) } } } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift index 32c06a95bf..ea06826e99 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift @@ -2256,8 +2256,7 @@ public extension EmojiPagerContentComponent { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count - //TODO:localize - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: i == 0 ? nil : "Message Effects", subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, hasEdit: false, headerItem: nil, items: [resultItem])) + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: i == 0 ? nil : strings.Chat_MessageEffectMenu_SectionMessageEffects, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, hasEdit: false, headerItem: nil, items: [resultItem])) } } } diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift index 6ddc480eb5..e902ce5358 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift @@ -691,24 +691,44 @@ final class PeerSelectionControllerNode: ASDisplayNode { hasEntityKeyboard = true } - let controller = makeChatSendMessageActionSheetController(context: strongSelf.context, peerId: strongSelf.presentationInterfaceState.chatLocation.peerId, forwardMessageIds: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds, hasEntityKeyboard: hasEntityKeyboard, gesture: gesture, sourceSendButton: node, textInputView: textInputNode.textView, emojiViewProvider: textInputPanelNode.emojiViewProvider, canSendWhenOnline: false, completion: { - }, sendMessage: { [weak textInputPanelNode] mode, messageEffect in - switch mode { - case .generic: - textInputPanelNode?.sendMessage(.generic, messageEffect) - case .silently: - textInputPanelNode?.sendMessage(.silent, messageEffect) - case .whenOnline: - textInputPanelNode?.sendMessage(.whenOnline, messageEffect) + let controller = makeChatSendMessageActionSheetController( + context: strongSelf.context, + peerId: strongSelf.presentationInterfaceState.chatLocation.peerId, + params: .sendMessage(SendMessageActionSheetControllerParams.SendMessage( + isScheduledMessages: false, + mediaPreview: nil, + mediaCaptionIsAbove: nil, + attachment: false, + canSendWhenOnline: false, + forwardMessageIds: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds ?? [] + )), + hasEntityKeyboard: hasEntityKeyboard, + gesture: gesture, + sourceSendButton: node, + textInputView: textInputNode.textView, + emojiViewProvider: textInputPanelNode.emojiViewProvider, + completion: { + }, + sendMessage: { [weak textInputPanelNode] mode, messageEffect in + switch mode { + case .generic: + textInputPanelNode?.sendMessage(.generic, messageEffect) + case .silently: + textInputPanelNode?.sendMessage(.silent, messageEffect) + case .whenOnline: + textInputPanelNode?.sendMessage(.whenOnline, messageEffect) + } + }, + schedule: { [weak textInputPanelNode] messageEffect in + textInputPanelNode?.sendMessage(.schedule, messageEffect) + }, + openPremiumPaywall: { [weak controller] c in + guard let controller else { + return + } + controller.push(c) } - }, schedule: { [weak textInputPanelNode] messageEffect in - textInputPanelNode?.sendMessage(.schedule, messageEffect) - }, openPremiumPaywall: { [weak controller] c in - guard let controller else { - return - } - controller.push(c) - }) + ) strongSelf.presentInGlobalOverlay(controller, nil) }, openScheduledMessages: { }, openPeersNearby: { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 61f5b9b18e..8abf1537a3 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -2918,13 +2918,13 @@ final class StoryItemSetContainerSendMessage { return } if !hashtag.isEmpty { - if "".isEmpty { + /*if !"".isEmpty { let searchController = component.context.sharedContext.makeStorySearchController(context: component.context, query: hashtag) navigationController.pushViewController(searchController) - } else { + } else {*/ let searchController = component.context.sharedContext.makeHashtagSearchController(context: component.context, peer: peer.flatMap(EnginePeer.init), query: hashtag, all: true) navigationController.pushViewController(searchController) - } + //} } })) } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index 46ae16d63f..a90fef4c61 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -1276,8 +1276,7 @@ extension ChatControllerImpl { strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() case let .businessLinkSetup(link): if messages.count > 1 { - //TODO:localize - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: "The message text limit is 4096 characters", actions: [ + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.BusinessLink_AlertTextLimitText, actions: [ TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}) ]), in: .window(.root)) diff --git a/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift b/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift index bfa1333bb4..c76786848a 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift @@ -55,106 +55,178 @@ func chatMessageDisplaySendMessageOptions(selfController: ChatControllerImpl, no return user.isPremium } + let editMessages: Signal<[EngineMessage], NoError> + if let editMessage = selfController.presentationInterfaceState.interfaceState.editMessage { + editMessages = selfController.context.engine.data.get( + TelegramEngine.EngineData.Item.Messages.Message(id: editMessage.messageId) + ) + |> map { message -> [EngineMessage] in + if let message { + return [message] + } else { + return [] + } + } + } else { + editMessages = .single([]) + } + let _ = (combineLatest( selfController.context.account.viewTracker.peerView(peerId) |> take(1), effectItems, availableMessageEffects, - hasPremium + hasPremium, + editMessages ) - |> deliverOnMainQueue).startStandalone(next: { [weak selfController] peerView, effectItems, availableMessageEffects, hasPremium in + |> deliverOnMainQueue).startStandalone(next: { [weak selfController] peerView, effectItems, availableMessageEffects, hasPremium, editMessages in guard let selfController, let peer = peerViewMainPeer(peerView) else { return } - var sendWhenOnlineAvailable = false - if let presence = peerView.peerPresences[peer.id] as? TelegramUserPresence, case let .present(until) = presence.status { - let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) - if currentTime > until { - sendWhenOnlineAvailable = true + + if let _ = selfController.presentationInterfaceState.interfaceState.editMessage { + if editMessages.isEmpty { + return } - } - if peer.id.namespace == Namespaces.Peer.CloudUser && peer.id.id._internalGetInt64Value() == 777000 { - sendWhenOnlineAvailable = false - } - - if sendWhenOnlineAvailable { - let _ = ApplicationSpecificNotice.incrementSendWhenOnlineTip(accountManager: selfController.context.sharedContext.accountManager, count: 4).startStandalone() - } - - var mediaPreview: ChatSendMessageContextScreenMediaPreview? - if let videoRecorderValue = selfController.videoRecorderValue { - mediaPreview = videoRecorderValue.makeSendMessageContextPreview() - } - if let mediaDraftState = selfController.presentationInterfaceState.interfaceState.mediaDraftState { - if case let .audio(audio) = mediaDraftState { - mediaPreview = ChatSendAudioMessageContextPreview( - context: selfController.context, - presentationData: selfController.presentationData, - wallpaperBackgroundNode: selfController.chatDisplayNode.backgroundNode, - waveform: audio.waveform - ) - } - } - - let controller = makeChatSendMessageActionSheetController( - context: selfController.context, - updatedPresentationData: selfController.updatedPresentationData, - peerId: selfController.presentationInterfaceState.chatLocation.peerId, - forwardMessageIds: selfController.presentationInterfaceState.interfaceState.forwardMessageIds, - hasEntityKeyboard: hasEntityKeyboard, - gesture: gesture, - sourceSendButton: node, - textInputView: textInputView, - mediaPreview: mediaPreview, - emojiViewProvider: selfController.chatDisplayNode.textInputPanelNode?.emojiViewProvider, - wallpaperBackgroundNode: selfController.chatDisplayNode.backgroundNode, - canSendWhenOnline: sendWhenOnlineAvailable, - completion: { [weak selfController] in - guard let selfController else { - return - } - selfController.supportedOrientations = previousSupportedOrientations - }, - sendMessage: { [weak selfController] mode, parameters in - guard let selfController else { - return - } - switch mode { - case .generic: - selfController.controllerInteraction?.sendCurrentMessage(false, parameters?.effect.flatMap(ChatSendMessageEffect.init)) - case .silently: - selfController.controllerInteraction?.sendCurrentMessage(true, parameters?.effect.flatMap(ChatSendMessageEffect.init)) - case .whenOnline: - selfController.chatDisplayNode.sendCurrentMessage(scheduleTime: scheduleWhenOnlineTimestamp, messageEffect: parameters?.effect.flatMap(ChatSendMessageEffect.init)) { [weak selfController] in - guard let selfController else { - return - } - selfController.updateChatPresentationInterfaceState(animated: true, interactive: false, saveInterfaceState: selfController.presentationInterfaceState.subject != .scheduledMessages, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))) } - }) - selfController.openScheduledMessages() + let controller = makeChatSendMessageActionSheetController( + context: selfController.context, + updatedPresentationData: selfController.updatedPresentationData, + peerId: selfController.presentationInterfaceState.chatLocation.peerId, + params: .editMessage(SendMessageActionSheetControllerParams.EditMessage( + messages: editMessages, + mediaPreview: nil + )), + hasEntityKeyboard: hasEntityKeyboard, + gesture: gesture, + sourceSendButton: node, + textInputView: textInputView, + emojiViewProvider: selfController.chatDisplayNode.textInputPanelNode?.emojiViewProvider, + wallpaperBackgroundNode: selfController.chatDisplayNode.backgroundNode, + completion: { [weak selfController] in + guard let selfController else { + return } - } - }, - schedule: { [weak selfController] effect in - guard let selfController else { - return - } - selfController.controllerInteraction?.scheduleCurrentMessage() - }, openPremiumPaywall: { [weak selfController] c in - guard let selfController else { - return - } - selfController.push(c) - }, - reactionItems: (!textInputView.text.isEmpty || mediaPreview != nil) ? effectItems : nil, - availableMessageEffects: availableMessageEffects, - isPremium: hasPremium - ) - selfController.sendMessageActionsController = controller - if layout.isNonExclusive { - selfController.present(controller, in: .window(.root)) + selfController.supportedOrientations = previousSupportedOrientations + }, + sendMessage: { [weak selfController] mode, parameters in + guard let selfController else { + return + } + selfController.interfaceInteraction?.editMessage() + }, + schedule: { _ in + }, openPremiumPaywall: { [weak selfController] c in + guard let selfController else { + return + } + selfController.push(c) + }, + reactionItems: nil, + availableMessageEffects: nil, + isPremium: hasPremium + ) + selfController.sendMessageActionsController = controller + if layout.isNonExclusive { + selfController.present(controller, in: .window(.root)) + } else { + selfController.presentInGlobalOverlay(controller, with: nil) + } } else { - selfController.presentInGlobalOverlay(controller, with: nil) + var sendWhenOnlineAvailable = false + if let presence = peerView.peerPresences[peer.id] as? TelegramUserPresence, case let .present(until) = presence.status { + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + if currentTime > until { + sendWhenOnlineAvailable = true + } + } + if peer.id.namespace == Namespaces.Peer.CloudUser && peer.id.id._internalGetInt64Value() == 777000 { + sendWhenOnlineAvailable = false + } + + if sendWhenOnlineAvailable { + let _ = ApplicationSpecificNotice.incrementSendWhenOnlineTip(accountManager: selfController.context.sharedContext.accountManager, count: 4).startStandalone() + } + + var mediaPreview: ChatSendMessageContextScreenMediaPreview? + if let videoRecorderValue = selfController.videoRecorderValue { + mediaPreview = videoRecorderValue.makeSendMessageContextPreview() + } + if let mediaDraftState = selfController.presentationInterfaceState.interfaceState.mediaDraftState { + if case let .audio(audio) = mediaDraftState { + mediaPreview = ChatSendAudioMessageContextPreview( + context: selfController.context, + presentationData: selfController.presentationData, + wallpaperBackgroundNode: selfController.chatDisplayNode.backgroundNode, + waveform: audio.waveform + ) + } + } + + let controller = makeChatSendMessageActionSheetController( + context: selfController.context, + updatedPresentationData: selfController.updatedPresentationData, + peerId: selfController.presentationInterfaceState.chatLocation.peerId, + params: .sendMessage(SendMessageActionSheetControllerParams.SendMessage( + isScheduledMessages: false, + mediaPreview: mediaPreview, + mediaCaptionIsAbove: nil, + attachment: false, + canSendWhenOnline: sendWhenOnlineAvailable, + forwardMessageIds: selfController.presentationInterfaceState.interfaceState.forwardMessageIds ?? [] + )), + hasEntityKeyboard: hasEntityKeyboard, + gesture: gesture, + sourceSendButton: node, + textInputView: textInputView, + emojiViewProvider: selfController.chatDisplayNode.textInputPanelNode?.emojiViewProvider, + wallpaperBackgroundNode: selfController.chatDisplayNode.backgroundNode, + completion: { [weak selfController] in + guard let selfController else { + return + } + selfController.supportedOrientations = previousSupportedOrientations + }, + sendMessage: { [weak selfController] mode, parameters in + guard let selfController else { + return + } + switch mode { + case .generic: + selfController.controllerInteraction?.sendCurrentMessage(false, parameters?.effect.flatMap(ChatSendMessageEffect.init)) + case .silently: + selfController.controllerInteraction?.sendCurrentMessage(true, parameters?.effect.flatMap(ChatSendMessageEffect.init)) + case .whenOnline: + selfController.chatDisplayNode.sendCurrentMessage(scheduleTime: scheduleWhenOnlineTimestamp, messageEffect: parameters?.effect.flatMap(ChatSendMessageEffect.init)) { [weak selfController] in + guard let selfController else { + return + } + selfController.updateChatPresentationInterfaceState(animated: true, interactive: false, saveInterfaceState: selfController.presentationInterfaceState.subject != .scheduledMessages, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))) } + }) + selfController.openScheduledMessages() + } + } + }, + schedule: { [weak selfController] effect in + guard let selfController else { + return + } + selfController.controllerInteraction?.scheduleCurrentMessage() + }, openPremiumPaywall: { [weak selfController] c in + guard let selfController else { + return + } + selfController.push(c) + }, + reactionItems: (!textInputView.text.isEmpty || mediaPreview != nil) ? effectItems : nil, + availableMessageEffects: availableMessageEffects, + isPremium: hasPremium + ) + selfController.sendMessageActionsController = controller + if layout.isNonExclusive { + selfController.present(controller, in: .window(.root)) + } else { + selfController.presentInGlobalOverlay(controller, with: nil) + } } }) } diff --git a/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift b/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift index c892f45674..09c63ace83 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift @@ -67,7 +67,7 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode { self.sendButton.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { - if strongSelf.sendButtonHasApplyIcon || !strongSelf.sendButtonLongPressEnabled { + if !strongSelf.sendButtonLongPressEnabled { if highlighted { strongSelf.sendContainerNode.layer.removeAnimation(forKey: "opacity") strongSelf.sendContainerNode.alpha = 0.4 @@ -109,9 +109,7 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode { guard let strongSelf = self else { return } - if !strongSelf.sendButtonHasApplyIcon { - strongSelf.sendButtonLongPressed?(strongSelf, recognizer) - } + strongSelf.sendButtonLongPressed?(strongSelf, recognizer) } self.micButtonPointerInteraction = PointerInteraction(view: self.micButton, style: .circle(36.0))