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,28 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AlertCheckComponent",
module_name = "AlertCheckComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/ComponentFlow",
"//submodules/TelegramPresentationData",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TextFormat",
"//submodules/Markdown",
"//submodules/TelegramUI/Components/AlertComponent",
"//submodules/TelegramUI/Components/CheckComponent",
"//submodules/Components/MultilineTextComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,186 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import SwiftSignalKit
import TelegramPresentationData
import AlertComponent
import PlainButtonComponent
import MultilineTextComponent
import CheckComponent
import TextFormat
import Markdown
public final class AlertCheckComponent: Component {
public typealias EnvironmentType = AlertComponentEnvironment
public class ExternalState {
public fileprivate(set) var value: Bool
fileprivate var valuePromise = Promise<Bool>()
public var valueSignal: Signal<Bool, NoError>
public init() {
self.value = false
self.valueSignal = self.valuePromise.get()
}
}
let title: String
let initialValue: Bool
let externalState: ExternalState
let linkAction: (() -> Void)?
public init(
title: String,
initialValue: Bool,
externalState: ExternalState,
linkAction: (() -> Void)? = nil
) {
self.title = title
self.initialValue = initialValue
self.externalState = externalState
self.linkAction = linkAction
}
public static func ==(lhs: AlertCheckComponent, rhs: AlertCheckComponent) -> Bool {
return true
}
public final class View: UIView {
private let button = ComponentView<Empty>()
private var component: AlertCheckComponent?
private weak var state: EmptyComponentState?
private var isUpdating = false
private var valuePromise = ValuePromise<Bool>(false)
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
func findTextView(view: UIView?) -> ImmediateTextView? {
if let view {
if let view = view as? ImmediateTextView {
return view
}
for view in view.subviews {
if let result = findTextView(view: view) {
return result
}
}
}
return nil
}
let result = super.hitTest(point, with: event)
if let textView = findTextView(view: result) {
if let (_, attributes) = textView.attributesAtPoint(self.convert(point, to: textView)) {
if attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] != nil {
return textView
}
}
}
return result
}
func update(component: AlertCheckComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
if self.component == nil {
component.externalState.value = component.initialValue
component.externalState.valuePromise.set(self.valuePromise.get())
}
self.component = component
self.state = state
let environment = environment[AlertComponentEnvironment.self]
let checkTheme = CheckComponent.Theme(
backgroundColor: environment.theme.list.itemCheckColors.fillColor,
strokeColor: environment.theme.list.itemCheckColors.foregroundColor,
borderColor: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.15),
overlayBorder: false,
hasInset: false,
hasShadow: false
)
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
let textColor = environment.theme.actionSheet.primaryTextColor
let linkColor = environment.theme.actionSheet.controlAccentColor
let markdownAttributes = MarkdownAttributes(
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
link: MarkdownAttributeSet(font: textFont, textColor: linkColor),
linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
}
)
let buttonSize = self.button.update(
transition: transition,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(HStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(CheckComponent(
theme: checkTheme,
size: CGSize(width: 18.0, height: 18.0),
selected: component.externalState.value
))),
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
text: .markdown(text: component.title, attributes: markdownAttributes),
maximumNumberOfLines: 2,
highlightColor: linkColor.withAlphaComponent(0.1),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
},
tapAction: { attributes, _ in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
component.linkAction?()
}
}
)))
], spacing: 10.0)),
effectAlignment: .center,
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.externalState.value = !component.externalState.value
self.valuePromise.set(component.externalState.value)
if !self.isUpdating {
self.state?.updated(transition: .spring(duration: 0.4))
}
},
animateAlpha: false,
animateScale: false
)),
environment: {
},
containerSize: CGSize(width: availableSize.width + 20.0, height: 1000.0)
)
let buttonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - buttonSize.width) / 2.0), y: 7.0), size: buttonSize)
if let buttonView = self.button.view {
if buttonView.superview == nil {
self.addSubview(buttonView)
}
transition.setFrame(view: buttonView, frame: buttonFrame)
}
return CGSize(width: availableSize.width, height: buttonSize.height + 7.0)
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,28 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AlertInputFieldComponent",
module_name = "AlertInputFieldComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/AccountContext",
"//submodules/ComponentFlow",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramPresentationData",
"//submodules/TextFormat",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/TelegramUI/Components/AlertComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,353 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import AlertComponent
import MultilineTextComponent
import AccountContext
import TextFormat
import PlainButtonComponent
import BundleIconComponent
public final class AlertInputFieldComponent: Component {
public typealias EnvironmentType = AlertComponentEnvironment
public class ExternalState {
public fileprivate(set) var value: String = ""
public fileprivate(set) var animateError: () -> Void = {}
public fileprivate(set) var activateInput: () -> Void = {}
fileprivate let valuePromise = ValuePromise<String>("")
public var valueSignal: Signal<String, NoError> {
return self.valuePromise.get()
}
public init() {
}
}
let context: AccountContext
let initialValue: String?
let placeholder: String
let characterLimit: Int?
let hasClearButton: Bool
let isSecureTextEntry: Bool
let returnKeyType: UIReturnKeyType
let keyboardType: UIKeyboardType
let autocapitalizationType: UITextAutocapitalizationType
let autocorrectionType: UITextAutocorrectionType
let isInitiallyFocused: Bool
let externalState: ExternalState
let shouldChangeText: ((String) -> Bool)?
let returnKeyAction: (() -> Void)?
public init(
context: AccountContext,
initialValue: String? = nil,
placeholder: String,
characterLimit: Int? = nil,
hasClearButton: Bool = false,
isSecureTextEntry: Bool = false,
returnKeyType: UIReturnKeyType = .done,
keyboardType: UIKeyboardType = .default,
autocapitalizationType: UITextAutocapitalizationType = .sentences,
autocorrectionType: UITextAutocorrectionType = .default,
isInitiallyFocused: Bool = false,
externalState: ExternalState,
shouldChangeText: ((String) -> Bool)? = nil,
returnKeyAction: (() -> Void)? = nil
) {
self.context = context
self.initialValue = initialValue
self.placeholder = placeholder
self.characterLimit = characterLimit
self.hasClearButton = hasClearButton
self.isSecureTextEntry = isSecureTextEntry
self.returnKeyType = returnKeyType
self.keyboardType = keyboardType
self.autocapitalizationType = autocapitalizationType
self.autocorrectionType = autocorrectionType
self.isInitiallyFocused = isInitiallyFocused
self.externalState = externalState
self.shouldChangeText = shouldChangeText
self.returnKeyAction = returnKeyAction
}
public static func ==(lhs: AlertInputFieldComponent, rhs: AlertInputFieldComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.initialValue != rhs.initialValue {
return false
}
if lhs.placeholder != rhs.placeholder {
return false
}
if lhs.characterLimit != rhs.characterLimit {
return false
}
if lhs.hasClearButton != rhs.hasClearButton {
return false
}
if lhs.isSecureTextEntry != rhs.isSecureTextEntry {
return false
}
if lhs.returnKeyType != rhs.returnKeyType {
return false
}
if lhs.keyboardType != rhs.keyboardType {
return false
}
if lhs.autocapitalizationType != rhs.autocapitalizationType {
return false
}
if lhs.autocorrectionType != rhs.autocorrectionType {
return false
}
if lhs.isInitiallyFocused != rhs.isInitiallyFocused {
return false
}
return true
}
private final class TextField: UITextField {
var sideInset: CGFloat = 0.0
override func textRect(forBounds bounds: CGRect) -> CGRect {
return CGRect(origin: CGPoint(x: self.sideInset, y: 0.0), size: CGSize(width: bounds.width - self.sideInset * 2.0, height: bounds.height))
}
override func editingRect(forBounds bounds: CGRect) -> CGRect {
return CGRect(origin: CGPoint(x: self.sideInset, y: 0.0), size: CGSize(width: bounds.width - self.sideInset * 2.0, height: bounds.height))
}
}
public final class View: UIView, UITextFieldDelegate {
private let background = ComponentView<Empty>()
private let textField = TextField()
private let placeholder = ComponentView<Empty>()
private let clearButton = ComponentView<Empty>()
private var component: AlertInputFieldComponent?
private weak var state: EmptyComponentState?
private var isUpdating = false
var currentText: String {
return self.textField.text ?? ""
}
private var clearOnce: Bool = false
func activateInput() {
self.textField.becomeFirstResponder()
}
func animateError() {
if let component = self.component, component.isInitiallyFocused {
self.clearOnce = true
}
self.textField.layer.addShakeAnimation()
HapticFeedback().error()
}
public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
self.component?.returnKeyAction?()
return false
}
@objc private func textDidChange() {
if !self.isUpdating {
self.state?.updated(transition: .immediate)
}
}
public func textFieldDidBeginEditing(_ textField: UITextField) {
self.clearButton.view?.isHidden = self.currentText.isEmpty
}
public func textFieldDidEndEditing(_ textField: UITextField) {
self.clearButton.view?.isHidden = true
}
public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard let component = self.component else {
return true
}
if self.clearOnce {
self.clearOnce = false
if range.length > string.count {
textField.text = ""
return false
}
}
let updatedText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string)
if let shouldChangeText = component.shouldChangeText {
return shouldChangeText(updatedText)
}
return true
}
public func setText(text: String) {
self.textField.text = text
if !self.isUpdating {
self.state?.updated(transition: .immediate, isLocal: true)
}
}
func update(component: AlertInputFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
var resetText: String?
if self.component == nil {
resetText = component.initialValue
component.externalState.animateError = { [weak self] in
self?.animateError()
}
component.externalState.activateInput = { [weak self] in
self?.activateInput()
}
}
let isFirstTime = self.component == nil
self.component = component
self.state = state
let environment = environment[AlertComponentEnvironment.self]
let topInset: CGFloat = 15.0
if self.textField.superview == nil {
self.addSubview(self.textField)
self.textField.delegate = self
self.textField.addTarget(self, action: #selector(self.textDidChange), for: .editingChanged)
}
if self.textField.autocapitalizationType != component.autocapitalizationType {
self.textField.autocapitalizationType = component.autocapitalizationType
}
if self.textField.autocorrectionType != component.autocorrectionType {
self.textField.autocorrectionType = component.autocorrectionType
}
if self.textField.isSecureTextEntry != component.isSecureTextEntry {
self.textField.isSecureTextEntry = component.isSecureTextEntry
}
if self.textField.returnKeyType != component.returnKeyType {
self.textField.returnKeyType = component.returnKeyType
}
self.textField.keyboardAppearance = environment.theme.overallDarkAppearance ? .dark : .light
if let resetText {
self.textField.text = resetText
}
self.textField.font = Font.regular(17.0)
self.textField.textColor = environment.theme.actionSheet.primaryTextColor
self.textField.tintColor = environment.theme.actionSheet.controlAccentColor
self.textField.sideInset = 16.0
let backgroundPadding: CGFloat = 14.0
let size = CGSize(width: availableSize.width, height: 50.0)
let backgroundSize = self.background.update(
transition: transition,
component: AnyComponent(
FilledRoundedRectangleComponent(color: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.1), cornerRadius: .value(25.0), smoothCorners: false)
),
environment: {},
containerSize: CGSize(width: size.width + backgroundPadding * 2.0, height: size.height)
)
let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - backgroundSize.width) / 2.0), y: topInset ), size: backgroundSize)
if let backgroundView = self.background.view {
if backgroundView.superview == nil {
self.addSubview(backgroundView)
}
transition.setFrame(view: backgroundView, frame: backgroundFrame)
}
let textFieldSize = CGSize(width: availableSize.width - 24.0, height: 50.0)
let textFieldFrame = CGRect(origin: CGPoint(x: -12.0, y: topInset), size: textFieldSize)
transition.setFrame(view: self.textField, frame: textFieldFrame)
let placeholderSize = self.placeholder.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(
string: component.placeholder,
font: Font.regular(17.0),
textColor: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.4)
)))
),
environment: {},
containerSize: CGSize(width: size.width, height: 50.0)
)
let placeholderFrame = CGRect(origin: CGPoint(x: 4.0, y: floorToScreenPixels(textFieldFrame.midY - placeholderSize.height / 2.0)), size: placeholderSize)
if let placeholderView = self.placeholder.view {
if placeholderView.superview == nil {
placeholderView.isUserInteractionEnabled = false
self.addSubview(placeholderView)
}
placeholderView.frame = placeholderFrame
placeholderView.isHidden = !self.currentText.isEmpty
}
if component.hasClearButton {
let clearButtonSize = self.clearButton.update(
transition: transition,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(BundleIconComponent(
name: "Components/Search Bar/Clear",
tintColor: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.4)
)),
effectAlignment: .center,
minSize: CGSize(width: 44.0, height: 44.0),
action: { [weak self] in
guard let self else {
return
}
self.setText(text: "")
},
animateAlpha: false,
animateScale: true
)),
environment: {},
containerSize: CGSize(width: 44.0, height: 44.0)
)
if let clearButtonView = self.clearButton.view {
if clearButtonView.superview == nil {
self.addSubview(clearButtonView)
}
transition.setFrame(view: clearButtonView, frame: CGRect(origin: CGPoint(x: availableSize.width - clearButtonSize.width + 11.0, y: topInset + floor((size.height - clearButtonSize.height) * 0.5)), size: clearButtonSize))
clearButtonView.isHidden = self.currentText.isEmpty || !self.textField.isFirstResponder
}
} else if let clearButtonView = self.clearButton.view, clearButtonView.superview != nil {
clearButtonView.removeFromSuperview()
}
if isFirstTime && component.isInitiallyFocused {
self.activateInput()
}
component.externalState.value = self.currentText
component.externalState.valuePromise.set(self.currentText)
return CGSize(width: availableSize.width, height: size.height + topInset)
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,27 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AlertMultilineInputFieldComponent",
module_name = "AlertMultilineInputFieldComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/AccountContext",
"//submodules/ComponentFlow",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramPresentationData",
"//submodules/TextFormat",
"//submodules/Components/MultilineTextComponent",
"//submodules/TelegramUI/Components/AlertComponent",
"//submodules/TelegramUI/Components/TextFieldComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,361 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import AlertComponent
import TextFieldComponent
import MultilineTextComponent
import AccountContext
import TextFormat
public final class AlertMultilineInputFieldComponent: Component {
public typealias EnvironmentType = AlertComponentEnvironment
public class ExternalState {
public fileprivate(set) var value: NSAttributedString = NSAttributedString()
public fileprivate(set) var animateError: () -> Void = {}
public fileprivate(set) var activateInput: () -> Void = {}
fileprivate let valuePromise = ValuePromise<NSAttributedString>(NSAttributedString())
public var valueSignal: Signal<NSAttributedString, NoError> {
return self.valuePromise.get()
}
public var textAndEntities: (String, [MessageTextEntity]) {
let text = self.value.string
let entities = generateChatInputTextEntities(self.value)
return (text, entities)
}
public init() {
}
}
public enum FormatMenuAvailability: Equatable {
public enum Action: CaseIterable {
case bold
case italic
case monospace
case link
case strikethrough
case underline
case spoiler
case quote
case code
public static var all: [Action] = [
.bold,
.italic,
.monospace,
.link,
.strikethrough,
.underline,
.spoiler,
.quote,
.code
]
var textFieldValue: TextFieldComponent.FormatMenuAvailability.Action {
switch self {
case .bold:
return .bold
case .italic:
return .italic
case .monospace:
return .monospace
case .link:
return .link
case .strikethrough:
return .strikethrough
case .underline:
return .underline
case .spoiler:
return .spoiler
case .quote:
return .quote
case .code:
return .code
}
}
}
case available([Action])
case none
var textFieldValue: TextFieldComponent.FormatMenuAvailability {
switch self {
case let .available(actions):
return .available(actions.map { $0.textFieldValue })
case .none:
return .none
}
}
}
public enum EmptyLineHandling {
case allowed
case oneConsecutive
case notAllowed
var textFieldValue: TextFieldComponent.EmptyLineHandling {
switch self {
case .allowed:
return .allowed
case .oneConsecutive:
return .oneConsecutive
case .notAllowed:
return .notAllowed
}
}
}
let context: AccountContext
let initialValue: NSAttributedString?
let placeholder: String
let prefix: NSAttributedString?
let characterLimit: Int?
let returnKeyType: UIReturnKeyType
let keyboardType: UIKeyboardType
let autocapitalizationType: UITextAutocapitalizationType
let autocorrectionType: UITextAutocorrectionType
let formatMenuAvailability: FormatMenuAvailability
let emptyLineHandling: EmptyLineHandling
let isInitiallyFocused: Bool
let externalState: ExternalState
let present: (ViewController) -> Void
let returnKeyAction: (() -> Void)?
public init(
context: AccountContext,
initialValue: NSAttributedString? = nil,
placeholder: String,
prefix: NSAttributedString? = nil,
characterLimit: Int? = nil,
returnKeyType: UIReturnKeyType = .default,
keyboardType: UIKeyboardType = .default,
autocapitalizationType: UITextAutocapitalizationType = .sentences,
autocorrectionType: UITextAutocorrectionType = .default,
formatMenuAvailability: FormatMenuAvailability = .none,
emptyLineHandling: EmptyLineHandling = .allowed,
isInitiallyFocused: Bool = false,
externalState: ExternalState,
present: @escaping (ViewController) -> Void = { _ in },
returnKeyAction: (() -> Void)? = nil
) {
self.context = context
self.initialValue = initialValue
self.placeholder = placeholder
self.prefix = prefix
self.characterLimit = characterLimit
self.returnKeyType = returnKeyType
self.keyboardType = keyboardType
self.autocapitalizationType = autocapitalizationType
self.autocorrectionType = autocorrectionType
self.formatMenuAvailability = formatMenuAvailability
self.emptyLineHandling = emptyLineHandling
self.isInitiallyFocused = isInitiallyFocused
self.externalState = externalState
self.present = present
self.returnKeyAction = returnKeyAction
}
public static func ==(lhs: AlertMultilineInputFieldComponent, rhs: AlertMultilineInputFieldComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.initialValue != rhs.initialValue {
return false
}
if lhs.placeholder != rhs.placeholder {
return false
}
if lhs.prefix != rhs.prefix {
return false
}
if lhs.returnKeyType != rhs.returnKeyType {
return false
}
if lhs.characterLimit != rhs.characterLimit {
return false
}
if lhs.keyboardType != rhs.keyboardType {
return false
}
if lhs.autocapitalizationType != rhs.autocapitalizationType {
return false
}
if lhs.autocorrectionType != rhs.autocorrectionType {
return false
}
if lhs.formatMenuAvailability != rhs.formatMenuAvailability {
return false
}
if lhs.emptyLineHandling != rhs.emptyLineHandling {
return false
}
if lhs.isInitiallyFocused != rhs.isInitiallyFocused {
return false
}
return true
}
public final class View: UIView {
private let background = ComponentView<Empty>()
private let textField = ComponentView<Empty>()
private let textFieldExternalState = TextFieldComponent.ExternalState()
private let placeholder = ComponentView<Empty>()
private var component: AlertMultilineInputFieldComponent?
private weak var state: EmptyComponentState?
func activateInput() {
if let textFieldView = self.textField.view as? TextFieldComponent.View {
textFieldView.activateInput()
}
}
func animateError() {
if let textFieldView = self.textField.view {
textFieldView.layer.addShakeAnimation()
}
HapticFeedback().error()
}
func update(component: AlertMultilineInputFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
var resetText: NSAttributedString?
if self.component == nil {
resetText = component.initialValue
component.externalState.animateError = { [weak self] in
self?.animateError()
}
component.externalState.activateInput = { [weak self] in
self?.activateInput()
}
}
let isFirstTime = self.component == nil
self.component = component
self.state = state
let environment = environment[AlertComponentEnvironment.self]
let topInset: CGFloat = 15.0
let horizontalInset: CGFloat = 4.0
let verticalInset: CGFloat = 11.0 - UIScreenPixel
let textFieldSize = self.textField.update(
transition: transition,
component: AnyComponent(TextFieldComponent(
context: component.context,
theme: environment.theme,
strings: environment.strings,
externalState: self.textFieldExternalState,
fontSize: 17.0,
textColor: environment.theme.actionSheet.primaryTextColor,
accentColor: environment.theme.actionSheet.controlAccentColor,
insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0),
hideKeyboard: false,
customInputView: nil,
resetText: resetText,
isOneLineWhenUnfocused: false,
characterLimit: component.characterLimit,
emptyLineHandling: component.emptyLineHandling.textFieldValue,
formatMenuAvailability: component.formatMenuAvailability.textFieldValue,
returnKeyType: component.returnKeyType,
keyboardType: component.keyboardType,
autocapitalizationType: component.autocapitalizationType,
autocorrectionType: component.autocorrectionType,
lockedFormatAction: {
},
present: { [weak self] c in
guard let self, let component = self.component else {
return
}
component.present(c)
},
paste: { _ in
},
returnKeyAction: { [weak self] in
guard let self, let component = self.component else {
return
}
component.returnKeyAction?()
},
backspaceKeyAction: {
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width + horizontalInset * 2.0, height: availableSize.height)
)
component.externalState.value = self.textFieldExternalState.text
component.externalState.valuePromise.set(component.externalState.value)
let backgroundPadding: CGFloat = 14.0
let size = CGSize(width: availableSize.width, height: max(50.0, floor(textFieldSize.height + verticalInset * 2.0)))
let backgroundSize = self.background.update(
transition: transition,
component: AnyComponent(
FilledRoundedRectangleComponent(color: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.1), cornerRadius: .value(25.0), smoothCorners: false)
),
environment: {},
containerSize: CGSize(width: size.width + backgroundPadding * 2.0, height: size.height)
)
let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - backgroundSize.width) / 2.0), y: topInset ), size: backgroundSize)
if let backgroundView = self.background.view {
if backgroundView.superview == nil {
self.addSubview(backgroundView)
}
transition.setFrame(view: backgroundView, frame: backgroundFrame)
}
let textFieldFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - textFieldSize.width) / 2.0), y: topInset + 11.0 - UIScreenPixel), size: textFieldSize)
if let textFieldView = self.textField.view {
if textFieldView.superview == nil {
self.addSubview(textFieldView)
self.textField.parentState = state
}
transition.setFrame(view: textFieldView, frame: textFieldFrame)
}
let placeholderSize = self.placeholder.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(
string: component.placeholder,
font: Font.regular(17.0),
textColor: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.4)
)))
),
environment: {},
containerSize: CGSize(width: size.width, height: 50.0)
)
let placeholderFrame = CGRect(origin: CGPoint(x: 4.0, y: floorToScreenPixels(textFieldFrame.midY - placeholderSize.height / 2.0)), size: placeholderSize)
if let placeholderView = self.placeholder.view {
if placeholderView.superview == nil {
placeholderView.isUserInteractionEnabled = false
self.addSubview(placeholderView)
}
placeholderView.frame = placeholderFrame
placeholderView.isHidden = self.textFieldExternalState.hasText
}
if isFirstTime && component.isInitiallyFocused {
self.activateInput()
}
return CGSize(width: availableSize.width, height: size.height + topInset)
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,23 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AlertTableComponent",
module_name = "AlertTableComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/ComponentFlow",
"//submodules/TelegramPresentationData",
"//submodules/TelegramUI/Components/AlertComponent",
"//submodules/TelegramUI/Components/Gifts/TableComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,70 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import TelegramPresentationData
import AlertComponent
import TableComponent
public final class AlertTableComponent: Component {
public typealias EnvironmentType = AlertComponentEnvironment
let items: [TableComponent.Item]
public init(
items: [TableComponent.Item]
) {
self.items = items
}
public static func ==(lhs: AlertTableComponent, rhs: AlertTableComponent) -> Bool {
if lhs.items != rhs.items {
return false
}
return true
}
public final class View: UIView {
private let table = ComponentView<Empty>()
private var component: AlertTableComponent?
private weak var state: EmptyComponentState?
func update(component: AlertTableComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let environment = environment[AlertComponentEnvironment.self]
let tableSize = self.table.update(
transition: transition,
component: AnyComponent(
TableComponent(
theme: environment.theme,
items: component.items,
semiTransparent: true
)
),
environment: {},
containerSize: CGSize(width: availableSize.width + 20.0, height: availableSize.height)
)
let tableFrame = CGRect(origin: CGPoint(x: -10.0, y: 5.0), size: tableSize)
if let tableView = self.table.view {
if tableView.superview == nil {
self.addSubview(tableView)
}
transition.setFrame(view: tableView, frame: tableFrame)
}
return CGSize(width: availableSize.width, height: tableSize.height + 10.0)
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,23 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AlertTransferHeaderComponent",
module_name = "AlertTransferHeaderComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/ComponentFlow",
"//submodules/TelegramPresentationData",
"//submodules/Components/BundleIconComponent",
"//submodules/TelegramUI/Components/AlertComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,126 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import TelegramPresentationData
import AlertComponent
import BundleIconComponent
public final class AlertTransferHeaderComponent: Component {
public typealias EnvironmentType = AlertComponentEnvironment
public enum IconType {
case transfer
case take
}
let fromComponent: AnyComponentWithIdentity<Empty>
let toComponent: AnyComponentWithIdentity<Empty>
let type: IconType
public init(
fromComponent: AnyComponentWithIdentity<Empty>,
toComponent: AnyComponentWithIdentity<Empty>,
type: IconType
) {
self.fromComponent = fromComponent
self.toComponent = toComponent
self.type = type
}
public static func ==(lhs: AlertTransferHeaderComponent, rhs: AlertTransferHeaderComponent) -> Bool {
if lhs.fromComponent != rhs.fromComponent {
return false
}
if lhs.toComponent != rhs.toComponent {
return false
}
if lhs.type != rhs.type {
return false
}
return true
}
public final class View: UIView {
private let from = ComponentView<Empty>()
private let to = ComponentView<Empty>()
private let arrow = ComponentView<Empty>()
private var component: AlertTransferHeaderComponent?
private weak var state: EmptyComponentState?
func update(component: AlertTransferHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let environment = environment[AlertComponentEnvironment.self]
let size: CGSize
let iconName: String
switch component.type {
case .transfer:
iconName = "Peer Info/AlertArrow"
size = CGSize(width: 148.0, height: 60.0)
case .take:
iconName = "Media Editor/CutoutUndo"
size = CGSize(width: 154.0, height: 60.0)
}
let sideInset = floorToScreenPixels((availableSize.width - size.width) / 2.0)
let fromSize = self.from.update(
transition: transition,
component: component.fromComponent.component,
environment: {},
containerSize: CGSize(width: 60.0, height: 60.0)
)
let fromFrame = CGRect(origin: CGPoint(x: sideInset, y: 0.0), size: fromSize)
if let fromView = self.from.view {
if fromView.superview == nil {
self.addSubview(fromView)
}
transition.setFrame(view: fromView, frame: fromFrame)
}
let arrowSize = self.arrow.update(
transition: transition,
component: AnyComponent(
BundleIconComponent(name: iconName, tintColor: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.2))
),
environment: {},
containerSize: availableSize
)
let arrowFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - arrowSize.width) / 2.0), y: floorToScreenPixels((size.height - arrowSize.height) / 2.0)), size: arrowSize)
if let arrowView = self.arrow.view {
if arrowView.superview == nil {
self.addSubview(arrowView)
}
transition.setFrame(view: arrowView, frame: arrowFrame)
}
let toSize = self.to.update(
transition: transition,
component: component.toComponent.component,
environment: {},
containerSize: CGSize(width: 60.0, height: 60.0)
)
let toFrame = CGRect(origin: CGPoint(x: availableSize.width - toSize.width - sideInset, y: 0.0), size: toSize)
if let toView = self.to.view {
if toView.superview == nil {
self.addSubview(toView)
}
transition.setFrame(view: toView, frame: toFrame)
}
return CGSize(width: availableSize.width, height: size.height + 11.0)
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,32 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AlertComponent",
module_name = "AlertComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/ComponentFlow",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramPresentationData",
"//submodules/TelegramCore",
"//submodules/AccountContext",
"//submodules/Markdown",
"//submodules/TextFormat",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/Components/ViewControllerComponent",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/MultilineTextWithEntitiesComponent",
"//submodules/Components/ActivityIndicatorComponent",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,228 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import SwiftSignalKit
import AccountContext
import TelegramPresentationData
import MultilineTextComponent
import GlassBackgroundComponent
import ActivityIndicatorComponent
private let titleFont = Font.medium(17.0)
private let boldTitleFont = Font.semibold(17.0)
final class AlertActionComponent: Component {
typealias EnvironmentType = AlertComponentEnvironment
static let actionHeight: CGFloat = 48.0
struct Theme: Equatable {
enum Font {
case regular
case bold
}
let background: UIColor
let foreground: UIColor
let secondary: UIColor
let font: Font
}
let theme: Theme
let title: String
let isHighlighted: Bool
let isEnabled: Signal<Bool, NoError>
let progress: Signal<Bool, NoError>
init(
theme: Theme,
title: String,
isHighlighted: Bool,
isEnabled: Signal<Bool, NoError>,
progress: Signal<Bool, NoError>
) {
self.theme = theme
self.title = title
self.isHighlighted = isHighlighted
self.isEnabled = isEnabled
self.progress = progress
}
static func ==(lhs: AlertActionComponent, rhs: AlertActionComponent) -> Bool {
if lhs.theme != rhs.theme {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.isHighlighted != rhs.isHighlighted {
return false
}
return true
}
final class View: UIView {
private let backgroundView = UIView()
private let title = ComponentView<Empty>()
private var activity: ComponentView<Empty>?
private var component: AlertActionComponent?
private weak var state: EmptyComponentState?
private var isEnabledDisposable: Disposable?
private var isEnabled = true
private var progressDisposable: Disposable?
private var hasProgress = false
private var isUpdating = false
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundView.clipsToBounds = true
self.addSubview(self.backgroundView)
}
required init?(coder: NSCoder) {
preconditionFailure()
}
deinit {
self.isEnabledDisposable?.dispose()
self.progressDisposable?.dispose()
}
func update(component: AlertActionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
if self.component == nil {
self.isEnabledDisposable = (component.isEnabled
|> deliverOnMainQueue).start(next: { [weak self] isEnabled in
guard let self else {
return
}
self.isEnabled = isEnabled
if !self.isUpdating {
self.state?.updated(transition: .easeInOut(duration: 0.25))
}
})
self.progressDisposable = (component.progress
|> deliverOnMainQueue).start(next: { [weak self] hasProgress in
guard let self else {
return
}
self.hasProgress = hasProgress
if !self.isUpdating {
self.state?.updated(transition: .easeInOut(duration: 0.25))
}
})
}
self.component = component
self.state = state
let attributedString = NSMutableAttributedString(string: component.title, font: component.theme.font == .bold ? boldTitleFont : titleFont, textColor: .white, paragraphAlignment: .center)
if let range = attributedString.string.range(of: "$") {
attributedString.addAttribute(.attachment, value: UIImage(bundleImageName: "Item List/PremiumIcon")!, range: NSRange(range, in: attributedString.string))
attributedString.addAttribute(.foregroundColor, value: UIColor.white, range: NSRange(range, in: attributedString.string))
attributedString.addAttribute(.baselineOffset, value: 2.0, range: NSRange(range, in: attributedString.string))
}
let titlePadding: CGFloat = 16.0
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(attributedString),
horizontalAlignment: .center,
maximumNumberOfLines: 1,
tintColor: component.theme.foreground
)),
environment: {},
containerSize: CGSize(width: availableSize.width - titlePadding * 2.0, height: availableSize.height)
)
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
titleView.bounds = CGRect(origin: .zero, size: titleSize)
transition.setAlpha(view: titleView, alpha: self.hasProgress ? 0.0 : 1.0)
}
if self.hasProgress {
let activity: ComponentView<Empty>
if let current = self.activity {
activity = current
} else {
activity = ComponentView()
self.activity = activity
}
let activitySize = CGSize(width: 18.0, height: 18.0)
let _ = activity.update(
transition: transition,
component: AnyComponent(ActivityIndicatorComponent(color: component.theme.secondary)),
environment: {},
containerSize: activitySize
)
if let activityView = activity.view {
activityView.bounds = CGRect(origin: .zero, size: activitySize)
}
} else if let activity = self.activity {
self.activity = nil
if let activityView = activity.view {
transition.setAlpha(view: activityView, alpha: 0.0, completion: { _ in
activityView.removeFromSuperview()
})
}
}
let buttonAlpha: CGFloat
if self.isEnabled {
buttonAlpha = component.isHighlighted ? 0.35 : 1.0
} else {
buttonAlpha = 0.2
}
transition.setBackgroundColor(view: self.backgroundView, color: component.theme.background)
transition.setAlpha(view: self.backgroundView, alpha: buttonAlpha)
self.backgroundView.layer.cornerRadius = availableSize.height * 0.5
return CGSize(width: titleSize.width + titlePadding * 2.0, height: availableSize.height)
}
func applySize(size: CGSize, transition: ComponentTransition) {
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: size))
if let titleView = self.title.view {
let titleSize = titleView.bounds.size
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize)
transition.setFrame(view: titleView, frame: titleFrame)
}
if let activityView = self.activity?.view {
var activityTransition = transition
if activityView.superview == nil {
self.addSubview(activityView)
transition.animateAlpha(view: activityView, from: 0.0, to: 1.0)
activityTransition = .immediate
}
let activitySize = activityView.bounds.size
let activityFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - activitySize.width) / 2.0), y: floorToScreenPixels((size.height - activitySize.height) / 2.0)), size: activitySize)
activityTransition.setPosition(view: activityView, position: activityFrame.center)
activityView.transform = CGAffineTransformMakeScale(0.7, 0.7)
}
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,952 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import SwiftSignalKit
import AccountContext
import TelegramPresentationData
import MultilineTextComponent
import ViewControllerComponent
import ComponentDisplayAdapters
import GlassBackgroundComponent
public final class AlertComponentEnvironment: Equatable {
public let theme: PresentationTheme
public let strings: PresentationStrings
public init(
theme: PresentationTheme,
strings: PresentationStrings
) {
self.theme = theme
self.strings = strings
}
public static func ==(lhs: AlertComponentEnvironment, rhs: AlertComponentEnvironment) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
return true
}
}
private final class AlertScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let configuration: AlertScreen.Configuration
let content: Signal<[AnyComponentWithIdentity<AlertComponentEnvironment>], NoError>
let actions: Signal<[AlertScreen.Action], NoError>
let ready: Promise<Bool>
init(
configuration: AlertScreen.Configuration,
content: Signal<[AnyComponentWithIdentity<AlertComponentEnvironment>], NoError>,
actions: Signal<[AlertScreen.Action], NoError>,
ready: Promise<Bool>
) {
self.configuration = configuration
self.content = content
self.actions = actions
self.ready = ready
}
static func ==(lhs: AlertScreenComponent, rhs: AlertScreenComponent) -> Bool {
return true
}
enum KeyCommand {
case up
case down
case left
case right
case escape
case enter
}
final class View: UIView, UIGestureRecognizerDelegate {
private let dimView = UIView()
private let containerView = GlassBackgroundContainerView()
private let backgroundView = GlassBackgroundView()
private var disposable: Disposable?
private var content: [AnyComponentWithIdentity<AlertComponentEnvironment>]?
private var actions: [AlertScreen.Action]?
private var contentItems: [AnyHashable: ComponentView<AlertComponentEnvironment>] = [:]
private var actionItems: [AnyHashable: ComponentView<AlertComponentEnvironment>] = [:]
private var highlightedAction: AnyHashable?
private let hapticFeedback = HapticFeedback()
private enum ActionLayout {
case horizontal
case vertical
case verticalReversed
var isVertical: Bool {
switch self {
case .vertical, .verticalReversed:
return true
default:
return false
}
}
}
private var effectiveActionLayout: ActionLayout = .horizontal
fileprivate var dismissedByTapOutside = false
private var isUpdating: Bool = false
private var component: AlertScreenComponent?
private var environment: EnvironmentType?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
self.dimView.alpha = 0.0
self.dimView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.2)
self.addSubview(self.dimView)
self.addSubview(self.containerView)
self.containerView.contentView.addSubview(self.backgroundView)
self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapped)))
let tapRecognizer = ActionSelectionGestureRecognizer(target: self, action: #selector(self.actionTapped(_:)))
tapRecognizer.delegate = self
self.backgroundView.addGestureRecognizer(tapRecognizer)
}
required init?(coder: NSCoder) {
preconditionFailure()
}
deinit {
self.disposable?.dispose()
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer is ActionSelectionGestureRecognizer {
let location = gestureRecognizer.location(in: self.backgroundView)
for (_, action) in self.actionItems {
if let actionView = action.view, actionView.frame.contains(location) {
return true
}
}
return false
} else {
return super.gestureRecognizerShouldBegin(gestureRecognizer)
}
}
@objc private func actionTapped(_ gestureRecognizer: ActionSelectionGestureRecognizer) {
let location = gestureRecognizer.location(in: self.backgroundView)
switch gestureRecognizer.state {
case .began, .changed:
var highlightedActionId: AnyHashable?
for (actionId, action) in self.actionItems {
if let actionView = action.view, actionView.frame.contains(location) {
highlightedActionId = actionId
break
}
}
if self.highlightedAction != highlightedActionId {
self.highlightedAction = highlightedActionId
self.state?.updated(transition: .easeInOut(duration: 0.2))
if case .changed = gestureRecognizer.state, highlightedActionId != nil {
self.hapticFeedback.tap()
}
}
case .ended:
if let _ = self.highlightedAction {
self.performHighlightedAction()
self.highlightedAction = nil
self.state?.updated(transition: .easeInOut(duration: 0.2))
}
case .cancelled:
self.highlightedAction = nil
self.state?.updated(transition: .easeInOut(duration: 0.2))
default:
break
}
}
@objc private func dimTapped() {
guard let component = self.component, component.configuration.dismissOnOutsideTap else {
return
}
self.dismissedByTapOutside = true
self.requestDismiss()
}
func animateIn() {
let alphaTransition = ComponentTransition(animation: .curve(duration: 0.2, curve: .linear))
let scaleTransition = ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))
alphaTransition.setAlpha(view: self.dimView, alpha: 1.0)
scaleTransition.animateScale(view: self.backgroundView, from: 1.15, to: 1.0)
alphaTransition.animateAlpha(view: self.containerView, from: 0.0, to: 1.0)
}
func animateOut(completion: @escaping () -> Void) {
let transition = ComponentTransition(animation: .curve(duration: 0.2, curve: .linear))
transition.setAlpha(view: self.dimView, alpha: 0.0, completion: { _ in
completion()
})
var initialAlpha: CGFloat = 1.0
if let presentationLayer = self.containerView.layer.presentation() {
initialAlpha = CGFloat(presentationLayer.opacity)
}
self.containerView.layer.animateAlpha(from: initialAlpha, to: 0.0, duration: 0.2, removeOnCompletion: false)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
if result === self.containerView.contentView {
return self.dimView
}
return result
}
func requestDismiss() {
guard let controller = self.environment?.controller() as? AlertScreen else {
return
}
controller.dismiss(completion: nil)
}
func handleKeyCommand(_ command: KeyCommand) {
switch command {
case .up:
guard self.effectiveActionLayout.isVertical else {
return
}
self.updateActionHighlight(previous: false)
case .down:
guard self.effectiveActionLayout.isVertical else {
return
}
self.updateActionHighlight(previous: true)
case .left:
guard !self.effectiveActionLayout.isVertical else {
return
}
self.updateActionHighlight(previous: true)
case .right:
guard !self.effectiveActionLayout.isVertical else {
return
}
self.updateActionHighlight(previous: false)
case .escape:
self.requestDismiss()
case .enter:
self.performHighlightedAction()
}
}
func updateActionHighlight(previous: Bool) {
guard let actions = self.actions else {
return
}
guard let highlightedAction = self.highlightedAction else {
if let action = actions.first(where: { $0.type == .default }) {
self.highlightedAction = action.id
} else if let action = actions.first(where: { $0.type == .defaultDestructive }) {
self.highlightedAction = action.id
} else if case .verticalReversed = self.effectiveActionLayout, let action = actions.last {
self.highlightedAction = action.id
} else if let action = actions.first {
self.highlightedAction = action.id
}
self.state?.updated(transition: .easeInOut(duration: 0.2))
return
}
let sequence = previous ? actions.reversed() : actions
var selectNext = false
var newHighlightedAction: AnyHashable?
for action in sequence {
let id = AnyHashable(action.id)
if selectNext {
newHighlightedAction = id
break
} else if id == highlightedAction {
selectNext = true
}
}
guard let newHighlightedAction else {
return
}
self.highlightedAction = newHighlightedAction
self.state?.updated(transition: .easeInOut(duration: 0.2))
}
func performHighlightedAction() {
guard let actions = self.actions else {
return
}
guard let highlightedAction = self.highlightedAction else {
return
}
guard let action = actions.first(where: { AnyHashable($0.id) == highlightedAction }) else {
return
}
action.action()
if action.autoDismiss {
self.requestDismiss()
}
}
func update(component: AlertScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
let environment = environment[ViewControllerComponentContainer.Environment.self].value
self.environment = environment
self.state = state
if self.component == nil {
self.disposable = (combineLatest(
queue: Queue.mainQueue(),
component.content,
component.actions
) |> deliverOnMainQueue).start(next: { [weak self] content, actions in
guard let self else {
return
}
self.content = content
self.actions = actions
if !self.isUpdating {
self.state?.updated(transition: .easeInOut(duration: 0.25))
}
})
}
self.component = component
var alertHeight: CGFloat = 0.0
let alertWidth: CGFloat = 300.0
let contentTopInset: CGFloat = 22.0
let contentBottomInset: CGFloat = 21.0
let contentSideInset: CGFloat = 30.0
let contentSpacing: CGFloat = 8.0
let actionSideInset: CGFloat = 16.0
let actionSpacing: CGFloat = 8.0
let fullWidthActionSize = CGSize(width: alertWidth - actionSideInset * 2.0, height: AlertActionComponent.actionHeight)
let halfWidthActionSize = CGSize(width: (alertWidth - actionSideInset * 2.0 - actionSpacing) / 2.0, height: AlertActionComponent.actionHeight)
let alertEnvironment = AlertComponentEnvironment(theme: environment.theme, strings: environment.strings)
var contentOriginY: CGFloat = 0.0
var validContentIds: Set<AnyHashable> = Set()
if let content = self.content {
for content in content {
if contentOriginY.isZero {
contentOriginY += contentTopInset
} else {
contentOriginY += contentSpacing
}
validContentIds.insert(content.id)
let item: ComponentView<AlertComponentEnvironment>
var itemTransition = transition
if let current = self.contentItems[content.id] {
item = current
} else {
item = ComponentView()
if !transition.animation.isImmediate {
itemTransition = .immediate
}
self.contentItems[content.id] = item
}
let itemSize = item.update(
transition: itemTransition,
component: content.component,
environment: { alertEnvironment },
containerSize: CGSize(width: alertWidth - contentSideInset * 2.0, height: availableSize.height)
)
let itemFrame = CGRect(origin: CGPoint(x: contentSideInset, y: contentOriginY), size: itemSize)
if let itemView = item.view {
if itemView.superview == nil {
self.backgroundView.contentView.addSubview(itemView)
item.parentState = state
}
transition.setFrame(view: itemView, frame: itemFrame)
}
contentOriginY += itemSize.height
}
}
if !contentOriginY.isZero {
alertHeight += contentOriginY
alertHeight += contentBottomInset
}
if let actions = self.actions {
let genericActionTheme = AlertActionComponent.Theme(
background: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.1),
foreground: environment.theme.actionSheet.primaryTextColor,
secondary: environment.theme.actionSheet.secondaryTextColor,
font: .regular
)
let defaultActionTheme = AlertActionComponent.Theme(
background: environment.theme.actionSheet.controlAccentColor,
foreground: environment.theme.list.itemCheckColors.foregroundColor,
secondary: environment.theme.list.itemCheckColors.foregroundColor.withMultipliedAlpha(0.85),
font: .bold
)
let destructiveActionTheme = AlertActionComponent.Theme(
background: environment.theme.list.itemDestructiveColor,
foreground: .white,
secondary: .white.withMultipliedAlpha(0.6),
font: .regular
)
let defaultDestructiveActionTheme = AlertActionComponent.Theme(
background: environment.theme.list.itemDestructiveColor,
foreground: .white,
secondary: .white.withMultipliedAlpha(0.6),
font: .bold
)
var effectiveActionLayout: ActionLayout = .horizontal
if case .vertical = component.configuration.actionAlignment {
effectiveActionLayout = .vertical
} else if actions.count == 1 {
effectiveActionLayout = .vertical
}
var actionTransitions: [AnyHashable: ComponentTransition] = [:]
var validActionIds: Set<AnyHashable> = Set()
for action in actions {
validActionIds.insert(action.id)
let item: ComponentView<AlertComponentEnvironment>
var itemTransition = transition
if let current = self.actionItems[action.id] {
item = current
} else {
item = ComponentView()
if !transition.animation.isImmediate {
itemTransition = .immediate
}
self.actionItems[action.id] = item
}
actionTransitions[action.id] = itemTransition
let actionTheme: AlertActionComponent.Theme
switch action.type {
case .generic:
actionTheme = genericActionTheme
case .default:
actionTheme = defaultActionTheme
case .destructive:
actionTheme = destructiveActionTheme
case .defaultDestructive:
actionTheme = defaultDestructiveActionTheme
}
let itemSize = item.update(
transition: itemTransition,
component: AnyComponent(AlertActionComponent(
theme: actionTheme,
title: action.title,
isHighlighted: AnyHashable(action.id) == self.highlightedAction,
isEnabled: action.isEnabled,
progress: action.progress
)),
environment: { alertEnvironment },
containerSize: fullWidthActionSize
)
if let itemView = item.view {
if itemView.superview == nil {
self.backgroundView.contentView.addSubview(itemView)
}
}
if case .horizontal = effectiveActionLayout, itemSize.width > halfWidthActionSize.width {
effectiveActionLayout = .verticalReversed
}
}
self.effectiveActionLayout = effectiveActionLayout
if !actions.isEmpty {
let actionsHeight: CGFloat
if self.effectiveActionLayout.isVertical {
actionsHeight = fullWidthActionSize.height * CGFloat(actions.count) + actionSpacing * CGFloat(actions.count - 1)
} else {
actionsHeight = fullWidthActionSize.height
}
alertHeight += actionsHeight
alertHeight += actionSideInset
}
var actionOriginX: CGFloat = actionSideInset
var actionOriginY: CGFloat
switch self.effectiveActionLayout {
case .horizontal, .verticalReversed:
actionOriginY = alertHeight - actionSideInset - fullWidthActionSize.height
case .vertical:
actionOriginY = alertHeight - actionSideInset - fullWidthActionSize.height * CGFloat( actions.count) - actionSpacing * CGFloat(actions.count - 1)
}
for action in actions {
guard let item = self.actionItems[action.id], let itemView = item.view as? AlertActionComponent.View else {
continue
}
let itemTransition = actionTransitions[action.id] ?? transition
let itemFrame: CGRect
switch self.effectiveActionLayout {
case .horizontal:
itemFrame = CGRect(origin: CGPoint(x: actionOriginX, y: actionOriginY), size: halfWidthActionSize)
actionOriginX += halfWidthActionSize.width + actionSpacing
case .vertical:
itemFrame = CGRect(origin: CGPoint(x: actionOriginX, y: actionOriginY), size: fullWidthActionSize)
actionOriginY += fullWidthActionSize.height + actionSpacing
case .verticalReversed:
itemFrame = CGRect(origin: CGPoint(x: actionOriginX, y: actionOriginY), size: fullWidthActionSize)
actionOriginY -= fullWidthActionSize.height + actionSpacing
}
itemView.applySize(size: itemFrame.size, transition: itemTransition)
itemTransition.setFrame(view: itemView, frame: itemFrame)
}
var removeActionIds: [AnyHashable] = []
for (id, item) in self.actionItems {
if !validActionIds.contains(id) {
removeActionIds.append(id)
if let itemView = item.view {
if !transition.animation.isImmediate {
itemView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false)
itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
itemView.removeFromSuperview()
})
} else {
itemView.removeFromSuperview()
}
}
}
}
for id in removeActionIds {
self.actionItems.removeValue(forKey: id)
}
}
let alertSize = CGSize(width: alertWidth, height: alertHeight)
let bounds = CGRect(origin: .zero, size: availableSize)
transition.setFrame(view: self.dimView, frame: bounds)
transition.setFrame(view: self.containerView, frame: bounds)
self.containerView.update(size: availableSize, isDark: environment.theme.overallDarkAppearance, transition: transition)
var availableHeight = availableSize.height
availableHeight -= environment.statusBarHeight
if component.configuration.allowInputInset, environment.inputHeight > 0.0 {
availableHeight -= environment.inputHeight
}
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - alertSize.width) / 2.0), y: environment.statusBarHeight + floorToScreenPixels((availableHeight - alertSize.height) / 2.0)), size: alertSize))
self.backgroundView.update(size: alertSize, cornerRadius: 35.0, isDark: environment.theme.overallDarkAppearance, tintColor: .init(kind: .panel), isInteractive: true, transition: transition)
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
open class AlertScreen: ViewControllerComponentContainer, KeyShortcutResponder {
public enum ActionAligmnent: Equatable {
case `default`
case vertical
}
public struct Configuration: Equatable {
let actionAlignment: ActionAligmnent
let dismissOnOutsideTap: Bool
let allowInputInset: Bool
public init(
actionAlignment: ActionAligmnent = .default,
dismissOnOutsideTap: Bool = true,
allowInputInset: Bool = false
) {
self.actionAlignment = actionAlignment
self.dismissOnOutsideTap = dismissOnOutsideTap
self.allowInputInset = allowInputInset
}
}
public struct Action: Equatable {
public enum ActionType: Equatable {
case generic
case `default`
case destructive
case defaultDestructive
}
public let title: String
public let type: ActionType
public let action: () -> Void
public let autoDismiss: Bool
public let isEnabled: Signal<Bool, NoError>
public let progress: Signal<Bool, NoError>
public init(
id: AnyHashable? = nil,
title: String,
type: ActionType = .generic,
action: @escaping () -> Void = {},
autoDismiss: Bool = true,
isEnabled: Signal<Bool, NoError> = .single(true),
progress: Signal<Bool, NoError> = .single(false)
) {
self.type = type
self.title = title
self.action = action
self.autoDismiss = autoDismiss
self.isEnabled = isEnabled
self.progress = progress
if let id {
self.id = id
} else {
self.id = title
}
}
public static func ==(lhs: Action, rhs: Action) -> Bool {
if lhs.title != rhs.title {
return false
}
if lhs.type != rhs.type {
return false
}
if lhs.autoDismiss != rhs.autoDismiss {
return false
}
return true
}
fileprivate let id: AnyHashable
}
private var processedDidAppear: Bool = false
private var processedDidDisappear: Bool = false
private let readyValue = Promise<Bool>(true)
override public var ready: Promise<Bool> {
return self.readyValue
}
public var dismissed: ((Bool) -> Void)?
public init(
configuration: Configuration = Configuration(),
contentSignal: Signal<[AnyComponentWithIdentity<AlertComponentEnvironment>], NoError>,
actionsSignal: Signal<[Action], NoError>,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)
) {
let componentReady = Promise<Bool>()
super.init(
component: AlertScreenComponent(
configuration: configuration,
content: contentSignal,
actions: actionsSignal,
ready: componentReady
),
navigationBarAppearance: .none,
statusBarStyle: .ignore,
presentationMode: .default,
updatedPresentationData: updatedPresentationData
)
self.navigationPresentation = .flatModal
//self.readyValue.set(componentReady.get() |> timeout(1.0, queue: .mainQueue(), alternate: .single(true)))
}
public convenience init(
configuration: Configuration = Configuration(),
content: [AnyComponentWithIdentity<AlertComponentEnvironment>],
actions: [Action],
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)
) {
self.init(
configuration: configuration,
contentSignal: .single(content),
actionsSignal: .single(actions),
updatedPresentationData: updatedPresentationData
)
}
public convenience init(
context: AccountContext,
configuration: Configuration = Configuration(),
content: [AnyComponentWithIdentity<AlertComponentEnvironment>],
actions: [Action]
) {
self.init(
sharedContext: context.sharedContext,
configuration: configuration,
content: content,
actions: actions,
)
}
public convenience init(
sharedContext: SharedAccountContext,
configuration: Configuration = Configuration(),
content: [AnyComponentWithIdentity<AlertComponentEnvironment>],
actions: [Action]
) {
let presentationData = sharedContext.currentPresentationData.with { $0 }
let updatedPresentationDataSignal = sharedContext.presentationData
self.init(
configuration: configuration,
content: content,
actions: actions,
updatedPresentationData: (initial: presentationData, signal: updatedPresentationDataSignal)
)
}
public convenience init(
configuration: Configuration = Configuration(),
title: String? = nil,
text: String,
textAction: @escaping ([NSAttributedString.Key: Any]) -> Void = { _ in },
actions: [Action],
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)
) {
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
if let title {
content.append(AnyComponentWithIdentity(
id: "title",
component: AnyComponent(
AlertTitleComponent(title: title)
)
))
}
if !text.isEmpty {
content.append(AnyComponentWithIdentity(
id: "text",
component: AnyComponent(
AlertTextComponent(content: .plain(text), action: textAction)
)
))
}
if content.isEmpty {
content.append(AnyComponentWithIdentity(
id: "text",
component: AnyComponent(
AlertTextComponent(content: .plain(" "), action: textAction)
)
))
}
self.init(
configuration: configuration,
content: content,
actions: actions,
updatedPresentationData: updatedPresentationData
)
}
public convenience init(
context: AccountContext,
configuration: Configuration = Configuration(),
title: String? = nil,
text: String,
actions: [Action]
) {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let updatedPresentationDataSignal = context.sharedContext.presentationData
self.init(
configuration: configuration,
title: title,
text: text,
actions: actions,
updatedPresentationData: (initial: presentationData, signal: updatedPresentationDataSignal)
)
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !self.processedDidAppear {
self.processedDidAppear = true
if let componentView = self.node.hostView.componentView as? AlertScreenComponent.View {
componentView.animateIn()
}
}
}
private func superDismiss() {
super.dismiss()
}
override open func dismiss(completion: (() -> Void)? = nil) {
if !self.processedDidDisappear {
self.processedDidDisappear = true
self.view.window?.endEditing(true)
if let componentView = self.node.hostView.componentView as? AlertScreenComponent.View {
let dismissedByTapOutside = componentView.dismissedByTapOutside
componentView.animateOut(completion: { [weak self] in
if let self {
self.dismissed?(dismissedByTapOutside)
self.superDismiss()
}
completion?()
})
} else {
super.dismiss(completion: completion)
}
}
}
public var keyShortcuts: [KeyShortcut] {
return [
KeyShortcut(
input: UIKeyCommand.inputEscape,
modifiers: [],
action: { [weak self] in
if let componentView = self?.node.hostView.componentView as? AlertScreenComponent.View {
componentView.handleKeyCommand(.escape)
}
}
),
KeyShortcut(
input: "W",
modifiers: [.command],
action: { [weak self] in
if let componentView = self?.node.hostView.componentView as? AlertScreenComponent.View {
componentView.handleKeyCommand(.escape)
}
}
),
KeyShortcut(
input: "\r",
modifiers: [],
action: { [weak self] in
if let componentView = self?.node.hostView.componentView as? AlertScreenComponent.View {
componentView.handleKeyCommand(.enter)
}
}
),
KeyShortcut(
input: UIKeyCommand.inputUpArrow,
modifiers: [],
action: { [weak self] in
if let componentView = self?.node.hostView.componentView as? AlertScreenComponent.View {
componentView.handleKeyCommand(.up)
}
}
),
KeyShortcut(
input: UIKeyCommand.inputDownArrow,
modifiers: [],
action: { [weak self] in
if let componentView = self?.node.hostView.componentView as? AlertScreenComponent.View {
componentView.handleKeyCommand(.down)
}
}
),
KeyShortcut(
input: UIKeyCommand.inputLeftArrow,
modifiers: [],
action: { [weak self] in
if let componentView = self?.node.hostView.componentView as? AlertScreenComponent.View {
componentView.handleKeyCommand(.left)
}
}
),
KeyShortcut(
input: UIKeyCommand.inputRightArrow,
modifiers: [],
action: { [weak self] in
if let componentView = self?.node.hostView.componentView as? AlertScreenComponent.View {
componentView.handleKeyCommand(.right)
}
}
)
]
}
}
public final class ActionSelectionGestureRecognizer: UIGestureRecognizer {
private var initialLocation: CGPoint?
private var currentLocation: CGPoint?
public override init(target: Any?, action: Selector?) {
super.init(target: target, action: action)
self.delaysTouchesBegan = false
self.delaysTouchesEnded = false
}
public override func reset() {
super.reset()
self.initialLocation = nil
}
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
if self.initialLocation == nil {
self.initialLocation = touches.first?.location(in: self.view)
}
self.currentLocation = self.initialLocation
self.state = .began
}
public override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
self.state = .ended
}
public override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesCancelled(touches, with: event)
self.state = .cancelled
}
public override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
self.currentLocation = touches.first?.location(in: self.view)
self.state = .changed
}
public func translation(in: UIView?) -> CGPoint {
if let initialLocation = self.initialLocation, let currentLocation = self.currentLocation {
return CGPoint(x: currentLocation.x - initialLocation.x, y: currentLocation.y - initialLocation.y)
}
return CGPoint()
}
}
@@ -0,0 +1,369 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import TelegramCore
import TelegramPresentationData
import MultilineTextComponent
import MultilineTextWithEntitiesComponent
import Markdown
import TextFormat
import AccountContext
private let titleFont = Font.bold(17.0)
private let defaultTextFont = Font.regular(15.0)
private let defaultBoldTextFont = Font.semibold(15.0)
private let defaultItalicTextFont = Font.italic(15.0)
private let defaultBoldItalicTextFont = Font.with(size: 15.0, weight: .semibold, traits: [.italic])
private let defaultFixedTextFont = Font.monospace(15.0)
private let smallTextFont = Font.regular(14.0)
private let smallBoldTextFont = Font.semibold(14.0)
private let smallItalicTextFont = Font.italic(14.0)
private let smallBoldItalicTextFont = Font.with(size: 14.0, weight: .semibold, traits: [.italic])
private let smallFixedTextFont = Font.monospace(14.0)
private let backgroundInset: CGFloat = 8.0
public final class AlertTitleComponent: Component {
public typealias EnvironmentType = AlertComponentEnvironment
public enum Alignment {
case `default`
case center
}
let title: String
let alignment: Alignment
public init(
title: String,
alignment: Alignment = .default
) {
self.title = title
self.alignment = alignment
}
public static func ==(lhs: AlertTitleComponent, rhs: AlertTitleComponent) -> Bool {
if lhs.title != rhs.title {
return false
}
if lhs.alignment != rhs.alignment {
return false
}
return true
}
public final class View: UIView {
private let title = ComponentView<Empty>()
private var component: AlertTitleComponent?
private weak var state: EmptyComponentState?
func update(component: AlertTitleComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let environment = environment[AlertComponentEnvironment.self]
let inset: CGFloat = -6.0
let titleConstrainedSize = CGSize(width: availableSize.width - inset * 2.0, height: availableSize.height)
let titleSize = self.title.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.title,
font: titleFont,
textColor: environment.theme.actionSheet.primaryTextColor
)),
horizontalAlignment: component.alignment == .center ? .center : .natural,
maximumNumberOfLines: 0
)),
environment: {},
containerSize: titleConstrainedSize
)
let titleOriginX: CGFloat
switch component.alignment {
case .default:
titleOriginX = inset
case .center:
titleOriginX = floorToScreenPixels((availableSize.width - titleSize.width) / 2.0)
}
let titleFrame = CGRect(origin: CGPoint(x: titleOriginX, y: 0.0), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
transition.setFrame(view: titleView, frame: titleFrame)
}
return CGSize(width: availableSize.width, height: titleSize.height)
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public final class AlertTextComponent: Component {
public typealias EnvironmentType = AlertComponentEnvironment
public enum Content: Equatable {
case plain(String)
case attributed(NSAttributedString)
case textWithEntities(AccountContext, String, [MessageTextEntity])
public static func ==(lhs: Content, rhs: Content) -> Bool {
switch lhs {
case let .plain(text):
if case .plain(text) = rhs {
return true
} else {
return false
}
case let .attributed(text):
if case .attributed(text) = rhs {
return true
} else {
return false
}
case let .textWithEntities(_, lhsText, lhsEntities):
if case let .textWithEntities(_, rhsText, rhsEntities) = rhs {
return lhsText == rhsText && lhsEntities == rhsEntities
} else {
return false
}
}
}
}
public enum Alignment: Equatable {
case `default`
case center
}
public enum Color: Equatable {
case primary
case secondary
case destructive
}
public enum TextStyle: Equatable {
case `default`
case small
case bold
}
public enum Style: Equatable {
case plain(TextStyle)
case background(TextStyle)
}
let content: Content
let alignment: Alignment
let color: Color
let style: Style
let insets: UIEdgeInsets
let action: ([NSAttributedString.Key: Any]) -> Void
public init(
content: Content,
alignment: Alignment = .default,
color: Color = .primary,
style: Style = .plain(.default),
insets: UIEdgeInsets = .zero,
action: @escaping ([NSAttributedString.Key: Any]) -> Void = { _ in }
) {
self.content = content
self.alignment = alignment
self.color = color
self.style = style
self.insets = insets
self.action = action
}
public static func ==(lhs: AlertTextComponent, rhs: AlertTextComponent) -> Bool {
if lhs.content != rhs.content {
return false
}
if lhs.alignment != rhs.alignment {
return false
}
if lhs.color != rhs.color {
return false
}
if lhs.style != rhs.style {
return false
}
if lhs.insets != rhs.insets {
return false
}
return true
}
public final class View: UIView {
private let background = ComponentView<Empty>()
private let text = ComponentView<Empty>()
private var component: AlertTextComponent?
private weak var state: EmptyComponentState?
func update(component: AlertTextComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let environment = environment[AlertComponentEnvironment.self]
let textColor: UIColor
switch component.color {
case .primary:
textColor = environment.theme.actionSheet.primaryTextColor
case .secondary:
textColor = environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.35)
case .destructive:
textColor = environment.theme.actionSheet.destructiveActionTextColor
}
let linkColor = environment.theme.actionSheet.controlAccentColor
let textFont: UIFont
let boldTextFont: UIFont
let italicTextFont: UIFont
let fixedTextFont: UIFont
switch component.style {
case let .plain(textStyle), let .background(textStyle):
switch textStyle {
case .default:
textFont = defaultTextFont
boldTextFont = defaultBoldTextFont
italicTextFont = defaultItalicTextFont
fixedTextFont = defaultFixedTextFont
case .small:
textFont = smallTextFont
boldTextFont = smallBoldTextFont
italicTextFont = smallItalicTextFont
fixedTextFont = smallFixedTextFont
case .bold:
textFont = defaultBoldTextFont
boldTextFont = defaultBoldTextFont
italicTextFont = defaultBoldItalicTextFont
fixedTextFont = defaultFixedTextFont
}
}
var finalText: NSAttributedString
var context: AccountContext?
switch component.content {
case let .plain(text):
let markdownAttributes = MarkdownAttributes(
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
link: MarkdownAttributeSet(font: textFont, textColor: linkColor),
linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
}
)
finalText = parseMarkdownIntoAttributedString(text, attributes: markdownAttributes)
case let .attributed(attributedText):
finalText = attributedText
case let .textWithEntities(accountContext, text, entities):
context = accountContext
finalText = stringWithAppliedEntities(text, entities: entities, baseColor: textColor, linkColor: linkColor, baseFont: textFont, linkFont: textFont, boldFont: boldTextFont, italicFont: italicTextFont, boldItalicFont: italicTextFont, fixedFont: fixedTextFont, blockQuoteFont: textFont, message: nil)
}
var hasCenterAlignment = component.alignment == .center
switch component.style {
case .background:
hasCenterAlignment = true
default:
break
}
let inset: CGFloat = -6.0
let textConstrainedSize = CGSize(width: availableSize.width - inset * 2.0, height: availableSize.height)
let textSize = self.text.update(
transition: transition,
component: AnyComponent(
MultilineTextWithEntitiesComponent(
context: context,
animationCache: context?.animationCache,
animationRenderer: context?.animationRenderer,
placeholderColor: textColor.withMultipliedAlpha(0.1),
text: .plain(finalText),
horizontalAlignment: hasCenterAlignment ? .center : .natural,
maximumNumberOfLines: 0,
lineSpacing: 0.2,
spoilerColor: textColor,
highlightColor: linkColor.withAlphaComponent(0.2),
manualVisibilityControl: true,
resetAnimationsOnVisibilityChange: true,
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
},
tapAction: { attributes, _ in
component.action(attributes)
}
)
),
environment: {},
containerSize: textConstrainedSize
)
var textOffset = CGPoint(x: inset, y: 0.0)
if hasCenterAlignment {
textOffset.x = floorToScreenPixels((availableSize.width - textSize.width) / 2.0)
}
var size = CGSize(width: availableSize.width, height: textSize.height)
if case .background = component.style {
let backgroundSize = CGSize(width: availableSize.width + 20.0, height: textSize.height + backgroundInset * 2.0)
size = backgroundSize
textOffset = CGPoint(x: textOffset.x, y: backgroundInset)
let _ = self.background.update(
transition: transition,
component: AnyComponent(
FilledRoundedRectangleComponent(
color: textColor.withMultipliedAlpha(0.1),
cornerRadius: .value(10.0),
smoothCorners: true
)
),
environment: {},
containerSize: backgroundSize
)
let backgroundFrame = CGRect(origin: CGPoint(x: -10.0, y: component.insets.top), size: backgroundSize)
if let backgroundView = self.background.view {
if backgroundView.superview == nil {
self.addSubview(backgroundView)
}
transition.setFrame(view: backgroundView, frame: backgroundFrame)
}
}
let textFrame = CGRect(origin: textOffset.offsetBy(dx: 0.0, dy: component.insets.top), size: textSize)
if let textView = self.text.view {
if textView.superview == nil {
self.addSubview(textView)
}
transition.setFrame(view: textView, frame: textFrame)
}
return CGSize(width: size.width, height: size.height + component.insets.top + component.insets.bottom)
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}