Various improvements

This commit is contained in:
Ilya Laktyushin 2025-03-24 05:27:50 +04:00
parent a651bb589d
commit a8b02015ce
7 changed files with 319 additions and 39 deletions

View File

@ -14096,3 +14096,13 @@ Sorry for the inconvenience.";
"Gift.Unpin.Unpin" = "Unpin";
"ChatList.Search.Ad" = "Ad";
"Login.Fee.Title" = "SMS Fee";
"Login.Fee.SmsCost.Title" = "High SMS Costs";
"Login.Fee.SmsCost.Text" = "Telecom providers in your country (%@) charge Telegram very high prices for SMS.";
"Login.Fee.Verification.Title" = "Verification Required";
"Login.Fee.Verification.Text" = "Telegram needs to send you an SMS with a verification code to confirm your phone number.";
"Login.Fee.Support.Title" = "Support via [Telegram Premium >]()";
"Login.Fee.Support.Text" = "Sign up for a 1-week Telegram Premium subscription to help cover the SMS costs.";
"Login.Fee.SignUp" = "Sign Up for %@";
"Login.Fee.GetPremiumForAWeek" = "Get Telegram Premium for 1 week";

View File

@ -848,7 +848,13 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF
let pasteSize = self.pasteButton.measure(layout.size)
let pasteButtonSize = CGSize(width: pasteSize.width + 16.0, height: 24.0)
transition.updateFrame(node: self.pasteButton, frame: CGRect(origin: CGPoint(x: layout.size.width - 40.0 - pasteButtonSize.width, y: self.textField.frame.midY - pasteButtonSize.height / 2.0), size: pasteButtonSize))
let pasteOriginX: CGFloat
if case .compact = layout.metrics.widthClass {
pasteOriginX = layout.size.width - 40.0 - pasteButtonSize.width
} else {
pasteOriginX = self.textField.frame.maxX + 32.0
}
transition.updateFrame(node: self.pasteButton, frame: CGRect(origin: CGPoint(x: pasteOriginX, y: self.textField.frame.midY - pasteButtonSize.height / 2.0), size: pasteButtonSize))
self.hintArrowNode.isHidden = true
} else if case .word = codeType {
self.hintButtonNode.alpha = 0.0

View File

@ -198,7 +198,7 @@ final class AuthorizationSequencePaymentScreenComponent: Component {
let titleSize = self.title.update(
transition: transition,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: "SMS Fee", font: Font.bold(28.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)))
MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Login_Fee_Title, font: Font.bold(28.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)))
),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
@ -218,9 +218,9 @@ final class AuthorizationSequencePaymentScreenComponent: Component {
AnyComponentWithIdentity(
id: "cost",
component: AnyComponent(ParagraphComponent(
title: "High SMS Costs",
title: environment.strings.Login_Fee_SmsCost_Title,
titleColor: textColor,
text: "Telecom providers in your country (\(countryName)) charge Telegram very high prices for SMS.",
text: environment.strings.Login_Fee_SmsCost_Text(countryName).string,
textColor: secondaryTextColor,
iconName: "Premium/Authorization/Cost",
iconColor: linkColor
@ -231,9 +231,9 @@ final class AuthorizationSequencePaymentScreenComponent: Component {
AnyComponentWithIdentity(
id: "verification",
component: AnyComponent(ParagraphComponent(
title: "Verification Required",
title: environment.strings.Login_Fee_Verification_Title,
titleColor: textColor,
text: "Telegram needs to send you an SMS with a verification code to confirm your phone number.",
text: environment.strings.Login_Fee_Verification_Text,
textColor: secondaryTextColor,
iconName: "Premium/Authorization/Verification",
iconColor: linkColor
@ -242,11 +242,11 @@ final class AuthorizationSequencePaymentScreenComponent: Component {
)
items.append(
AnyComponentWithIdentity(
id: "withdrawal",
id: "support",
component: AnyComponent(ParagraphComponent(
title: "Support via [Telegram Premium >]()",
title: environment.strings.Login_Fee_Support_Title,
titleColor: textColor,
text: "Sign up for a 1-week Telegram Premium subscription to help cover the SMS costs.",
text: environment.strings.Login_Fee_Support_Text,
textColor: secondaryTextColor,
iconName: "Premium/Authorization/Support",
iconColor: linkColor,
@ -315,7 +315,8 @@ final class AuthorizationSequencePaymentScreenComponent: Component {
} else {
priceString = ""
}
let buttonString = "Sign up for \(priceString)"
let buttonString = environment.strings.Login_Fee_SignUp(priceString).string
let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
let buttonSize = self.button.update(
transition: transition,
@ -331,7 +332,7 @@ final class AuthorizationSequencePaymentScreenComponent: Component {
component: AnyComponent(
VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))),
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "Get Telegram Premium for 1 week", font: Font.medium(11.0), textColor: environment.theme.list.itemCheckColors.foregroundColor.withAlphaComponent(0.7), paragraphAlignment: .center)))))
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Login_Fee_GetPremiumForAWeek, font: Font.medium(11.0), textColor: environment.theme.list.itemCheckColors.foregroundColor.withAlphaComponent(0.7), paragraphAlignment: .center)))))
], spacing: 1.0)
)
),

