mirror of
https://github.com/ichmagmaus111/ghostgram.git
synced 2026-11-03 14:17:19 +01:00
Ghostgram for iOS, rebuilt from scratch on Telegram iOS 13.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
106 lines
5.3 KiB
Swift
106 lines
5.3 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Display
|
|
import AppBundle
|
|
import TelegramCore
|
|
|
|
/// The type icon a button pill carries — the same asset set and the same action → icon mapping as a
|
|
/// bot keyboard button (`ChatMessageActionButtonsNode.swift:262-306`), so a `pageBlockButtonRow`, an
|
|
/// inline `RichText.textButton` and a reply-markup row all read alike.
|
|
///
|
|
/// This lives in its own module rather than in `InstantPageUI` because BOTH the V2 renderer and the
|
|
/// rich-text editor draw these, and the editor's host (`ChatRichTextEditorComposer`) cannot import
|
|
/// `InstantPageUI` — that edge is a cycle, since `InstantPageUI` already depends on the composer
|
|
/// (`InstantPageUI/BUILD`). A leaf module both sides can reach is the only shape that works, and it
|
|
/// keeps the mapping single-sourced.
|
|
///
|
|
/// Two deliberate differences from the keyboard mapping:
|
|
/// - `.url` cannot be resolved into an app / attach-bot link here. That test needs an
|
|
/// `AccountContext`, which neither surface is handed; a bare `?startgroup=` check is all that
|
|
/// survives, so app links get the plain link icon.
|
|
/// - `.openWebApp` gets the web-app icon. The keyboard path leaves it iconless, but only because its
|
|
/// switch predates the case — the icon is unambiguous.
|
|
///
|
|
/// Actions absent here are iconless by intent: `.callback` (nothing to promise the user),
|
|
/// `.requestPeer` / `.setupPoll` (keyboard-only, unreachable from `Api.InlineButtonType`), and
|
|
/// `.disabled`, which must read as inert.
|
|
public func richTextButtonIconName(for action: ReplyMarkupButtonAction) -> String? {
|
|
switch action {
|
|
case .text:
|
|
return "Chat/Message/BotMessage"
|
|
case let .url(value):
|
|
if value.lowercased().contains("?startgroup=") {
|
|
return "Chat/Message/BotAddToChat"
|
|
}
|
|
return "Chat/Message/BotLink"
|
|
case .urlAuth:
|
|
return "Chat/Message/BotLink"
|
|
case .requestPhone:
|
|
return "Chat/Message/BotPhone"
|
|
case .requestMap:
|
|
return "Chat/Message/BotLocation"
|
|
case .switchInline:
|
|
return "Chat/Message/BotShare"
|
|
case .payment:
|
|
return "Chat/Message/BotPayment"
|
|
case .openUserProfile:
|
|
return "Chat/Message/BotProfile"
|
|
case .openWebView, .openWebApp:
|
|
return "Chat/Message/BotWebApp"
|
|
case .copyText:
|
|
return "Chat/Message/BotCopy"
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
/// The icon's own size, shared by both placements. The assets are 10x10 vectors rasterised at that
|
|
/// natural size (no `preserves-vector-representation`), so this is also the largest size that draws
|
|
/// without softening; the keyboard path draws them into a 12x12 node with `contentMode = .center`,
|
|
/// which comes to the same ink.
|
|
public let richTextButtonIconSize = CGSize(width: 10.0, height: 10.0)
|
|
|
|
// MARK: - Block placement (a corner badge)
|
|
|
|
/// Distance from a BLOCK pill's top-right corner. Larger than the keyboard button's 4pt because a pill
|
|
/// is fully rounded (`cornerRadius = height / 2`) rather than a rounded rect: at these insets the whole
|
|
/// badge box stays inside the arc on a 40pt-tall pill, where the keyboard button's own 4/4 would put
|
|
/// the badge's outer corner past it and `clipsToBounds` would shave it.
|
|
public let richTextBlockButtonIconInset = CGPoint(x: 8.0, y: 6.0)
|
|
|
|
/// Horizontal room a badge-bearing BLOCK pill must keep clear on *each* side, so a centred label cannot
|
|
/// run under the badge. Mirrors the keyboard path's `minimumSideInset` (`4.0 + iconWidth`).
|
|
public let richTextBlockButtonIconReserve: CGFloat = richTextButtonIconSize.width + richTextBlockButtonIconInset.x
|
|
|
|
// MARK: - Inline placement (trailing the label)
|
|
|
|
/// Gap between an INLINE pill's label ink and the leading edge of its icon box.
|
|
///
|
|
/// An inline pill is far too short to carry a corner badge — its height is the label's ink box plus
|
|
/// 2pt — so the icon sits after the label instead, on the same optical line, and the pill simply grows
|
|
/// to hold it. The icon's trailing edge then lands on the pill's ordinary horizontal padding, exactly
|
|
/// where the label's would have.
|
|
public let richTextInlineButtonIconSpacing: CGFloat = 4.0
|
|
|
|
/// Width an icon-bearing INLINE pill adds to its own box: the gap plus the icon.
|
|
///
|
|
/// Unlike `richTextBlockButtonIconReserve`, which is a side inset that only binds in the row layout's
|
|
/// tight fallback, this is unconditional — it widens the pill, and therefore moves the line break of
|
|
/// the paragraph the pill sits in. That is why the editor must reserve the identical amount
|
|
/// (`RichTextButtonMetrics.inlineIconReserve`) even where it resolves no image.
|
|
public let richTextInlineButtonIconReserve: CGFloat = richTextButtonIconSize.width + richTextInlineButtonIconSpacing
|
|
|
|
// MARK: - Rasterisation
|
|
|
|
/// Tinted icon for `action`, or nil when the action has none.
|
|
///
|
|
/// Rasterises on every call rather than caching: the keyboard path pre-generates these per theme in
|
|
/// `PresentationThemeEssentialGraphics`, which neither surface has an equivalent of, and a 10x10 tint
|
|
/// is far cheaper than the global mutable cache it would take to avoid.
|
|
public func richTextButtonIcon(for action: ReplyMarkupButtonAction, color: UIColor) -> UIImage? {
|
|
guard let name = richTextButtonIconName(for: action) else {
|
|
return nil
|
|
}
|
|
return generateTintedImage(image: UIImage(bundleImageName: name), color: color)
|
|
}
|