Telegram-iOS/submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode/Sources/ChatMessageSelectionInputPanelNode.swift
isaac 86d1456552 Postbox -> TelegramEngine waves 107-137 (squashed)
31 waves of consumer-side migration from `import Postbox` to TelegramEngine
typealiases. Net: 173 import drops + 39 BUILD-dep drops + 1 new typealias
(`EngineStoryId = StoryId`, wave 113).

Wave shapes used:
- Orphan-import sweeps (107, 108, 128): drop `import Postbox` from files
  whose only Postbox-symbol reference was the import line itself, then
  resolve build failures. Methodology requires token-level (`grep -oE`)
  filtering, not line-level, to avoid masking real Postbox usage on lines
  that also contain `Namespaces.X` references.
- Identifier-swap mini-waves (109-127, 129-134, 136-137): rename
  Postbox-typealiased identifiers to engine equivalents
  (PeerId -> EnginePeer.Id, MessageId -> EngineMessage.Id,
  MediaId -> EngineMedia.Id, MessageIndex -> EngineMessage.Index,
  StoryId -> EngineStoryId, ItemCollectionId -> EngineItemCollectionId,
  PreferencesEntry -> EnginePreferencesEntry,
  FetchResourceSourceType/Error -> EngineFetchResourceSourceType/Error,
  MemoryBuffer -> EngineMemoryBuffer, MessageTags -> EngineMessage.Tags,
  MessageAttribute -> EngineMessage.Attribute,
  TempBox -> EngineTempBox).
- Asset-string FP-only orphans (124).
- Typealias addition + drain (113): added `EngineStoryId` typealias to
  TelegramCore, then drained 3+11 consumer sites.

Hard blockers identified during these waves (must restore `import Postbox`
when present): MediaResource[A-Za-z]* (any suffix -- the literal
`MediaResource` matches don't catch MediaResourceData/MediaResourceId/etc.),
Postbox/MediaBox/MediaResource raw types, PostboxCoding/PostboxEncoder/
PostboxDecoder, TempBoxFile, ValueBoxKey, PostboxView, combinedView,
HashFunctions, postboxLog, openPostbox, declareEncodable, PeerView,
MessageHistoryView, MessageHistoryThreadData, CachedPeerData, RenderedPeer,
SelectivePrivacyPeer, SimpleDictionary, ItemCollectionInfosView,
ItemCollectionItem, ItemCollectionItemIndex, ItemCollectionViewEntryIndex,
ChatListIndex, ChatListEntrySummaryComponents, CodableEntry,
MessageHistoryThread, MessageHistoryAnchorIndex,
MessageHistoryEntryLocation, PeerStoryStats, PeerNameIndex,
PeerSummaryCounterTags, ChatListTotalUnreadStateCategory/Stats,
arePeersEqual. Protocol-shape blockers: bare `Peer`/`Message`/`Media`
in function signatures, generic args, enum-case payloads, or dict value
types (e.g., `[PeerId: Peer]`, `case messages([Message])`,
`Signal<(Peer?, ...), NoError>`).

`replace_all PeerId -> EnginePeer.Id` is dangerous: mangles compound
names like `failedPeerId`, `ContactListPeerId`, `nextRemoteMediaId`,
`replyToMessageId`. Pre-flight grep `\b[a-z][a-zA-Z]*PeerId\b` and only
replace_all if 0 matches.

Also removes unneeded design/plan docs from a separate (link-highlighting)
feature branch:
- docs/superpowers/plans/2026-05-02-link-highlighting-modern-path-fixes.md
- docs/superpowers/specs/2026-05-02-link-highlighting-modern-path-fixes-design.md

Squashed commits: 6d82c2980d..e6de5d53a3 (59 commits, including
per-wave content commits and per-wave CLAUDE.md bumps).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:28:50 +02:00

633 lines
28 KiB
Swift

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
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)
if #available(iOS 26.0, *) {
} else {
self.button.highligthedChanged = { [weak self] highlighted in
guard let self else {
return
}
if highlighted && self.isEnabled && !self.isImplicitlyDisabled {
self.backgroundView.contentView.alpha = 0.6
} else {
self.backgroundView.contentView.alpha = 1.0
self.backgroundView.contentView.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2)
}
}
}
}
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: isEnabled, transition: transition)
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
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<EngineMessage.Id>() {
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.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()
}
@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
}
let buttons: [GlassButtonView]
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
]
}
}
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)
}
}