import Foundation import UIKit import AsyncDisplayKit import Display import TelegramCore import SwiftSignalKit import TelegramPresentationData import AccountContext import AppBundle import ChatPresentationInterfaceState import ChatInputPanelNode import ReactionSelectionNode import EntityKeyboard import TopMessageReactions import GlassBackgroundComponent import ComponentFlow import ComponentDisplayAdapters import GGCore // GHOST: Ghostgram strings (W5-SG) import GGFeaturesShared // GHOST: Swiftgram-parity settings (W5-SG) import GGExtrasHooks // GHOST: Swiftgram-parity points (W5-SG) private final class ChatMessageSelectionInputPanelNodeViewForOverlayContent: UIView, ChatInputPanelViewForOverlayContent { var reactionContextNode: ReactionContextNode? var anchorRect: CGRect? override init(frame: CGRect) { super.init(frame: frame) self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.backgroundTapGesture(_:)))) } required init(coder: NSCoder) { preconditionFailure() } @objc private func backgroundTapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { self.dismissReactionSelection() } } func dismissReactionSelection() { if let reactionContextNode = self.reactionContextNode { self.reactionContextNode = nil reactionContextNode.animateOut(to: self.anchorRect, animatingOutToReaction: false) ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut).updateAlpha(node: reactionContextNode, alpha: 0.0, completion: { [weak reactionContextNode] _ in reactionContextNode?.removeFromSupernode() }) } } func maybeDismissContent(point: CGPoint) { if self.hitTest(point, with: nil) == self { self.dismissReactionSelection() } } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if let reactionContextNode = self.reactionContextNode { if let result = reactionContextNode.view.hitTest(self.convert(point, to: reactionContextNode.view), with: event) { return result } return self } return nil } } private final class GlassButtonView: UIView { private struct Params: Equatable { let theme: PresentationTheme let preferClearGlass: Bool let size: CGSize init(theme: PresentationTheme, preferClearGlass: Bool, size: CGSize) { self.theme = theme self.preferClearGlass = preferClearGlass self.size = size } static func ==(lhs: Params, rhs: Params) -> Bool { if lhs.theme !== rhs.theme { return false } if lhs.preferClearGlass != rhs.preferClearGlass { return false } if lhs.size != rhs.size { return false } return true } } private let backgroundView: GlassBackgroundView let button: HighlightTrackingButton private let iconView: GlassBackgroundView.ContentImageView private var params: Params? var isImplicitlyDisabled: Bool = false { didSet { self.updateIsEnabled() } } var isEnabled: Bool = true { didSet { self.updateIsEnabled() } } var icon: String? { didSet { if self.icon == oldValue { return } if let icon = self.icon { self.iconView.image = UIImage(bundleImageName: icon)?.withRenderingMode(.alwaysTemplate) } else { self.iconView.image = nil } if let params = self.params { self.updateImpl(params: params, transition: .immediate) } } } override init(frame: CGRect) { self.backgroundView = GlassBackgroundView() self.iconView = GlassBackgroundView.ContentImageView() self.backgroundView.contentView.addSubview(self.iconView) self.button = HighlightTrackingButton() self.backgroundView.contentView.addSubview(self.button) super.init(frame: frame) self.addSubview(self.backgroundView) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func update(theme: PresentationTheme, preferClearGlass: Bool, size: CGSize, transition: ComponentTransition) { let params = Params(theme: theme, preferClearGlass: preferClearGlass, size: size) if self.params != params { self.iconView.tintColor = params.theme.chat.inputPanel.panelControlColor self.params = params self.updateImpl(params: params, transition: transition) } } private func updateImpl(params: Params, transition: ComponentTransition) { let isEnabled = self.isEnabled && !self.isImplicitlyDisabled if let image = self.iconView.image { let iconFrame = image.size.centered(in: CGRect(origin: CGPoint(), size: params.size)) transition.setFrame(view: self.iconView, frame: iconFrame) } transition.setFrame(view: self.button, frame: CGRect(origin: CGPoint(), size: params.size)) transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: params.size)) self.backgroundView.update(size: params.size, cornerRadius: min(params.size.width, params.size.height) * 0.5, isDark: params.theme.overallDarkAppearance, tintColor: .init(kind: params.preferClearGlass ? .clear : .panel), isInteractive: true, transition: transition) self.backgroundView.isUserInteractionEnabled = isEnabled self.iconView.alpha = isEnabled ? 1.0 : 0.5 self.iconView.tintMask.alpha = self.iconView.alpha self.button.isEnabled = isEnabled } private func updateIsEnabled() { if let params = self.params { self.updateImpl(params: params, transition: .immediate) } } } public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { private let deleteButton: GlassButtonView private let reportButton: GlassButtonView private let forwardButton: GlassButtonView private let shareButton: GlassButtonView private let tagButton: GlassButtonView private let tagEditButton: GlassButtonView // GHOST: «В Избранное» / «Переслать без автора» (GGExtrasPolicy.showsSelectionPanelButtons, GGExtrasUIHooks.selectionAction) private let ggCloudButton = GlassButtonView() private let ggForwardWithoutAuthorButton = GlassButtonView() private let reactionOverlayContainer: ChatMessageSelectionInputPanelNodeViewForOverlayContent private var validLayout: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, maxOverlayHeight: CGFloat, metrics: LayoutMetrics, isSecondary: Bool, isMediaInputExpanded: Bool, deviceMetrics: DeviceMetrics)? private var presentationInterfaceState: ChatPresentationInterfaceState? private var actions: ChatAvailableMessageActions? private var theme: PresentationTheme private let peerMedia: Bool private let canDeleteMessagesDisposable = MetaDisposable() public var selectedMessages = Set() { didSet { if oldValue != self.selectedMessages { self.updateActions() } } } public init(theme: PresentationTheme, strings: PresentationStrings, peerMedia: Bool = false) { self.theme = theme self.peerMedia = peerMedia self.deleteButton = GlassButtonView() self.deleteButton.icon = "Chat/Input/Accessory Panels/MessageSelectionTrash" self.deleteButton.isEnabled = false self.deleteButton.isAccessibilityElement = true self.deleteButton.accessibilityLabel = strings.VoiceOver_MessageContextDelete self.reportButton = GlassButtonView() self.reportButton.icon = "Chat/Input/Accessory Panels/MessageSelectionReport" self.reportButton.isEnabled = false self.reportButton.isAccessibilityElement = true self.reportButton.accessibilityLabel = strings.VoiceOver_MessageContextReport self.forwardButton = GlassButtonView() self.forwardButton.icon = "Chat/Input/Accessory Panels/MessageSelectionForward" self.forwardButton.isAccessibilityElement = true self.forwardButton.accessibilityLabel = strings.VoiceOver_MessageContextForward self.shareButton = GlassButtonView() self.shareButton.icon = "Chat/Input/Accessory Panels/MessageSelectionAction" self.shareButton.isAccessibilityElement = true self.shareButton.accessibilityLabel = strings.VoiceOver_MessageContextShare self.tagButton = GlassButtonView() self.tagButton.icon = "Chat/Input/Accessory Panels/TagIcon" self.tagButton.isAccessibilityElement = true self.tagButton.accessibilityLabel = strings.VoiceOver_MessageSelectionButtonTag self.tagEditButton = GlassButtonView() self.tagEditButton.icon = "Chat/Input/Accessory Panels/TagEditIcon" self.tagEditButton.isAccessibilityElement = true self.tagEditButton.accessibilityLabel = strings.VoiceOver_MessageSelectionButtonTag self.reactionOverlayContainer = ChatMessageSelectionInputPanelNodeViewForOverlayContent() super.init() self.view.addSubview(self.deleteButton) self.view.addSubview(self.reportButton) self.view.addSubview(self.forwardButton) self.view.addSubview(self.shareButton) self.view.addSubview(self.tagButton) self.view.addSubview(self.tagEditButton) self.ggSetupExtraButtons() // GHOST: see the properties self.viewForOverlayContent = self.reactionOverlayContainer self.forwardButton.isImplicitlyDisabled = true self.shareButton.isImplicitlyDisabled = true self.deleteButton.button.addTarget(self, action: #selector(self.deleteButtonPressed), for: .touchUpInside) self.reportButton.button.addTarget(self, action: #selector(self.reportButtonPressed), for: .touchUpInside) self.forwardButton.button.addTarget(self, action: #selector(self.forwardButtonPressed), for: .touchUpInside) self.shareButton.button.addTarget(self, action: #selector(self.shareButtonPressed), for: .touchUpInside) self.tagButton.button.addTarget(self, action: #selector(self.tagButtonPressed), for: .touchUpInside) self.tagEditButton.button.addTarget(self, action: #selector(self.tagButtonPressed), for: .touchUpInside) } deinit { self.canDeleteMessagesDisposable.dispose() } private func updateActions() { self.forwardButton.isEnabled = self.selectedMessages.count != 0 if self.selectedMessages.isEmpty { self.actions = nil if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, maxOverlayHeight, metrics, isSecondary, isMediaInputExpanded, deviceMetrics) = self.validLayout, let interfaceState = self.presentationInterfaceState { let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, maxOverlayHeight: maxOverlayHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: interfaceState, metrics: metrics, deviceMetrics: deviceMetrics, isMediaInputExpanded: isMediaInputExpanded) } self.canDeleteMessagesDisposable.set(nil) } else if let context = self.context { self.canDeleteMessagesDisposable.set((context.sharedContext.chatAvailableMessageActions(engine: context.engine, accountPeerId: context.account.peerId, messageIds: self.selectedMessages, keepUpdated: true) |> deliverOnMainQueue).startStrict(next: { [weak self] actions in if let strongSelf = self { strongSelf.actions = actions if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, maxOverlayHeight: maxOverlayHeight, metrics, isSecondary, isMediaInputExpanded, deviceMetrics) = strongSelf.validLayout, let interfaceState = strongSelf.presentationInterfaceState { let _ = strongSelf.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, maxOverlayHeight: maxOverlayHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: interfaceState, metrics: metrics, deviceMetrics: deviceMetrics, isMediaInputExpanded: isMediaInputExpanded) } } })) } } public func updateTheme(theme: PresentationTheme) { if self.theme !== theme { self.theme = theme } } @objc private func deleteButtonPressed() { self.interfaceInteraction?.deleteSelectedMessages(self.deleteButton) } @objc private func reportButtonPressed() { self.interfaceInteraction?.reportSelectedMessages() } @objc private func forwardButtonPressed() { if let _ = self.presentationInterfaceState?.renderedPeer?.peer as? TelegramSecretChat { return } if let actions = self.actions, actions.isCopyProtected { self.interfaceInteraction?.displayCopyProtectionTip(self.forwardButton, false) } else if !self.forwardButton.isImplicitlyDisabled { self.interfaceInteraction?.forwardSelectedMessages() } } @objc private func shareButtonPressed() { if let _ = self.presentationInterfaceState?.renderedPeer?.peer as? TelegramSecretChat { return } if let actions = self.actions, actions.isCopyProtected { self.interfaceInteraction?.displayCopyProtectionTip(self.shareButton, true) } else if !self.shareButton.isImplicitlyDisabled { self.interfaceInteraction?.shareSelectedMessages() } } @objc private func tagButtonPressed() { guard let context = self.context else { return } if self.reactionOverlayContainer.reactionContextNode != nil { return } let reactionItems: Signal<[ReactionItem], NoError> = tagMessageReactions(context: context, subPeerId: self.presentationInterfaceState?.chatLocation.threadId.flatMap(EnginePeer.Id.init)) let _ = (reactionItems |> take(1) |> deliverOnMainQueue).start(next: { [weak self] reactionItems in guard let self, let actions = self.actions, let context = self.context else { return } let presentationData = context.sharedContext.currentPresentationData.with { $0 } let reactionContextNode = ReactionContextNode( context: context, animationCache: context.animationCache, presentationData: presentationData, items: reactionItems.map { ReactionContextItem.reaction(item: $0, icon: .none) }, selectedItems: actions.editTags, title: actions.editTags.isEmpty ? presentationData.strings.Chat_ReactionSelectionTitleAddTag : presentationData.strings.Chat_ReactionSelectionTitleEditTag, reactionsLocked: false, alwaysAllowPremiumReactions: false, allPresetReactionsAreAvailable: true, getEmojiContent: { animationCache, animationRenderer in let mappedReactionItems: [EmojiComponentReactionItem] = reactionItems.map { reaction -> EmojiComponentReactionItem in return EmojiComponentReactionItem(reaction: reaction.reaction.rawValue, file: reaction.stillAnimation) } return EmojiPagerContentComponent.emojiInputData( context: context, animationCache: animationCache, animationRenderer: animationRenderer, isStandalone: false, subject: .messageTag, hasTrending: false, topReactionItems: mappedReactionItems, areUnicodeEmojiEnabled: false, areCustomEmojiEnabled: true, chatPeerId: context.account.peerId, selectedItems: Set(), premiumIfSavedMessages: 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 context = self.context, let presentationInterfaceState = self.presentationInterfaceState, let actions = self.actions else { return } self.interfaceInteraction?.cancelMessageSelection(.animated(duration: 0.4, curve: .spring)) if actions.editTags.contains(updateReaction.reaction) { var reactions = actions.editTags reactions.remove(updateReaction.reaction) let mappedUpdatedReactions = reactions.map { reaction -> UpdateMessageReaction in switch reaction { case let .builtin(value): return .builtin(value) case let .custom(fileId): return .custom(fileId: fileId, file: nil) case .stars: return .stars } } if let selectionState = presentationInterfaceState.interfaceState.selectionState { context.engine.messages.setMessageReactions(ids: Array(selectionState.selectedIds), reactions: mappedUpdatedReactions) } else { context.engine.messages.setMessageReactions(ids: Array(self.selectedMessages), reactions: mappedUpdatedReactions) } } else { if let selectionState = presentationInterfaceState.interfaceState.selectionState { context.engine.messages.addMessageReactions(ids: Array(selectionState.selectedIds), reactions: [updateReaction]) } else { context.engine.messages.addMessageReactions(ids: Array(self.selectedMessages), reactions: [updateReaction]) } } self.reactionOverlayContainer.dismissReactionSelection() } reactionContextNode.displayTail = true reactionContextNode.forceTailToRight = true reactionContextNode.forceDark = false self.reactionOverlayContainer.reactionContextNode = reactionContextNode self.reactionOverlayContainer.addSubnode(reactionContextNode) self.update(transition: .immediate) }) } private func update(transition: ContainedViewLayoutTransition) { if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, maxOverlayHeight, metrics, isSecondary, isMediaInputExpanded, deviceMetrics) = self.validLayout, let interfaceState = self.presentationInterfaceState { let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, maxOverlayHeight: maxOverlayHeight, isSecondary: isSecondary, transition: transition, interfaceState: interfaceState, metrics: metrics, deviceMetrics: deviceMetrics, isMediaInputExpanded: isMediaInputExpanded) } } override public func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, maxOverlayHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, isMediaInputExpanded: Bool) -> CGFloat { self.validLayout = (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, maxOverlayHeight, metrics, isSecondary, isMediaInputExpanded, deviceMetrics) var leftInset = leftInset + 8.0 var rightInset = rightInset + 8.0 let compactBottomSideInset = self.compactBottomSideInset(bottomInset: bottomInset, deviceMetrics: deviceMetrics) leftInset += compactBottomSideInset rightInset += compactBottomSideInset let panelHeight = defaultHeight(metrics: metrics) if self.presentationInterfaceState != interfaceState { self.presentationInterfaceState = interfaceState } if let actions = self.actions { self.deleteButton.isEnabled = false self.reportButton.isEnabled = false self.forwardButton.isImplicitlyDisabled = !actions.options.contains(.forward) if self.peerMedia { self.deleteButton.isEnabled = !actions.options.intersection([.deleteLocally, .deleteGlobally]).isEmpty } else { self.deleteButton.isEnabled = !actions.disableDelete } self.shareButton.isImplicitlyDisabled = actions.options.intersection(.forward).isEmpty || actions.options.intersection(.externalShare).isEmpty self.reportButton.isEnabled = !actions.options.intersection([.report]).isEmpty if self.peerMedia { self.deleteButton.isHidden = !self.deleteButton.isEnabled } else { self.deleteButton.isHidden = false } self.reportButton.isHidden = !self.reportButton.isEnabled if actions.setTag { if !actions.editTags.isEmpty { self.tagButton.isHidden = true self.tagEditButton.isHidden = false } else { self.tagButton.isHidden = false self.tagEditButton.isHidden = true } } else { self.tagButton.isHidden = true self.tagEditButton.isHidden = true } } else { self.deleteButton.isEnabled = false self.deleteButton.isHidden = self.peerMedia self.reportButton.isEnabled = false self.reportButton.isHidden = true self.forwardButton.isImplicitlyDisabled = true self.shareButton.isImplicitlyDisabled = true self.tagButton.isHidden = true self.tagEditButton.isHidden = true self.tagButton.isHidden = true self.tagEditButton.isHidden = true } if self.reportButton.isHidden || (self.peerMedia && self.deleteButton.isHidden && self.reportButton.isHidden) { if let peer = interfaceState.renderedPeer?.peer as? TelegramChannel, case .broadcast = peer.info { self.reportButton.isHidden = false } else if self.peerMedia { self.deleteButton.isHidden = false } } var width = width if additionalSideInsets.right > 0.0 { width -= additionalSideInsets.right } var tagButton: GlassButtonView? if !self.tagButton.isHidden { tagButton = self.tagButton } else if !self.tagEditButton.isHidden { tagButton = self.tagEditButton } var buttons: [GlassButtonView] // GHOST: var — the extra buttons are inserted below if self.reportButton.isHidden { if let tagButton { buttons = [ self.deleteButton, tagButton, self.shareButton, self.forwardButton ] } else { buttons = [ self.deleteButton, self.shareButton, self.forwardButton ] } } else if !self.deleteButton.isHidden { if let tagButton { buttons = [ self.deleteButton, self.reportButton, tagButton, self.shareButton, self.forwardButton ] } else { buttons = [ self.deleteButton, self.reportButton, self.shareButton, self.forwardButton ] } } else { if let tagButton { buttons = [ self.deleteButton, self.reportButton, tagButton, self.shareButton, self.forwardButton ] } else { buttons = [ self.deleteButton, self.reportButton, self.shareButton, self.forwardButton ] } } buttons = self.ggArrangeExtraButtons(buttons) // GHOST: see the properties let buttonSize = CGSize(width: 40.0, height: 40.0) let availableWidth = width - leftInset - rightInset let spacing: CGFloat = floor((availableWidth - buttonSize.width * CGFloat(buttons.count)) / CGFloat(buttons.count - 1)) var offset: CGFloat = leftInset for i in 0 ..< buttons.count { let button = buttons[i] let buttonFrame: CGRect if i == buttons.count - 1 { buttonFrame = CGRect(origin: CGPoint(x: width - rightInset - buttonSize.width, y: 0.0), size: buttonSize) } else { buttonFrame = CGRect(origin: CGPoint(x: offset, y: 0.0), size: buttonSize) } transition.updateFrame(view: button, frame: buttonFrame) button.update(theme: interfaceState.theme, preferClearGlass: interfaceState.preferredGlassType == .clear, size: buttonFrame.size, transition: ComponentTransition(transition)) offset += buttonSize.width + spacing } if let reactionContextNode = self.reactionOverlayContainer.reactionContextNode, let tagButton { let isFirstTime = reactionContextNode.bounds.isEmpty let size = CGSize(width: width, height: maxHeight) let reactionsAnchorRect = tagButton.frame.offsetBy(dx: -54.0, dy: -(panelHeight - size.height) + 14.0) transition.updateFrame(node: reactionContextNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelHeight - size.height), size: size)) reactionContextNode.updateLayout(size: size, insets: UIEdgeInsets(), anchorRect: reactionsAnchorRect, centerAligned: true, isCoveredByInput: false, isAnimatingOut: false, transition: transition) reactionContextNode.updateIsIntersectingContent(isIntersectingContent: true, transition: .immediate) if isFirstTime { reactionContextNode.animateIn(from: reactionsAnchorRect) } } return panelHeight } override public func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { return defaultHeight(metrics: metrics) } } // GHOST: the selection panel's extra buttons «В Избранное» / «Переслать без автора» (W5-SG). Off = the // buttons stay hidden and out of the row: upstream layout. The action itself is GGExtras' handler. extension ChatMessageSelectionInputPanelNode { fileprivate func ggSetupExtraButtons() { self.ggCloudButton.icon = "Chat/Context Menu/Fave" self.ggCloudButton.isAccessibilityElement = true self.ggCloudButton.accessibilityLabel = GGStrings.Extras.Selection.saveToCloud self.ggCloudButton.isHidden = true self.ggForwardWithoutAuthorButton.icon = "Chat/Input/Accessory Panels/ForwardSettingsIcon" self.ggForwardWithoutAuthorButton.isAccessibilityElement = true self.ggForwardWithoutAuthorButton.accessibilityLabel = GGStrings.Extras.Selection.forwardWithoutAuthor self.ggForwardWithoutAuthorButton.isHidden = true self.view.addSubview(self.ggCloudButton) self.view.addSubview(self.ggForwardWithoutAuthorButton) self.ggCloudButton.button.addTarget(self, action: #selector(self.ggCloudButtonPressed), for: .touchUpInside) self.ggForwardWithoutAuthorButton.button.addTarget(self, action: #selector(self.ggForwardWithoutAuthorButtonPressed), for: .touchUpInside) } /// The row with the extra buttons right before «Переслать», enabled like it. fileprivate func ggArrangeExtraButtons(_ buttons: [GlassButtonView]) -> [GlassButtonView] { let isSecretChat = self.presentationInterfaceState?.renderedPeer?.peer is TelegramSecretChat let isShown = GGExtrasPolicy.showsSelectionPanelButtons && !isSecretChat && !self.peerMedia self.ggCloudButton.isHidden = !isShown self.ggForwardWithoutAuthorButton.isHidden = !isShown if !isShown { return buttons } for button in [self.ggCloudButton, self.ggForwardWithoutAuthorButton] { button.isEnabled = self.forwardButton.isEnabled button.isImplicitlyDisabled = self.forwardButton.isImplicitlyDisabled } var result = buttons // GHOST: as in Swiftgram (W6-SGD), the two buttons take the place of «Пожаловаться», which stays in the // message menu. Upstream decides its visibility again at the start of every layout pass. self.reportButton.isHidden = true result.removeAll(where: { $0 === self.reportButton }) let index = result.firstIndex(where: { $0 === self.forwardButton }) ?? result.count result.insert(contentsOf: [self.ggCloudButton, self.ggForwardWithoutAuthorButton], at: index) return result } @objc fileprivate func ggCloudButtonPressed() { self.ggPerformExtraAction(.saveToCloud, sourceView: self.ggCloudButton) } @objc fileprivate func ggForwardWithoutAuthorButtonPressed() { self.ggPerformExtraAction(.forwardWithoutAuthor, sourceView: self.ggForwardWithoutAuthorButton) } private func ggPerformExtraAction(_ action: GGExtrasSelectionAction, sourceView: UIView) { if self.presentationInterfaceState?.renderedPeer?.peer is TelegramSecretChat { return } if let actions = self.actions, actions.isCopyProtected { self.interfaceInteraction?.displayCopyProtectionTip(sourceView, false) return } guard let context = self.context, !self.selectedMessages.isEmpty, !self.forwardButton.isImplicitlyDisabled else { return } GGExtrasUIHooks.performSelectionAction(action, context: context, messageIds: Array(self.selectedMessages)) self.interfaceInteraction?.cancelMessageSelection(.animated(duration: 0.4, curve: .spring)) } }