View File

@ -300,6 +300,8 @@ private final class ScrollContent: CombinedComponent {
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.2,
highlightColor: linkColor.withAlphaComponent(0.1),
highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
@ -598,6 +600,7 @@ private final class ParagraphComponent: CombinedComponent {
horizontalAlignment: .natural,
maximumNumberOfLines: 0,
lineSpacing: 0.2,
highlightColor: accentColor.withAlphaComponent(0.1),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
@ -949,6 +952,7 @@ public class AdsInfoScreen: ViewController {
context: controller.context,
theme: self.presentationData.theme,
title: self.presentationData.strings.AdsInfo_Understood,
showBackground: controller.mode != .search,
action: { [weak self] in
guard let self else {
return
@ -992,7 +996,7 @@ public class AdsInfoScreen: ViewController {
}
private var defaultTopInset: CGFloat {
guard let layout = self.currentLayout else {
guard let layout = self.currentLayout, let controller = self.controller else {
return 210.0
}
if case .compact = layout.metrics.widthClass {
@ -1006,9 +1010,13 @@ public class AdsInfoScreen: ViewController {
let contentHeight = self.containerExternalState.contentHeight
let footerHeight = self.footerHeight
if contentHeight > 0.0 {
let delta = (layout.size.height - defaultTopInset - containerTopInset) - contentHeight - footerHeight - 16.0
if delta > 0.0 {
defaultTopInset += delta
if case .search = controller.mode {
return (layout.size.height - containerTopInset) - contentHeight
} else {
let delta = (layout.size.height - defaultTopInset - containerTopInset) - contentHeight - footerHeight - 16.0
if delta > 0.0 {
defaultTopInset += delta
}
}
}
return defaultTopInset
@ -1029,7 +1037,7 @@ public class AdsInfoScreen: ViewController {
}
@objc func panGesture(_ recognizer: UIPanGestureRecognizer) {
guard let layout = self.currentLayout else {
guard let layout = self.currentLayout, let controller = self.controller else {
return
}
@ -1064,6 +1072,9 @@ public class AdsInfoScreen: ViewController {
let contentOffset = scrollView?.contentOffset.y ?? 0.0
var translation = recognizer.translation(in: self.view).y
if case .search = controller.mode {
translation = max(0.0, translation)
}
var currentOffset = topInset + translation
@ -1111,9 +1122,13 @@ public class AdsInfoScreen: ViewController {
let contentOffset = scrollView?.contentOffset.y ?? 0.0
let translation = recognizer.translation(in: self.view).y
var translation = recognizer.translation(in: self.view).y
var velocity = recognizer.velocity(in: self.view)
if case .search = controller.mode {
translation = max(0.0, translation)
velocity.y = max(0.0, velocity.y)
}
if self.isExpanded {
if contentOffset > 0.1 {
velocity = CGPoint()
@ -1435,12 +1450,14 @@ private final class FooterComponent: Component {
let context: AccountContext
let theme: PresentationTheme
let title: String
let showBackground: Bool
let action: () -> Void
init(context: AccountContext, theme: PresentationTheme, title: String, action: @escaping () -> Void) {
init(context: AccountContext, theme: PresentationTheme, title: String, showBackground: Bool, action: @escaping () -> Void) {
self.context = context
self.theme = theme
self.title = title
self.showBackground = showBackground
self.action = action
}
@ -1454,6 +1471,9 @@ private final class FooterComponent: Component {
if lhs.title != rhs.title {
return false
}
if lhs.showBackground != rhs.showBackground {
return false
}
return true
}
@ -1494,6 +1514,9 @@ private final class FooterComponent: Component {
self.separator.backgroundColor = component.theme.rootController.tabBar.separatorColor.cgColor
transition.setFrame(layer: self.separator, frame: CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: UIScreenPixel)))
self.backgroundView.isHidden = !component.showBackground
self.separator.isHidden = !component.showBackground
let buttonSize = self.button.update(
transition: .immediate,
component: AnyComponent(

View File

@ -1868,15 +1868,15 @@ public class StarsTransactionScreen: ViewControllerComponentContainer {
guard let self, let navigationController = self.navigationController as? NavigationController else {
return
}
self.dismissAnimated()
let _ = (context.engine.privacy.requestAccountPrivacySettings()
|> deliverOnMainQueue).start(next: { [weak self, weak navigationController] privacySettings in
|> deliverOnMainQueue).start(next: { [weak navigationController] privacySettings in
let controller = context.sharedContext.makeIncomingMessagePrivacyScreen(context: context, value: privacySettings.globalSettings.nonContactChatsPrivacy, exceptions: privacySettings.noPaidMessages, update: { settingValue in
let _ = context.engine.privacy.updateNonContactChatsPrivacy(value: settingValue).start()
})
navigationController?.pushViewController(controller)
Queue.mainQueue().after(1.0) {
self?.dismissAnimated()
Queue.mainQueue().after(0.4) {
navigationController?.pushViewController(controller)
}
})
}

View File

@ -1558,25 +1558,46 @@ public final class WebAppController: ViewController, AttachmentContainable {
controller.parentController()?.lockOrientation = lock
}
case "web_app_device_storage_save_key":
if let json, let requestId = json["req_id"] as? String, let key = json["key"] as? String, let value = json["value"] {
var effectiveValue: String?
if let stringValue = value as? String {
effectiveValue = stringValue
if let json, let requestId = json["req_id"] as? String {
if let key = json["key"] as? String {
let value = json["value"]
var effectiveValue: String?
if let stringValue = value as? String {
effectiveValue = stringValue
} else if value is NSNull {
effectiveValue = nil
} else {
let data: JSON = [
"req_id": requestId,
"error": "VALUE_INVALID"
]
self.webView?.sendEvent(name: "device_storage_failed", data: data.string)
return
}
let _ = self.context.engine.peers.setBotStorageValue(peerId: controller.botId, key: key, value: effectiveValue).start(error: { [weak self] error in
var errorValue = "UNKNOWN_ERROR"
if case .quotaExceeded = error {
errorValue = "QUOTA_EXCEEDED"
}
let data: JSON = [
"req_id": requestId,
"error": errorValue
]
self?.webView?.sendEvent(name: "device_storage_failed", data: data.string)
}, completed: { [weak self] in
let data: JSON = [
"req_id": requestId
]
self?.webView?.sendEvent(name: "device_storage_key_saved", data: data.string)
})
} else {
effectiveValue = nil
}
let _ = self.context.engine.peers.setBotStorageValue(peerId: controller.botId, key: key, value: effectiveValue).start(error: { [weak self] _ in
let data: JSON = [
"req_id": requestId,
"error": "UNKNOWN_ERROR"
"error": "KEY_INVALID"
]
self?.webView?.sendEvent(name: "device_storage_failed", data: data.string)
}, completed: { [weak self] in
let data: JSON = [
"req_id": requestId
]
self?.webView?.sendEvent(name: "device_storage_key_saved", data: data.string)
})
self.webView?.sendEvent(name: "device_storage_failed", data: data.string)
}
}
case "web_app_device_storage_get_key":
if let json, let requestId = json["req_id"] as? String {
@ -1607,6 +1628,78 @@ public final class WebAppController: ViewController, AttachmentContainable {
self?.webView?.sendEvent(name: "device_storage_cleared", data: data.string)
})
}
case "web_app_secure_storage_save_key":
if let json, let requestId = json["req_id"] as? String {
if let key = json["key"] as? String {
let value = json["value"]
var effectiveValue: String?
if let stringValue = value as? String {
effectiveValue = stringValue
} else if value is NSNull {
effectiveValue = nil
} else {
let data: JSON = [
"req_id": requestId,
"error": "VALUE_INVALID"
]
self.webView?.sendEvent(name: "secure_storage_failed", data: data.string)
return
}
let _ = (WebAppSecureStorage.setValue(userId: self.context.account.peerId, botId: controller.botId, key: key, value: effectiveValue)
|> deliverOnMainQueue).start(error: { [weak self] error in
var errorValue = "UNKNOWN_ERROR"
if case .quotaExceeded = error {
errorValue = "QUOTA_EXCEEDED"
}
let data: JSON = [
"req_id": requestId,
"error": errorValue
]
self?.webView?.sendEvent(name: "secure_storage_failed", data: data.string)
}, completed: { [weak self] in
let data: JSON = [
"req_id": requestId
]
self?.webView?.sendEvent(name: "secure_storage_key_saved", data: data.string)
})
} else {
let data: JSON = [
"req_id": requestId,
"error": "KEY_INVALID"
]
self.webView?.sendEvent(name: "secure_storage_failed", data: data.string)
}
}
case "web_app_secure_storage_get_key":
if let json, let requestId = json["req_id"] as? String {
if let key = json["key"] as? String {
let _ = (WebAppSecureStorage.getValue(userId: self.context.account.peerId, botId: controller.botId, key: key)
|> deliverOnMainQueue).start(next: { [weak self] value in
let data: JSON = [
"req_id": requestId,
"value": value ?? NSNull()
]
self?.webView?.sendEvent(name: "secure_storage_key_received", data: data.string)
})
} else {
let data: JSON = [
"req_id": requestId,
"error": "KEY_INVALID"
]
self.webView?.sendEvent(name: "secure_storage_failed", data: data.string)
}
}
case "web_app_secure_storage_clear":
if let json, let requestId = json["req_id"] as? String {
let _ = (WebAppSecureStorage.clearStorage(userId: self.context.account.peerId, botId: controller.botId)
|> deliverOnMainQueue).start(completed: { [weak self] in
let data: JSON = [
"req_id": requestId
]
self?.webView?.sendEvent(name: "secure_storage_cleared", data: data.string)
})
}
default:
break
}

View File

@ -0,0 +1,147 @@
import Foundation
import Security
import SwiftSignalKit
import TelegramCore
final class WebAppSecureStorage {
enum Error {
case quotaExceeded
case unknown
}
static private let maxKeyCount = 10
private init() {
}
static private func keyPrefix(userId: EnginePeer.Id, botId: EnginePeer.Id) -> String {
return "A\(UInt64(bitPattern: userId.toInt64()))WebBot\(UInt64(bitPattern: botId.toInt64()))Key_"
}
static private func makeQuery(userId: EnginePeer.Id, botId: EnginePeer.Id, key: String) -> [String: Any] {
let identifier = self.keyPrefix(userId: userId, botId: botId) + key
return [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: identifier,
kSecAttrService as String: "TMASecureStorage"
]
}
static private func countKeys(userId: EnginePeer.Id, botId: EnginePeer.Id) -> Int {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "TMASecureStorage",
kSecMatchLimit as String: kSecMatchLimitAll,
kSecReturnAttributes as String: true
]
var result: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &result)
if status == errSecSuccess, let items = result as? [[String: Any]] {
let relevantPrefix = self.keyPrefix(userId: userId, botId: botId)
let count = items.filter {
if let account = $0[kSecAttrAccount as String] as? String {
return account.hasPrefix(relevantPrefix)
}
return false
}.count
return count
}
return 0
}
static func setValue(userId: EnginePeer.Id, botId: EnginePeer.Id, key: String, value: String?) -> Signal<Never, WebAppSecureStorage.Error> {
var query = makeQuery(userId: userId, botId: botId, key: key)
if value == nil {
let status = SecItemDelete(query as CFDictionary)
if status == errSecSuccess || status == errSecItemNotFound {
return .complete()
} else {
return .fail(.unknown)
}
}
guard let valueData = value?.data(using: .utf8) else {
return .fail(.unknown)
}
query[kSecAttrAccessible as String] = kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
let status = SecItemCopyMatching(query as CFDictionary, nil)
if status == errSecSuccess {
let updateQuery: [String: Any] = [
kSecValueData as String: valueData
]
let updateStatus = SecItemUpdate(query as CFDictionary, updateQuery as CFDictionary)
if updateStatus == errSecSuccess {
return .complete()
} else {
return .fail(.unknown)
}
} else if status == errSecItemNotFound {
let currentCount = countKeys(userId: userId, botId: botId)
if currentCount >= maxKeyCount {
return .fail(.quotaExceeded)
}
query[kSecValueData as String] = valueData
let createStatus = SecItemAdd(query as CFDictionary, nil)
if createStatus == errSecSuccess {
return .complete()
} else {
return .fail(.unknown)
}
} else {
return .fail(.unknown)
}
}
static func getValue(userId: EnginePeer.Id, botId: EnginePeer.Id, key: String) -> Signal<String?, WebAppSecureStorage.Error> {
var query = makeQuery(userId: userId, botId: botId, key: key)
query[kSecReturnData as String] = true
query[kSecMatchLimit as String] = kSecMatchLimitOne
var result: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &result)
if status == errSecSuccess, let data = result as? Data, let value = String(data: data, encoding: .utf8) {
return .single(value)
} else if status == errSecItemNotFound {
return .single(nil)
} else {
return .fail(.unknown)
}
}
static func clearStorage(userId: EnginePeer.Id, botId: EnginePeer.Id) -> Signal<Never, WebAppSecureStorage.Error> {
let serviceQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "TMASecureStorage",
kSecMatchLimit as String: kSecMatchLimitAll,
kSecReturnAttributes as String: true
]
var result: CFTypeRef?
let status = SecItemCopyMatching(serviceQuery as CFDictionary, &result)
if status == errSecSuccess, let items = result as? [[String: Any]] {
let relevantPrefix = self.keyPrefix(userId: userId, botId: botId)
for item in items {
if let account = item[kSecAttrAccount as String] as? String, account.hasPrefix(relevantPrefix) {
let deleteQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: account,
kSecAttrService as String: "TMASecureStorage"
]
SecItemDelete(deleteQuery as CFDictionary)
}
}
}
return .complete()
}
}