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
+74
View File
@@ -0,0 +1,74 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
sgdeps = [
"//Swiftgram/SGAPIWebSettings:SGAPIWebSettings",
"//Swiftgram/SGConfig:SGConfig",
"//Swiftgram/SGLogging:SGLogging"
]
swift_library(
name = "WebUI",
module_name = "WebUI",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = sgdeps + [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/TelegramCore:TelegramCore",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/AccountContext:AccountContext",
"//submodules/AttachmentUI:AttachmentUI",
"//submodules/HexColor:HexColor",
"//submodules/PhotoResources:PhotoResources",
"//submodules/MediaResources",
"//submodules/ShimmerEffect:ShimmerEffect",
"//submodules/LegacyComponents:LegacyComponents",
"//submodules/UrlHandling:UrlHandling",
"//submodules/MoreButtonNode:MoreButtonNode",
"//submodules/BotPaymentsUI:BotPaymentsUI",
"//submodules/PromptUI:PromptUI",
"//submodules/PhoneNumberFormat:PhoneNumberFormat",
"//submodules/QrCodeUI:QrCodeUI",
"//submodules/InstantPageUI:InstantPageUI",
"//submodules/CheckNode:CheckNode",
"//submodules/Markdown:Markdown",
"//submodules/TextFormat:TextFormat",
"//submodules/LocalAuth",
"//submodules/InstantPageCache",
"//submodules/OpenInExternalAppUI",
"//submodules/ComponentFlow",
"//submodules/TelegramUI/Components/EmojiStatusComponent",
"//submodules/ShareController",
"//submodules/UndoUI",
"//submodules/OverlayStatusController",
"//submodules/TelegramUIPreferences",
"//submodules/Components/LottieAnimationComponent",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
"//submodules/TelegramUI/Components/Gifts/GiftAnimationComponent",
"//submodules/TelegramUI/Components/ListItemComponentAdaptor",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/TelegramUI/Components/ListActionItemComponent",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemImpl",
"//submodules/TelegramUI/Components/PremiumPeerShortcutComponent",
"//submodules/DeviceLocationManager",
"//submodules/DeviceAccess",
"//submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage",
"//submodules/AvatarNode",
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/AlertComponent",
"//submodules/TelegramUI/Components/AvatarComponent",
"//submodules/TelegramUI/Components/AlertComponent/AlertCheckComponent",
"//submodules/TelegramUI/Components/AlertComponent/AlertTransferHeaderComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,91 @@
import Foundation
import SwiftSignalKit
final class FileDownload: NSObject, URLSessionDownloadDelegate {
let fileName: String
let fileSize: Int64?
let isMedia: Bool
private var urlSession: URLSession!
private var completion: ((URL?, Error?) -> Void)?
private var progressHandler: ((Double) -> Void)?
private var task: URLSessionDownloadTask!
private let progressPromise = ValuePromise<Double>(0.0)
var progressSignal: Signal<Double, NoError> {
return self.progressPromise.get()
}
init(from url: URL, fileName: String, fileSize: Int64?, isMedia: Bool, progressHandler: @escaping (Double) -> Void, completion: @escaping (URL?, Error?) -> Void) {
self.fileName = fileName
self.fileSize = fileSize
self.isMedia = isMedia
self.completion = completion
self.progressHandler = progressHandler
super.init()
let configuration = URLSessionConfiguration.default
self.urlSession = URLSession(configuration: configuration, delegate: self, delegateQueue: OperationQueue.main)
let downloadTask = self.urlSession.downloadTask(with: url)
downloadTask.resume()
self.task = downloadTask
}
func cancel() {
self.task.cancel()
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
var totalBytesExpectedToWrite = totalBytesExpectedToWrite
if totalBytesExpectedToWrite == -1, let fileSize = self.fileSize {
totalBytesExpectedToWrite = fileSize
}
let progress = max(0.0, min(1.0, Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)))
self.progressHandler?(progress)
self.progressPromise.set(progress)
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
self.completion?(location, nil)
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
self.completion?(nil, error)
}
}
static func getFileSize(url: String) -> Signal<Int64?, NoError> {
if #available(iOS 13.0, *) {
guard let url = URL(string: url) else {
return .single(nil)
}
var request = URLRequest(url: url)
request.httpMethod = "HEAD"
return Signal { subscriber in
let task = URLSession.shared.dataTask(with: request) { _, response, error in
if let _ = error {
subscriber.putNext(nil)
subscriber.putCompletion()
return
}
var fileSize: Int64?
if let httpResponse = response as? HTTPURLResponse, let contentLength = httpResponse.value(forHTTPHeaderField: "Content-Length"), let size = Int64(contentLength) {
fileSize = size
}
subscriber.putNext(fileSize)
subscriber.putCompletion()
}
task.resume()
return ActionDisposable {
task.cancel()
}
}
} else {
return .single(nil)
}
}
}
@@ -0,0 +1,373 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import BundleIconComponent
import MultilineTextComponent
import MoreButtonNode
import AccountContext
import TelegramPresentationData
import LottieAnimationComponent
final class FullscreenControlsComponent: Component {
let context: AccountContext
let strings: PresentationStrings
let title: String
let isVerified: Bool
let insets: UIEdgeInsets
let statusBarStyle: StatusBarStyle
var hasBack: Bool
let backPressed: () -> Void
let minimizePressed: () -> Void
let morePressed: (ASDisplayNode, ContextGesture?) -> Void
init(
context: AccountContext,
strings: PresentationStrings,
title: String,
isVerified: Bool,
insets: UIEdgeInsets,
statusBarStyle: StatusBarStyle,
hasBack: Bool,
backPressed: @escaping () -> Void,
minimizePressed: @escaping () -> Void,
morePressed: @escaping (ASDisplayNode, ContextGesture?) -> Void
) {
self.context = context
self.strings = strings
self.title = title
self.isVerified = isVerified
self.insets = insets
self.statusBarStyle = statusBarStyle
self.hasBack = hasBack
self.backPressed = backPressed
self.minimizePressed = minimizePressed
self.morePressed = morePressed
}
static func ==(lhs: FullscreenControlsComponent, rhs: FullscreenControlsComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.isVerified != rhs.isVerified {
return false
}
if lhs.insets != rhs.insets {
return false
}
if lhs.statusBarStyle != rhs.statusBarStyle {
return false
}
if lhs.hasBack != rhs.hasBack {
return false
}
return true
}
final class View: UIView {
private let leftBackgroundView: BlurredBackgroundView
private let rightBackgroundView: BlurredBackgroundView
private let closeIcon = ComponentView<Empty>()
private let leftButton = HighlightTrackingButton()
private let titleClippingView = UIView()
private let title = ComponentView<Empty>()
private let credibility = ComponentView<Empty>()
private let buttonTitle = ComponentView<Empty>()
private let minimizeButton = ComponentView<Empty>()
private let moreNode = MoreButtonNode(theme: defaultPresentationTheme, size: CGSize(width: 36.0, height: 36.0), encircled: false)
private var displayTitle = true
private var timer: Timer?
private var component: FullscreenControlsComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.leftBackgroundView = BlurredBackgroundView(color: nil)
self.rightBackgroundView = BlurredBackgroundView(color: nil)
super.init(frame: frame)
self.titleClippingView.clipsToBounds = true
self.titleClippingView.isUserInteractionEnabled = false
self.leftBackgroundView.clipsToBounds = true
self.addSubview(self.leftBackgroundView)
self.addSubview(self.leftButton)
self.addSubview(self.titleClippingView)
self.rightBackgroundView.clipsToBounds = true
self.addSubview(self.rightBackgroundView)
self.addSubview(self.moreNode.view)
self.moreNode.updateColor(.white, transition: .immediate)
self.leftButton.highligthedChanged = { [weak self] highlighted in
guard let self else {
return
}
if highlighted {
if let view = self.closeIcon.view {
view.layer.removeAnimation(forKey: "opacity")
view.alpha = 0.6
}
if let view = self.buttonTitle.view {
view.layer.removeAnimation(forKey: "opacity")
view.alpha = 0.6
}
} else {
if let view = self.closeIcon.view {
view.alpha = 1.0
view.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2)
}
if let view = self.buttonTitle.view {
view.alpha = 1.0
view.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2)
}
}
}
self.leftButton.addTarget(self, action: #selector(self.closePressed), for: .touchUpInside)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.timer?.invalidate()
}
@objc private func closePressed() {
guard let component = self.component else {
return
}
component.backPressed()
}
@objc private func timerEvent() {
self.timer?.invalidate()
self.timer = nil
self.displayTitle = false
self.state?.updated(transition: .spring(duration: 0.3))
}
func update(component: FullscreenControlsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let isFirstTime = self.component == nil
let previousComponent = self.component
self.component = component
self.state = state
let sideInset: CGFloat = 16.0
let leftBackgroundSize = CGSize(width: 30.0, height: 30.0)
let rightBackgroundSize = CGSize(width: 72.0, height: 30.0)
let backgroundColor: UIColor = component.statusBarStyle == .Black ? UIColor(white: 0.7, alpha: 0.35) : UIColor(white: 0.45, alpha: 0.25)
let textColor: UIColor = component.statusBarStyle == .Black ? UIColor(rgb: 0x808080) : .white
self.leftBackgroundView.updateColor(color: backgroundColor, transition: transition.containedViewLayoutTransition)
self.rightBackgroundView.updateColor(color: backgroundColor, transition: transition.containedViewLayoutTransition)
let rightBackgroundFrame = CGRect(origin: CGPoint(x: availableSize.width - component.insets.right - sideInset - rightBackgroundSize.width, y: 0.0), size: rightBackgroundSize)
self.rightBackgroundView.update(size: rightBackgroundSize, cornerRadius: rightBackgroundFrame.height / 2.0, transition: transition.containedViewLayoutTransition)
transition.setFrame(view: self.rightBackgroundView, frame: rightBackgroundFrame)
var isAnimatingTextTransition = false
self.moreNode.updateColor(textColor, transition: .immediate)
var additionalLeftWidth: CGFloat = 0.0
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: component.title, font: Font.with(size: 13.0, design: .round, weight: .semibold), textColor: textColor)))),
environment: {},
containerSize: availableSize
)
let titleFrame = CGRect(origin: CGPoint(x: self.displayTitle ? 3.0 : -titleSize.width - 15.0, y: floorToScreenPixels((leftBackgroundSize.height - titleSize.height) / 2.0)), size: titleSize)
if let view = self.title.view {
if view.superview == nil {
self.titleClippingView.addSubview(view)
}
if !view.alpha.isZero && !self.displayTitle {
isAnimatingTextTransition = true
}
transition.setFrame(view: view, frame: titleFrame)
transition.setAlpha(view: view, alpha: self.displayTitle ? 1.0 : 0.0)
}
let buttonTitleUpdated = (previousComponent?.hasBack ?? false) != component.hasBack
let animationMultiplier = !component.hasBack ? -1.0 : 1.0
if buttonTitleUpdated && !self.displayTitle {
isAnimatingTextTransition = true
if let view = self.buttonTitle.view, let snapshotView = view.snapshotView(afterScreenUpdates: false) {
snapshotView.frame = view.frame
self.titleClippingView.addSubview(snapshotView)
snapshotView.layer.animatePosition(from: .zero, to: CGPoint(x: -(snapshotView.frame.width * 1.5) * animationMultiplier, y: 0.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { _ in
snapshotView.removeFromSuperview()
})
}
}
let buttonTitleSize = self.buttonTitle.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: component.hasBack ? component.strings.Common_Back : component.strings.Common_Close, font: Font.with(size: 13.0, design: .round, weight: .semibold), textColor: textColor)))),
environment: {},
containerSize: availableSize
)
if self.displayTitle {
additionalLeftWidth += titleSize.width + 10.0
} else {
additionalLeftWidth += buttonTitleSize.width + 10.0
}
let buttonTitleFrame = CGRect(origin: CGPoint(x: self.displayTitle ? leftBackgroundSize.width + additionalLeftWidth + 3.0 : 3.0, y: floorToScreenPixels((leftBackgroundSize.height - buttonTitleSize.height) / 2.0)), size: buttonTitleSize)
if let view = self.buttonTitle.view {
if view.superview == nil {
self.titleClippingView.addSubview(view)
}
transition.setFrame(view: view, frame: buttonTitleFrame)
if buttonTitleUpdated {
view.layer.animatePosition(from: CGPoint(x: (view.frame.width * 1.5) * animationMultiplier, y: 0.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
}
}
if component.isVerified {
let credibilitySize = self.credibility.update(
transition: .immediate,
component: AnyComponent(BundleIconComponent(name: "Instant View/Verified", tintColor: textColor)),
environment: {},
containerSize: availableSize
)
if let view = self.credibility.view {
if view.superview == nil {
view.alpha = 0.6
self.titleClippingView.addSubview(view)
}
let credibilityFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + 2.0, y: floorToScreenPixels((leftBackgroundSize.height - credibilitySize.height) / 2.0)), size: credibilitySize)
transition.setFrame(view: view, frame: credibilityFrame)
}
if self.displayTitle {
additionalLeftWidth += credibilitySize.width + 2.0
}
}
var leftBackgroundTransition = transition
if buttonTitleUpdated {
leftBackgroundTransition = .spring(duration: 0.3)
}
let leftBackgroundFrame = CGRect(origin: CGPoint(x: sideInset + component.insets.left, y: 0.0), size: CGSize(width: leftBackgroundSize.width + additionalLeftWidth, height: leftBackgroundSize.height))
self.leftBackgroundView.update(size: leftBackgroundFrame.size, cornerRadius: leftBackgroundSize.height / 2.0, transition: leftBackgroundTransition.containedViewLayoutTransition)
leftBackgroundTransition.setFrame(view: self.leftBackgroundView, frame: leftBackgroundFrame)
self.leftButton.frame = leftBackgroundFrame
if isAnimatingTextTransition, self.titleClippingView.mask == nil {
if let maskImage = generateGradientImage(size: CGSize(width: 42.0, height: 10.0), colors: [UIColor.clear, UIColor.black, UIColor.black, UIColor.clear], locations: [0.0, 0.1, 0.9, 1.0], direction: .horizontal) {
let maskView = UIImageView(image: maskImage.stretchableImage(withLeftCapWidth: 4, topCapHeight: 0))
self.titleClippingView.mask = maskView
maskView.frame = CGRect(origin: .zero, size: CGSize(width: self.titleClippingView.bounds.width, height: self.titleClippingView.bounds.height))
}
}
transition.setFrame(view: self.titleClippingView, frame: CGRect(origin: CGPoint(x: sideInset + component.insets.left + leftBackgroundSize.height - 3.0, y: 0.0), size: CGSize(width: leftBackgroundFrame.width - leftBackgroundSize.height, height: leftBackgroundSize.height)))
if let maskView = self.titleClippingView.mask {
leftBackgroundTransition.setFrame(view: maskView, frame: CGRect(origin: .zero, size: CGSize(width: self.titleClippingView.bounds.width, height: self.titleClippingView.bounds.height)), completion: { _ in
self.titleClippingView.mask = nil
})
}
let backButtonSize = self.closeIcon.update(
transition: .immediate,
component: AnyComponent(
LottieAnimationComponent(
animation: LottieAnimationComponent.AnimationItem(
name: "web_backToCancel",
mode: .animating(loop: false),
range: component.hasBack ? (0.5, 1.0) : (0.0, 0.5)
),
colors: ["__allcolors__": textColor],
size: CGSize(width: 30.0, height: 30.0)
)
),
environment: {},
containerSize: CGSize(width: 30.0, height: 30.0)
)
if let view = self.closeIcon.view {
if view.superview == nil {
view.isUserInteractionEnabled = false
self.addSubview(view)
}
let buttonFrame = CGRect(origin: CGPoint(x: leftBackgroundFrame.minX, y: 0.0), size: backButtonSize)
transition.setFrame(view: view, frame: buttonFrame)
}
let minimizeButtonSize = self.minimizeButton.update(
transition: .immediate,
component: AnyComponent(Button(
content: AnyComponent(
BundleIconComponent(name: "Instant View/MinimizeArrow", tintColor: textColor)
),
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.minimizePressed()
}
).minSize(CGSize(width: 30.0, height: 30.0))),
environment: {},
containerSize: CGSize(width: 30.0, height: 30.0)
)
if let view = self.minimizeButton.view {
if view.superview == nil {
self.addSubview(view)
}
let buttonFrame = CGRect(origin: CGPoint(x: rightBackgroundFrame.minX + 2.0, y: 0.0), size: minimizeButtonSize)
transition.setFrame(view: view, frame: buttonFrame)
}
transition.setFrame(view: self.moreNode.view, frame: CGRect(origin: CGPoint(x: rightBackgroundFrame.maxX - 42.0, y: -4.0), size: CGSize(width: 36.0, height: 36.0)))
self.moreNode.action = { [weak self] node, gesture in
guard let self, let component = self.component else {
return
}
component.morePressed(node, gesture)
}
if isFirstTime {
let timer = Timer(timeInterval: 2.5, target: self, selector: #selector(self.timerEvent), userInfo: nil, repeats: false)
self.timer = timer
RunLoop.main.add(timer, forMode: .common)
}
return CGSize(width: availableSize.width, height: leftBackgroundSize.height)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
if result === self {
return nil
}
return result
}
}
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,174 @@
import Foundation
import UIKit
import SwiftSignalKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import AppBundle
import PhotoResources
import ComponentFlow
import AlertComponent
import AlertCheckComponent
import BundleIconComponent
public func addWebAppToAttachmentController(context: AccountContext, peerName: String, icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile], requestWriteAccess: Bool, completion: @escaping (Bool) -> Void) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
let checkState = AlertCheckComponent.ExternalState()
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
content.append(AnyComponentWithIdentity(
id: "header",
component: AnyComponent(
AlertWebAppAttachmentHeaderComponent(context: context, icons: icons)
)
))
content.append(AnyComponentWithIdentity(
id: "title",
component: AnyComponent(
AlertTitleComponent(title: strings.WebApp_AddToAttachmentTitle)
)
))
content.append(AnyComponentWithIdentity(
id: "text",
component: AnyComponent(
AlertTextComponent(content: .plain(strings.WebApp_AddToAttachmentText(peerName).string))
)
))
if requestWriteAccess {
content.append(AnyComponentWithIdentity(
id: "check",
component: AnyComponent(
AlertCheckComponent(title: strings.WebApp_AddToAttachmentAllowMessages(peerName).string, initialValue: false, externalState: checkState)
)
))
}
let alertController = AlertScreen(
context: context,
content: content,
actions: [
.init(title: strings.Common_Cancel),
.init(title: strings.WebApp_AddToAttachmentAdd, type: .default, action: {
completion(requestWriteAccess && checkState.value)
})
]
)
return alertController
}
private final class AlertWebAppAttachmentHeaderComponent: Component {
public typealias EnvironmentType = AlertComponentEnvironment
let context: AccountContext
let icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile]
public init(
context: AccountContext,
icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile]
) {
self.context = context
self.icons = icons
}
public static func ==(lhs: AlertWebAppAttachmentHeaderComponent, rhs: AlertWebAppAttachmentHeaderComponent) -> Bool {
return true
}
public final class View: UIView {
private let appIcon = ComponentView<Empty>()
private let icon = ComponentView<Empty>()
private var appIconImage: UIImage?
private var appIconDisposable: Disposable?
private var component: AlertWebAppAttachmentHeaderComponent?
private weak var state: EmptyComponentState?
deinit {
self.appIconDisposable?.dispose()
}
func update(component: AlertWebAppAttachmentHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
if self.component == nil {
var peerIcon: TelegramMediaFile?
if let icon = component.icons[.iOSStatic] {
peerIcon = icon
} else if let icon = component.icons[.default] {
peerIcon = icon
}
if let peerIcon {
let _ = freeMediaFileInteractiveFetched(account: component.context.account, userLocation: .other, fileReference: .standalone(media: peerIcon)).start()
self.appIconDisposable = (svgIconImageFile(account: component.context.account, fileReference: .standalone(media: peerIcon))
|> deliverOnMainQueue).start(next: { [weak self] transform in
if let self {
let availableSize = CGSize(width: 48.0, height: 48.0)
let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: availableSize, boundingSize: availableSize, intrinsicInsets: UIEdgeInsets())
let drawingContext = transform(arguments)
self.appIconImage = drawingContext?.generateImage()?.withRenderingMode(.alwaysTemplate)
self.state?.updated()
}
})
}
}
self.component = component
self.state = state
let environment = environment[AlertComponentEnvironment.self]
let appIconSize = CGSize(width: 42.0, height: 42.0)
let _ = self.appIcon.update(
transition: .immediate,
component: AnyComponent(
Image(image: self.appIconImage, tintColor: environment.theme.actionSheet.controlAccentColor)
),
environment: {},
containerSize: appIconSize
)
let iconSize = self.icon.update(
transition: .immediate,
component: AnyComponent(
BundleIconComponent(name: "Chat/Attach Menu/BotPlus", tintColor: environment.theme.actionSheet.controlAccentColor)
),
environment: {},
containerSize: availableSize
)
let totalWidth: CGFloat = 42.0 + iconSize.width
let appIconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - totalWidth) / 2.0) - 2.0, y: 3.0), size: appIconSize)
if let imageView = self.appIcon.view {
if imageView.superview == nil {
self.addSubview(imageView)
}
imageView.frame = appIconFrame
}
let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - totalWidth) / 2.0) + appIconSize.width, y: 0.0), size: iconSize)
if let imageView = self.icon.view {
if imageView.superview == nil {
self.addSubview(imageView)
}
imageView.frame = iconFrame
}
return CGSize(width: availableSize.width, height: appIconSize.height + 17.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)
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,249 @@
import Foundation
import UIKit
import SwiftSignalKit
import AsyncDisplayKit
import Display
import Postbox
import ComponentFlow
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import AppBundle
import AvatarNode
import EmojiTextAttachmentView
import TextFormat
import Markdown
import AlertComponent
import AvatarComponent
import MultilineTextComponent
func webAppEmojiStatusAlertController(
context: AccountContext,
accountPeer: EnginePeer,
botName: String,
icons: [TelegramMediaFile.Accessor],
completion: @escaping (Bool, Bool) -> Void
) -> ViewController {
let strings = context.sharedContext.currentPresentationData.with { $0 }.strings
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
content.append(AnyComponentWithIdentity(
id: "status",
component: AnyComponent(
AlertEmojiStatusComponent(context: context, peer: accountPeer, files: icons)
)
))
content.append(AnyComponentWithIdentity(
id: "text",
component: AnyComponent(
AlertTextComponent(content: .plain(strings.WebApp_EmojiPermission_Text(botName, botName).string))
)
))
let alertController = AlertScreen(
context: context,
content: content,
actions: [
.init(title: strings.WebApp_EmojiPermission_Decline, action: {
completion(false, false)
}),
.init(title: strings.WebApp_EmojiPermission_Allow, type: .default, action: {
completion(true, false)
})
]
)
alertController.dismissed = { byOutsideTap in
if byOutsideTap {
completion(false, true)
}
}
return alertController
}
private final class AlertEmojiStatusComponent: Component {
public typealias EnvironmentType = AlertComponentEnvironment
let context: AccountContext
let peer: EnginePeer
let files: [TelegramMediaFile.Accessor]
public init(
context: AccountContext,
peer: EnginePeer,
files: [TelegramMediaFile.Accessor]
) {
self.context = context
self.peer = peer
self.files = files
}
public static func ==(lhs: AlertEmojiStatusComponent, rhs: AlertEmojiStatusComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.peer != rhs.peer {
return false
}
if lhs.files != rhs.files {
return false
}
return true
}
final class View: UIView {
private let background = ComponentView<Empty>()
private let title = ComponentView<Empty>()
private let avatar = ComponentView<Empty>()
private var animationLayer: InlineStickerItemLayer?
private var currentIndex = 0
private var switchingToNext = false
private var timer: SwiftSignalKit.Timer?
private var component: AlertEmojiStatusComponent?
private weak var state: EmptyComponentState?
func update(component: AlertEmojiStatusComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let environment = environment[AlertComponentEnvironment.self]
let titleSize = self.title.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.peer.compactDisplayTitle,
font: Font.medium(15.0),
textColor: environment.theme.actionSheet.primaryTextColor
)),
maximumNumberOfLines: 0
)),
environment: {},
containerSize: availableSize
)
let avatarSize = CGSize(width: 30.0, height: 30.0)
let iconSize = CGSize(width: 20.0, height: 20.0)
let avatarMargin: CGFloat = 1.0
let avatarSpacing: CGFloat = 7.0
let titleSpacing: CGFloat = 4.0
let statusMargin: CGFloat = 12.0
let backgroundSize = CGSize(width: avatarMargin + avatarSize.width + avatarSpacing + titleSize.width + titleSpacing + iconSize.width + statusMargin, height: 32.0)
let _ = self.background.update(
transition: transition,
component: AnyComponent(FilledRoundedRectangleComponent(color: environment.theme.actionSheet.primaryTextColor.withMultipliedAlpha(0.1), cornerRadius: .minEdge, smoothCorners: false)),
environment: {},
containerSize: backgroundSize
)
let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - backgroundSize.width) / 2.0), y: 0.0), size: backgroundSize)
if let backgroundView = self.background.view {
if backgroundView.superview == nil {
self.addSubview(backgroundView)
}
transition.setFrame(view: backgroundView, frame: backgroundFrame)
}
let _ = self.avatar.update(
transition: transition,
component: AnyComponent(AvatarComponent(
context: component.context,
theme: environment.theme,
peer: component.peer
)),
environment: {},
containerSize: avatarSize
)
let avatarFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + avatarMargin, y: backgroundFrame.minY + avatarMargin), size: avatarSize)
if let avatarView = self.avatar.view {
if avatarView.superview == nil {
self.addSubview(avatarView)
}
transition.setFrame(view: avatarView, frame: avatarFrame)
}
let titleFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + avatarMargin + avatarSize.width + avatarSpacing, y: backgroundFrame.minY + floorToScreenPixels((backgroundSize.height - titleSize.height) / 2.0)), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
transition.setFrame(view: titleView, frame: titleFrame)
}
if self.timer == nil {
self.timer = SwiftSignalKit.Timer(timeout: 2.5, repeat: true, completion: { [weak self] in
guard let self else {
return
}
self.switchingToNext = true
self.state?.updated()
}, queue: Queue.mainQueue())
self.timer?.start()
}
let animationLayer: InlineStickerItemLayer
var disappearingAnimationLayer: InlineStickerItemLayer?
if let current = self.animationLayer, !self.switchingToNext {
animationLayer = current
} else {
if self.switchingToNext {
self.currentIndex = (self.currentIndex + 1) % component.files.count
disappearingAnimationLayer = self.animationLayer
self.switchingToNext = false
}
let file = component.files[self.currentIndex]._parse()
let emoji = ChatTextInputTextCustomEmojiAttribute(
interactivelySelectedFromPackId: nil,
fileId: file.fileId.id,
file: file
)
animationLayer = InlineStickerItemLayer(
context: .account(component.context),
userLocation: .other,
attemptSynchronousLoad: false,
emoji: emoji,
file: file,
cache: component.context.animationCache,
renderer: component.context.animationRenderer,
unique: true,
placeholderColor: environment.theme.list.mediaPlaceholderColor,
pointSize: iconSize,
loopCount: 1
)
animationLayer.isVisibleForAnimations = true
animationLayer.dynamicColor = environment.theme.actionSheet.controlAccentColor
self.layer.addSublayer(animationLayer)
self.animationLayer = animationLayer
animationLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
animationLayer.animatePosition(from: CGPoint(x: 0.0, y: 10.0), to: .zero, duration: 0.2, additive: true)
animationLayer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
}
animationLayer.frame = CGRect(origin: CGPoint(x: backgroundFrame.maxX - iconSize.width - statusMargin, y: backgroundFrame.minY + floorToScreenPixels((backgroundFrame.height - iconSize.height) / 2.0)), size: iconSize)
if let disappearingAnimationLayer {
disappearingAnimationLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
disappearingAnimationLayer.removeFromSuperlayer()
})
disappearingAnimationLayer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: -10.0), duration: 0.2, removeOnCompletion: false, additive: true)
disappearingAnimationLayer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
}
return CGSize(width: availableSize.width, height: backgroundSize.height + 12.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,234 @@
import Foundation
import UIKit
import SwiftSignalKit
import AsyncDisplayKit
import Display
import ComponentFlow
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import AppBundle
import EmojiStatusComponent
import AlertComponent
import AlertCheckComponent
import AvatarComponent
import MultilineTextComponent
import BundleIconComponent
import PlainButtonComponent
public func webAppLaunchConfirmationController(
context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?,
peer: EnginePeer,
requestWriteAccess: Bool = false,
completion: @escaping (Bool) -> Void,
showMore: (() -> Void)?,
openTerms: @escaping () -> Void
) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
let checkState = AlertCheckComponent.ExternalState()
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
content.append(AnyComponentWithIdentity(
id: "header",
component: AnyComponent(
AlertWebAppHeaderComponent(context: context, peer: peer, showMore: showMore)
)
))
content.append(AnyComponentWithIdentity(
id: "text",
component: AnyComponent(
AlertTextComponent(content: .plain(strings.WebApp_LaunchTermsConfirmation))
)
))
if requestWriteAccess {
content.append(AnyComponentWithIdentity(
id: "check",
component: AnyComponent(
AlertCheckComponent(title: strings.WebApp_AddToAttachmentAllowMessages(peer.compactDisplayTitle).string, initialValue: false, externalState: checkState)
)
))
}
let alertController = AlertScreen(
context: context,
configuration: AlertScreen.Configuration(actionAlignment: .vertical),
content: content,
actions: [
.init(title: strings.WebApp_LaunchOpenApp, type: .default, action: {
completion(requestWriteAccess && checkState.value)
}),
.init(title: strings.Common_Cancel)
]
)
return alertController
}
private final class AlertWebAppHeaderComponent: Component {
public typealias EnvironmentType = AlertComponentEnvironment
let context: AccountContext
let peer: EnginePeer
let showMore: (() -> Void)?
public init(
context: AccountContext,
peer: EnginePeer,
showMore: (() -> Void)?
) {
self.context = context
self.peer = peer
self.showMore = showMore
}
public static func ==(lhs: AlertWebAppHeaderComponent, rhs: AlertWebAppHeaderComponent) -> Bool {
return true
}
public final class View: UIView {
private let avatar = ComponentView<Empty>()
private let title = ComponentView<Empty>()
private let titleIcon = ComponentView<Empty>()
private let showMore = ComponentView<Empty>()
private var component: AlertWebAppHeaderComponent?
private weak var state: EmptyComponentState?
func update(component: AlertWebAppHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<AlertComponentEnvironment>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let environment = environment[AlertComponentEnvironment.self]
var contentHeight: CGFloat = 0.0
let avatarSize = self.avatar.update(
transition: .immediate,
component: AnyComponent(
AvatarComponent(
context: component.context,
theme: environment.theme,
peer: component.peer
)
),
environment: {},
containerSize: CGSize(width: 60.0, height: 60.0)
)
let avatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - avatarSize.width) / 2.0), y: contentHeight), size: avatarSize)
if let avatarView = self.avatar.view {
if avatarView.superview == nil {
self.addSubview(avatarView)
}
avatarView.frame = avatarFrame
}
contentHeight += avatarSize.height
contentHeight += 17.0
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.peer.compactDisplayTitle,
font: Font.bold(17.0),
textColor: environment.theme.actionSheet.primaryTextColor
)),
horizontalAlignment: .natural,
maximumNumberOfLines: 0
)
),
environment: {},
containerSize: CGSize(width: availableSize.width - 32.0, height: availableSize.height)
)
var totalWidth = titleSize.width
var statusContent: EmojiStatusComponent.Content?
if component.peer.isScam {
statusContent = .text(color: environment.theme.list.itemDestructiveColor, string: environment.strings.Message_ScamAccount.uppercased())
} else if component.peer.isFake {
statusContent = .text(color: environment.theme.list.itemDestructiveColor, string: environment.strings.Message_FakeAccount.uppercased())
} else if component.peer.isVerified {
statusContent = .verified(fillColor: environment.theme.list.itemCheckColors.fillColor, foregroundColor: environment.theme.list.itemCheckColors.foregroundColor, sizeType: .large)
}
if let statusContent {
let titleIconSize = self.titleIcon.update(
transition: .immediate,
component: AnyComponent(EmojiStatusComponent(
context: component.context,
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
content: statusContent,
isVisibleForAnimations: true,
action: {
}
)),
environment: {},
containerSize: CGSize(width: 20.0, height: 20.0)
)
totalWidth += titleIconSize.width + 2.0
if let titleIconView = self.titleIcon.view {
if titleIconView.superview == nil {
self.addSubview(titleIconView)
}
transition.setFrame(view: titleIconView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - totalWidth) / 2.0) + titleSize.width + 2.0, y: contentHeight), size: titleIconSize))
}
}
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - totalWidth) / 2.0), y: contentHeight), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
titleView.frame = titleFrame
}
contentHeight += titleSize.height
if let showMore = component.showMore {
contentHeight += 6.0
let showMoreSize = self.showMore.update(
transition: .immediate,
component: AnyComponent(
PlainButtonComponent(
content: AnyComponent(
HStack([
AnyComponentWithIdentity(id: "label", component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.WebApp_LaunchMoreInfo, font: Font.regular(14.0), textColor: environment.theme.actionSheet.controlAccentColor))))),
AnyComponentWithIdentity(id: "arrow", component: AnyComponent(BundleIconComponent(name: "Item List/InlineTextRightArrow", tintColor: environment.theme.actionSheet.controlAccentColor)))
], spacing: 3.0)
),
action: {
showMore()
},
animateScale: false
)
),
environment: {},
containerSize: availableSize
)
let showMoreFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - showMoreSize.width) / 2.0), y: contentHeight), size: showMoreSize)
if let showMoreView = self.showMore.view {
if showMoreView.superview == nil {
self.addSubview(showMoreView)
}
showMoreView.frame = showMoreFrame
}
contentHeight += showMoreSize.height
}
contentHeight += 12.0
return CGSize(width: availableSize.width, height: contentHeight)
}
}
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,93 @@
import Foundation
import UIKit
import SwiftSignalKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import AppBundle
import ComponentFlow
import AlertComponent
import AvatarComponent
import AlertTransferHeaderComponent
private func generateLocationIcon() -> UIImage? {
let size = CGSize(width: 24.0, height: 24.0)
return generateImage(size, contextGenerator: { size, context in
let bounds = CGRect(origin: .zero, size: size)
context.clear(bounds)
context.addEllipse(in: bounds)
context.clip()
var locations: [CGFloat] = [1.0, 0.0]
let colors: [CGColor] = [UIColor(rgb: 0x36c089).cgColor, UIColor(rgb: 0x3ca5eb).cgColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: size.height), options: CGGradientDrawingOptions())
if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Attach Menu/Location"), color: .white), let cgImage = image.cgImage {
context.draw(cgImage, in: bounds.insetBy(dx: 4.0, dy: 4.0))
}
context.resetClip()
}, opaque: false)
}
func webAppLocationAlertController(context: AccountContext, accountPeer: EnginePeer, botPeer: EnginePeer, completion: @escaping (Bool) -> Void) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
let locationIcon = generateLocationIcon()
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
content.append(AnyComponentWithIdentity(
id: "header",
component: AnyComponent(
AlertTransferHeaderComponent(
fromComponent: AnyComponentWithIdentity(id: "user", component: AnyComponent(
AvatarComponent(
context: context,
theme: presentationData.theme,
peer: accountPeer,
icon: AnyComponent(Image(image: locationIcon, contentMode: .center))
)
)),
toComponent: AnyComponentWithIdentity(id: "bot", component: AnyComponent(
AvatarComponent(
context: context,
theme: presentationData.theme,
peer: botPeer
)
)),
type: .transfer
)
)
))
content.append(AnyComponentWithIdentity(
id: "text",
component: AnyComponent(
AlertTextComponent(content: .plain(strings.WebApp_LocationPermission_Text(botPeer.compactDisplayTitle, botPeer.compactDisplayTitle).string))
)
))
let alertController = AlertScreen(
context: context,
content: content,
actions: [
.init(title: strings.WebApp_LocationPermission_Decline, action: {
completion(false)
}),
.init(title: strings.WebApp_LocationPermission_Allow, type: .default, action: {
completion(true)
})
]
)
return alertController
}
@@ -0,0 +1,406 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import Postbox
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AccountContext
import WallpaperBackgroundNode
import ListItemComponentAdaptor
import ChatMessageItemImpl
final class PeerNameColorChatPreviewItem: ListViewItem, ItemListItem, ListItemComponentAdaptor.ItemGenerator {
struct MessageItem: Equatable {
static func ==(lhs: MessageItem, rhs: MessageItem) -> Bool {
if lhs.text != rhs.text {
return false
}
if areMediaArraysEqual(lhs.media, rhs.media) {
return false
}
if lhs.botAddress != rhs.botAddress {
return false
}
return true
}
let text: String
let entities: TextEntitiesMessageAttribute?
let media: [Media]
let replyMarkup: ReplyMarkupMessageAttribute?
let botAddress: String
}
let context: AccountContext
let theme: PresentationTheme
let componentTheme: PresentationTheme
let strings: PresentationStrings
let sectionId: ItemListSectionId
let fontSize: PresentationFontSize
let chatBubbleCorners: PresentationChatBubbleCorners
let wallpaper: TelegramWallpaper
let dateTimeFormat: PresentationDateTimeFormat
let nameDisplayOrder: PresentationPersonNameOrder
let messageItems: [MessageItem]
init(context: AccountContext, theme: PresentationTheme, componentTheme: PresentationTheme, strings: PresentationStrings, sectionId: ItemListSectionId, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, wallpaper: TelegramWallpaper, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, messageItems: [MessageItem]) {
self.context = context
self.theme = theme
self.componentTheme = componentTheme
self.strings = strings
self.sectionId = sectionId
self.fontSize = fontSize
self.chatBubbleCorners = chatBubbleCorners
self.wallpaper = wallpaper
self.dateTimeFormat = dateTimeFormat
self.nameDisplayOrder = nameDisplayOrder
self.messageItems = messageItems
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = PeerNameColorChatPreviewItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? PeerNameColorChatPreviewItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
public func item() -> ListViewItem {
return self
}
public static func ==(lhs: PeerNameColorChatPreviewItem, rhs: PeerNameColorChatPreviewItem) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.componentTheme !== rhs.componentTheme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.fontSize != rhs.fontSize {
return false
}
if lhs.chatBubbleCorners != rhs.chatBubbleCorners {
return false
}
if lhs.wallpaper != rhs.wallpaper {
return false
}
if lhs.dateTimeFormat != rhs.dateTimeFormat {
return false
}
if lhs.nameDisplayOrder != rhs.nameDisplayOrder {
return false
}
if lhs.messageItems != rhs.messageItems {
return false
}
return true
}
}
final class PeerNameColorChatPreviewItemNode: ListViewItemNode {
private var backgroundNode: WallpaperBackgroundNode?
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let containerNode: ASDisplayNode
private var messageNodes: [ListViewItemNode]?
private var itemHeaderNodes: [ListViewItemNode.HeaderId: ListViewItemHeaderNode] = [:]
private var item: PeerNameColorChatPreviewItem?
private let disposable = MetaDisposable()
init() {
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.containerNode = ASDisplayNode()
self.containerNode.subnodeTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
super.init(layerBacked: false)
self.clipsToBounds = true
self.isUserInteractionEnabled = false
self.addSubnode(self.containerNode)
}
deinit {
self.disposable.dispose()
}
func asyncLayout() -> (_ item: PeerNameColorChatPreviewItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentNodes = self.messageNodes
var currentBackgroundNode = self.backgroundNode
return { item, params, neighbors in
if currentBackgroundNode == nil {
currentBackgroundNode = createWallpaperBackgroundNode(context: item.context, forChatDisplay: false)
currentBackgroundNode?.update(wallpaper: item.wallpaper, animated: false)
currentBackgroundNode?.updateBubbleTheme(bubbleTheme: item.componentTheme, bubbleCorners: item.chatBubbleCorners)
}
var insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
var items: [ListViewItem] = []
for messageItem in item.messageItems.reversed() {
let authorPeerId = EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(0))
let botPeerId = EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1))
var peers = SimpleDictionary<PeerId, Peer>()
let messages = SimpleDictionary<MessageId, Message>()
peers[authorPeerId] = TelegramUser(id: authorPeerId, accessHash: nil, firstName: nil, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)
peers[botPeerId] = TelegramUser(id: botPeerId, accessHash: nil, firstName: messageItem.botAddress, lastName: "", username: messageItem.botAddress, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)
let media = messageItem.media
var attributes: [MessageAttribute] = []
if let entities = messageItem.entities {
attributes.append(entities)
}
if let replyMarkup = messageItem.replyMarkup {
attributes.append(replyMarkup)
}
attributes.append(InlineBotMessageAttribute(peerId: botPeerId, title: nil))
let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: authorPeerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[authorPeerId], text: messageItem.text, attributes: attributes, media: media, peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false, rank: nil, rankRole: nil))
}
var nodes: [ListViewItemNode] = []
if let messageNodes = currentNodes {
nodes = messageNodes
for i in 0 ..< items.count {
let itemNode = messageNodes[i]
items[i].updateNode(async: { $0() }, node: {
return itemNode
}, params: params, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], animation: .None, completion: { (layout, apply) in
let nodeFrame = CGRect(origin: itemNode.frame.origin, size: CGSize(width: layout.size.width, height: layout.size.height))
itemNode.contentSize = layout.contentSize
itemNode.insets = layout.insets
itemNode.frame = nodeFrame
itemNode.isUserInteractionEnabled = false
Queue.mainQueue().after(0.01) {
apply(ListViewItemApply(isOnScreen: true))
}
})
}
} else {
var messageNodes: [ListViewItemNode] = []
for i in 0 ..< items.count {
var itemNode: ListViewItemNode?
items[i].nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], completion: { node, apply in
itemNode = node
apply().1(ListViewItemApply(isOnScreen: true))
})
itemNode!.isUserInteractionEnabled = false
messageNodes.append(itemNode!)
}
nodes = messageNodes
}
var contentSize = CGSize(width: params.width, height: 4.0 + 4.0)
for node in nodes {
contentSize.height += node.frame.size.height
}
insets = itemListNeighborsGroupedInsets(neighbors, params)
if params.width <= 320.0 {
insets.top = 0.0
}
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
let leftInset = params.leftInset
let rightInset = params.leftInset
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
if let currentBackgroundNode {
currentBackgroundNode.update(wallpaper: item.wallpaper, animated: false)
currentBackgroundNode.updateBubbleTheme(bubbleTheme: item.theme, bubbleCorners: item.chatBubbleCorners)
}
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: contentSize)
strongSelf.messageNodes = nodes
var topOffset: CGFloat = 4.0
for node in nodes {
if node.supernode == nil {
strongSelf.containerNode.addSubnode(node)
}
node.updateFrame(CGRect(origin: CGPoint(x: 0.0, y: topOffset), size: node.frame.size), within: layoutSize)
topOffset += node.frame.size.height
if let header = node.headers()?.first(where: { $0 is ChatMessageAvatarHeader }) {
let headerFrame = CGRect(origin: CGPoint(x: 0.0, y: 3.0 + node.frame.minY), size: CGSize(width: layoutSize.width, height: header.height))
let stickLocationDistanceFactor: CGFloat = 0.0
let id = header.id
let headerNode: ListViewItemHeaderNode
if let current = strongSelf.itemHeaderNodes[id] {
headerNode = current
headerNode.updateFrame(headerFrame, within: layoutSize)
if headerNode.item !== header {
header.updateNode(headerNode, previous: nil, next: nil)
headerNode.item = header
}
headerNode.updateLayoutInternal(size: headerFrame.size, leftInset: leftInset, rightInset: rightInset, transition: .immediate)
headerNode.updateStickDistanceFactor(stickLocationDistanceFactor, distance: 0.0, transition: .immediate)
} else {
headerNode = header.node(synchronousLoad: true)
if headerNode.item !== header {
header.updateNode(headerNode, previous: nil, next: nil)
headerNode.item = header
}
headerNode.frame = headerFrame
headerNode.updateLayoutInternal(size: headerFrame.size, leftInset: leftInset, rightInset: rightInset, transition: .immediate)
strongSelf.itemHeaderNodes[id] = headerNode
strongSelf.containerNode.addSubnode(headerNode)
headerNode.updateStickDistanceFactor(stickLocationDistanceFactor, distance: 0.0, transition: .immediate)
}
}
}
if let currentBackgroundNode = currentBackgroundNode, strongSelf.backgroundNode !== currentBackgroundNode {
strongSelf.backgroundNode = currentBackgroundNode
strongSelf.insertSubnode(currentBackgroundNode, at: 0)
}
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
if params.isStandalone {
strongSelf.topStripeNode.isHidden = true
strongSelf.bottomStripeNode.isHidden = true
strongSelf.maskNode.isHidden = true
} else {
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = 0.0
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.componentTheme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
}
let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
let displayMode: WallpaperDisplayMode
if abs(params.availableHeight - params.width) < 100.0, params.availableHeight > 700.0 {
displayMode = .halfAspectFill
} else {
if backgroundFrame.width > backgroundFrame.height * 4.0 {
if params.availableHeight < 700.0 {
displayMode = .halfAspectFill
} else {
displayMode = .aspectFill
}
} else {
displayMode = .aspectFill
}
}
if let backgroundNode = strongSelf.backgroundNode {
backgroundNode.frame = backgroundFrame
backgroundNode.updateLayout(size: backgroundNode.bounds.size, displayMode: displayMode, transition: .immediate)
}
strongSelf.maskNode.frame = backgroundFrame.insetBy(dx: params.leftInset, dy: 0.0)
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}
@@ -0,0 +1,579 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import SwiftSignalKit
import Postbox
import TelegramCore
import Markdown
import TextFormat
import TelegramPresentationData
import ViewControllerComponent
import SheetComponent
import BalancedTextComponent
import MultilineTextComponent
import BundleIconComponent
import ButtonComponent
import ItemListUI
import AccountContext
import PresentationDataUtils
import ListSectionComponent
import ListItemComponentAdaptor
import TelegramStringFormatting
import UndoUI
import ChatMessagePaymentAlertController
import GlassBarButtonComponent
private final class SheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let botName: String
let botAddress: String
let preparedMessage: PreparedInlineMessage
let dismiss: () -> Void
init(
context: AccountContext,
botName: String,
botAddress: String,
preparedMessage: PreparedInlineMessage,
dismiss: @escaping () -> Void
) {
self.context = context
self.botName = botName
self.botAddress = botAddress
self.preparedMessage = preparedMessage
self.dismiss = dismiss
}
static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool {
if lhs.context !== rhs.context {
return false
}
return true
}
static var body: Body {
let closeButton = Child(GlassBarButtonComponent.self)
let title = Child(Text.self)
let previewSection = Child(ListSectionComponent.self)
let button = Child(ButtonComponent.self)
return { context in
let environment = context.environment[EnvironmentType.self]
let component = context.component
let controller = environment.controller
let theme = environment.theme.withModalBlocksBackground()
let strings = environment.strings
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let sideInset: CGFloat = 16.0
var contentSize = CGSize(width: context.availableSize.width, height: 38.0)
let constrainedTitleWidth = context.availableSize.width - 16.0 * 2.0
let closeButton = closeButton.update(
component: GlassBarButtonComponent(
size: CGSize(width: 44.0, height: 44.0),
backgroundColor: nil,
isDark: theme.overallDarkAppearance,
state: .glass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: theme.rootController.navigationBar.glassBarButtonForegroundColor
)
)),
action: { _ in
component.dismiss()
}
),
availableSize: CGSize(width: 44.0, height: 44.0),
transition: .immediate
)
context.add(closeButton
.position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0))
)
let title = title.update(
component: Text(text: environment.strings.WebApp_ShareMessage_Title, font: Font.bold(17.0), color: theme.list.itemPrimaryTextColor),
availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height),
transition: .immediate
)
context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height))
)
contentSize.height += title.size.height
contentSize.height += 40.0
let amountFont = Font.regular(13.0)
let amountTextColor = theme.list.freeTextColor
let amountMarkdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: amountFont, textColor: amountTextColor), bold: MarkdownAttributeSet(font: amountFont, textColor: amountTextColor), link: MarkdownAttributeSet(font: amountFont, textColor: theme.list.itemAccentColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
})
let amountInfoString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(environment.strings.WebApp_ShareMessage_Info(component.botName).string, attributes: amountMarkdownAttributes, textAlignment: .natural))
let previewFooter = AnyComponent(MultilineTextComponent(
text: .plain(amountInfoString),
maximumNumberOfLines: 0,
highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.1),
highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0),
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 controller = controller() as? WebAppMessagePreviewScreen, let navigationController = controller.navigationController as? NavigationController {
component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Stars_PaidContent_AmountInfo_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {})
}
}
))
var text: String = ""
var entities: TextEntitiesMessageAttribute?
var media: [Media] = []
var replyMarkup: ReplyMarkupMessageAttribute?
switch component.preparedMessage.result {
case let .internalReference(reference):
switch reference.message {
case let .auto(textValue, entitiesValue, replyMarkupValue):
text = textValue
entities = entitiesValue
if let file = reference.file {
media = [file]
} else if let image = reference.image {
media = [image]
}
replyMarkup = replyMarkupValue
case let .text(textValue, entitiesValue, disableUrlPreview, previewParameters, replyMarkupValue):
text = textValue
entities = entitiesValue
let _ = disableUrlPreview
let _ = previewParameters
replyMarkup = replyMarkupValue
case let .contact(contact, replyMarkupValue):
media = [contact]
replyMarkup = replyMarkupValue
case let .mapLocation(map, replyMarkupValue):
media = [map]
replyMarkup = replyMarkupValue
case let .invoice(invoice, replyMarkupValue):
media = [invoice]
replyMarkup = replyMarkupValue
default:
break
}
case let .externalReference(reference):
switch reference.message {
case let .auto(textValue, entitiesValue, replyMarkupValue):
text = textValue
entities = entitiesValue
if let content = reference.content {
media = [content]
}
replyMarkup = replyMarkupValue
case let .text(textValue, entitiesValue, disableUrlPreview, previewParameters, replyMarkupValue):
text = textValue
entities = entitiesValue
let _ = disableUrlPreview
let _ = previewParameters
replyMarkup = replyMarkupValue
case let .contact(contact, replyMarkupValue):
media = [contact]
replyMarkup = replyMarkupValue
case let .mapLocation(map, replyMarkupValue):
media = [map]
replyMarkup = replyMarkupValue
case let .invoice(invoice, replyMarkupValue):
media = [invoice]
replyMarkup = replyMarkupValue
default:
break
}
}
let messageItem = PeerNameColorChatPreviewItem.MessageItem(
text: text,
entities: entities,
media: media,
replyMarkup: replyMarkup,
botAddress: component.botAddress
)
let listItemParams = ListViewItemLayoutParams(width: context.availableSize.width - sideInset * 2.0, leftInset: 0.0, rightInset: 0.0, availableHeight: 10000.0, isStandalone: true)
let previewSection = previewSection.update(
component: ListSectionComponent(
theme: theme,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.WebApp_ShareMessage_PreviewTitle.uppercased(),
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
footer: previewFooter,
items: [
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListItemComponentAdaptor(
itemGenerator: PeerNameColorChatPreviewItem(
context: component.context,
theme: environment.theme,
componentTheme: environment.theme,
strings: environment.strings,
sectionId: 0,
fontSize: presentationData.chatFontSize,
chatBubbleCorners: presentationData.chatBubbleCorners,
wallpaper: presentationData.chatWallpaper,
dateTimeFormat: environment.dateTimeFormat,
nameDisplayOrder: presentationData.nameDisplayOrder,
messageItems: [messageItem]
),
params: listItemParams
)))
]
),
environment: {},
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude),
transition: context.transition
)
context.add(previewSection
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + previewSection.size.height / 2.0))
.clipsToBounds(true)
.cornerRadius(10.0)
)
contentSize.height += previewSection.size.height
contentSize.height += 32.0
let buttonString: String = environment.strings.WebApp_ShareMessage_Share
let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0)
let button = button.update(
component: ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
color: theme.list.itemCheckColors.fillColor,
foreground: theme.list.itemCheckColors.foregroundColor,
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9),
),
content: AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))
),
isEnabled: true,
displaysProgress: false,
action: {
if let controller = controller() as? WebAppMessagePreviewScreen {
controller.proceed()
}
}
),
availableSize: CGSize(width: context.availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0),
transition: .immediate
)
context.add(button
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0))
)
contentSize.height += button.size.height
contentSize.height += buttonInsets.bottom
return contentSize
}
}
final class State: ComponentState {
var cachedCloseImage: (UIImage, PresentationTheme)?
}
func makeState() -> State {
return State()
}
}
private final class WebAppMessagePreviewSheetComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
private let context: AccountContext
private let botName: String
private let botAddress: String
private let preparedMessage: PreparedInlineMessage
init(
context: AccountContext,
botName: String,
botAddress: String,
preparedMessage: PreparedInlineMessage
) {
self.context = context
self.botName = botName
self.botAddress = botAddress
self.preparedMessage = preparedMessage
}
static func ==(lhs: WebAppMessagePreviewSheetComponent, rhs: WebAppMessagePreviewSheetComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
return true
}
static var body: Body {
let sheet = Child(SheetComponent<(EnvironmentType)>.self)
let animateOut = StoredActionSlot(Action<Void>.self)
return { context in
let environment = context.environment[EnvironmentType.self]
let controller = environment.controller
let theme = environment.theme.withModalBlocksBackground()
let sheet = sheet.update(
component: SheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(SheetContent(
context: context.component.context,
botName: context.component.botName,
botAddress: context.component.botAddress,
preparedMessage: context.component.preparedMessage,
dismiss: {
animateOut.invoke(Action { _ in
if let controller = controller() as? WebAppMessagePreviewScreen {
controller.completeWithResult(false)
controller.dismiss(completion: nil)
}
})
}
)),
style: .glass,
backgroundColor: .color(theme.list.blocksBackgroundColor),
followContentSizeChanges: false,
clipsContent: true,
isScrollEnabled: false,
animateOut: animateOut
),
environment: {
environment
SheetComponentEnvironment(
metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics,
isDisplaying: environment.value.isVisible,
isCentered: environment.metrics.widthClass == .regular,
hasInputHeight: !environment.inputHeight.isZero,
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
dismiss: { animated in
if animated {
animateOut.invoke(Action { _ in
if let controller = controller() as? WebAppMessagePreviewScreen {
controller.completeWithResult(false)
controller.dismiss(completion: nil)
}
})
} else {
if let controller = controller() as? WebAppMessagePreviewScreen {
controller.completeWithResult(false)
controller.dismiss(completion: nil)
}
}
}
)
},
availableSize: context.availableSize,
transition: context.transition
)
context.add(sheet
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
return context.availableSize
}
}
}
public final class WebAppMessagePreviewScreen: ViewControllerComponentContainer {
private let context: AccountContext
private let preparedMessage: PreparedInlineMessage
fileprivate let completion: (Bool) -> Void
public init(
context: AccountContext,
botName: String,
botAddress: String,
preparedMessage: PreparedInlineMessage,
completion: @escaping (Bool) -> Void
) {
self.context = context
self.preparedMessage = preparedMessage
self.completion = completion
super.init(
context: context,
component: WebAppMessagePreviewSheetComponent(
context: context,
botName: botName,
botAddress: botAddress,
preparedMessage: preparedMessage
),
navigationBarAppearance: .none,
statusBarStyle: .ignore,
theme: .default
)
self.navigationPresentation = .flatModal
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func presentPaidMessageAlertIfNeeded(peers: [EnginePeer], requiresStars: [EnginePeer.Id: Int64], completion: @escaping () -> Void) {
}
fileprivate func complete(peers: [EnginePeer], controller: ViewController?) {
let _ = (self.context.engine.data.get(
EngineDataMap(
peers.map { TelegramEngine.EngineData.Item.Peer.SendPaidMessageStars.init(id: $0.id) }
),
EngineDataList(
peers.map { TelegramEngine.EngineData.Item.Peer.RenderedPeer.init(id: $0.id) }
)
)
|> deliverOnMainQueue).start(next: { [weak self] sendPaidMessageStars, renderedPeers in
guard let self else {
return
}
let renderedPeers = renderedPeers.compactMap({ $0 })
var totalAmount: StarsAmount = .zero
var chargingPeers: [EngineRenderedPeer] = []
for peer in renderedPeers {
if let maybeAmount = sendPaidMessageStars[peer.peerId], let amount = maybeAmount {
totalAmount = totalAmount + amount
chargingPeers.append(peer)
}
}
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let proceed = { [weak self] in
guard let self else {
return
}
for peer in peers {
var starsAmount: StarsAmount?
if let maybeAmount = sendPaidMessageStars[peer.id], let amount = maybeAmount {
starsAmount = amount
}
let _ = self.context.engine.messages.enqueueOutgoingMessage(
to: peer.id,
replyTo: nil,
storyId: nil,
content: .preparedInlineMessage(self.preparedMessage),
sendPaidMessageStars: starsAmount
).start()
}
let text: String
if peers.count == 1, let peer = peers.first {
let peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
text = presentationData.strings.Conversation_ForwardTooltip_Chat_One(peerName).string
} else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last {
let firstPeerName = firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
let secondPeerName = secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
text = presentationData.strings.Conversation_ForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string
} else if let peer = peers.first {
let peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
text = presentationData.strings.Conversation_ForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string
} else {
text = ""
}
if let navigationController = self.navigationController as? NavigationController {
Queue.mainQueue().after(1.0) {
guard let lastController = navigationController.viewControllers.last as? ViewController else {
return
}
lastController.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: false, text: text), elevatedLayout: false, position: .top, animateInAsReplacement: true, action: { action in
return false
}), in: .window(.root))
}
}
self.completeWithResult(true)
self.dismiss()
controller?.dismiss()
}
if totalAmount.value > 0 {
let controller = chatMessagePaymentAlertController(
context: nil,
presentationData: presentationData,
updatedPresentationData: nil,
peers: chargingPeers,
count: 1,
amount: totalAmount,
totalAmount: totalAmount,
hasCheck: false,
navigationController: self.navigationController as? NavigationController,
completion: { _ in
proceed()
}
)
self.present(controller, in: .window(.root))
} else {
proceed()
}
})
}
private var completed = false
fileprivate func completeWithResult(_ result: Bool) {
guard !self.completed else {
return
}
self.completion(result)
}
fileprivate func proceed() {
let peerTypes = self.preparedMessage.peerTypes
var types: [ReplyMarkupButtonRequestPeerType] = []
if peerTypes.contains(.users) {
types.append(.user(.init(isBot: false, isPremium: nil)))
}
if peerTypes.contains(.bots) {
types.append(.user(.init(isBot: true, isPremium: nil)))
}
if peerTypes.contains(.channels) {
types.append(.channel(.init(isCreator: false, hasUsername: nil, userAdminRights: TelegramChatAdminRights(rights: [.canPostMessages]), botAdminRights: nil)))
}
if peerTypes.contains(.groups) {
types.append(.group(.init(isCreator: false, hasUsername: nil, isForum: nil, botParticipant: false, userAdminRights: nil, botAdminRights: nil)))
}
let controller = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, filter: [.excludeRecent, .doNotSearchMessages], requestPeerType: types, hasContactSelector: false, multipleSelection: true, selectForumThreads: true, immediatelyActivateMultipleSelection: true))
controller.multiplePeersSelected = { [weak self, weak controller] peers, _, _, _, _, _ in
guard let self else {
return
}
self.complete(peers: peers, controller: controller)
}
self.push(controller)
}
public func dismissAnimated() {
self.completeWithResult(false)
if let view = self.node.hostView.findTaggedView(tag: SheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
}
}
}
@@ -0,0 +1,130 @@
import Foundation
import NaturalLanguage
import SwiftSignalKit
import TelegramCore
import AccountContext
import TelegramUIPreferences
public struct WebAppPermissionsState: Codable {
enum CodingKeys: String, CodingKey {
case location
case emojiStatus
}
public struct Location: Codable {
enum CodingKeys: String, CodingKey {
case isRequested
case isAllowed
}
public let isRequested: Bool
public let isAllowed: Bool
public init(
isRequested: Bool,
isAllowed: Bool
) {
self.isRequested = isRequested
self.isAllowed = isAllowed
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.isRequested = try container.decode(Bool.self, forKey: .isRequested)
self.isAllowed = try container.decode(Bool.self, forKey: .isAllowed)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.isRequested, forKey: .isRequested)
try container.encode(self.isAllowed, forKey: .isAllowed)
}
}
public struct EmojiStatus: Codable {
enum CodingKeys: String, CodingKey {
case isRequested
}
public let isRequested: Bool
public init(
isRequested: Bool
) {
self.isRequested = isRequested
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.isRequested = try container.decode(Bool.self, forKey: .isRequested)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.isRequested, forKey: .isRequested)
}
}
public let location: Location?
public let emojiStatus: EmojiStatus?
public init(
location: Location?,
emojiStatus: EmojiStatus?
) {
self.location = location
self.emojiStatus = emojiStatus
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.location = try container.decodeIfPresent(WebAppPermissionsState.Location.self, forKey: .location)
self.emojiStatus = try container.decodeIfPresent(WebAppPermissionsState.EmojiStatus.self, forKey: .emojiStatus)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(self.location, forKey: .location)
try container.encodeIfPresent(self.emojiStatus, forKey: .emojiStatus)
}
}
public func webAppPermissionsState(context: AccountContext, peerId: EnginePeer.Id) -> Signal<WebAppPermissionsState?, NoError> {
let key = EngineDataBuffer(length: 8)
key.setInt64(0, value: peerId.id._internalGetInt64Value())
return context.engine.data.subscribe(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.webAppPermissionsState, id: key))
|> map { entry -> WebAppPermissionsState? in
return entry?.get(WebAppPermissionsState.self)
}
}
private func updateWebAppPermissionsState(context: AccountContext, peerId: EnginePeer.Id, state: WebAppPermissionsState?) -> Signal<Never, NoError> {
let key = EngineDataBuffer(length: 8)
key.setInt64(0, value: peerId.id._internalGetInt64Value())
if let state {
return context.engine.itemCache.put(collectionId: ApplicationSpecificItemCacheCollectionId.webAppPermissionsState, id: key, item: state)
} else {
return context.engine.itemCache.remove(collectionId: ApplicationSpecificItemCacheCollectionId.webAppPermissionsState, id: key)
}
}
public func updateWebAppPermissionsStateInteractively(context: AccountContext, peerId: EnginePeer.Id, _ f: @escaping (WebAppPermissionsState?) -> WebAppPermissionsState?) -> Signal<Never, NoError> {
let key = EngineDataBuffer(length: 8)
key.setInt64(0, value: peerId.id._internalGetInt64Value())
return context.engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.webAppPermissionsState, id: key))
|> map { entry -> WebAppPermissionsState? in
return entry?.get(WebAppPermissionsState.self)
}
|> mapToSignal { current -> Signal<Never, NoError> in
return updateWebAppPermissionsState(context: context, peerId: peerId, state: f(current))
}
}
@@ -0,0 +1,303 @@
import Foundation
import Security
import SwiftSignalKit
import TelegramCore
import AccountContext
final class WebAppSecureStorage {
enum Error {
case quotaExceeded
case canRestore
case storageNotEmpty
case unknown
}
struct StorageValue: Codable {
let timestamp: Int32
let accountName: String
let value: String
}
static private let maxKeyCount = 10
private init() {
}
static private func keyPrefix(uuid: String, botId: EnginePeer.Id) -> String {
return "WebBot\(UInt64(bitPattern: botId.toInt64()))U\(uuid)Key_"
}
static private func makeQuery(uuid: String, botId: EnginePeer.Id, key: String) -> [String: Any] {
let identifier = self.keyPrefix(uuid: uuid, botId: botId) + key
return [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: identifier,
kSecAttrService as String: "TMASecureStorage"
]
}
static private func countKeys(uuid: String, botId: EnginePeer.Id) -> Int {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "TMASecureStorage",
kSecMatchLimit as String: kSecMatchLimitAll,
kSecReturnAttributes as String: true
]
var result: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &result)
if status == errSecSuccess, let items = result as? [[String: Any]] {
let relevantPrefix = self.keyPrefix(uuid: uuid, botId: botId)
let count = items.filter {
if let account = $0[kSecAttrAccount as String] as? String {
return account.hasPrefix(relevantPrefix)
}
return false
}.count
return count
}
return 0
}
static func setValue(context: AccountContext, botId: EnginePeer.Id, key: String, value: String?) -> Signal<Never, WebAppSecureStorage.Error> {
return combineLatest(
context.engine.peers.secureBotStorageUuid(),
context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
)
|> castError(WebAppSecureStorage.Error.self)
|> mapToSignal { uuid, accountPeer in
var query = makeQuery(uuid: uuid, botId: botId, key: key)
guard let value else {
let status = SecItemDelete(query as CFDictionary)
if status == errSecSuccess || status == errSecItemNotFound {
return .complete()
} else {
return .fail(.unknown)
}
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let storageValue = StorageValue(
timestamp: Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970),
accountName: accountPeer?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) ?? "",
value: value
)
guard let storageValueData = try? JSONEncoder().encode(storageValue) else {
return .fail(.unknown)
}
query[kSecAttrAccessible as String] = kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
let status = SecItemCopyMatching(query as CFDictionary, nil)
if status == errSecSuccess {
let updateQuery: [String: Any] = [
kSecValueData as String: storageValueData
]
let updateStatus = SecItemUpdate(query as CFDictionary, updateQuery as CFDictionary)
if updateStatus == errSecSuccess {
return .complete()
} else {
return .fail(.unknown)
}
} else if status == errSecItemNotFound {
let currentCount = countKeys(uuid: uuid, botId: botId)
if currentCount >= maxKeyCount {
return .fail(.quotaExceeded)
}
query[kSecValueData as String] = storageValueData
let createStatus = SecItemAdd(query as CFDictionary, nil)
if createStatus == errSecSuccess {
return .complete()
} else {
return .fail(.unknown)
}
} else {
return .fail(.unknown)
}
}
}
static func getValue(context: AccountContext, botId: EnginePeer.Id, key: String) -> Signal<String?, WebAppSecureStorage.Error> {
return context.engine.peers.secureBotStorageUuid()
|> castError(WebAppSecureStorage.Error.self)
|> mapToSignal { uuid in
var query = makeQuery(uuid: uuid, botId: botId, key: key)
query[kSecReturnData as String] = true
query[kSecMatchLimit as String] = kSecMatchLimitOne
var result: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &result)
if status == errSecSuccess, let storageValueData = result as? Data, let storageValue = try? JSONDecoder().decode(StorageValue.self, from: storageValueData) {
return .single(storageValue.value)
} else if status == errSecItemNotFound {
return findRestorableKeys(context: context, botId: botId, key: key)
|> castError(WebAppSecureStorage.Error.self)
|> mapToSignal { restorableKeys in
if !restorableKeys.isEmpty {
return .fail(.canRestore)
} else {
return .single(nil)
}
}
} else {
return .fail(.unknown)
}
}
}
static func checkRestoreAvailability(context: AccountContext, botId: EnginePeer.Id, key: String) -> Signal<[ExistingKey], WebAppSecureStorage.Error> {
return context.engine.peers.secureBotStorageUuid()
|> castError(WebAppSecureStorage.Error.self)
|> mapToSignal { uuid in
let currentCount = countKeys(uuid: uuid, botId: botId)
guard currentCount == 0 else {
return .fail(.storageNotEmpty)
}
return findRestorableKeys(context: context, botId: botId, key: key)
|> castError(WebAppSecureStorage.Error.self)
}
}
private static func findRestorableKeys(context: AccountContext, botId: EnginePeer.Id, key: String) -> Signal<[ExistingKey], NoError> {
let storedKeys = getAllStoredKeys(botId: botId, key: key)
guard !storedKeys.isEmpty else {
return .single([])
}
return context.sharedContext.activeAccountContexts
|> take(1)
|> mapToSignal { _, accountContexts, _ in
let signals = accountContexts.map { $0.1.engine.peers.secureBotStorageUuid() }
return combineLatest(signals)
|> map { activeUuids in
let inactiveAccountKeys = storedKeys.filter { !activeUuids.contains($0.uuid) }
return inactiveAccountKeys
}
}
}
static func transferAllValues(context: AccountContext, fromUuid: String, botId: EnginePeer.Id) -> Signal<Never, WebAppSecureStorage.Error> {
return context.engine.peers.secureBotStorageUuid()
|> castError(WebAppSecureStorage.Error.self)
|> mapToSignal { toUuid in
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "TMASecureStorage",
kSecMatchLimit as String: kSecMatchLimitAll,
kSecReturnAttributes as String: true,
kSecReturnData as String: true
]
var result: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &result)
if status == errSecSuccess, let items = result as? [[String: Any]] {
let fromPrefix = keyPrefix(uuid: fromUuid, botId: botId)
let toPrefix = keyPrefix(uuid: toUuid, botId: botId)
for item in items {
if let account = item[kSecAttrAccount as String] as? String, account.hasPrefix(fromPrefix), let data = item[kSecValueData as String] as? Data {
let keySuffix = account.dropFirst(fromPrefix.count)
let newKeyIdentifier = toPrefix + keySuffix
let newKeyQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: newKeyIdentifier,
kSecAttrService as String: "TMASecureStorage",
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
]
SecItemAdd(newKeyQuery as CFDictionary, nil)
}
}
return clearStorage(uuid: fromUuid, botId: botId)
} else {
return .complete()
}
}
}
struct ExistingKey: Equatable {
let uuid: String
let accountName: String
let timestamp: Int32
}
private static func getAllStoredKeys(botId: EnginePeer.Id, key: String) -> [ExistingKey] {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "TMASecureStorage",
kSecMatchLimit as String: kSecMatchLimitAll,
kSecReturnAttributes as String: true,
kSecReturnData as String: true
]
var result: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &result)
var storedKeys: [ExistingKey] = []
if status == errSecSuccess, let items = result as? [[String: Any]] {
let botIdString = "\(UInt64(bitPattern: botId.toInt64()))"
for item in items {
if let account = item[kSecAttrAccount as String] as? String, account.contains("WebBot\(botIdString)U"), account.hasSuffix("Key_\(key)"), let valueData = item[kSecValueData as String] as? Data, let value = try? JSONDecoder().decode(StorageValue.self, from: valueData) {
if let range = account.range(of: "WebBot\(botIdString)U"), let endRange = account.range(of: "Key_\(key)") {
let startIndex = range.upperBound
let endIndex = endRange.lowerBound
let uuid = String(account[startIndex..<endIndex])
storedKeys.append(ExistingKey(
uuid: uuid,
accountName: value.accountName,
timestamp: value.timestamp
))
}
}
}
}
return storedKeys
}
static func clearStorage(context: AccountContext, botId: EnginePeer.Id) -> Signal<Never, WebAppSecureStorage.Error> {
return context.engine.peers.secureBotStorageUuid()
|> castError(WebAppSecureStorage.Error.self)
|> mapToSignal { uuid in
return clearStorage(uuid: uuid, botId: botId)
}
}
static func clearStorage(uuid: String, botId: EnginePeer.Id) -> Signal<Never, WebAppSecureStorage.Error> {
let serviceQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "TMASecureStorage",
kSecMatchLimit as String: kSecMatchLimitAll,
kSecReturnAttributes as String: true
]
var result: CFTypeRef?
let status = SecItemCopyMatching(serviceQuery as CFDictionary, &result)
if status == errSecSuccess, let items = result as? [[String: Any]] {
let relevantPrefix = self.keyPrefix(uuid: uuid, botId: botId)
for item in items {
if let account = item[kSecAttrAccount as String] as? String, account.hasPrefix(relevantPrefix) {
let deleteQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: account,
kSecAttrService as String: "TMASecureStorage"
]
SecItemDelete(deleteQuery as CFDictionary)
}
}
}
return .complete()
}
}
@@ -0,0 +1,495 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import TelegramCore
import Markdown
import TextFormat
import TelegramPresentationData
import TelegramStringFormatting
import ViewControllerComponent
import SheetComponent
import BundleIconComponent
import MultilineTextComponent
import ButtonComponent
import ListSectionComponent
import ListActionItemComponent
import AccountContext
import AvatarNode
private final class SheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let peer: EnginePeer
let existingKeys: [WebAppSecureStorage.ExistingKey]
let completion: (String) -> Void
let dismiss: () -> Void
init(
context: AccountContext,
peer: EnginePeer,
existingKeys: [WebAppSecureStorage.ExistingKey],
completion: @escaping (String) -> Void,
dismiss: @escaping () -> Void
) {
self.context = context
self.peer = peer
self.existingKeys = existingKeys
self.completion = completion
self.dismiss = dismiss
}
static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.peer != rhs.peer {
return false
}
if lhs.existingKeys != rhs.existingKeys {
return false
}
return true
}
final class State: ComponentState {
var selectedUuid: String?
}
func makeState() -> State {
return State()
}
static var body: Body {
let closeButton = Child(Button.self)
let title = Child(MultilineTextComponent.self)
let avatar = Child(AvatarComponent.self)
let text = Child(MultilineTextComponent.self)
let keys = Child(ListSectionComponent.self)
let button = Child(ButtonComponent.self)
return { context in
let environment = context.environment[EnvironmentType.self]
let component = context.component
let state = context.state
let theme = environment.theme.withModalBlocksBackground()
let strings = environment.strings
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let textSideInset: CGFloat = 32.0 + environment.safeInsets.left
let titleFont = Font.semibold(17.0)
let textFont = Font.regular(13.0)
let boldTextFont = Font.semibold(13.0)
let textColor = theme.actionSheet.primaryTextColor
var contentSize = CGSize(width: context.availableSize.width, height: 18.0)
let closeButton = closeButton.update(
component: Button(
content: AnyComponent(Text(text: strings.Common_Cancel, font: Font.regular(17.0), color: theme.actionSheet.controlAccentColor)),
action: { [weak component] in
component?.dismiss()
}
),
availableSize: CGSize(width: 100.0, height: 30.0),
transition: .immediate
)
context.add(closeButton
.position(CGPoint(x: environment.safeInsets.left + 16.0 + closeButton.size.width / 2.0, y: 28.0))
)
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(string: strings.WebApp_ImportData_Title, font: titleFont, textColor: textColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 1,
lineSpacing: 0.1
),
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
transition: .immediate
)
context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0))
)
contentSize.height += title.size.height
contentSize.height += 24.0
let avatar = avatar.update(
component: AvatarComponent(
context: component.context,
peer: component.peer,
size: CGSize(width: 80.0, height: 80.0)
),
availableSize: CGSize(width: 80.0, height: 80.0),
transition: .immediate
)
context.add(avatar
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + avatar.size.height / 2.0))
)
contentSize.height += avatar.size.height
contentSize.height += 22.0
let text = text.update(
component: MultilineTextComponent(
text: .markdown(
text: strings.WebApp_ImportData_Description(component.peer.compactDisplayTitle).string,
attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
link: MarkdownAttributeSet(font: textFont, textColor: textColor),
linkAttribute: { _ in return nil }
)
),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.2
),
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
transition: .immediate
)
context.add(text
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + text.size.height / 2.0))
)
contentSize.height += text.size.height
contentSize.height += 29.0
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
var items: [AnyComponentWithIdentity<Empty>] = []
for key in component.existingKeys {
var titleComponents: [AnyComponentWithIdentity<Empty>] = []
titleComponents.append(
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: key.accountName,
font: Font.semibold(presentationData.listsFontSize.itemListBaseFontSize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
)))
)
titleComponents.append(
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: strings.WebApp_ImportData_CreatedOn(stringForMediumCompactDate(timestamp: key.timestamp, strings: strings, dateTimeFormat: environment.dateTimeFormat)).string,
font: Font.regular(floor(presentationData.listsFontSize.itemListBaseFontSize * 15.0 / 17.0)),
textColor: environment.theme.list.itemSecondaryTextColor
)),
maximumNumberOfLines: 1
)))
)
items.append(AnyComponentWithIdentity(id: key.uuid, component: AnyComponent(ListActionItemComponent(
theme: theme,
title: AnyComponent(VStack(titleComponents, alignment: .left, spacing: 3.0)),
contentInsets: UIEdgeInsets(top: 10.0, left: 0.0, bottom: 10.0, right: 0.0),
leftIcon: .check(ListActionItemComponent.LeftIcon.Check(isSelected: key.uuid == state.selectedUuid, isEnabled: true, toggle: nil)),
accessory: nil,
action: { [weak state] _ in
if let state {
state.selectedUuid = key.uuid
state.updated(transition: .spring(duration: 0.3))
}
}
))))
}
let keys = keys.update(
component: ListSectionComponent(
theme: environment.theme,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: strings.WebApp_ImportData_AccountHeader.uppercased(),
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
footer: nil,
items: items
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 1000.0),
transition: context.transition
)
context.add(keys
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + keys.size.height / 2.0))
)
contentSize.height += keys.size.height
contentSize.height += 24.0
let button = button.update(
component: ButtonComponent(
background: ButtonComponent.Background(
color: theme.list.itemCheckColors.fillColor,
foreground: theme.list.itemCheckColors.foregroundColor,
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
),
content: AnyComponentWithIdentity(
id: AnyHashable("import"),
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: strings.WebApp_ImportData_Import, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)))
)
),
isEnabled: state.selectedUuid != nil,
allowActionWhenDisabled: true,
displaysProgress: false,
action: { [weak state] in
guard let state else {
return
}
if let selectedUuid = state.selectedUuid {
component.completion(selectedUuid)
component.dismiss()
}
}
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0),
transition: context.transition
)
context.add(button
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0))
.cornerRadius(10.0)
)
contentSize.height += button.size.height
contentSize.height += 7.0
let effectiveBottomInset: CGFloat = environment.metrics.isTablet ? 0.0 : environment.safeInsets.bottom
contentSize.height += 5.0 + effectiveBottomInset
return contentSize
}
}
}
private final class SheetContainerComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let peer: EnginePeer
let existingKeys: [WebAppSecureStorage.ExistingKey]
let completion: (String) -> Void
init(
context: AccountContext,
peer: EnginePeer,
existingKeys: [WebAppSecureStorage.ExistingKey],
completion: @escaping (String) -> Void
) {
self.context = context
self.peer = peer
self.existingKeys = existingKeys
self.completion = completion
}
static func ==(lhs: SheetContainerComponent, rhs: SheetContainerComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.peer != rhs.peer {
return false
}
if lhs.existingKeys != rhs.existingKeys {
return false
}
return true
}
static var body: Body {
let sheet = Child(SheetComponent<EnvironmentType>.self)
let animateOut = StoredActionSlot(Action<Void>.self)
let sheetExternalState = SheetComponent<EnvironmentType>.ExternalState()
return { context in
let environment = context.environment[EnvironmentType.self]
let theme = environment.theme.withModalBlocksBackground()
let controller = environment.controller
let sheet = sheet.update(
component: SheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(SheetContent(
context: context.component.context,
peer: context.component.peer,
existingKeys: context.component.existingKeys,
completion: context.component.completion,
dismiss: {
animateOut.invoke(Action { _ in
if let controller = controller() {
controller.dismiss(completion: nil)
}
})
}
)),
backgroundColor: .color(theme.list.blocksBackgroundColor),
followContentSizeChanges: true,
externalState: sheetExternalState,
animateOut: animateOut
),
environment: {
environment
SheetComponentEnvironment(
metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics,
isDisplaying: environment.value.isVisible,
isCentered: environment.metrics.widthClass == .regular,
hasInputHeight: !environment.inputHeight.isZero,
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
dismiss: { animated in
if animated {
animateOut.invoke(Action { _ in
if let controller = controller() {
controller.dismiss(completion: nil)
}
})
} else {
if let controller = controller() {
controller.dismiss(completion: nil)
}
}
}
)
},
availableSize: context.availableSize,
transition: context.transition
)
context.add(sheet
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
if let controller = controller(), !controller.automaticallyControlPresentationContextLayout {
let layout = ContainerViewLayout(
size: context.availableSize,
metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics,
intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: max(environment.safeInsets.bottom, sheetExternalState.contentHeight), right: 0.0),
safeInsets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: 0.0, right: environment.safeInsets.right),
additionalInsets: .zero,
statusBarHeight: environment.statusBarHeight,
inputHeight: nil,
inputHeightIsInteractivellyChanging: false,
inVoiceOver: false
)
controller.presentationContext.containerLayoutUpdated(layout, transition: context.transition.containedViewLayoutTransition)
}
return context.availableSize
}
}
}
final class WebAppSecureStorageTransferScreen: ViewControllerComponentContainer {
init(
context: AccountContext,
peer: EnginePeer,
existingKeys: [WebAppSecureStorage.ExistingKey],
completion: @escaping (String?) -> Void
) {
super.init(
context: context,
component: SheetContainerComponent(
context: context,
peer: peer,
existingKeys: existingKeys,
completion: completion
),
navigationBarAppearance: .none,
statusBarStyle: .ignore,
theme: .default
)
self.navigationPresentation = .flatModal
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func dismissAnimated() {
if let view = self.node.hostView.findTaggedView(tag: SheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
}
}
}
private final class AvatarComponent: Component {
let context: AccountContext
let peer: EnginePeer
let size: CGSize?
init(context: AccountContext, peer: EnginePeer, size: CGSize? = nil) {
self.context = context
self.peer = peer
self.size = size
}
static func ==(lhs: AvatarComponent, rhs: AvatarComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.peer != rhs.peer {
return false
}
if lhs.size != rhs.size {
return false
}
return true
}
final class View: UIView {
private var avatarNode: AvatarNode?
private var component: AvatarComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: AvatarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let size = component.size ?? availableSize
let avatarNode: AvatarNode
if let current = self.avatarNode {
avatarNode = current
} else {
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: floor(size.width * 0.5)))
avatarNode.displaysAsynchronously = false
self.avatarNode = avatarNode
self.addSubview(avatarNode.view)
}
avatarNode.frame = CGRect(origin: CGPoint(), size: size)
avatarNode.setPeer(
context: component.context,
theme: component.context.sharedContext.currentPresentationData.with({ $0 }).theme,
peer: component.peer,
synchronousLoad: true,
displayDimensions: size
)
avatarNode.updateSize(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,418 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import TelegramCore
import Markdown
import TextFormat
import TelegramPresentationData
import ViewControllerComponent
import SheetComponent
import BalancedTextComponent
import MultilineTextComponent
import BundleIconComponent
import ButtonComponent
import AccountContext
import PresentationDataUtils
import PremiumPeerShortcutComponent
import GiftAnimationComponent
import GlassBarButtonComponent
private final class SheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let botName: String
let accountPeer: EnginePeer
let file: TelegramMediaFile
let duration: Int32?
let dismiss: () -> Void
init(
context: AccountContext,
botName: String,
accountPeer: EnginePeer,
file: TelegramMediaFile,
duration: Int32?,
dismiss: @escaping () -> Void
) {
self.context = context
self.botName = botName
self.accountPeer = accountPeer
self.file = file
self.duration = duration
self.dismiss = dismiss
}
static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.botName != rhs.botName {
return false
}
if lhs.file != rhs.file {
return false
}
return true
}
final class State: ComponentState {
}
func makeState() -> State {
return State()
}
static var body: Body {
let background = Child(RoundedRectangle.self)
let animation = Child(GiftAnimationComponent.self)
let closeButton = Child(GlassBarButtonComponent.self)
let title = Child(Text.self)
let text = Child(BalancedTextComponent.self)
let peerShortcut = Child(PremiumPeerShortcutComponent.self)
let button = Child(ButtonComponent.self)
return { context in
let environment = context.environment[EnvironmentType.self]
let component = context.component
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let theme = presentationData.theme
let strings = presentationData.strings
var contentSize = CGSize(width: context.availableSize.width, height: 18.0)
let background = background.update(
component: RoundedRectangle(color: theme.actionSheet.opaqueItemBackgroundColor, cornerRadius: 8.0),
availableSize: CGSize(width: context.availableSize.width, height: 1000.0),
transition: .immediate
)
context.add(background
.position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0))
)
let animation = animation.update(
component: GiftAnimationComponent(
context: component.context,
theme: environment.theme,
file: component.file
),
availableSize: CGSize(width: 128.0, height: 128.0),
transition: .immediate
)
context.add(animation
.position(CGPoint(x: context.availableSize.width / 2.0, y: animation.size.height / 2.0 + 12.0))
)
let closeButton = closeButton.update(
component: GlassBarButtonComponent(
size: CGSize(width: 44.0, height: 44.0),
backgroundColor: nil,
isDark: theme.overallDarkAppearance,
state: .glass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: theme.chat.inputPanel.panelControlColor
)
)),
action: { _ in
component.dismiss()
}
),
availableSize: CGSize(width: 44.0, height: 44.0),
transition: .immediate
)
context.add(closeButton
.position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0))
)
let constrainedTitleWidth = context.availableSize.width - 16.0 * 2.0
contentSize.height += 128.0
let title = title.update(
component: Text(text: strings.WebApp_Emoji_Title, font: Font.bold(24.0), color: theme.list.itemPrimaryTextColor),
availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height),
transition: .immediate
)
context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0))
)
contentSize.height += title.size.height
contentSize.height += 13.0
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
let textColor = theme.actionSheet.primaryTextColor
let linkColor = 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)
})
var textString: String
if let duration = component.duration {
let durationString = scheduledTimeIntervalString(strings: strings, value: duration)
textString = strings.WebApp_Emoji_DurationText(component.botName, durationString).string
} else {
textString = strings.WebApp_Emoji_Text(component.botName).string
}
let text = text.update(
component: BalancedTextComponent(
text: .markdown(
text: textString,
attributes: markdownAttributes
),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.2
),
availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height),
transition: .immediate
)
context.add(text
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + text.size.height / 2.0))
)
contentSize.height += text.size.height
contentSize.height += 15.0
let peerShortcut = peerShortcut.update(
component: PremiumPeerShortcutComponent(
context: component.context,
theme: theme,
peer: component.accountPeer,
icon: component.file
),
availableSize: CGSize(width: context.availableSize.width - 32.0, height: context.availableSize.height),
transition: .immediate
)
context.add(peerShortcut
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + peerShortcut.size.height / 2.0))
)
contentSize.height += peerShortcut.size.height
contentSize.height += 32.0
let controller = environment.controller() as? WebAppSetEmojiStatusScreen
let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0)
let button = button.update(
component: ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
color: theme.list.itemCheckColors.fillColor,
foreground: theme.list.itemCheckColors.foregroundColor,
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9),
),
content: AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(MultilineTextComponent(text: .plain(NSMutableAttributedString(string: strings.WebApp_Emoji_Confirm, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center))))
),
isEnabled: true,
displaysProgress: false,
action: { [weak controller] in
controller?.complete(result: true)
controller?.dismissAnimated()
}
),
availableSize: CGSize(width: context.availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0),
transition: .immediate
)
context.add(button
.clipsToBounds(true)
.cornerRadius(10.0)
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0))
)
contentSize.height += button.size.height
contentSize.height += buttonInsets.bottom
return contentSize
}
}
}
private final class WebAppSetEmojiStatusSheetComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
private let context: AccountContext
private let botName: String
private let accountPeer: EnginePeer
private let file: TelegramMediaFile
private let duration: Int32?
init(
context: AccountContext,
botName: String,
accountPeer: EnginePeer,
file: TelegramMediaFile,
duration: Int32?
) {
self.context = context
self.botName = botName
self.accountPeer = accountPeer
self.file = file
self.duration = duration
}
static func ==(lhs: WebAppSetEmojiStatusSheetComponent, rhs: WebAppSetEmojiStatusSheetComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.botName != rhs.botName {
return false
}
if lhs.accountPeer != rhs.accountPeer {
return false
}
if lhs.duration != rhs.duration {
return false
}
return true
}
static var body: Body {
let sheet = Child(SheetComponent<(EnvironmentType)>.self)
let animateOut = StoredActionSlot(Action<Void>.self)
return { context in
let environment = context.environment[EnvironmentType.self]
let controller = environment.controller
let sheet = sheet.update(
component: SheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(SheetContent(
context: context.component.context,
botName: context.component.botName,
accountPeer: context.component.accountPeer,
file: context.component.file,
duration: context.component.duration,
dismiss: {
animateOut.invoke(Action { _ in
if let controller = controller() as? WebAppSetEmojiStatusScreen {
controller.complete(result: false)
controller.dismiss(completion: nil)
}
})
}
)),
style: .glass,
backgroundColor: .color(environment.theme.list.modalBlocksBackgroundColor),
followContentSizeChanges: true,
clipsContent: true,
animateOut: animateOut
),
environment: {
environment
SheetComponentEnvironment(
metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics,
isDisplaying: environment.value.isVisible,
isCentered: environment.metrics.widthClass == .regular,
hasInputHeight: !environment.inputHeight.isZero,
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
dismiss: { animated in
if animated {
animateOut.invoke(Action { _ in
if let controller = controller() as? WebAppSetEmojiStatusScreen {
controller.complete(result: false)
controller.dismiss(completion: nil)
}
})
} else {
if let controller = controller() as? WebAppSetEmojiStatusScreen {
controller.complete(result: false)
controller.dismiss(completion: nil)
}
}
}
)
},
availableSize: context.availableSize,
transition: context.transition
)
context.add(sheet
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
return context.availableSize
}
}
}
public final class WebAppSetEmojiStatusScreen: ViewControllerComponentContainer {
private let context: AccountContext
private let completion: (Bool) -> Void
public init(
context: AccountContext,
botName: String,
accountPeer: EnginePeer,
file: TelegramMediaFile,
duration: Int32?,
completion: @escaping (Bool) -> Void
) {
self.context = context
self.completion = completion
super.init(
context: context,
component: WebAppSetEmojiStatusSheetComponent(
context: context,
botName: botName,
accountPeer: accountPeer,
file: file,
duration: duration
),
navigationBarAppearance: .none,
statusBarStyle: .ignore,
theme: .default
)
self.navigationPresentation = .flatModal
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private var didComplete = false
fileprivate func complete(result: Bool) {
guard !self.didComplete else {
return
}
self.didComplete = true
self.completion(result)
}
public func dismissAnimated() {
if let view = self.node.hostView.findTaggedView(tag: SheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
}
}
}
func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? {
return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(backgroundColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
context.setLineWidth(2.0)
context.setLineCap(.round)
context.setStrokeColor(foregroundColor.cgColor)
context.move(to: CGPoint(x: 10.0, y: 10.0))
context.addLine(to: CGPoint(x: 20.0, y: 20.0))
context.strokePath()
context.move(to: CGPoint(x: 20.0, y: 10.0))
context.addLine(to: CGPoint(x: 10.0, y: 20.0))
context.strokePath()
})
}
@@ -0,0 +1,69 @@
import Foundation
import UIKit
import SwiftSignalKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import AppBundle
import ComponentFlow
import AlertComponent
import AlertCheckComponent
public func webAppTermsAlertController(
context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?,
bot: AttachMenuBot,
completion: @escaping (Bool) -> Void,
dismissed: @escaping () -> Void = {}
) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
let checkState = AlertCheckComponent.ExternalState()
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
content.append(AnyComponentWithIdentity(
id: "title",
component: AnyComponent(
AlertTitleComponent(title: strings.WebApp_DisclaimerTitle)
)
))
content.append(AnyComponentWithIdentity(
id: "text",
component: AnyComponent(
AlertTextComponent(content: .plain(strings.WebApp_DisclaimerText))
)
))
content.append(AnyComponentWithIdentity(
id: "check",
component: AnyComponent(
AlertCheckComponent(title: strings.WebApp_DisclaimerAgree, initialValue: false, externalState: checkState, linkAction: {
context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: strings.WebApp_Disclaimer_URL, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {})
})
)
))
var effectiveUpdatedPresentationData: (PresentationData, Signal<PresentationData, NoError>)
if let updatedPresentationData {
effectiveUpdatedPresentationData = updatedPresentationData
} else {
effectiveUpdatedPresentationData = (presentationData, context.sharedContext.presentationData)
}
let alertController = AlertScreen(
configuration: AlertScreen.Configuration(actionAlignment: .vertical),
content: content,
actions: [
.init(title: strings.WebApp_DisclaimerContinue, type: .default, action: {
completion(checkState.value)
}, isEnabled: checkState.valueSignal),
.init(title: strings.Common_Cancel)
],
updatedPresentationData: effectiveUpdatedPresentationData
)
return alertController
}
@@ -0,0 +1,162 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramPresentationData
import ComponentFlow
import EmojiStatusComponent
import AccountContext
public struct WebAppTitle: Equatable {
public var title: String
public var counter: String
public var isVerified: Bool
public init(title: String, counter: String, isVerified: Bool) {
self.title = title
self.counter = counter
self.isVerified = isVerified
}
}
public final class WebAppTitleView: UIView {
private let context: AccountContext
private let titleNode: ImmediateTextNode
private let subtitleNode: ImmediateTextNode
private var titleCredibilityIconView: ComponentHostView<Empty>?
public var title: WebAppTitle = WebAppTitle(title: "", counter: "", isVerified: false) {
didSet {
if self.title != oldValue {
self.update()
}
}
}
public var theme: PresentationTheme {
didSet {
self.update()
}
}
private let isAttachMenu: Bool
private var primaryTextColor: UIColor?
private var secondaryTextColor: UIColor?
public func updateTextColors(primary: UIColor?, secondary: UIColor?, transition: ContainedViewLayoutTransition) {
self.primaryTextColor = primary
self.secondaryTextColor = secondary
if case let .animated(duration, curve) = transition {
if let snapshotView = self.snapshotContentTree() {
snapshotView.frame = self.bounds
self.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false, completion: { _ in
snapshotView.removeFromSuperview()
})
self.titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction)
self.subtitleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction)
}
}
self.update()
}
private func update() {
let primaryTextColor = self.primaryTextColor ?? self.theme.rootController.navigationBar.primaryTextColor
let secondaryTextColor = self.secondaryTextColor ?? self.theme.rootController.navigationBar.secondaryTextColor
self.titleNode.attributedText = NSAttributedString(string: self.title.title, font: Font.semibold(17.0), textColor: primaryTextColor)
self.subtitleNode.attributedText = NSAttributedString(string: self.title.counter, font: Font.regular(13.0), textColor: secondaryTextColor)
self.accessibilityLabel = self.title.title
self.accessibilityValue = self.title.counter
self.setNeedsLayout()
}
public init(context: AccountContext, theme: PresentationTheme, isAttachMenu: Bool) {
self.context = context
self.theme = theme
self.isAttachMenu = isAttachMenu
self.titleNode = ImmediateTextNode()
self.titleNode.displaysAsynchronously = false
self.titleNode.maximumNumberOfLines = 1
self.titleNode.truncationType = .end
self.titleNode.isOpaque = false
self.subtitleNode = ImmediateTextNode()
self.subtitleNode.displaysAsynchronously = false
self.subtitleNode.maximumNumberOfLines = 1
self.subtitleNode.truncationType = .end
self.subtitleNode.isOpaque = false
super.init(frame: CGRect())
self.isAccessibilityElement = true
self.accessibilityTraits = .header
self.addSubnode(self.titleNode)
self.addSubnode(self.subtitleNode)
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func layoutSubviews() {
super.layoutSubviews()
let size = self.bounds.size
let spacing: CGFloat = 0.0
let titleSize = self.titleNode.updateLayout(CGSize(width: max(1.0, size.width), height: size.height))
let subtitleSize = self.subtitleNode.updateLayout(CGSize(width: max(1.0, size.width), height: size.height))
let combinedHeight = titleSize.height + subtitleSize.height + spacing
var totalWidth = titleSize.width
let topOffset = self.isAttachMenu ? 3.0 : 0.0
if self.title.isVerified {
let statusContent: EmojiStatusComponent.Content = .verified(fillColor: self.theme.list.itemCheckColors.fillColor, foregroundColor: self.theme.list.itemCheckColors.foregroundColor, sizeType: .large)
let titleCredibilityIconTransition: ComponentTransition = .immediate
let titleCredibilityIconView: ComponentHostView<Empty>
if let current = self.titleCredibilityIconView {
titleCredibilityIconView = current
} else {
titleCredibilityIconView = ComponentHostView<Empty>()
self.titleCredibilityIconView = titleCredibilityIconView
self.addSubview(titleCredibilityIconView)
}
let titleIconSize = titleCredibilityIconView.update(
transition: titleCredibilityIconTransition,
component: AnyComponent(EmojiStatusComponent(
context: self.context,
animationCache: self.context.animationCache,
animationRenderer: self.context.animationRenderer,
content: statusContent,
isVisibleForAnimations: true,
action: {
}
)),
environment: {},
containerSize: CGSize(width: 20.0, height: 20.0)
)
totalWidth += titleIconSize.width + 2.0
titleCredibilityIconTransition.setFrame(view: titleCredibilityIconView, frame: CGRect(origin: CGPoint(x:floorToScreenPixels((size.width - totalWidth) / 2.0) + titleSize.width + 2.0, y: topOffset + floorToScreenPixels(floorToScreenPixels((size.height - combinedHeight) / 2.0 + titleSize.height / 2.0) - titleIconSize.height / 2.0)), size: titleIconSize))
}
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - totalWidth) / 2.0), y: topOffset + floorToScreenPixels((size.height - combinedHeight) / 2.0)), size: titleSize)
self.titleNode.frame = titleFrame
let subtitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - subtitleSize.width) / 2.0), y: topOffset + floorToScreenPixels((size.height - combinedHeight) / 2.0) + titleSize.height + spacing), size: subtitleSize)
self.subtitleNode.frame = subtitleFrame
}
}
@@ -0,0 +1,300 @@
import Foundation
import UIKit
import Display
import WebKit
import SwiftSignalKit
import TelegramCore
private let findActiveElementY = """
function getOffset(el) {
const rect = el.getBoundingClientRect();
return {
left: rect.left + window.scrollX,
top: rect.top + window.scrollY
};
}
getOffset(document.activeElement).top;
"""
private class WeakGameScriptMessageHandler: NSObject, WKScriptMessageHandler {
private let f: (WKScriptMessage) -> ()
init(_ f: @escaping (WKScriptMessage) -> ()) {
self.f = f
super.init()
}
func userContentController(_ controller: WKUserContentController, didReceive scriptMessage: WKScriptMessage) {
self.f(scriptMessage)
}
}
private class WebViewTouchGestureRecognizer: UITapGestureRecognizer {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
self.state = .began
}
}
private let eventProxySource = "var TelegramWebviewProxyProto = function() {}; " +
"TelegramWebviewProxyProto.prototype.postEvent = function(eventName, eventData) { " +
"window.webkit.messageHandlers.performAction.postMessage({'eventName': eventName, 'eventData': eventData}); " +
"}; " +
"var TelegramWebviewProxy = new TelegramWebviewProxyProto();"
private let selectionSource = "var css = '*{-webkit-touch-callout:none;} :not(input):not(textarea):not([\"contenteditable\"=\"true\"]){-webkit-user-select:none;}';"
+ " var head = document.head || document.getElementsByTagName('head')[0];"
+ " var style = document.createElement('style'); style.type = 'text/css';" +
" style.appendChild(document.createTextNode(css)); head.appendChild(style);"
private let videoSource = """
document.addEventListener('DOMContentLoaded', () => {
function tgBrowserDisableWebkitEnterFullscreen(videoElement) {
if (videoElement && videoElement.webkitEnterFullscreen) {
videoElement.setAttribute('playsinline', '');
}
}
function tgBrowserDisableFullscreenOnExistingVideos() {
document.querySelectorAll('video').forEach(tgBrowserDisableWebkitEnterFullscreen);
}
function tgBrowserHandleMutations(mutations) {
mutations.forEach((mutation) => {
if (mutation.addedNodes && mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach((newNode) => {
if (newNode.tagName === 'VIDEO') {
tgBrowserDisableWebkitEnterFullscreen(newNode);
}
if (newNode.querySelectorAll) {
newNode.querySelectorAll('video').forEach(tgBrowserDisableWebkitEnterFullscreen);
}
});
}
});
}
tgBrowserDisableFullscreenOnExistingVideos();
const _tgbrowser_observer = new MutationObserver(tgBrowserHandleMutations);
_tgbrowser_observer.observe(document.body, {
childList: true,
subtree: true
});
function tgBrowserDisconnectObserver() {
_tgbrowser_observer.disconnect();
}
});
"""
final class WebAppWebView: WKWebView {
var handleScriptMessage: (WKScriptMessage) -> Void = { _ in }
var customInsets: UIEdgeInsets = .zero {
didSet {
if self.customInsets != oldValue {
self.setNeedsLayout()
}
}
}
override var safeAreaInsets: UIEdgeInsets {
return UIEdgeInsets(top: self.customInsets.top, left: self.customInsets.left, bottom: self.customInsets.bottom, right: self.customInsets.right)
}
init(userScripts: [WKUserScript] = [], account: Account) {
let configuration = WKWebViewConfiguration()
if #available(iOS 17.0, *) {
var uuid: UUID?
if let current = UserDefaults.standard.object(forKey: "TelegramWebStoreUUID_\(account.id.int64)") as? String {
uuid = UUID(uuidString: current)!
} else {
let mainAccountId: Int64
if let current = UserDefaults.standard.object(forKey: "TelegramWebStoreMainAccountId") as? Int64 {
mainAccountId = current
} else {
mainAccountId = account.id.int64
UserDefaults.standard.set(mainAccountId, forKey: "TelegramWebStoreMainAccountId")
}
if account.id.int64 != mainAccountId {
uuid = UUID()
UserDefaults.standard.set(uuid!.uuidString, forKey: "TelegramWebStoreUUID_\(account.id.int64)")
}
}
if let uuid {
configuration.websiteDataStore = WKWebsiteDataStore(forIdentifier: uuid)
}
}
let contentController = WKUserContentController()
var handleScriptMessageImpl: ((WKScriptMessage) -> Void)?
let eventProxyScript = WKUserScript(source: eventProxySource, injectionTime: .atDocumentStart, forMainFrameOnly: true)
contentController.addUserScript(eventProxyScript)
contentController.add(WeakGameScriptMessageHandler { message in
handleScriptMessageImpl?(message)
}, name: "performAction")
let selectionScript = WKUserScript(source: selectionSource, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
contentController.addUserScript(selectionScript)
let videoScript = WKUserScript(source: videoSource, injectionTime: .atDocumentStart, forMainFrameOnly: false)
contentController.addUserScript(videoScript)
for userScript in userScripts {
contentController.addUserScript(userScript)
}
configuration.userContentController = contentController
configuration.allowsInlineMediaPlayback = true
configuration.allowsPictureInPictureMediaPlayback = false
if #available(iOS 10.0, *) {
configuration.mediaTypesRequiringUserActionForPlayback = []
} else {
configuration.mediaPlaybackRequiresUserAction = false
}
super.init(frame: CGRect(), configuration: configuration)
self.disablesInteractiveKeyboardGestureRecognizer = true
self.isOpaque = false
self.backgroundColor = .clear
self.allowsLinkPreview = false
self.scrollView.contentInsetAdjustmentBehavior = .never
self.interactiveTransitionGestureRecognizerTest = { point -> Bool in
return point.x > 30.0
}
self.allowsBackForwardNavigationGestures = false
if #available(iOS 16.4, *) {
self.isInspectable = true
}
handleScriptMessageImpl = { [weak self] message in
if let strongSelf = self {
strongSelf.handleScriptMessage(message)
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
print()
}
override func didMoveToSuperview() {
super.didMoveToSuperview()
if #available(iOS 11.0, *) {
let webScrollView = self.subviews.compactMap { $0 as? UIScrollView }.first
Queue.mainQueue().after(0.1, {
let contentView = webScrollView?.subviews.first(where: { $0.interactions.count > 1 })
guard let dragInteraction = (contentView?.interactions.compactMap { $0 as? UIDragInteraction }.first) else {
return
}
contentView?.removeInteraction(dragInteraction)
})
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
}
}
func hideScrollIndicators() {
var hiddenViews: [UIView] = []
for view in self.scrollView.subviews.reversed() {
let minSize = min(view.frame.width, view.frame.height)
if minSize < 4.0 {
view.isHidden = true
hiddenViews.append(view)
}
}
Queue.mainQueue().after(2.0) {
for view in hiddenViews {
view.isHidden = false
}
}
}
func sendEvent(name: String, data: String?) {
let script = "window.TelegramGameProxy && window.TelegramGameProxy.receiveEvent && window.TelegramGameProxy.receiveEvent(\"\(name)\", \(data ?? "null"))"
self.evaluateJavaScript(script, completionHandler: { _, _ in
})
}
func updateMetrics(height: CGFloat, isExpanded: Bool, isStable: Bool, transition: ContainedViewLayoutTransition) {
let viewportData = "{height:\(height), is_expanded:\(isExpanded ? "true" : "false"), is_state_stable:\(isStable ? "true" : "false")}"
self.sendEvent(name: "viewport_changed", data: viewportData)
let safeInsetsData = "{top:\(self.customInsets.top), bottom:\(self.customInsets.bottom), left:\(self.customInsets.left), right:\(self.customInsets.right)}"
self.sendEvent(name: "safe_area_changed", data: safeInsetsData)
}
var lastTouchTimestamp: Double?
private(set) var didTouchOnce = false
var onFirstTouch: () -> Void = {}
func scrollToActiveElement(layout: ContainerViewLayout, completion: @escaping (CGPoint) -> Void, transition: ContainedViewLayoutTransition) {
self.evaluateJavaScript(findActiveElementY, completionHandler: { result, _ in
if let result = result as? CGFloat {
Queue.mainQueue().async {
let convertedY = result - self.scrollView.contentOffset.y
let viewportHeight = self.frame.height
if convertedY < 0.0 || (convertedY + 44.0) > viewportHeight {
let targetOffset: CGFloat
if convertedY < 0.0 {
targetOffset = max(0.0, result - 36.0)
} else {
targetOffset = max(0.0, result + 60.0 - viewportHeight)
}
let contentOffset = CGPoint(x: 0.0, y: targetOffset)
completion(contentOffset)
transition.animateView({
self.scrollView.contentOffset = contentOffset
})
}
}
}
})
}
// MARK: Swiftgram
public private(set) var monkeyClickerActive = false
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
self.lastTouchTimestamp = CACurrentMediaTime()
if result != nil && !self.didTouchOnce {
self.didTouchOnce = true
self.onFirstTouch()
}
return result
}
override var inputAccessoryView: UIView? {
return nil
}
}
// MARK: Swiftgram
extension WebAppWebView {
public func toggleClicker(enableJS: String, disableJS: String) {
if self.monkeyClickerActive {
self.evaluateJavaScript(disableJS, completionHandler: nil)
} else {
self.evaluateJavaScript(enableJS, completionHandler: nil)
}
self.monkeyClickerActive = !self.monkeyClickerActive
}
}