chore: migrate to new version + fixed several critical bugs

- Migrated project to latest Telegram iOS base (v12.3.2+)
- Fixed circular dependency between GhostModeManager and MiscSettingsManager
- Fixed multiple Bazel build configuration errors (select() default conditions)
- Fixed duplicate type definitions in PeerInfoScreen
- Fixed swiftmodule directory resolution in build scripts
- Added Ghostgram Settings tab in main Settings menu with all 5 features
- Cleared sensitive credentials from config.json (template-only now)
- Excluded bazel-cache from version control
This commit is contained in:
ichmagmaus 812
2026-02-23 23:04:32 +01:00
parent 703e291bcb
commit db53826061
1017 changed files with 62337 additions and 40559 deletions
+4
View File
@@ -57,6 +57,10 @@ swift_library(
"//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,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)
}
}
+200 -132
View File
@@ -43,6 +43,7 @@ import GlassBarButtonComponent
import BundleIconComponent
import LottieComponent
import CryptoKit
import AlertComponent
private let durgerKingBotIds: [Int64] = [5104055776, 2200339955]
@@ -685,7 +686,14 @@ public final class WebAppController: ViewController, AttachmentContainable {
if let data = try? Data(contentsOf: url), let pass = try? PKPass(data: data) {
let passLibrary = PKPassLibrary()
if passLibrary.containsPass(pass) {
let alertController = textAlertController(context: self.context, updatedPresentationData: nil, title: nil, text: self.presentationData.strings.WebBrowser_PassExistsError, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_OK, action: {})])
let alertController = AlertScreen(
context: self.context,
title: nil,
text: self.presentationData.strings.WebBrowser_PassExistsError,
actions: [
.init(title: self.presentationData.strings.Common_OK, type: .default)
]
)
self.controller?.present(alertController, in: .window(.root))
} else if let controller = PKAddPassesViewController(pass: pass) {
self.controller?.view.window?.rootViewController?.present(controller, animated: true)
@@ -761,12 +769,19 @@ public final class WebAppController: ViewController, AttachmentContainable {
func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
var completed = false
let alertController = textAlertController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, title: nil, text: message, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {
if !completed {
completed = true
completionHandler()
}
})])
let alertController = AlertScreen(
context: self.context,
title: nil,
text: message,
actions: [
.init(title: self.presentationData.strings.Common_OK, action: {
if !completed {
completed = true
completionHandler()
}
})
]
)
alertController.dismissed = { byOutsideTap in
if byOutsideTap {
if !completed {
@@ -780,17 +795,25 @@ public final class WebAppController: ViewController, AttachmentContainable {
func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) {
var completed = false
let alertController = textAlertController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, title: nil, text: message, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {
if !completed {
completed = true
completionHandler(false)
}
}), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {
if !completed {
completed = true
completionHandler(true)
}
})])
let alertController = AlertScreen(
context: self.context,
title: nil,
text: message,
actions: [
.init(title: self.presentationData.strings.Common_Cancel, action: {
if !completed {
completed = true
completionHandler(false)
}
}),
.init(title: self.presentationData.strings.Common_OK, type: .default, action: {
if !completed {
completed = true
completionHandler(true)
}
})
]
)
alertController.dismissed = { byOutsideTap in
if byOutsideTap {
if !completed {
@@ -804,24 +827,28 @@ public final class WebAppController: ViewController, AttachmentContainable {
func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) {
var completed = false
let promptController = promptController(sharedContext: self.context.sharedContext, updatedPresentationData: self.controller?.updatedPresentationData, text: prompt, value: defaultText, apply: { value in
if !completed {
completed = true
if let value = value {
completionHandler(value)
} else {
completionHandler(nil)
let promptController = promptController(
context: self.context,
updatedPresentationData: self.controller?.updatedPresentationData,
text: prompt,
value: defaultText,
apply: { value in
if !completed {
completed = true
if let value = value {
completionHandler(value)
} else {
completionHandler(nil)
}
}
}
})
promptController.dismissed = { byOutsideTap in
if byOutsideTap {
},
dismissed: {
if !completed {
completed = true
completionHandler(nil)
}
}
}
)
self.controller?.present(promptController, in: .window(.root))
}
@@ -1385,7 +1412,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
let presentationData = self.presentationData
let title = json["title"] as? String
var alertButtons: [TextAlertAction] = []
var actions: [AlertScreen.Action] = []
for buttonJson in buttons.reversed() {
if let button = buttonJson as? [String: Any], let id = button["id"] as? String, let type = button["type"] as? String {
@@ -1395,27 +1422,27 @@ public final class WebAppController: ViewController, AttachmentContainable {
let text = button["text"] as? String
switch type {
case "default":
if let text = text {
alertButtons.append(TextAlertAction(type: .genericAction, title: text, action: {
if let text {
actions.append(AlertScreen.Action(title: text, action: {
buttonAction()
}))
}
case "destructive":
if let text = text {
alertButtons.append(TextAlertAction(type: .destructiveAction, title: text, action: {
if let text {
actions.append(AlertScreen.Action(title: text, type: .destructive, action: {
buttonAction()
}))
}
case "ok":
alertButtons.append(TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
actions.append(AlertScreen.Action(title: presentationData.strings.Common_OK, type: .default, action: {
buttonAction()
}))
case "cancel":
alertButtons.append(TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
actions.append(AlertScreen.Action(title: presentationData.strings.Common_Cancel, action: {
buttonAction()
}))
case "close":
alertButtons.append(TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Close, action: {
actions.append(AlertScreen.Action(title: presentationData.strings.Common_Close, action: {
buttonAction()
}))
default:
@@ -1424,12 +1451,18 @@ public final class WebAppController: ViewController, AttachmentContainable {
}
}
var actionLayout: TextAlertContentActionLayout = .horizontal
if alertButtons.count > 2 {
var actionLayout: AlertScreen.ActionAligmnent = .default
if actions.count > 2 {
actionLayout = .vertical
alertButtons = Array(alertButtons.reversed())
actions = Array(actions.reversed())
}
let alertController = textAlertController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, title: title, text: message, actions: alertButtons, actionLayout: actionLayout)
let alertController = AlertScreen(
context: self.context,
configuration: AlertScreen.Configuration(actionAlignment: actionLayout, dismissOnOutsideTap: true, allowInputInset: false),
title: title,
text: message,
actions: actions
)
alertController.dismissed = { byOutsideTap in
if byOutsideTap {
self.sendAlertButtonEvent(id: nil)
@@ -2113,18 +2146,26 @@ public final class WebAppController: ViewController, AttachmentContainable {
if result {
sendEvent(true)
} else {
let alertController = textAlertController(context: self.context, updatedPresentationData: controller.updatedPresentationData, title: self.presentationData.strings.WebApp_AllowWriteTitle, text: self.presentationData.strings.WebApp_AllowWriteConfirmation(controller.botName).string, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {
sendEvent(false)
}), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { [weak self] in
guard let self else {
return
}
let _ = (self.context.engine.messages.allowBotSendMessages(botId: controller.botId)
|> deliverOnMainQueue).start(completed: {
sendEvent(true)
})
})], parseMarkdown: true)
let alertController = AlertScreen(
context: self.context,
title: self.presentationData.strings.WebApp_AllowWriteTitle,
text: self.presentationData.strings.WebApp_AllowWriteConfirmation(controller.botName).string,
actions: [
.init(title: self.presentationData.strings.Common_Cancel, action: {
sendEvent(false)
}),
.init(title: self.presentationData.strings.Common_OK, type: .default, action: { [weak self] in
guard let self else {
return
}
let _ = (self.context.engine.messages.allowBotSendMessages(botId: controller.botId)
|> deliverOnMainQueue).start(completed: {
sendEvent(true)
})
})
]
)
alertController.dismissed = { byOutsideTap in
if byOutsideTap {
sendEvent(false)
@@ -2166,48 +2207,56 @@ public final class WebAppController: ViewController, AttachmentContainable {
text = self.presentationData.strings.WebApp_SharePhoneConfirmation(botName).string
}
let alertController = textAlertController(context: self.context, updatedPresentationData: controller.updatedPresentationData, title: self.presentationData.strings.WebApp_SharePhoneTitle, text: text, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {
sendEvent(false)
}), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { [weak self] in
guard let self, case let .user(user) = accountPeer, let phone = user.phone, !phone.isEmpty else {
return
}
let sendMessageSignal = enqueueMessages(account: self.context.account, peerId: botId, messages: [
.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaContact(firstName: user.firstName ?? "", lastName: user.lastName ?? "", phoneNumber: phone, peerId: user.id, vCardData: nil)), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])
])
|> mapToSignal { messageIds in
if let maybeMessageId = messageIds.first, let messageId = maybeMessageId {
return context.account.pendingMessageManager.pendingMessageStatus(messageId)
|> mapToSignal { status, _ -> Signal<Bool, NoError> in
if status != nil {
return .never()
let alertController = AlertScreen(
context: self.context,
title: self.presentationData.strings.WebApp_SharePhoneTitle,
text: text,
actions: [
.init(title: self.presentationData.strings.Common_Cancel, action: {
sendEvent(false)
}),
.init(title: self.presentationData.strings.Common_OK, type: .default, action: { [weak self] in
guard let self, case let .user(user) = accountPeer, let phone = user.phone, !phone.isEmpty else {
return
}
let sendMessageSignal = enqueueMessages(account: self.context.account, peerId: botId, messages: [
.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaContact(firstName: user.firstName ?? "", lastName: user.lastName ?? "", phoneNumber: phone, peerId: user.id, vCardData: nil)), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])
])
|> mapToSignal { messageIds in
if let maybeMessageId = messageIds.first, let messageId = maybeMessageId {
return context.account.pendingMessageManager.pendingMessageStatus(messageId)
|> mapToSignal { status, _ -> Signal<Bool, NoError> in
if status != nil {
return .never()
} else {
return .single(true)
}
}
|> take(1)
} else {
return .single(true)
return .complete()
}
}
|> take(1)
} else {
return .complete()
}
}
let sendMessage = {
let _ = (sendMessageSignal
|> deliverOnMainQueue).start(completed: {
sendEvent(true)
let sendMessage = {
let _ = (sendMessageSignal
|> deliverOnMainQueue).start(completed: {
sendEvent(true)
})
}
if requiresUnblock {
let _ = (context.engine.privacy.requestUpdatePeerIsBlocked(peerId: botId, isBlocked: false)
|> deliverOnMainQueue).start(completed: {
sendMessage()
})
} else {
sendMessage()
}
})
}
if requiresUnblock {
let _ = (context.engine.privacy.requestUpdatePeerIsBlocked(peerId: botId, isBlocked: false)
|> deliverOnMainQueue).start(completed: {
sendMessage()
})
} else {
sendMessage()
}
})], parseMarkdown: true)
]
)
alertController.dismissed = { byOutsideTap in
if byOutsideTap {
sendEvent(false)
@@ -2328,14 +2377,21 @@ public final class WebAppController: ViewController, AttachmentContainable {
alertText = self.presentationData.strings.WebApp_AlertBiometryAccessText(botPeer.compactDisplayTitle).string
}
}
controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: alertTitle, text: alertText, actions: [
TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_No, action: {
updateAccessGranted(false)
}),
TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Yes, action: {
updateAccessGranted(true)
})
], parseMarkdown: false), in: .window(.root))
let alertController = AlertScreen(
context: self.context,
title: alertTitle,
text: alertText,
actions: [
.init(title: self.presentationData.strings.Common_No, action: {
updateAccessGranted(false)
}),
.init(title: self.presentationData.strings.Common_Yes, type: .default, action: {
updateAccessGranted(true)
})
]
)
controller.present(alertController, in: .window(.root))
})
}
@@ -2774,17 +2830,23 @@ public final class WebAppController: ViewController, AttachmentContainable {
}
let text: String = self.presentationData.strings.WebApp_Download_Text(controller.botName, fileName, fileSizeString).string
let alertController = standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: title, text: text, actions: [
TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: { [weak self] in
let data: JSON = [
"status": "cancelled"
]
self?.webView?.sendEvent(name: "file_download_requested", data: data.string)
}),
TextAlertAction(type: .defaultAction, title: self.presentationData.strings.WebApp_Download_Download, action: { [weak self] in
self?.startDownload(url: url, fileName: fileName, fileSize: fileSize, isMedia: isMedia)
})
], parseMarkdown: true)
let alertController = AlertScreen(
context: self.context,
title: title,
text: text,
actions: [
.init(title: self.presentationData.strings.Common_Cancel, action: { [weak self] in
let data: JSON = [
"status": "cancelled"
]
self?.webView?.sendEvent(name: "file_download_requested", data: data.string)
}),
.init(title: self.presentationData.strings.WebApp_Download_Download, type: .default, action: { [weak self] in
self?.startDownload(url: url, fileName: fileName, fileSize: fileSize, isMedia: isMedia)
})
]
)
alertController.dismissed = { [weak self] byOutsideTap in
let data: JSON = [
"status": "cancelled"
@@ -2962,7 +3024,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
accountPeer: accountPeer,
botName: controller.botName,
icons: iconStatusEmoji,
completion: { [weak self] result in
completion: { [weak self] result, byOutsideTap in
guard let self, let controller = self.controller else {
return
}
@@ -3017,17 +3079,13 @@ public final class WebAppController: ViewController, AttachmentContainable {
self.webView?.sendEvent(name: "emoji_status_access_requested", data: data.string)
}
let _ = updateWebAppPermissionsStateInteractively(context: context, peerId: botId) { current in
return WebAppPermissionsState(location: current?.location, emojiStatus: WebAppPermissionsState.EmojiStatus(isRequested: true))
}.startStandalone()
if !byOutsideTap {
let _ = updateWebAppPermissionsStateInteractively(context: context, peerId: botId) { current in
return WebAppPermissionsState(location: current?.location, emojiStatus: WebAppPermissionsState.EmojiStatus(isRequested: true))
}.startStandalone()
}
}
)
alertController.dismissed = { [weak self] byOutsideTap in
let data: JSON = [
"status": "cancelled"
]
self?.webView?.sendEvent(name: "emoji_status_access_requested", data: data.string)
}
controller.present(alertController, in: .window(.root))
})
}
@@ -3510,7 +3568,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
private func updateNavigationButtons() {
if case .attachMenu = self.source {
let barButtonSize = CGSize(width: 40.0, height: 40.0)
let barButtonSize = CGSize(width: 44.0, height: 44.0)
let closeComponent: AnyComponentWithIdentity<Empty> = AnyComponentWithIdentity(
id: "close",
component: AnyComponent(GlassBarButtonComponent(
@@ -3521,7 +3579,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
component: AnyComponentWithIdentity(id: self.controllerNode.hasBackButton ? "back" : "close", component: AnyComponent(
BundleIconComponent(
name: self.controllerNode.hasBackButton ? "Navigation/Back" : "Navigation/Close",
tintColor: self.presentationData.theme.rootController.navigationBar.glassBarButtonForegroundColor
tintColor: self.presentationData.theme.chat.inputPanel.panelControlColor
)
)),
action: { [weak self] _ in
@@ -3542,7 +3600,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
content: LottieComponent.AppBundleContent(
name: "anim_morewide"
),
color: self.presentationData.theme.rootController.navigationBar.glassBarButtonForegroundColor,
color: self.presentationData.theme.chat.inputPanel.panelControlColor,
size: CGSize(width: 34.0, height: 34.0),
playOnce: self.moreButtonPlayOnce
)
@@ -3621,6 +3679,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
if let backgroundColor = self.controllerNode.headerColor, let textColor = self.controllerNode.headerPrimaryTextColor {
navigationBarPresentationData = NavigationBarPresentationData(
theme: NavigationBarTheme(
overallDarkAppearance: false,
buttonColor: textColor,
disabledButtonColor: textColor,
primaryTextColor: textColor,
@@ -3639,7 +3698,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
strings: NavigationBarStrings(back: "", close: "")
)
}
self.navigationBar?.updatePresentationData(navigationBarPresentationData)
self.navigationBar?.updatePresentationData(navigationBarPresentationData, transition: .immediate)
}
@objc fileprivate func cancelPressed() {
@@ -3857,13 +3916,22 @@ public final class WebAppController: ViewController, AttachmentContainable {
private func removeAttachBot() {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
self.present(textAlertController(context: context, title: presentationData.strings.WebApp_RemoveConfirmationTitle, text: presentationData.strings.WebApp_RemoveAllConfirmationText(self.botName).string, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { [weak self] in
guard let self else {
return
}
let _ = self.context.engine.messages.removeBotFromAttachMenu(botId: self.botId).start()
self.dismiss()
})], parseMarkdown: true), in: .window(.root))
let alertController = AlertScreen(
context: self.context,
title: presentationData.strings.WebApp_RemoveConfirmationTitle,
text: presentationData.strings.WebApp_RemoveAllConfirmationText(self.botName).string,
actions: [
.init(title: presentationData.strings.Common_Cancel),
.init(title: presentationData.strings.Common_OK, type: .default, action: { [weak self] in
guard let self else {
return
}
let _ = self.context.engine.messages.removeBotFromAttachMenu(botId: self.botId).start()
self.dismiss()
})
]
)
self.present(alertController, in: .window(.root))
}
override public func loadDisplayNode() {
@@ -4,6 +4,7 @@ import SwiftSignalKit
import AsyncDisplayKit
import Display
import Postbox
import ComponentFlow
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
@@ -13,352 +14,236 @@ import AvatarNode
import EmojiTextAttachmentView
import TextFormat
import Markdown
private final class IconsNode: ASDisplayNode {
private let context: AccountContext
private var animationLayer: InlineStickerItemLayer?
private var files: [TelegramMediaFile.Accessor]
private var currentIndex = 0
private var switchingToNext = false
private var timer: SwiftSignalKit.Timer?
private var currentParams: (size: CGSize, theme: PresentationTheme)?
init(context: AccountContext, files: [TelegramMediaFile.Accessor]) {
self.context = context
self.files = files
super.init()
}
deinit {
self.timer?.invalidate()
}
func updateLayout(size: CGSize, theme: PresentationTheme) {
self.currentParams = (size, theme)
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
if let (size, theme) = self.currentParams {
self.updateLayout(size: size, theme: theme)
}
}, 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) % self.files.count
disappearingAnimationLayer = self.animationLayer
self.switchingToNext = false
}
let file = self.files[self.currentIndex]._parse()
let emoji = ChatTextInputTextCustomEmojiAttribute(
interactivelySelectedFromPackId: nil,
fileId: file.fileId.id,
file: file
)
animationLayer = InlineStickerItemLayer(
context: .account(self.context),
userLocation: .other,
attemptSynchronousLoad: false,
emoji: emoji,
file: file,
cache: self.context.animationCache,
renderer: self.context.animationRenderer,
unique: true,
placeholderColor: theme.list.mediaPlaceholderColor,
pointSize: CGSize(width: 20.0, height: 20.0),
loopCount: 1
)
animationLayer.isVisibleForAnimations = true
animationLayer.dynamicColor = theme.actionSheet.controlAccentColor
self.view.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: .zero, size: CGSize(width: 20.0, height: 20.0))
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)
}
}
}
private final class WebAppEmojiStatusAlertContentNode: AlertContentNode {
private let strings: PresentationStrings
private let presentationTheme: PresentationTheme
private let botName: String
private let textNode: ASTextNode
private let iconBackgroundNode: ASImageNode
private let iconAvatarNode: AvatarNode
private let iconNameNode: ASTextNode
private let iconAnimationNode: IconsNode
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private var validLayout: CGSize?
override var dismissOnOutsideTap: Bool {
return self.isUserInteractionEnabled
}
init(
context: AccountContext,
theme: AlertControllerTheme,
ptheme: PresentationTheme,
strings: PresentationStrings,
accountPeer: EnginePeer,
botName: String,
icons: [TelegramMediaFile.Accessor],
actions: [TextAlertAction]
) {
self.strings = strings
self.presentationTheme = ptheme
self.botName = botName
self.textNode = ASTextNode()
self.textNode.maximumNumberOfLines = 0
self.iconBackgroundNode = ASImageNode()
self.iconBackgroundNode.displaysAsynchronously = false
self.iconBackgroundNode.image = generateStretchableFilledCircleImage(radius: 16.0, color: theme.separatorColor)
self.iconAvatarNode = AvatarNode(font: avatarPlaceholderFont(size: 14.0))
self.iconAvatarNode.setPeer(context: context, theme: ptheme, peer: accountPeer)
self.iconNameNode = ASTextNode()
self.iconNameNode.attributedText = NSAttributedString(string: accountPeer.compactDisplayTitle, font: Font.medium(15.0), textColor: theme.primaryColor)
self.iconAnimationNode = IconsNode(context: context, files: icons)
self.actionNodesSeparator = ASDisplayNode()
self.actionNodesSeparator.isLayerBacked = true
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
return TextAlertContentActionNode(theme: theme, action: action)
}
var actionVerticalSeparators: [ASDisplayNode] = []
if actions.count > 1 {
for _ in 0 ..< actions.count - 1 {
let separatorNode = ASDisplayNode()
separatorNode.isLayerBacked = true
actionVerticalSeparators.append(separatorNode)
}
}
self.actionVerticalSeparators = actionVerticalSeparators
super.init()
self.addSubnode(self.textNode)
self.addSubnode(self.iconBackgroundNode)
self.addSubnode(self.iconAvatarNode)
self.addSubnode(self.iconNameNode)
self.addSubnode(self.iconAnimationNode)
self.addSubnode(self.actionNodesSeparator)
for actionNode in self.actionNodes {
self.addSubnode(actionNode)
}
for separatorNode in self.actionVerticalSeparators {
self.addSubnode(separatorNode)
}
self.updateTheme(theme)
}
override func updateTheme(_ theme: AlertControllerTheme) {
let string = self.strings.WebApp_EmojiPermission_Text(self.botName, self.botName).string
let attributedText = parseMarkdownIntoAttributedString(string, attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor),
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.primaryColor),
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor),
linkAttribute: { url in
return ("URL", url)
}
), textAlignment: .center)
self.textNode.attributedText = attributedText
self.actionNodesSeparator.backgroundColor = theme.separatorColor
for actionNode in self.actionNodes {
actionNode.updateTheme(theme)
}
for separatorNode in self.actionVerticalSeparators {
separatorNode.backgroundColor = theme.separatorColor
}
if let size = self.validLayout {
_ = self.updateLayout(size: size, transition: .immediate)
}
}
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
var size = size
size.width = min(size.width , 270.0)
self.validLayout = size
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
let iconSpacing: CGFloat = 6.0
let iconSize = CGSize(width: 32.0, height: 32.0)
let nameSize = self.iconNameNode.measure(size)
let totalIconWidth = iconSize.width + iconSpacing + nameSize.width + 4.0 + iconSize.width
let iconBackgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - totalIconWidth) / 2.0), y: origin.y), size: CGSize(width: totalIconWidth, height: iconSize.height))
transition.updateFrame(node: self.iconBackgroundNode, frame: iconBackgroundFrame)
transition.updateFrame(node: self.iconAvatarNode, frame: CGRect(origin: iconBackgroundFrame.origin, size: iconSize).insetBy(dx: 1.0, dy: 1.0))
transition.updateFrame(node: self.iconNameNode, frame: CGRect(origin: CGPoint(x: iconBackgroundFrame.minX + iconSize.width + iconSpacing, y: iconBackgroundFrame.minY + floorToScreenPixels((iconBackgroundFrame.height - nameSize.height) / 2.0)), size: nameSize))
self.iconAnimationNode.updateLayout(size: CGSize(width: 20.0, height: 20.0), theme: self.presentationTheme)
self.iconAnimationNode.frame = CGRect(origin: CGPoint(x: iconBackgroundFrame.maxX - iconSize.width - 3.0, y: iconBackgroundFrame.minY), size: iconSize).insetBy(dx: 6.0, dy: 6.0)
origin.y += iconSize.height + 16.0
let textSize = self.textNode.measure(CGSize(width: size.width - 32.0, height: size.height))
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
let actionButtonHeight: CGFloat = 44.0
var minActionsWidth: CGFloat = 0.0
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
let actionTitleInsets: CGFloat = 8.0
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
for actionNode in self.actionNodes {
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
effectiveActionLayout = .vertical
}
switch effectiveActionLayout {
case .horizontal:
minActionsWidth += actionTitleSize.width + actionTitleInsets
case .vertical:
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
}
}
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
var contentWidth = minActionsWidth
contentWidth = max(contentWidth, 234.0)
var actionsHeight: CGFloat = 0.0
switch effectiveActionLayout {
case .horizontal:
actionsHeight = actionButtonHeight
case .vertical:
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
}
let resultWidth = contentWidth + insets.left + insets.right
let resultSize = CGSize(width: resultWidth, height: iconSize.height + textSize.height + actionsHeight + 16.0 + insets.top + insets.bottom)
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
var actionOffset: CGFloat = 0.0
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
var separatorIndex = -1
var nodeIndex = 0
for actionNode in self.actionNodes {
if separatorIndex >= 0 {
let separatorNode = self.actionVerticalSeparators[separatorIndex]
switch effectiveActionLayout {
case .horizontal:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
case .vertical:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
}
}
separatorIndex += 1
let currentActionWidth: CGFloat
switch effectiveActionLayout {
case .horizontal:
if nodeIndex == self.actionNodes.count - 1 {
currentActionWidth = resultSize.width - actionOffset
} else {
currentActionWidth = actionWidth
}
case .vertical:
currentActionWidth = resultSize.width
}
let actionNodeFrame: CGRect
switch effectiveActionLayout {
case .horizontal:
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += currentActionWidth
case .vertical:
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += actionButtonHeight
}
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
nodeIndex += 1
}
return resultSize
}
}
import AlertComponent
import AvatarComponent
import MultilineTextComponent
func webAppEmojiStatusAlertController(
context: AccountContext,
accountPeer: EnginePeer,
botName: String,
icons: [TelegramMediaFile.Accessor],
completion: @escaping (Bool) -> Void
) -> AlertController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let theme = presentationData.theme
let strings = presentationData.strings
var dismissImpl: ((Bool) -> Void)?
var contentNode: WebAppEmojiStatusAlertContentNode?
let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: strings.WebApp_EmojiPermission_Decline, action: {
dismissImpl?(true)
completion: @escaping (Bool, Bool) -> Void
) -> ViewController {
let strings = context.sharedContext.currentPresentationData.with { $0 }.strings
completion(false)
}), TextAlertAction(type: .defaultAction, title: strings.WebApp_EmojiPermission_Allow, action: {
dismissImpl?(true)
completion(true)
})]
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))
)
))
contentNode = WebAppEmojiStatusAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, accountPeer: accountPeer, botName: botName, icons: icons, actions: actions)
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!)
dismissImpl = { [weak controller] animated in
if animated {
controller?.dismissAnimated()
} else {
controller?.dismiss()
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 controller
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)
}
}
@@ -10,365 +10,13 @@ import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import AppBundle
import AvatarNode
import CheckNode
import Markdown
import EmojiStatusComponent
private let textFont = Font.regular(13.0)
private let boldTextFont = Font.semibold(13.0)
private func formattedText(_ text: String, color: UIColor, linkColor: UIColor, textAlignment: NSTextAlignment = .natural) -> NSAttributedString {
return parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: color), bold: MarkdownAttributeSet(font: boldTextFont, textColor: color), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { _ in return nil}), textAlignment: textAlignment)
}
private final class WebAppLaunchConfirmationAlertContentNode: AlertContentNode {
private let context: AccountContext
private let presentationTheme: PresentationTheme
private let strings: PresentationStrings
private let peer: EnginePeer
private let title: String
private let text: String
private let showMore: Bool
private let titleNode: ImmediateTextNode
private var titleCredibilityIconView: ComponentHostView<Empty>?
private let textNode: ASTextNode
private let avatarNode: AvatarNode
private let moreButton: HighlightableButtonNode
private let arrowNode: ASImageNode
private let allowWriteCheckNode: InteractiveCheckNode
private let allowWriteLabelNode: ASTextNode
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private var validLayout: CGSize?
private let morePressed: () -> Void
private let termsPressed: () -> Void
override var dismissOnOutsideTap: Bool {
return self.isUserInteractionEnabled
}
var allowWriteAccess: Bool = true {
didSet {
self.allowWriteCheckNode.setSelected(self.allowWriteAccess, animated: true)
}
}
init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, peer: EnginePeer, title: String, text: String, showMore: Bool, requestWriteAccess: Bool, actions: [TextAlertAction], morePressed: @escaping () -> Void, termsPressed: @escaping () -> Void) {
self.context = context
self.strings = strings
self.presentationTheme = ptheme
self.peer = peer
self.title = title
self.text = text
self.showMore = showMore
self.morePressed = morePressed
self.termsPressed = termsPressed
self.titleNode = ImmediateTextNode()
self.titleNode.displaysAsynchronously = false
self.titleNode.maximumNumberOfLines = 1
self.titleNode.textAlignment = .center
self.textNode = ASTextNode()
self.textNode.displaysAsynchronously = false
self.textNode.maximumNumberOfLines = 0
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0))
self.moreButton = HighlightableButtonNode()
self.arrowNode = ASImageNode()
self.arrowNode.displaysAsynchronously = false
self.arrowNode.displayWithoutProcessing = true
self.arrowNode.isHidden = !showMore
self.arrowNode.contentMode = .scaleAspectFit
self.allowWriteCheckNode = InteractiveCheckNode(theme: CheckNodeTheme(backgroundColor: theme.accentColor, strokeColor: theme.contrastColor, borderColor: theme.controlBorderColor, overlayBorder: false, hasInset: false, hasShadow: false))
self.allowWriteCheckNode.setSelected(true, animated: false)
self.allowWriteLabelNode = ASTextNode()
self.allowWriteLabelNode.maximumNumberOfLines = 4
self.allowWriteLabelNode.isUserInteractionEnabled = true
self.actionNodesSeparator = ASDisplayNode()
self.actionNodesSeparator.isLayerBacked = true
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
return TextAlertContentActionNode(theme: theme, action: action)
}
var actionVerticalSeparators: [ASDisplayNode] = []
if actions.count > 1 {
for _ in 0 ..< actions.count - 1 {
let separatorNode = ASDisplayNode()
separatorNode.isLayerBacked = true
actionVerticalSeparators.append(separatorNode)
}
}
self.actionVerticalSeparators = actionVerticalSeparators
super.init()
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.avatarNode)
self.addSubnode(self.moreButton)
self.moreButton.addSubnode(self.arrowNode)
if requestWriteAccess {
self.addSubnode(self.allowWriteCheckNode)
self.addSubnode(self.allowWriteLabelNode)
}
self.addSubnode(self.actionNodesSeparator)
for actionNode in self.actionNodes {
self.addSubnode(actionNode)
}
for separatorNode in self.actionVerticalSeparators {
self.addSubnode(separatorNode)
}
self.allowWriteCheckNode.valueChanged = { [weak self] value in
if let strongSelf = self {
strongSelf.allowWriteAccess = !strongSelf.allowWriteAccess
}
}
self.updateTheme(theme)
self.avatarNode.setPeer(context: context, theme: ptheme, peer: peer)
self.moreButton.addTarget(self, action: #selector(self.moreButtonPressed), forControlEvents: .touchUpInside)
}
override func didLoad() {
super.didLoad()
self.allowWriteLabelNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.allowWriteTap(_:))))
self.textNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.termsTap(_:))))
}
@objc private func allowWriteTap(_ gestureRecognizer: UITapGestureRecognizer) {
if self.allowWriteCheckNode.isUserInteractionEnabled {
self.allowWriteAccess = !self.allowWriteAccess
}
}
@objc private func termsTap(_ gestureRecognizer: UITapGestureRecognizer) {
self.termsPressed()
}
@objc private func moreButtonPressed() {
self.morePressed()
}
override func updateTheme(_ theme: AlertControllerTheme) {
self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.semibold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
self.textNode.attributedText = formattedText(self.text, color: theme.primaryColor, linkColor: theme.accentColor, textAlignment: .center)
self.moreButton.setAttributedTitle(NSAttributedString(string: self.strings.WebApp_LaunchMoreInfo, font: Font.regular(13.0), textColor: theme.accentColor), for: .normal)
self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Peer Info/AlertArrow"), color: theme.accentColor)
self.allowWriteLabelNode.attributedText = formattedText(strings.WebApp_AddToAttachmentAllowMessages(self.peer.compactDisplayTitle).string, color: theme.primaryColor, linkColor: theme.primaryColor)
self.actionNodesSeparator.backgroundColor = theme.separatorColor
for actionNode in self.actionNodes {
actionNode.updateTheme(theme)
}
for separatorNode in self.actionVerticalSeparators {
separatorNode.backgroundColor = theme.separatorColor
}
if let size = self.validLayout {
_ = self.updateLayout(size: size, transition: .immediate)
}
}
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
var size = size
size.width = min(size.width, 270.0)
self.validLayout = size
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
let avatarSize = CGSize(width: 60.0, height: 60.0)
self.avatarNode.updateSize(size: avatarSize)
let avatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0), y: origin.y), size: avatarSize)
transition.updateFrame(node: self.avatarNode, frame: avatarFrame)
origin.y += avatarSize.height + 17.0
if let arrowImage = self.arrowNode.image {
let arrowFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - arrowImage.size.width) / 2.0), y: origin.y + floorToScreenPixels((avatarSize.height - arrowImage.size.height) / 2.0)), size: arrowImage.size)
transition.updateFrame(node: self.arrowNode, frame: arrowFrame)
}
let titleSize = self.titleNode.updateLayout(CGSize(width: size.width - 32.0, height: size.height))
var totalWidth = titleSize.width
var statusContent: EmojiStatusComponent.Content?
if self.peer.isScam {
statusContent = .text(color: self.presentationTheme.list.itemDestructiveColor, string: self.strings.Message_ScamAccount.uppercased())
} else if self.peer.isFake {
statusContent = .text(color: self.presentationTheme.list.itemDestructiveColor, string: self.strings.Message_FakeAccount.uppercased())
} else if self.peer.isVerified {
statusContent = .verified(fillColor: self.presentationTheme.list.itemCheckColors.fillColor, foregroundColor: self.presentationTheme.list.itemCheckColors.foregroundColor, sizeType: .large)
}
if let statusContent {
let titleCredibilityIconTransition: ComponentTransition = .immediate
let titleCredibilityIconView: ComponentHostView<Empty>
if let current = self.titleCredibilityIconView {
titleCredibilityIconView = current
} else {
titleCredibilityIconView = ComponentHostView<Empty>()
self.titleCredibilityIconView = titleCredibilityIconView
self.view.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: origin.y), size: titleIconSize))
}
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - totalWidth) / 2.0), y: origin.y), size: titleSize))
origin.y += titleSize.height + 6.0
var entriesHeight: CGFloat = 0.0
if self.showMore {
let moreButtonSize = self.moreButton.measure(CGSize(width: size.width - 32.0, height: size.height))
transition.updateFrame(node: self.moreButton, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - moreButtonSize.width) / 2.0) - 5.0, y: origin.y), size: moreButtonSize))
transition.updateFrame(node: self.arrowNode, frame: CGRect(origin: CGPoint(x: moreButtonSize.width + 3.0, y: 4.0), size: CGSize(width: 9.0, height: 9.0)))
origin.y += moreButtonSize.height + 22.0
entriesHeight += 37.0
}
let textSize = self.textNode.measure(CGSize(width: size.width - 32.0, height: size.height))
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
origin.y += textSize.height
if self.allowWriteLabelNode.supernode != nil {
origin.y += 16.0
entriesHeight += 16.0
let checkSize = CGSize(width: 22.0, height: 22.0)
let condensedSize = CGSize(width: size.width - 76.0, height: size.height)
let allowWriteSize = self.allowWriteLabelNode.measure(condensedSize)
transition.updateFrame(node: self.allowWriteLabelNode, frame: CGRect(origin: CGPoint(x: 46.0, y: origin.y), size: allowWriteSize))
transition.updateFrame(node: self.allowWriteCheckNode, frame: CGRect(origin: CGPoint(x: 12.0, y: origin.y - 2.0), size: checkSize))
origin.y += allowWriteSize.height
entriesHeight += allowWriteSize.height
}
let actionButtonHeight: CGFloat = 44.0
var minActionsWidth: CGFloat = 0.0
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
let actionTitleInsets: CGFloat = 8.0
var effectiveActionLayout = TextAlertContentActionLayout.vertical
for actionNode in self.actionNodes {
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
effectiveActionLayout = .vertical
}
switch effectiveActionLayout {
case .horizontal:
minActionsWidth += actionTitleSize.width + actionTitleInsets
case .vertical:
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
}
}
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
let contentWidth = max(size.width, minActionsWidth)
var actionsHeight: CGFloat = 0.0
switch effectiveActionLayout {
case .horizontal:
actionsHeight = actionButtonHeight
case .vertical:
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
}
let resultSize = CGSize(width: contentWidth, height: avatarSize.height + titleSize.height + textSize.height + entriesHeight + actionsHeight + 25.0 + insets.top + insets.bottom)
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
var actionOffset: CGFloat = 0.0
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
var separatorIndex = -1
var nodeIndex = 0
for actionNode in self.actionNodes {
if separatorIndex >= 0 {
let separatorNode = self.actionVerticalSeparators[separatorIndex]
switch effectiveActionLayout {
case .horizontal:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
case .vertical:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
}
}
separatorIndex += 1
let currentActionWidth: CGFloat
switch effectiveActionLayout {
case .horizontal:
if nodeIndex == self.actionNodes.count - 1 {
currentActionWidth = resultSize.width - actionOffset
} else {
currentActionWidth = actionWidth
}
case .vertical:
currentActionWidth = resultSize.width
}
let actionNodeFrame: CGRect
switch effectiveActionLayout {
case .horizontal:
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += currentActionWidth
case .vertical:
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += actionButtonHeight
}
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
nodeIndex += 1
}
return resultSize
}
}
import AlertComponent
import AlertCheckComponent
import AvatarComponent
import MultilineTextComponent
import BundleIconComponent
import PlainButtonComponent
public func webAppLaunchConfirmationController(
context: AccountContext,
@@ -378,50 +26,209 @@ public func webAppLaunchConfirmationController(
completion: @escaping (Bool) -> Void,
showMore: (() -> Void)?,
openTerms: @escaping () -> Void
) -> AlertController {
let theme = defaultDarkColorPresentationTheme
let presentationData: PresentationData
if let updatedPresentationData {
presentationData = updatedPresentationData.initial
} else {
presentationData = context.sharedContext.currentPresentationData.with { $0 }
}
) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
let checkState = AlertCheckComponent.ExternalState()
var dismissImpl: ((Bool) -> Void)?
var getContentNodeImpl: (() -> WebAppLaunchConfirmationAlertContentNode?)?
let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: presentationData.strings.WebApp_LaunchOpenApp, action: {
if requestWriteAccess, let allowWriteAccess = getContentNodeImpl?()?.allowWriteAccess {
completion(allowWriteAccess)
} else {
completion(false)
}
dismissImpl?(true)
}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?(true)
})]
let title = peer.compactDisplayTitle
let text = presentationData.strings.WebApp_LaunchTermsConfirmation
let contentNode = WebAppLaunchConfirmationAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, peer: peer, title: title, text: text, showMore: showMore != nil, requestWriteAccess: requestWriteAccess, actions: actions, morePressed: {
dismissImpl?(true)
showMore?()
}, termsPressed: {
dismissImpl?(true)
openTerms()
})
getContentNodeImpl = { [weak contentNode] in
return contentNode
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 controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
dismissImpl = { [weak controller] animated in
if animated {
controller?.dismissAnimated()
} else {
controller?.dismiss()
}
}
return controller
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)
}
}
@@ -10,17 +10,18 @@ import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import AppBundle
import AvatarNode
import Markdown
import CheckNode
import ComponentFlow
import AlertComponent
import AvatarComponent
import AlertTransferHeaderComponent
private func generateBoostIcon(theme: PresentationTheme) -> UIImage? {
let size = CGSize(width: 28.0, height: 28.0)
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.insetBy(dx: 1.0, dy: 1.0))
context.addEllipse(in: bounds)
context.clip()
var locations: [CGFloat] = [1.0, 0.0]
@@ -32,261 +33,61 @@ private func generateBoostIcon(theme: PresentationTheme) -> UIImage? {
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: 6.0, dy: 6.0))
context.draw(cgImage, in: bounds.insetBy(dx: 4.0, dy: 4.0))
}
context.resetClip()
let lineWidth = 2.0 - UIScreenPixel
context.setLineWidth(lineWidth)
context.setStrokeColor(theme.actionSheet.opaqueItemBackgroundColor.cgColor)
context.strokeEllipse(in: bounds.insetBy(dx: lineWidth / 2.0 + UIScreenPixel, dy: lineWidth / 2.0 + UIScreenPixel))
}, opaque: false)
}
private final class WebAppLocationAlertContentNode: AlertContentNode {
private let strings: PresentationStrings
private let text: String
private let textNode: ASTextNode
private let avatarNode: AvatarNode
private let arrowNode: ASImageNode
private let secondAvatarNode: AvatarNode
private let iconNode: ASImageNode
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private var validLayout: CGSize?
override var dismissOnOutsideTap: Bool {
return self.isUserInteractionEnabled
}
init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, accountPeer: EnginePeer, botPeer: EnginePeer, text: String, actions: [TextAlertAction]) {
self.strings = strings
self.text = text
self.textNode = ASTextNode()
self.textNode.maximumNumberOfLines = 0
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0))
self.arrowNode = ASImageNode()
self.arrowNode.displaysAsynchronously = false
self.arrowNode.displayWithoutProcessing = true
self.secondAvatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0))
self.iconNode = ASImageNode()
self.iconNode.displaysAsynchronously = false
self.iconNode.image = generateBoostIcon(theme: ptheme)
self.actionNodesSeparator = ASDisplayNode()
self.actionNodesSeparator.isLayerBacked = true
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
return TextAlertContentActionNode(theme: theme, action: action)
}
var actionVerticalSeparators: [ASDisplayNode] = []
if actions.count > 1 {
for _ in 0 ..< actions.count - 1 {
let separatorNode = ASDisplayNode()
separatorNode.isLayerBacked = true
actionVerticalSeparators.append(separatorNode)
}
}
self.actionVerticalSeparators = actionVerticalSeparators
super.init()
self.addSubnode(self.textNode)
self.addSubnode(self.avatarNode)
self.addSubnode(self.arrowNode)
self.addSubnode(self.secondAvatarNode)
self.addSubnode(self.iconNode)
self.addSubnode(self.actionNodesSeparator)
for actionNode in self.actionNodes {
self.addSubnode(actionNode)
}
for separatorNode in self.actionVerticalSeparators {
self.addSubnode(separatorNode)
}
self.updateTheme(theme)
self.avatarNode.setPeer(context: context, theme: ptheme, peer: accountPeer)
self.secondAvatarNode.setPeer(context: context, theme: ptheme, peer: botPeer)
}
override func updateTheme(_ theme: AlertControllerTheme) {
self.textNode.attributedText = parseMarkdownIntoAttributedString(self.text, attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor),
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.primaryColor),
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor),
linkAttribute: { url in
return ("URL", url)
}
), textAlignment: .center)
self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Peer Info/AlertArrow"), color: theme.secondaryColor)
self.actionNodesSeparator.backgroundColor = theme.separatorColor
for actionNode in self.actionNodes {
actionNode.updateTheme(theme)
}
for separatorNode in self.actionVerticalSeparators {
separatorNode.backgroundColor = theme.separatorColor
}
if let size = self.validLayout {
_ = self.updateLayout(size: size, transition: .immediate)
}
}
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
var size = size
size.width = min(size.width, 270.0)
self.validLayout = size
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
let avatarSize = CGSize(width: 60.0, height: 60.0)
self.avatarNode.updateSize(size: avatarSize)
let avatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) - 44.0, y: origin.y), size: avatarSize)
transition.updateFrame(node: self.avatarNode, frame: avatarFrame)
if let arrowImage = self.arrowNode.image {
let arrowFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - arrowImage.size.width) / 2.0), y: origin.y + floorToScreenPixels((avatarSize.height - arrowImage.size.height) / 2.0)), size: arrowImage.size)
transition.updateFrame(node: self.arrowNode, frame: arrowFrame)
}
let secondAvatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) + 44.0, y: origin.y), size: avatarSize)
transition.updateFrame(node: self.secondAvatarNode, frame: secondAvatarFrame)
if let icon = self.iconNode.image {
let iconFrame = CGRect(origin: CGPoint(x: avatarFrame.maxX + 4.0 - icon.size.width, y: avatarFrame.maxY + 4.0 - icon.size.height), size: icon.size)
transition.updateFrame(node: self.iconNode, frame: iconFrame)
}
origin.y += avatarSize.height + 10.0
let textSize = self.textNode.measure(CGSize(width: size.width - 32.0, height: size.height))
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
origin.y += textSize.height + 10.0
let actionButtonHeight: CGFloat = 44.0
var minActionsWidth: CGFloat = 0.0
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
let actionTitleInsets: CGFloat = 8.0
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
for actionNode in self.actionNodes {
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
effectiveActionLayout = .vertical
}
switch effectiveActionLayout {
case .horizontal:
minActionsWidth += actionTitleSize.width + actionTitleInsets
case .vertical:
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
}
}
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
let contentWidth = max(size.width, minActionsWidth)
var actionsHeight: CGFloat = 0.0
switch effectiveActionLayout {
case .horizontal:
actionsHeight = actionButtonHeight
case .vertical:
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
}
let resultSize = CGSize(width: contentWidth, height: avatarSize.height + textSize.height + actionsHeight + 16.0 + insets.top + insets.bottom)
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
var actionOffset: CGFloat = 0.0
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
var separatorIndex = -1
var nodeIndex = 0
for actionNode in self.actionNodes {
if separatorIndex >= 0 {
let separatorNode = self.actionVerticalSeparators[separatorIndex]
switch effectiveActionLayout {
case .horizontal:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
case .vertical:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
}
}
separatorIndex += 1
let currentActionWidth: CGFloat
switch effectiveActionLayout {
case .horizontal:
if nodeIndex == self.actionNodes.count - 1 {
currentActionWidth = resultSize.width - actionOffset
} else {
currentActionWidth = actionWidth
}
case .vertical:
currentActionWidth = resultSize.width
}
let actionNodeFrame: CGRect
switch effectiveActionLayout {
case .horizontal:
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += currentActionWidth
case .vertical:
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += actionButtonHeight
}
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
nodeIndex += 1
}
return resultSize
}
}
func webAppLocationAlertController(context: AccountContext, accountPeer: EnginePeer, botPeer: EnginePeer, completion: @escaping (Bool) -> Void) -> AlertController {
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 text = strings.WebApp_LocationPermission_Text(botPeer.compactDisplayTitle, botPeer.compactDisplayTitle).string
var dismissImpl: ((Bool) -> Void)?
var contentNode: WebAppLocationAlertContentNode?
let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: strings.WebApp_LocationPermission_Decline, action: {
dismissImpl?(true)
completion(false)
}), TextAlertAction(type: .defaultAction, title: strings.WebApp_LocationPermission_Allow, action: {
dismissImpl?(true)
completion(true)
})]
contentNode = WebAppLocationAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: strings, accountPeer: accountPeer, botPeer: botPeer, text: text, actions: actions)
let locationIcon = generateLocationIcon()
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!)
dismissImpl = { [weak controller] animated in
if animated {
controller?.dismissAnimated()
} else {
controller?.dismiss()
}
}
return controller
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
}
@@ -160,7 +160,7 @@ final class PeerNameColorChatPreviewItemNode: ListViewItemNode {
self.containerNode = ASDisplayNode()
self.containerNode.subnodeTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
super.init(layerBacked: false, dynamicBounce: false)
super.init(layerBacked: false)
self.clipsToBounds = true
self.isUserInteractionEnabled = false
@@ -23,6 +23,7 @@ import ListItemComponentAdaptor
import TelegramStringFormatting
import UndoUI
import ChatMessagePaymentAlertController
import GlassBarButtonComponent
private final class SheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@@ -55,7 +56,7 @@ private final class SheetContent: CombinedComponent {
}
static var body: Body {
let closeButton = Child(Button.self)
let closeButton = Child(GlassBarButtonComponent.self)
let title = Child(Text.self)
let amountSection = Child(ListSectionComponent.self)
let button = Child(ButtonComponent.self)
@@ -71,22 +72,31 @@ private final class SheetContent: CombinedComponent {
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let sideInset: CGFloat = 16.0
var contentSize = CGSize(width: context.availableSize.width, height: 18.0)
var contentSize = CGSize(width: context.availableSize.width, height: 36.0)
let constrainedTitleWidth = context.availableSize.width - 16.0 * 2.0
let closeButton = closeButton.update(
component: Button(
content: AnyComponent(Text(text: environment.strings.Common_Cancel, font: Font.regular(17.0), color: theme.actionSheet.controlAccentColor)),
action: {
component: GlassBarButtonComponent(
size: CGSize(width: 40.0, height: 40.0),
backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor,
isDark: theme.overallDarkAppearance,
state: .generic,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: theme.rootController.navigationBar.glassBarButtonForegroundColor
)
)),
action: { _ in
component.dismiss()
}
),
availableSize: CGSize(width: 120.0, height: 30.0),
availableSize: CGSize(width: 40.0, height: 40.0),
transition: .immediate
)
context.add(closeButton
.position(CGPoint(x: closeButton.size.width / 2.0 + sideInset, y: 28.0))
.position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0))
)
let title = title.update(
@@ -95,7 +105,7 @@ private final class SheetContent: CombinedComponent {
transition: .immediate
)
context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0))
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height))
)
contentSize.height += title.size.height
contentSize.height += 40.0
@@ -246,13 +256,14 @@ private final class SheetContent: CombinedComponent {
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),
cornerRadius: 10.0
),
content: AnyComponentWithIdentity(
id: AnyHashable(0),
@@ -266,7 +277,7 @@ private final class SheetContent: CombinedComponent {
}
}
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50),
availableSize: CGSize(width: context.availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0),
transition: .immediate
)
context.add(button
@@ -275,10 +286,8 @@ private final class SheetContent: CombinedComponent {
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0))
)
contentSize.height += button.size.height
contentSize.height += 15.0
contentSize.height += buttonInsets.bottom
contentSize.height += max(environment.inputHeight, environment.safeInsets.bottom)
return contentSize
}
}
@@ -328,6 +337,7 @@ private final class WebAppMessagePreviewSheetComponent: CombinedComponent {
let controller = environment.controller
let theme = environment.theme.withModalBlocksBackground()
let sheet = sheet.update(
component: SheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(SheetContent(
@@ -344,7 +354,8 @@ private final class WebAppMessagePreviewSheetComponent: CombinedComponent {
})
}
)),
backgroundColor: .color(environment.theme.list.blocksBackgroundColor),
style: .glass,
backgroundColor: .color(theme.list.blocksBackgroundColor),
followContentSizeChanges: false,
clipsContent: true,
isScrollEnabled: false,
@@ -116,7 +116,7 @@ private final class SheetContent: CombinedComponent {
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: theme.rootController.navigationBar.glassBarButtonForegroundColor
tintColor: theme.chat.inputPanel.panelControlColor
)
)),
action: { _ in
@@ -198,6 +198,7 @@ private final class SheetContent: CombinedComponent {
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(
@@ -217,7 +218,7 @@ private final class SheetContent: CombinedComponent {
controller?.dismissAnimated()
}
),
availableSize: CGSize(width: context.availableSize.width - 30.0 * 2.0, height: 52.0),
availableSize: CGSize(width: context.availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0),
transition: .immediate
)
context.add(button
@@ -226,8 +227,7 @@ private final class SheetContent: CombinedComponent {
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0))
)
contentSize.height += button.size.height
contentSize.height += 48.0
contentSize.height += buttonInsets.bottom
return contentSize
}
@@ -9,345 +9,9 @@ import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import AppBundle
import AvatarNode
import CheckNode
import Markdown
import TextFormat
private let textFont = Font.regular(13.0)
private let boldTextFont = Font.semibold(13.0)
private func formattedText(_ text: String, fontSize: CGFloat, color: UIColor, linkColor: UIColor, textAlignment: NSTextAlignment = .natural) -> NSAttributedString {
return parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: Font.regular(fontSize), textColor: color), bold: MarkdownAttributeSet(font: Font.semibold(fontSize), textColor: color), link: MarkdownAttributeSet(font: Font.regular(fontSize), textColor: linkColor), linkAttribute: { _ in return (TelegramTextAttributes.URL, "") }), textAlignment: textAlignment)
}
private final class WebAppTermsAlertContentNode: AlertContentNode, ASGestureRecognizerDelegate {
private let strings: PresentationStrings
private let title: String
private let text: String
private let additionalText: String?
private let titleNode: ImmediateTextNode
private let textNode: ImmediateTextNode
private let additionalTextNode: ImmediateTextNode
private let acceptTermsCheckNode: InteractiveCheckNode
private let acceptTermsLabelNode: ImmediateTextNode
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private var validLayout: CGSize?
override var dismissOnOutsideTap: Bool {
return self.isUserInteractionEnabled
}
var acceptedTerms: Bool = false {
didSet {
self.acceptTermsCheckNode.setSelected(self.acceptedTerms, animated: true)
if let firstAction = self.actionNodes.first {
firstAction.actionEnabled = self.acceptedTerms
}
}
}
var openTerms: () -> Void = {}
init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, title: String, text: String, additionalText: String?, actions: [TextAlertAction]) {
self.strings = strings
self.title = title
self.text = text
self.additionalText = additionalText
self.titleNode = ImmediateTextNode()
self.titleNode.displaysAsynchronously = false
self.titleNode.maximumNumberOfLines = 1
self.titleNode.textAlignment = .center
self.textNode = ImmediateTextNode()
self.textNode.maximumNumberOfLines = 0
self.textNode.displaysAsynchronously = false
self.textNode.lineSpacing = 0.1
self.textNode.textAlignment = .center
self.additionalTextNode = ImmediateTextNode()
self.additionalTextNode.maximumNumberOfLines = 0
self.additionalTextNode.displaysAsynchronously = false
self.additionalTextNode.lineSpacing = 0.1
self.additionalTextNode.textAlignment = .center
self.acceptTermsCheckNode = InteractiveCheckNode(theme: CheckNodeTheme(backgroundColor: theme.accentColor, strokeColor: theme.contrastColor, borderColor: theme.controlBorderColor, overlayBorder: false, hasInset: false, hasShadow: false))
self.acceptTermsLabelNode = ImmediateTextNode()
self.acceptTermsLabelNode.maximumNumberOfLines = 4
self.actionNodesSeparator = ASDisplayNode()
self.actionNodesSeparator.isLayerBacked = true
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
return TextAlertContentActionNode(theme: theme, action: action)
}
var actionVerticalSeparators: [ASDisplayNode] = []
if actions.count > 1 {
for _ in 0 ..< actions.count - 1 {
let separatorNode = ASDisplayNode()
separatorNode.isLayerBacked = true
actionVerticalSeparators.append(separatorNode)
}
}
self.actionVerticalSeparators = actionVerticalSeparators
super.init()
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.additionalTextNode)
self.addSubnode(self.acceptTermsCheckNode)
self.addSubnode(self.acceptTermsLabelNode)
self.addSubnode(self.actionNodesSeparator)
for actionNode in self.actionNodes {
self.addSubnode(actionNode)
}
for separatorNode in self.actionVerticalSeparators {
self.addSubnode(separatorNode)
}
self.acceptTermsCheckNode.valueChanged = { [weak self] value in
if let strongSelf = self {
strongSelf.acceptedTerms = !strongSelf.acceptedTerms
}
}
self.acceptTermsLabelNode.highlightAttributeAction = { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
}
self.acceptTermsLabelNode.tapAttributeAction = { [weak self] attributes, _ in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
self?.openTerms()
}
}
self.updateTheme(theme)
}
override func didLoad() {
super.didLoad()
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.acceptTap(_:)))
tapGesture.delegate = self.wrappedGestureRecognizerDelegate
self.view.addGestureRecognizer(tapGesture)
if let firstAction = self.actionNodes.first {
firstAction.actionEnabled = false
}
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
let location = gestureRecognizer.location(in: self.acceptTermsLabelNode.view)
if self.acceptTermsLabelNode.bounds.contains(location) {
return true
}
return false
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.bounds.contains(point) {
return nil
}
if let (_, attributes) = self.acceptTermsLabelNode.attributesAtPoint(self.view.convert(point, to: self.acceptTermsLabelNode.view)) {
if attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] == nil {
return self.view
}
}
return super.hitTest(point, with: event)
}
@objc private func acceptTap(_ gestureRecognizer: UITapGestureRecognizer) {
let location = gestureRecognizer.location(in: self.acceptTermsLabelNode.view)
if self.acceptTermsCheckNode.isUserInteractionEnabled {
if let attributes = self.acceptTermsLabelNode.attributesAtPoint(location)?.1 {
if attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] != nil {
return
}
}
self.acceptedTerms = !self.acceptedTerms
}
}
override func updateTheme(_ theme: AlertControllerTheme) {
self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.semibold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
self.textNode.attributedText = formattedText(self.text, fontSize: 13.0, color: theme.primaryColor, linkColor: theme.accentColor, textAlignment: .center)
if let additionalText = self.additionalText {
self.additionalTextNode.attributedText = formattedText(additionalText, fontSize: 13.0, color: theme.primaryColor, linkColor: theme.accentColor, textAlignment: .center)
} else {
self.additionalTextNode.attributedText = nil
}
let attributedAgreeText = parseMarkdownIntoAttributedString(
self.strings.WebApp_DisclaimerAgree,
attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: textFont, textColor: theme.primaryColor),
bold: MarkdownAttributeSet(font: boldTextFont, textColor: theme.primaryColor),
link: MarkdownAttributeSet(font: textFont, textColor: theme.accentColor),
linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
}
)
)
self.acceptTermsLabelNode.attributedText = attributedAgreeText
self.acceptTermsLabelNode.linkHighlightColor = theme.accentColor.withAlphaComponent(0.2)
self.actionNodesSeparator.backgroundColor = theme.separatorColor
for actionNode in self.actionNodes {
actionNode.updateTheme(theme)
}
for separatorNode in self.actionVerticalSeparators {
separatorNode.backgroundColor = theme.separatorColor
}
if let size = self.validLayout {
_ = self.updateLayout(size: size, transition: .immediate)
}
}
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
var size = size
size.width = min(size.width, 270.0)
self.validLayout = size
var origin: CGPoint = CGPoint(x: 0.0, y: 17.0)
let titleSize = self.titleNode.updateLayout(CGSize(width: size.width - 32.0, height: size.height))
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize))
origin.y += titleSize.height + 4.0
var entriesHeight: CGFloat = 0.0
let textSize = self.textNode.updateLayout(CGSize(width: size.width - 48.0, height: size.height))
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
origin.y += textSize.height
if self.acceptTermsLabelNode.supernode != nil {
origin.y += 21.0
entriesHeight += 21.0
let checkSize = CGSize(width: 22.0, height: 22.0)
let condensedSize = CGSize(width: size.width - 76.0, height: size.height)
let spacing: CGFloat = 12.0
let acceptTermsSize = self.acceptTermsLabelNode.updateLayout(condensedSize)
let acceptTermsTotalWidth = checkSize.width + spacing + acceptTermsSize.width
let acceptTermsOriginX = floorToScreenPixels((size.width - acceptTermsTotalWidth) / 2.0)
transition.updateFrame(node: self.acceptTermsCheckNode, frame: CGRect(origin: CGPoint(x: acceptTermsOriginX, y: origin.y - 3.0), size: checkSize))
transition.updateFrame(node: self.acceptTermsLabelNode, frame: CGRect(origin: CGPoint(x: acceptTermsOriginX + checkSize.width + spacing, y: origin.y), size: acceptTermsSize))
origin.y += acceptTermsSize.height
entriesHeight += acceptTermsSize.height
origin.y += 21.0
}
let additionalTextSize = self.additionalTextNode.updateLayout(CGSize(width: size.width - 48.0, height: size.height))
transition.updateFrame(node: self.additionalTextNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - additionalTextSize.width) / 2.0), y: origin.y), size: additionalTextSize))
origin.y += additionalTextSize.height
if additionalTextSize.height > 0.0 {
entriesHeight += 20.0
}
let actionButtonHeight: CGFloat = 44.0
var minActionsWidth: CGFloat = 0.0
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
let actionTitleInsets: CGFloat = 8.0
var effectiveActionLayout = TextAlertContentActionLayout.vertical
for actionNode in self.actionNodes {
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
effectiveActionLayout = .vertical
}
switch effectiveActionLayout {
case .horizontal:
minActionsWidth += actionTitleSize.width + actionTitleInsets
case .vertical:
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
}
}
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
let contentWidth = max(size.width, minActionsWidth)
var actionsHeight: CGFloat = 0.0
switch effectiveActionLayout {
case .horizontal:
actionsHeight = actionButtonHeight
case .vertical:
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
}
let resultSize = CGSize(width: contentWidth, height: titleSize.height + textSize.height + additionalTextSize.height + entriesHeight + actionsHeight + 3.0 + insets.top + insets.bottom)
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
var actionOffset: CGFloat = 0.0
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
var separatorIndex = -1
var nodeIndex = 0
for actionNode in self.actionNodes {
if separatorIndex >= 0 {
let separatorNode = self.actionVerticalSeparators[separatorIndex]
switch effectiveActionLayout {
case .horizontal:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
case .vertical:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
}
}
separatorIndex += 1
let currentActionWidth: CGFloat
switch effectiveActionLayout {
case .horizontal:
if nodeIndex == self.actionNodes.count - 1 {
currentActionWidth = resultSize.width - actionOffset
} else {
currentActionWidth = actionWidth
}
case .vertical:
currentActionWidth = resultSize.width
}
let actionNodeFrame: CGRect
switch effectiveActionLayout {
case .horizontal:
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += currentActionWidth
case .vertical:
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += actionButtonHeight
}
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
nodeIndex += 1
}
return resultSize
}
}
import ComponentFlow
import AlertComponent
import AlertCheckComponent
public func webAppTermsAlertController(
context: AccountContext,
@@ -355,46 +19,51 @@ public func webAppTermsAlertController(
bot: AttachMenuBot,
completion: @escaping (Bool) -> Void,
dismissed: @escaping () -> Void = {}
) -> AlertController {
let theme = defaultDarkColorPresentationTheme
let presentationData: PresentationData
if let updatedPresentationData {
presentationData = updatedPresentationData.initial
} else {
presentationData = context.sharedContext.currentPresentationData.with { $0 }
}
) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
let checkState = AlertCheckComponent.ExternalState()
var dismissImpl: ((Bool) -> Void)?
let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: presentationData.strings.WebApp_DisclaimerContinue, action: {
completion(true)
dismissImpl?(true)
}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissed()
dismissImpl?(true)
})]
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: {})
})
)
))
let title = presentationData.strings.WebApp_DisclaimerTitle
let text = presentationData.strings.WebApp_DisclaimerText
let additionalText: String? = nil
var effectiveUpdatedPresentationData: (PresentationData, Signal<PresentationData, NoError>)
if let updatedPresentationData {
effectiveUpdatedPresentationData = updatedPresentationData
} else {
effectiveUpdatedPresentationData = (presentationData, context.sharedContext.presentationData)
}
let contentNode = WebAppTermsAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, title: title, text: text, additionalText: additionalText, actions: actions)
contentNode.openTerms = {
context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: presentationData.strings.WebApp_Disclaimer_URL, forceExternal: true, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {
})
}
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
controller.dismissed = { outside in
if outside {
dismissed()
}
}
dismissImpl = { [weak controller] animated in
if animated {
controller?.dismissAnimated()
} else {
controller?.dismiss()
}
}
return controller
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
}