GLEGram 12.5 — Initial public release

Based on Swiftgram 12.5 (Telegram iOS 12.5).
All GLEGram features ported and organized in GLEGram/ folder.

Features: Ghost Mode, Saved Deleted Messages, Content Protection Bypass,
Font Replacement, Fake Profile, Chat Export, Plugin System, and more.

See CHANGELOG_12.5.md for full details.
This commit is contained in:
Leeksov
2026-04-06 09:48:12 +03:00
commit 4647310322
39685 changed files with 11052678 additions and 0 deletions
@@ -0,0 +1,74 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatTextInputPanelNode",
module_name = "ChatTextInputPanelNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/AsyncDisplayKit",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/TelegramPresentationData",
"//submodules/TextFormat",
"//submodules/AccountContext",
"//submodules/TouchDownGesture",
"//submodules/ImageTransparency",
"//submodules/ActivityIndicator",
"//submodules/AnimationUI",
"//submodules/Speak",
"//submodules/ObjCRuntimeUtils",
"//submodules/AvatarNode",
"//submodules/ContextUI",
"//submodules/InvisibleInkDustNode",
"//submodules/TextInputMenu",
"//submodules/Pasteboard",
"//submodules/ChatPresentationInterfaceState",
"//submodules/ManagedAnimationNode",
"//submodules/TelegramUI/Components/EditableChatTextNode",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
"//submodules/Components/LottieAnimationComponent",
"//submodules/ComponentFlow",
"//submodules/TelegramUI/Components/EmojiSuggestionsComponent",
"//submodules/TelegramUI/Components/ChatControllerInteraction",
"//submodules/UndoUI",
"//submodules/PremiumUI",
"//submodules/StickerPeekUI",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/SolidRoundedButtonNode",
"//submodules/TooltipUI",
"//submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton",
"//submodules/ChatContextQuery",
"//submodules/TelegramUI/Components/Chat/ChatInputTextNode",
"//submodules/TelegramUI/Components/Chat/ChatInputPanelNode",
"//submodules/TelegramNotices",
"//submodules/AnimatedCountLabelNode",
"//submodules/TelegramStringFormatting",
"//submodules/TelegramUI/Components/TextNodeWithEntities",
"//submodules/Utils/DeviceModel",
"//submodules/PhotoResources",
"//submodules/TelegramUI/Components/Chat/ChatTextInputSlowmodePlaceholderNode",
"//submodules/TelegramUI/Components/Chat/ChatTextInputActionButtonsNode",
"//submodules/TelegramUI/Components/Chat/ChatTextInputAudioRecordingTimeNode",
"//submodules/TelegramUI/Components/Chat/ChatTextInputAudioRecordingCancelIndicator",
"//submodules/TelegramUI/Components/Chat/ChatRecordingViewOnceButtonNode",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
"//submodules/TelegramUI/Components/Chat/ChatInputAccessoryPanel",
"//submodules/TelegramUI/Components/Chat/ChatInputAutocompletePanel",
"//submodules/TelegramUI/Components/Chat/ChatRecordingPreviewInputPanelNode",
"//submodules/TelegramUI/Components/Chat/ChatInputContextPanelNode",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/TelegramUI/Components/RasterizedCompositionComponent",
"//submodules/TelegramUI/Components/StarsParticleEffect",
"//submodules/TelegramUI/Components/Calls/VideoChatMicButtonComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,361 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramPresentationData
import ChatPresentationInterfaceState
import GlassBackgroundComponent
import ComponentFlow
import LottieAnimationComponent
import LottieComponent
private let accessoryButtonFont = Font.medium(14.0)
final class AccessoryItemIconButton: HighlightTrackingButton, GlassBackgroundView.ContentView {
private var item: ChatTextInputAccessoryItem
private var theme: PresentationTheme
private var strings: PresentationStrings
private var width: CGFloat
private let iconImageView: GlassBackgroundView.ContentImageView
private var textView: ImmediateTextView?
private var tintMaskTextView: ImmediateTextView?
private var animationView: ComponentView<Empty>?
private var tintMaskAnimationView: UIImageView?
override static var layerClass: AnyClass {
return GlassBackgroundView.ContentLayer.self
}
let tintMask = UIView()
init(item: ChatTextInputAccessoryItem, theme: PresentationTheme, strings: PresentationStrings) {
self.item = item
self.theme = theme
self.strings = strings
self.iconImageView = GlassBackgroundView.ContentImageView()
let (image, text, accessibilityLabel, alpha, _) = AccessoryItemIconButton.imageAndInsets(item: item, theme: theme, strings: strings)
self.width = AccessoryItemIconButton.calculateWidth(item: item, image: image, text: text, strings: strings)
super.init(frame: CGRect())
(self.layer as? GlassBackgroundView.ContentLayer)?.targetLayer = self.tintMask.layer
self.isAccessibilityElement = true
self.accessibilityTraits = [.button]
self.iconImageView.isUserInteractionEnabled = false
self.addSubview(self.iconImageView)
self.tintMask.addSubview(self.iconImageView.tintMask)
switch item {
case .input, .botInput, .silentPost:
self.iconImageView.isHidden = true
self.iconImageView.tintMask.isHidden = true
self.animationView = ComponentView<Empty>()
self.tintMaskAnimationView = UIImageView()
default:
break
}
if let text {
if self.textView == nil {
let textView = ImmediateTextView()
textView.isUserInteractionEnabled = false
self.textView = textView
self.addSubview(textView)
}
if self.tintMaskTextView == nil {
let tintMaskTextView = ImmediateTextView()
self.tintMaskTextView = tintMaskTextView
self.tintMask.addSubview(tintMaskTextView)
}
self.textView?.attributedText = NSAttributedString(string: text, font: accessoryButtonFont, textColor: theme.chat.inputPanel.inputControlColor)
self.tintMaskTextView?.attributedText = NSAttributedString(string: text, font: accessoryButtonFont, textColor: .black)
} else {
if let textView = self.textView {
self.textView = nil
textView.removeFromSuperview()
}
if let tintMaskTextView = self.tintMaskTextView {
self.tintMaskTextView = nil
tintMaskTextView.removeFromSuperview()
}
}
self.iconImageView.image = image
self.iconImageView.tintColor = theme.chat.inputPanel.inputControlColor.withAlphaComponent(1.0)
self.iconImageView.alpha = alpha * theme.chat.inputPanel.inputControlColor.alpha
self.iconImageView.tintMask.alpha = alpha * theme.chat.inputPanel.inputControlColor.alpha
self.accessibilityLabel = accessibilityLabel
self.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.layer.removeAnimation(forKey: "opacity")
strongSelf.alpha = 0.4
strongSelf.layer.allowsGroupOpacity = true
} else {
strongSelf.alpha = 1.0
strongSelf.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.layer.allowsGroupOpacity = false
}
}
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let result = super.hitTest(point, with: event) else {
return nil
}
return result
}
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
self.theme = theme
self.strings = strings
let (image, text, accessibilityLabel, alpha, _) = AccessoryItemIconButton.imageAndInsets(item: item, theme: theme, strings: strings)
self.width = AccessoryItemIconButton.calculateWidth(item: item, image: image, text: text, strings: strings)
if let text {
self.textView?.attributedText = NSAttributedString(string: text, font: accessoryButtonFont, textColor: theme.chat.inputPanel.inputControlColor)
self.tintMaskTextView?.attributedText = NSAttributedString(string: text, font: accessoryButtonFont, textColor: .black)
}
self.iconImageView.image = image
self.iconImageView.tintColor = theme.chat.inputPanel.inputControlColor.withAlphaComponent(1.0)
self.iconImageView.alpha = alpha * theme.chat.inputPanel.inputControlColor.alpha
self.accessibilityLabel = accessibilityLabel
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private static func imageAndInsets(item: ChatTextInputAccessoryItem, theme: PresentationTheme, strings: PresentationStrings) -> (UIImage?, String?, String, CGFloat, UIEdgeInsets) {
switch item {
case let .input(isEnabled, inputMode), let .botInput(isEnabled, inputMode):
switch inputMode {
case .keyboard:
return (PresentationResourcesChat.chatInputTextFieldKeyboardImage(theme), nil, strings.VoiceOver_Keyboard, 1.0, UIEdgeInsets())
case .stickers, .emoji:
return (PresentationResourcesChat.chatInputTextFieldStickersImage(theme), nil, strings.VoiceOver_Stickers, isEnabled ? 1.0 : 0.4, UIEdgeInsets())
case .bot:
return (PresentationResourcesChat.chatInputTextFieldInputButtonsImage(theme), nil, strings.VoiceOver_BotKeyboard, 1.0, UIEdgeInsets())
}
case .commands:
return (PresentationResourcesChat.chatInputTextFieldCommandsImage(theme), nil, strings.VoiceOver_BotCommands, 1.0, UIEdgeInsets())
case let .silentPost(value):
if value {
return (PresentationResourcesChat.chatInputTextFieldSilentPostOnImage(theme), nil, strings.VoiceOver_SilentPostOn, 1.0, UIEdgeInsets())
} else {
return (PresentationResourcesChat.chatInputTextFieldSilentPostOffImage(theme), nil, strings.VoiceOver_SilentPostOff, 1.0, UIEdgeInsets())
}
case .suggestPost:
return (PresentationResourcesChat.chatInputTextFieldSuggestPostImage(theme), nil, strings.VoiceOver_SuggestPost, 1.0, UIEdgeInsets())
case let .messageAutoremoveTimeout(timeout):
if let timeout = timeout {
return (nil, shortTimeIntervalString(strings: strings, value: timeout), strings.VoiceOver_SelfDestructTimerOn(timeIntervalString(strings: strings, value: timeout)).string, 1.0, UIEdgeInsets())
} else {
return (PresentationResourcesChat.chatInputTextFieldTimerImage(theme), nil, strings.VoiceOver_SelfDestructTimerOff, 1.0, UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0))
}
case .scheduledMessages:
return (PresentationResourcesChat.chatInputTextFieldScheduleImage(theme), nil, strings.VoiceOver_ScheduledMessages, 1.0, UIEdgeInsets())
case .gift:
return (PresentationResourcesChat.chatInputTextFieldGiftImage(theme), nil, strings.VoiceOver_GiftPremium, 1.0, UIEdgeInsets())
}
}
private static func calculateWidth(item: ChatTextInputAccessoryItem, image: UIImage?, text: String?, strings: PresentationStrings) -> CGFloat {
switch item {
case .input, .botInput, .silentPost, .commands, .scheduledMessages, .gift, .suggestPost:
return 32.0
case let .messageAutoremoveTimeout(timeout):
var imageWidth = (image?.size.width ?? 0.0) + CGFloat(8.0)
if let _ = timeout, let text = text {
imageWidth = ceil((text as NSString).size(withAttributes: [.font: accessoryButtonFont]).width) + 10.0
}
return max(imageWidth, 24.0)
}
}
func updateLayout(item: ChatTextInputAccessoryItem, size: CGSize) {
let previousItem = self.item
self.item = item
let (updatedImage, text, _, _, _) = AccessoryItemIconButton.imageAndInsets(item: item, theme: self.theme, strings: self.strings)
if let image = self.iconImageView.image {
self.iconImageView.image = updatedImage
let bottomInset: CGFloat = 0.0
var imageFrame = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0) - bottomInset), size: image.size)
if case .scheduledMessages = item {
imageFrame.origin.y += 1.0
}
self.iconImageView.frame = imageFrame
self.iconImageView.tintMask.frame = imageFrame
if let animationView = self.animationView {
let width = AccessoryItemIconButton.calculateWidth(item: item, image: image, text: "", strings: self.strings)
let animationFrame = CGRect(origin: CGPoint(x: floor((size.width - width) / 2.0), y: floor((size.height - width) / 2.0) - bottomInset), size: CGSize(width: width, height: width))
let animationName: String
var animationMode: LottieAnimationComponent.AnimationItem.Mode = .still(position: .end)
if case let .silentPost(muted) = item {
if case let .silentPost(previousMuted) = previousItem {
if muted {
animationName = "input_anim_channelMute"
} else {
animationName = "input_anim_channelUnmute"
}
if muted != previousMuted {
animationMode = .animating(loop: false)
}
} else {
animationName = "input_anim_channelMute"
}
} else {
var previousInputMode: ChatTextInputAccessoryItem.InputMode?
var inputMode: ChatTextInputAccessoryItem.InputMode?
switch previousItem {
case let .input(_, itemInputMode), let .botInput(_, itemInputMode):
previousInputMode = itemInputMode
default:
break
}
switch item {
case let .input(_, itemInputMode), let .botInput(_, itemInputMode):
inputMode = itemInputMode
default:
break
}
if let inputMode = inputMode {
switch inputMode {
case .keyboard:
if let previousInputMode = previousInputMode {
if case .stickers = previousInputMode {
animationName = "input_anim_stickerToKey"
animationMode = .animating(loop: false)
} else if case .emoji = previousInputMode {
animationName = "input_anim_smileToKey"
animationMode = .animating(loop: false)
} else if case .bot = previousInputMode {
animationName = "input_anim_botToKey"
animationMode = .animating(loop: false)
} else {
animationName = "input_anim_stickerToKey"
}
} else {
animationName = "input_anim_stickerToKey"
}
case .stickers:
if let previousInputMode = previousInputMode {
if case .keyboard = previousInputMode {
animationName = "input_anim_keyToSticker"
animationMode = .animating(loop: false)
} else if case .emoji = previousInputMode {
animationName = "input_anim_smileToSticker"
animationMode = .animating(loop: false)
} else {
animationName = "input_anim_keyToSticker"
}
} else {
animationName = "input_anim_keyToSticker"
}
case .emoji:
if let previousInputMode = previousInputMode {
if case .keyboard = previousInputMode {
animationName = "input_anim_keyToSmile"
animationMode = .animating(loop: false)
} else if case .stickers = previousInputMode {
animationName = "input_anim_stickerToSmile"
animationMode = .animating(loop: false)
} else {
animationName = "input_anim_keyToSmile"
}
} else {
animationName = "input_anim_keyToSmile"
}
case .bot:
if let previousInputMode = previousInputMode {
if case .keyboard = previousInputMode {
animationName = "input_anim_keyToBot"
animationMode = .animating(loop: false)
} else {
animationName = "input_anim_keyToBot"
}
} else {
animationName = "input_anim_keyToBot"
}
}
} else {
animationName = ""
}
}
let animationSize = animationView.update(
transition: .immediate,
component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: animationName),
color: self.theme.chat.inputPanel.inputControlColor.withAlphaComponent(1.0)
)),
environment: {},
containerSize: animationFrame.size
)
if let view = animationView.view as? LottieComponent.View {
view.isUserInteractionEnabled = false
if view.superview == nil {
view.output = self.tintMaskAnimationView
self.addSubview(view)
if let tintMaskAnimationView = self.tintMaskAnimationView {
self.tintMask.addSubview(tintMaskAnimationView)
}
}
view.setMonochromaticEffect(tintColor: self.theme.chat.inputPanel.inputControlColor.withAlphaComponent(1.0))
view.alpha = self.theme.chat.inputPanel.inputControlColor.alpha
let animationFrameValue = CGRect(origin: CGPoint(x: animationFrame.minX + floor((animationFrame.width - animationSize.width) / 2.0), y: animationFrame.minY + floor((animationFrame.height - animationSize.height) / 2.0)), size: animationSize)
view.frame = animationFrameValue
if let tintMaskAnimationView = self.tintMaskAnimationView {
tintMaskAnimationView.frame = animationFrameValue
}
if case .animating = animationMode {
view.playOnce()
}
}
}
}
if let text {
self.textView?.attributedText = NSAttributedString(string: text, font: accessoryButtonFont, textColor: theme.chat.inputPanel.inputControlColor)
self.tintMaskTextView?.attributedText = NSAttributedString(string: text, font: accessoryButtonFont, textColor: .black)
}
if let textView = self.textView, let tintMaskTextView = self.tintMaskTextView {
let textSize = textView.updateLayout(CGSize(width: 100.0, height: 100.0))
let _ = tintMaskTextView.updateLayout(CGSize(width: 100.0, height: 100.0))
let textFrame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) * 0.5), y: floor((size.height - textSize.height) * 0.5)), size: textSize)
textView.frame = textFrame
tintMaskTextView.frame = textFrame
}
}
var buttonWidth: CGFloat {
return self.width
}
}
@@ -0,0 +1,132 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramPresentationData
import SwiftSignalKit
import ChatPresentationInterfaceState
import AnimatedCountLabelNode
import TelegramStringFormatting
private func generateClearImage(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 17.0, height: 17.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(color.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
context.setBlendMode(.copy)
context.setStrokeColor(UIColor.clear.cgColor)
context.setLineCap(.round)
context.setLineWidth(1.66)
context.move(to: CGPoint(x: 6.0, y: 6.0))
context.addLine(to: CGPoint(x: 11.0, y: 11.0))
context.strokePath()
context.move(to: CGPoint(x: size.width - 6.0, y: 6.0))
context.addLine(to: CGPoint(x: size.width - 11.0, y: 11.0))
context.strokePath()
})
}
final class BoostSlowModeButton: HighlightTrackingButtonNode {
let containerNode: ASDisplayNode
let backgroundNode: ASImageNode
let textNode: ImmediateAnimatedCountLabelNode
let iconNode: ASImageNode
private var updateTimer: SwiftSignalKit.Timer?
var requestUpdate: () -> Void = {}
override init(pointerStyle: PointerStyle? = nil) {
self.containerNode = ASDisplayNode()
self.backgroundNode = ASImageNode()
self.backgroundNode.displaysAsynchronously = false
self.backgroundNode.clipsToBounds = true
self.backgroundNode.image = generateGradientImage(size: CGSize(width: 100.0, height: 2.0), scale: 1.0, colors: [UIColor(rgb: 0x9076ff), UIColor(rgb: 0xbc6de8)], locations: [0.0, 1.0], direction: .horizontal)
self.iconNode = ASImageNode()
self.iconNode.displaysAsynchronously = false
self.iconNode.image = generateClearImage(color: .white)
self.textNode = ImmediateAnimatedCountLabelNode()
self.textNode.alwaysOneDirection = true
self.textNode.isUserInteractionEnabled = false
super.init(pointerStyle: pointerStyle)
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.backgroundNode)
self.containerNode.addSubnode(self.iconNode)
self.containerNode.addSubnode(self.textNode)
self.highligthedChanged = { [weak self] highlighted in
if let self {
if highlighted {
self.containerNode.layer.animateScale(from: 1.0, to: 0.75, duration: 0.4, removeOnCompletion: false)
} else if let presentationLayer = self.containerNode.layer.presentation() {
self.containerNode.layer.animateScale(from: CGFloat((presentationLayer.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0), to: 1.0, duration: 0.25, removeOnCompletion: false)
}
}
}
}
func update(size: CGSize, interfaceState: ChatPresentationInterfaceState) -> CGSize {
var text = ""
if let slowmodeState = interfaceState.slowmodeState {
let relativeTimestamp: CGFloat
switch slowmodeState.variant {
case let .timestamp(validUntilTimestamp):
let timestamp = CGFloat(Date().timeIntervalSince1970)
relativeTimestamp = CGFloat(validUntilTimestamp) - timestamp
case .pendingMessages:
relativeTimestamp = CGFloat(slowmodeState.timeout)
}
self.updateTimer?.invalidate()
if relativeTimestamp >= 0.0 {
text = stringForDuration(Int32(relativeTimestamp))
self.updateTimer = SwiftSignalKit.Timer(timeout: 1.0 / 60.0, repeat: false, completion: { [weak self] in
self?.requestUpdate()
}, queue: .mainQueue())
self.updateTimer?.start()
} else {
text = stringForDuration(0)
}
} else {
self.updateTimer?.invalidate()
self.updateTimer = nil
}
let font = Font.with(size: 15.0, design: .round, weight: .semibold, traits: [.monospacedNumbers])
let textColor = UIColor.white
var segments: [AnimatedCountLabelNode.Segment] = []
var textCount = 0
for char in text {
if let intValue = Int(String(char)) {
segments.append(.number(intValue, NSAttributedString(string: String(char), font: font, textColor: textColor)))
} else {
segments.append(.text(textCount, NSAttributedString(string: String(char), font: font, textColor: textColor)))
textCount += 1
}
}
self.textNode.segments = segments
let textSize = self.textNode.updateLayout(size: CGSize(width: 200.0, height: 100.0), animated: true)
let totalSize = CGSize(width: textSize.width > 0.0 ? textSize.width + 38.0 : 40.0, height: 40.0)
self.containerNode.bounds = CGRect(origin: .zero, size: totalSize)
self.containerNode.position = CGPoint(x: totalSize.width / 2.0, y: totalSize.height / 2.0)
self.backgroundNode.frame = CGRect(origin: .zero, size: totalSize)
self.backgroundNode.cornerRadius = totalSize.height / 2.0
self.textNode.frame = CGRect(origin: CGPoint(x: 9.0, y: floorToScreenPixels((totalSize.height - textSize.height) / 2.0)), size: textSize)
if let icon = self.iconNode.image {
self.iconNode.frame = CGRect(origin: CGPoint(x: totalSize.width - icon.size.width - 7.0, y: floorToScreenPixels((totalSize.height - icon.size.height) / 2.0)), size: icon.size)
}
return totalSize
}
}
@@ -0,0 +1,105 @@
import Foundation
import UIKit
import Display
import TelegramPresentationData
import ComponentFlow
import GlassBackgroundComponent
import AppBundle
final class InputIconButtonComponent: Component {
let theme: PresentationTheme
let name: String
let action: (UIView) -> Void
init(
theme: PresentationTheme,
name: String,
action: @escaping (UIView) -> Void
) {
self.theme = theme
self.name = name
self.action = action
}
static func ==(lhs: InputIconButtonComponent, rhs: InputIconButtonComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.name != rhs.name {
return false
}
return true
}
final class View: UIView {
private let backgroundView: GlassBackgroundView
private let button: HighlightTrackingButton
private let iconView: GlassBackgroundView.ContentImageView
private var component: InputIconButtonComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.backgroundView = GlassBackgroundView()
self.button = HighlightTrackingButton()
self.iconView = GlassBackgroundView.ContentImageView()
super.init(frame: frame)
self.addSubview(self.backgroundView)
self.backgroundView.contentView.addSubview(self.iconView)
self.backgroundView.contentView.addSubview(self.button)
self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
self.button.highligthedChanged = { [weak self] highlighted in
guard let self else {
return
}
if #available(iOS 26.0, *) {
} else {
self.iconView.alpha = highlighted ? 1.0 : 0.7
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func buttonPressed() {
self.component?.action(self)
}
func update(component: InputIconButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
if self.component?.name != component.name {
self.iconView.image = UIImage(bundleImageName: component.name)?.withRenderingMode(.alwaysTemplate)
}
self.iconView.tintColor = component.theme.chat.inputPanel.panelControlColor
self.component = component
self.state = state
let size = CGSize(width: 40.0, height: 40.0)
if let image = self.iconView.image {
self.iconView.frame = image.size.centered(in: CGRect(origin: CGPoint(), size: size))
}
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size))
self.backgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel), isInteractive: true, transition: transition)
self.button.frame = CGRect(origin: CGPoint(), size: size)
return size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,179 @@
import Foundation
import UIKit
import Display
import TelegramPresentationData
import ComponentFlow
import GlassBackgroundComponent
import SwiftSignalKit
import VideoChatMicButtonComponent
import AccountContext
final class LiveMicrophoneButtonComponent: Component {
let theme: PresentationTheme
let strings: PresentationStrings
let call: AnyObject?
init(
theme: PresentationTheme,
strings: PresentationStrings,
call: AnyObject?
) {
self.theme = theme
self.strings = strings
self.call = call
}
static func ==(lhs: LiveMicrophoneButtonComponent, rhs: LiveMicrophoneButtonComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.call !== rhs.call {
return false
}
return true
}
final class View: UIView {
private let button = ComponentView<Empty>()
private var component: LiveMicrophoneButtonComponent?
private weak var state: EmptyComponentState?
private var isUpdating: Bool = false
private var callStateDisposable: Disposable?
private var muteStateDisposable: Disposable?
private var callState: PresentationGroupCallState?
private var isMuted: Bool = false
private var isPushToTalkActive: Bool = false
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.muteStateDisposable?.dispose()
self.callStateDisposable?.dispose()
}
func update(component: LiveMicrophoneButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
if self.callStateDisposable == nil, let call = component.call as? PresentationGroupCall {
self.callStateDisposable = (call.state
|> deliverOnMainQueue).startStrict(next: { [weak self] callState in
guard let self else {
return
}
if self.callState != callState {
self.callState = callState
if !self.isUpdating {
self.state?.updated(transition: .spring(duration: 0.4))
}
}
})
}
if self.muteStateDisposable == nil, let call = component.call as? PresentationGroupCall {
self.muteStateDisposable = (call.isMuted
|> deliverOnMainQueue).startStrict(next: { [weak self] isMuted in
guard let self else {
return
}
if self.isMuted != isMuted {
self.isMuted = isMuted
if !self.isUpdating {
self.state?.updated(transition: .spring(duration: 0.4))
}
}
})
}
self.component = component
self.state = state
let size = CGSize(width: 40.0, height: 40.0)
let micButtonContent: VideoChatMicButtonComponent.Content
if let callState = self.callState {
switch callState.networkState {
case .connecting:
micButtonContent = .connecting
case .connected:
if let _ = callState.muteState {
if self.isPushToTalkActive {
micButtonContent = .unmuted(pushToTalk: self.isPushToTalkActive)
} else {
micButtonContent = .muted(forced: false)
}
} else {
micButtonContent = .unmuted(pushToTalk: false)
}
}
} else {
micButtonContent = .connecting
}
let _ = self.button.update(
transition: transition,
component: AnyComponent(VideoChatMicButtonComponent(
call: component.call.flatMap { call -> VideoChatCall? in
if let call = call as? PresentationGroupCall {
return .group(call)
} else {
return nil
}
},
strings: component.strings,
content: micButtonContent,
isCollapsed: true,
isCompact: true,
customIconScale: 0.45,
updateUnmutedStateIsPushToTalk: { [weak self] unmutedStateIsPushToTalk in
guard let self, let component = self.component, let call = component.call as? PresentationGroupCall else {
return
}
if let unmutedStateIsPushToTalk {
if unmutedStateIsPushToTalk {
self.isPushToTalkActive = true
call.setIsMuted(action: .muted(isPushToTalkActive: true))
} else {
call.setIsMuted(action: .unmuted)
self.isPushToTalkActive = false
}
self.state?.updated(transition: .spring(duration: 0.4))
} else {
call.setIsMuted(action: .muted(isPushToTalkActive: false))
self.isPushToTalkActive = false
self.state?.updated(transition: .spring(duration: 0.4))
}
},
raiseHand: {},
scheduleAction: {}
)),
environment: {},
containerSize: size
)
if let buttonView = self.button.view {
if buttonView.superview == nil {
self.addSubview(buttonView)
}
buttonView.frame = CGRect(origin: CGPoint(), size: size)
}
return size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,78 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramPresentationData
import ManagedAnimationNode
enum MenuIconNodeState: Equatable {
case menu
case app
case close
}
final class MenuIconNode: ManagedAnimationNode {
private let duration: Double = 0.33
var iconState: MenuIconNodeState = .menu
init() {
super.init(size: CGSize(width: 30.0, height: 30.0))
self.trackTo(item: ManagedAnimationItem(source: .local("anim_menuclose"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
}
func enqueueState(_ state: MenuIconNodeState, animated: Bool) {
guard self.iconState != state else {
return
}
let previousState = self.iconState
self.iconState = state
switch previousState {
case .close:
switch state {
case .menu:
if animated {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_closemenu"), frames: .range(startFrame: 0, endFrame: 20), duration: self.duration))
} else {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_menuclose"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
}
case .app:
if animated {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_webview"), frames: .range(startFrame: 0, endFrame: 22), duration: self.duration))
} else {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_webview"), frames: .range(startFrame: 22, endFrame: 22), duration: 0.01))
}
case .close:
break
}
case .menu:
switch state {
case .close:
if animated {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_menuclose"), frames: .range(startFrame: 0, endFrame: 20), duration: self.duration))
} else {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_menuclose"), frames: .range(startFrame: 20, endFrame: 20), duration: 0.01))
}
case .app:
self.trackTo(item: ManagedAnimationItem(source: .local("anim_webview"), frames: .range(startFrame: 22, endFrame: 22), duration: 0.01))
case .menu:
break
}
case .app:
switch state {
case .close:
if animated {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_webview"), frames: .range(startFrame: 22, endFrame: 0), duration: self.duration))
} else {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_webview"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
}
case .menu:
self.trackTo(item: ManagedAnimationItem(source: .local("anim_menuclose"), frames: .range(startFrame: 0, endFrame: 20), duration: 0.01))
case .app:
break
}
}
}
}
@@ -0,0 +1,372 @@
import Foundation
import UIKit
import Display
import TelegramPresentationData
import ComponentFlow
import GlassBackgroundComponent
import AnimatedTextComponent
import StarsParticleEffect
final class StarReactionButtonBadgeComponent: Component {
let theme: PresentationTheme
let count: Int
let isFilled: Bool
init(
theme: PresentationTheme,
count: Int,
isFilled: Bool
) {
self.theme = theme
self.count = count
self.isFilled = isFilled
}
static func ==(lhs: StarReactionButtonBadgeComponent, rhs: StarReactionButtonBadgeComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.count != rhs.count {
return false
}
if lhs.isFilled != rhs.isFilled {
return false
}
return true
}
final class View: UIView {
private let backgroundView: GlassBackgroundView
private let text = ComponentView<Empty>()
private var component: StarReactionButtonBadgeComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.backgroundView = GlassBackgroundView()
super.init(frame: frame)
self.addSubview(self.backgroundView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: StarReactionButtonBadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let height: CGFloat = 15.0
let sideInset: CGFloat = 4.0
let textSize = self.text.update(
transition: transition,
component: AnyComponent(AnimatedTextComponent(
font: Font.semibold(10.0),
color: component.theme.chat.inputPanel.panelControlColor,
items: [AnimatedTextComponent.Item(id: AnyHashable(0), content: .text(countString(Int64(component.count))))],
noDelay: true
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
let size = CGSize(width: max(height, textSize.width + sideInset * 2.0), height: height)
let backgroundFrame = CGRect(origin: CGPoint(), size: size)
let backgroundTintColor: GlassBackgroundView.TintColor
if component.isFilled {
backgroundTintColor = .init(kind: .custom(style: .default, color: UIColor(rgb: 0xFFB10D)))
} else {
backgroundTintColor = .init(kind: .panel)
}
self.backgroundView.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: backgroundTintColor, isInteractive: true, transition: transition)
if let textView = self.text.view {
let textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundFrame.width - textSize.width) * 0.5), y: floorToScreenPixels((backgroundFrame.height - textSize.height) * 0.5)), size: textSize)
if textView.superview == nil {
textView.isUserInteractionEnabled = false
self.backgroundView.contentView.addSubview(textView)
}
transition.setFrame(view: textView, frame: textFrame)
}
return size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class StarReactionButtonComponent: Component {
let theme: PresentationTheme
let count: Int
let isFilled: Bool
let action: (UIView) -> Void
let longPressAction: ((UIView) -> Void)?
init(
theme: PresentationTheme,
count: Int,
isFilled: Bool,
action: @escaping (UIView) -> Void,
longPressAction: ((UIView) -> Void)?
) {
self.theme = theme
self.count = count
self.isFilled = isFilled
self.action = action
self.longPressAction = longPressAction
}
static func ==(lhs: StarReactionButtonComponent, rhs: StarReactionButtonComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.count != rhs.count {
return false
}
if lhs.isFilled != rhs.isFilled {
return false
}
if (lhs.longPressAction == nil) != (rhs.longPressAction == nil) {
return false
}
return true
}
final class View: UIView {
private let containerView: UIView
private let backgroundView: GlassBackgroundView
private let backgroundEffectLayer: StarsParticleEffectLayer
private let backgroundMaskView: UIView
private var backgroundBadgeMask: UIImageView?
private let iconView: UIImageView
private var badge: ComponentView<Empty>?
private var longTapRecognizer: TapLongTapOrDoubleTapGestureRecognizer?
private var component: StarReactionButtonComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.containerView = UIView()
self.backgroundView = GlassBackgroundView()
self.backgroundMaskView = UIView()
self.backgroundEffectLayer = StarsParticleEffectLayer()
self.backgroundView.contentView.layer.addSublayer(self.backgroundEffectLayer)
self.backgroundView.mask = self.backgroundMaskView
self.backgroundMaskView.backgroundColor = .white
if let filter = CALayer.luminanceToAlpha() {
self.backgroundMaskView.layer.filters = [filter]
}
self.iconView = UIImageView()
super.init(frame: frame)
self.addSubview(self.containerView)
self.containerView.addSubview(self.backgroundView)
self.backgroundView.contentView.addSubview(self.iconView)
let longTapRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.longTapAction(_:)))
longTapRecognizer.tapActionAtPoint = { _ in
return .waitForSingleTap
}
longTapRecognizer.highlight = { [weak self] point in
guard let self else {
return
}
let currentTransform: CATransform3D
if self.containerView.layer.animation(forKey: "transform") != nil || self.containerView.layer.animation(forKey: "transform.scale") != nil {
currentTransform = self.containerView.layer.presentation()?.transform ?? layer.transform
} else {
currentTransform = self.containerView.layer.transform
}
let currentScale = sqrt((currentTransform.m11 * currentTransform.m11) + (currentTransform.m12 * currentTransform.m12) + (currentTransform.m13 * currentTransform.m13))
let updatedScale: CGFloat = point != nil ? 1.35 : 1.0
self.containerView.layer.transform = CATransform3DMakeScale(updatedScale, updatedScale, 1.0)
self.containerView.layer.animateSpring(from: currentScale as NSNumber, to: updatedScale as NSNumber, keyPath: "transform.scale", duration: point != nil ? 0.4 : 0.8, damping: 70.0)
}
self.longTapRecognizer = longTapRecognizer
self.backgroundView.contentView.addGestureRecognizer(longTapRecognizer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func longTapAction(_ recogizer: TapLongTapOrDoubleTapGestureRecognizer) {
guard let component = self.component else {
return
}
switch recogizer.state {
case .ended:
if let gesture = recogizer.lastRecognizedGestureAndLocation?.0 {
if case .tap = gesture {
HapticFeedback().tap()
component.action(self)
} else if case .longTap = gesture {
HapticFeedback().impact(.medium)
component.longPressAction?(self)
}
}
default:
break
}
}
func update(component: StarReactionButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let previousComponent = self.component
self.component = component
self.state = state
let size = CGSize(width: 40.0, height: 40.0)
if self.iconView.image == nil {
self.iconView.image = UIImage(bundleImageName: "Premium/Stars/ButtonStar")?.withRenderingMode(.alwaysTemplate)
}
if component.count != 0 {
let badge: ComponentView<Empty>
var badgeTransition = transition
if let current = self.badge {
badge = current
} else {
badgeTransition = badgeTransition.withAnimation(.none)
badge = ComponentView()
self.badge = badge
}
let backgroundBadgeMask: UIImageView
if let current = self.backgroundBadgeMask {
backgroundBadgeMask = current
} else {
backgroundBadgeMask = UIImageView()
self.backgroundBadgeMask = backgroundBadgeMask
}
let badgeSize = badge.update(
transition: badgeTransition,
component: AnyComponent(StarReactionButtonBadgeComponent(
theme: component.theme,
count: component.count,
isFilled: component.isFilled
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
if let badgeView = badge.view {
var badgeFrame = CGRect(origin: CGPoint(x: min(size.width + 6.0 - badgeSize.width, floorToScreenPixels(size.width - 4.0 - badgeSize.width * 0.5)), y: -3.0), size: badgeSize)
if badgeSize.width > size.width * 0.8 {
badgeFrame.origin.x = floor((size.width - badgeSize.width) * 0.5)
}
if badgeView.superview == nil {
badgeView.isUserInteractionEnabled = false
self.containerView.addSubview(badgeView)
badgeView.frame = badgeFrame
transition.animateScale(view: badgeView, from: 0.001, to: 1.0)
transition.animateAlpha(view: badgeView, from: 0.0, to: 1.0)
}
transition.setFrame(view: badgeView, frame: badgeFrame)
let badgeBorderWidth: CGFloat = 1.0
if backgroundBadgeMask.image?.size.height != (badgeFrame.height + badgeBorderWidth * 2.0) {
backgroundBadgeMask.image = generateStretchableFilledCircleImage(diameter: badgeFrame.height + badgeBorderWidth * 2.0, color: .black)
}
let backgroundBadgeFrame = badgeFrame.insetBy(dx: -badgeBorderWidth, dy: -badgeBorderWidth)
if backgroundBadgeMask.superview == nil {
self.backgroundMaskView.addSubview(backgroundBadgeMask)
backgroundBadgeMask.frame = backgroundBadgeFrame
transition.animateScale(view: backgroundBadgeMask, from: 0.001, to: 1.0)
transition.animateAlpha(view: backgroundBadgeMask, from: 0.0, to: 1.0)
}
transition.setFrame(view: backgroundBadgeMask, frame: backgroundBadgeFrame)
}
} else {
if let badge = self.badge {
if let previousComponent {
let _ = badge.update(
transition: transition,
component: AnyComponent(StarReactionButtonBadgeComponent(
theme: component.theme,
count: previousComponent.count,
isFilled: previousComponent.isFilled
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
}
self.badge = nil
if let badgeView = badge.view {
transition.setScale(view: badgeView, scale: 0.001)
transition.setAlpha(view: badgeView, alpha: 0.0, completion: { [weak badgeView] _ in
badgeView?.removeFromSuperview()
})
}
}
if let backgroundBadgeMask = self.backgroundBadgeMask {
self.backgroundBadgeMask = nil
transition.setScale(view: backgroundBadgeMask, scale: 0.001)
transition.setAlpha(view: backgroundBadgeMask, alpha: 0.0, completion: { [weak backgroundBadgeMask] _ in
backgroundBadgeMask?.removeFromSuperview()
})
}
}
let backgroundFrame = CGRect(origin: CGPoint(), size: size)
let backgroundTintColor: GlassBackgroundView.TintColor
if component.isFilled {
backgroundTintColor = .init(kind: .custom(style: .default, color: UIColor(rgb: 0xFFB10D)))
} else {
backgroundTintColor = .init(kind: .panel)
}
self.backgroundView.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: backgroundTintColor, isInteractive: false, transition: transition)
transition.setFrame(view: self.backgroundView, frame: backgroundFrame)
transition.setFrame(view: self.backgroundMaskView, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
transition.setFrame(layer: self.backgroundEffectLayer, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
self.backgroundEffectLayer.update(color: UIColor(white: 1.0, alpha: 0.25), rate: 10.0, size: backgroundFrame.size, cornerRadius: backgroundFrame.height * 0.5, transition: transition)
self.iconView.tintColor = component.theme.chat.inputPanel.panelControlColor
if let image = self.iconView.image {
let iconFrame = image.size.centered(in: CGRect(origin: CGPoint(), size: backgroundFrame.size))
transition.setFrame(view: self.iconView, frame: iconFrame)
}
transition.setPosition(view: self.containerView, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5))
transition.setBounds(view: self.containerView, bounds: CGRect(origin: CGPoint(), size: size))
return size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}