Merge branch 'beta' of gitlab.com:peter-iakovlev/telegram-ios into beta

This commit is contained in:
Isaac 2024-10-14 18:34:37 +04:00
commit df96d16938
39 changed files with 965 additions and 485 deletions

View File

@ -13008,6 +13008,8 @@ Sorry for the inconvenience.";
"Gift.Options.Gift.Filter.AllGifts" = "All Gifts"; "Gift.Options.Gift.Filter.AllGifts" = "All Gifts";
"Gift.Options.Gift.Filter.Limited" = "Limited"; "Gift.Options.Gift.Filter.Limited" = "Limited";
"Gift.Options.Gift.Limited" = "Limited"; "Gift.Options.Gift.Limited" = "Limited";
"Gift.Options.Gift.SoldOut" = "Sold Out";
"Gift.Options.SoldOut.Text" = "Sorry, this gift is sold out.";
"PeerInfo.PaneGifts" = "Gifts"; "PeerInfo.PaneGifts" = "Gifts";
@ -13039,6 +13041,11 @@ Sorry for the inconvenience.";
"Gift.Send.HideMyName.Info" = "Hide my name and message from visitors to %1$@'s profile. %2$@ will still see your name and message."; "Gift.Send.HideMyName.Info" = "Hide my name and message from visitors to %1$@'s profile. %2$@ will still see your name and message.";
"Gift.Send.Send" = "Send a Gift for"; "Gift.Send.Send" = "Send a Gift for";
"Gift.Send.Limited" = "Limited"; "Gift.Send.Limited" = "Limited";
"Gift.Send.Remains_1" = "%@ left";
"Gift.Send.Remains_any" = "%@ left";
"Gift.Send.ErrorUnknown" = "An error occurred. Please try again.";
"Gift.Send.ErrorOutOfStock" = "Sorry, this gift is sold out. Please choose another gift.";
"Profile.SendGift" = "Send a Gift"; "Profile.SendGift" = "Send a Gift";
"Settings.SendGift" = "Send a Gift"; "Settings.SendGift" = "Send a Gift";
@ -13061,6 +13068,7 @@ Sorry for the inconvenience.";
"Notification.PremiumGift.YearsTitle_1" = "%@ Year Premium"; "Notification.PremiumGift.YearsTitle_1" = "%@ Year Premium";
"Notification.PremiumGift.YearsTitle_any" = "%@ Years Premium"; "Notification.PremiumGift.YearsTitle_any" = "%@ Years Premium";
"Notification.PremiumGift.More" = "more";
"Notification.PremiumGift.SubscriptionDescription" = "Subscription for exclusive Telegram features."; "Notification.PremiumGift.SubscriptionDescription" = "Subscription for exclusive Telegram features.";

View File

@ -931,13 +931,13 @@ public final class AvatarNode: ASDisplayNode {
if let repliesIcon = repliesIcon { if let repliesIcon = repliesIcon {
context.draw(repliesIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - repliesIcon.size.width) / 2.0), y: floor((bounds.size.height - repliesIcon.size.height) / 2.0)), size: repliesIcon.size)) context.draw(repliesIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - repliesIcon.size.width) / 2.0), y: floor((bounds.size.height - repliesIcon.size.height) / 2.0)), size: repliesIcon.size))
} }
} else if case .anonymousSavedMessagesIcon = parameters.icon { } else if case let .anonymousSavedMessagesIcon(isColored) = parameters.icon {
let factor = bounds.size.width / 60.0 let factor = bounds.size.width / 60.0
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0) context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
context.scaleBy(x: factor, y: -factor) context.scaleBy(x: factor, y: -factor)
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0) context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
if let theme = parameters.theme, theme.overallDarkAppearance { if let theme = parameters.theme, theme.overallDarkAppearance, !isColored {
if let anonymousSavedMessagesDarkIcon = anonymousSavedMessagesDarkIcon { if let anonymousSavedMessagesDarkIcon = anonymousSavedMessagesDarkIcon {
context.draw(anonymousSavedMessagesDarkIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - anonymousSavedMessagesDarkIcon.size.width) / 2.0), y: floor((bounds.size.height - anonymousSavedMessagesDarkIcon.size.height) / 2.0)), size: anonymousSavedMessagesDarkIcon.size)) context.draw(anonymousSavedMessagesDarkIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - anonymousSavedMessagesDarkIcon.size.width) / 2.0), y: floor((bounds.size.height - anonymousSavedMessagesDarkIcon.size.height) / 2.0)), size: anonymousSavedMessagesDarkIcon.size))
} }

View File

@ -1558,19 +1558,23 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
applePayController.presentingViewController?.dismiss(animated: true, completion: nil) applePayController.presentingViewController?.dismiss(animated: true, completion: nil)
} }
let text: String let text: String?
switch error { switch error {
case .precheckoutFailed: case .precheckoutFailed:
text = strongSelf.presentationData.strings.Checkout_ErrorPrecheckoutFailed text = strongSelf.presentationData.strings.Checkout_ErrorPrecheckoutFailed
case .paymentFailed: case .paymentFailed:
text = strongSelf.presentationData.strings.Checkout_ErrorPaymentFailed text = strongSelf.presentationData.strings.Checkout_ErrorPaymentFailed
case .alreadyPaid: case .alreadyPaid:
text = strongSelf.presentationData.strings.Checkout_ErrorInvoiceAlreadyPaid text = strongSelf.presentationData.strings.Checkout_ErrorInvoiceAlreadyPaid
case .generic: case .generic:
text = strongSelf.presentationData.strings.Checkout_ErrorGeneric text = strongSelf.presentationData.strings.Checkout_ErrorGeneric
default:
text = nil
} }
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil) if let text {
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil)
}
strongSelf.failed() strongSelf.failed()
} }

View File

@ -500,6 +500,7 @@ public class BrowserScreen: ViewController, MinimizableController {
case closeAddressBar case closeAddressBar
case navigateTo(String, Bool) case navigateTo(String, Bool)
case expand case expand
case saveToFiles
} }
final class Node: ViewControllerTracingNode { final class Node: ViewControllerTracingNode {
@ -793,6 +794,10 @@ public class BrowserScreen: ViewController, MinimizableController {
if let content = self.content.last { if let content = self.content.last {
content.resetScrolling() content.resetScrolling()
} }
case .saveToFiles:
if let content = self.content.last as? BrowserWebContent {
content.requestSaveToFiles()
}
} }
} }
@ -1213,6 +1218,14 @@ public class BrowserScreen: ViewController, MinimizableController {
performAction.invoke(.addBookmark) performAction.invoke(.addBookmark)
action(.default) action(.default)
}))) })))
if contentState.contentType == .webPage {
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_SaveToFiles, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in
performAction.invoke(.saveToFiles)
action(.default)
})))
}
if !layout.metrics.isTablet && canOpenIn { if !layout.metrics.isTablet && canOpenIn {
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_OpenInBrowser(openInTitle).string, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Browser"), color: theme.contextMenu.primaryColor) }, action: { [weak self] (controller, action) in items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_OpenInBrowser(openInTitle).string, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Browser"), color: theme.contextMenu.primaryColor) }, action: { [weak self] (controller, action) in
if let self { if let self {

View File

@ -22,6 +22,7 @@ import UrlHandling
import SaveProgressScreen import SaveProgressScreen
import DeviceModel import DeviceModel
import LegacyMediaPickerUI import LegacyMediaPickerUI
import PassKit
private final class TonSchemeHandler: NSObject, WKURLSchemeHandler { private final class TonSchemeHandler: NSObject, WKURLSchemeHandler {
private final class PendingTask { private final class PendingTask {
@ -213,6 +214,8 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
self.presentationData = presentationData self.presentationData = presentationData
var handleScriptMessageImpl: ((WKScriptMessage) -> Void)? var handleScriptMessageImpl: ((WKScriptMessage) -> Void)?
var handleContentMessageImpl: ((WKScriptMessage) -> Void)?
var handleBlobMessageImpl: ((WKScriptMessage) -> Void)?
let configuration: WKWebViewConfiguration let configuration: WKWebViewConfiguration
if let preferredConfiguration { if let preferredConfiguration {
@ -242,7 +245,12 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
contentController.add(WeakScriptMessageHandler { message in contentController.add(WeakScriptMessageHandler { message in
handleScriptMessageImpl?(message) handleScriptMessageImpl?(message)
}, name: "performAction") }, name: "performAction")
contentController.add(WeakScriptMessageHandler { message in
handleContentMessageImpl?(message)
}, name: "contentInterface")
contentController.add(WeakScriptMessageHandler { message in
handleBlobMessageImpl?(message)
}, name: "blobInterface")
configuration.userContentController = contentController configuration.userContentController = contentController
configuration.applicationNameForUserAgent = computedUserAgent() configuration.applicationNameForUserAgent = computedUserAgent()
} }
@ -323,6 +331,12 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
handleScriptMessageImpl = { [weak self] message in handleScriptMessageImpl = { [weak self] message in
self?.handleScriptMessage(message) self?.handleScriptMessage(message)
} }
handleContentMessageImpl = { [weak self] message in
self?.handleContentRequest(message)
}
handleBlobMessageImpl = { [weak self] message in
self?.handleBlobRequest(message)
}
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
@ -342,13 +356,9 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
} }
private func handleScriptMessage(_ message: WKScriptMessage) { private func handleScriptMessage(_ message: WKScriptMessage) {
guard let body = message.body as? [String: Any] else { guard let body = message.body as? [String: Any], let eventName = body["eventName"] as? String else {
return return
} }
guard let eventName = body["eventName"] as? String else {
return
}
switch eventName { switch eventName {
case "cancellingTouch": case "cancellingTouch":
self.cancelInteractiveTransitionGestures() self.cancelInteractiveTransitionGestures()
@ -357,6 +367,35 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
} }
} }
private func handleContentRequest(_ message: WKScriptMessage) {
guard let string = message.body as? String else {
return
}
guard let data = Data(base64Encoded: string, options: [.ignoreUnknownCharacters]) else {
return
}
guard let url = URL(string: self._state.url) else {
return
}
let path = NSTemporaryDirectory() + NSUUID().uuidString
let _ = try? data.write(to: URL(fileURLWithPath: path), options: .atomic)
let fileName: String
if !url.lastPathComponent.isEmpty {
fileName = url.lastPathComponent
} else {
fileName = "default"
}
let tempFile = TempBox.shared.file(path: path, fileName: fileName)
let fileUrl = URL(fileURLWithPath: tempFile.path)
let controller = legacyICloudFilePicker(theme: self.presentationData.theme, mode: .export, url: fileUrl, documentTypes: [], forceDarkTheme: false, dismissed: {}, completion: { _ in
})
self.present(controller, nil)
}
func updatePresentationData(_ presentationData: PresentationData) { func updatePresentationData(_ presentationData: PresentationData) {
self.presentationData = presentationData self.presentationData = presentationData
if #available(iOS 15.0, *) { if #available(iOS 15.0, *) {
@ -735,13 +774,17 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
@available(iOS 13.0, *) @available(iOS 13.0, *)
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, preferences: WKWebpagePreferences, decisionHandler: @escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void) { func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, preferences: WKWebpagePreferences, decisionHandler: @escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void) {
if #available(iOS 14.5, *), navigationAction.shouldPerformDownload { if #available(iOS 14.5, *), navigationAction.shouldPerformDownload {
self.presentDownloadConfirmation(fileName: navigationAction.request.mainDocumentURL?.lastPathComponent ?? "file", proceed: { download in if navigationAction.request.url?.scheme == "blob" {
if download { decisionHandler(.allow, preferences)
decisionHandler(.download, preferences) } else {
} else { self.presentDownloadConfirmation(fileName: navigationAction.request.mainDocumentURL?.lastPathComponent ?? "file", proceed: { download in
decisionHandler(.cancel, preferences) if download {
} decisionHandler(.download, preferences)
}) } else {
decisionHandler(.cancel, preferences)
}
})
}
} else { } else {
if let url = navigationAction.request.url?.absoluteString { if let url = navigationAction.request.url?.absoluteString {
if (navigationAction.targetFrame == nil || navigationAction.targetFrame?.isMainFrame == true) && (isTelegramMeLink(url) || isTelegraPhLink(url) || url.hasPrefix("tg://")) && !url.contains("/auth/push?") && !self._state.url.contains("/auth/push?") { if (navigationAction.targetFrame == nil || navigationAction.targetFrame?.isMainFrame == true) && (isTelegramMeLink(url) || isTelegraPhLink(url) || url.hasPrefix("tg://")) && !url.contains("/auth/push?") && !self._state.url.contains("/auth/push?") {
@ -766,14 +809,22 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
if navigationResponse.canShowMIMEType { if navigationResponse.canShowMIMEType {
decisionHandler(.allow) decisionHandler(.allow)
} else if #available(iOS 14.5, *) { } else if #available(iOS 14.5, *) {
// decisionHandler(.download) if navigationResponse.response.suggestedFilename?.lowercased().hasSuffix(".pkpass") == true {
self.presentDownloadConfirmation(fileName: navigationResponse.response.suggestedFilename ?? "file", proceed: { download in decisionHandler(.download)
if download { } else {
decisionHandler(.download) if let url = navigationResponse.response.url, url.scheme == "blob" {
} else {
decisionHandler(.cancel) decisionHandler(.cancel)
self.requestBlobSaveToFiles(url: url)
} else {
self.presentDownloadConfirmation(fileName: navigationResponse.response.suggestedFilename ?? "file", proceed: { download in
if download {
decisionHandler(.download)
} else {
decisionHandler(.cancel)
}
})
} }
}) }
} else { } else {
decisionHandler(.cancel) decisionHandler(.cancel)
} }
@ -838,10 +889,23 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
let tempFile = TempBox.shared.file(path: path, fileName: fileName) let tempFile = TempBox.shared.file(path: path, fileName: fileName)
let url = URL(fileURLWithPath: tempFile.path) let url = URL(fileURLWithPath: tempFile.path)
let controller = legacyICloudFilePicker(theme: self.presentationData.theme, mode: .export, url: url, documentTypes: [], forceDarkTheme: false, dismissed: {}, completion: { _ in if fileName.hasSuffix(".pkpass") {
if let data = try? Data(contentsOf: url), let pass = try? PKPass(data: data) {
let passLibrary = PKPassLibrary()
if passLibrary.containsPass(pass) {
//TODO:localize
let alertController = textAlertController(context: self.context, updatedPresentationData: nil, title: nil, text: "This pass is already added to Wallet.", actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_OK, action: {})])
self.present(alertController, nil)
} else if let controller = PKAddPassesViewController(pass: pass) {
self.getNavigationController()?.view.window?.rootViewController?.present(controller, animated: true)
}
}
} else {
let controller = legacyICloudFilePicker(theme: self.presentationData.theme, mode: .export, url: url, documentTypes: [], forceDarkTheme: false, dismissed: {}, completion: { _ in
}) })
self.present(controller, nil) self.present(controller, nil)
}
self.downloadArguments = nil self.downloadArguments = nil
self.downloadProgressObserver = nil self.downloadProgressObserver = nil
@ -855,28 +919,35 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
} }
func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
if let url = webView.url, !url.absoluteString.contains("beatsnvibes") { guard [NSURLAuthenticationMethodDefault, NSURLAuthenticationMethodHTTPBasic, NSURLAuthenticationMethodHTTPDigest].contains(challenge.protectionSpace.authenticationMethod) else {
completionHandler(.performDefaultHandling, nil) completionHandler(.performDefaultHandling, nil)
return return
} }
var completed = false var completed = false
let host = webView.url?.host ?? "" let host = webView.url?.host ?? ""
let authController = authController(sharedContext: self.context.sharedContext, updatedPresentationData: nil, title: "Sign in to \(host)", text: "Your login information will be sent securely.", apply: { result in
if !completed { let authController = authController(
completed = true sharedContext: self.context.sharedContext,
if let (login, password) = result { updatedPresentationData: nil,
let credential = URLCredential( title: self.presentationData.strings.WebBrowser_AuthChallenge_Title(host).string,
user: login, text: self.presentationData.strings.WebBrowser_AuthChallenge_Text,
password: password, apply: { result in
persistence: .permanent if !completed {
) completed = true
completionHandler(.useCredential, credential) if let (login, password) = result {
} else { let credential = URLCredential(
completionHandler(.cancelAuthenticationChallenge, nil) user: login,
password: password,
persistence: .permanent
)
completionHandler(.useCredential, credential)
} else {
completionHandler(.cancelAuthenticationChallenge, nil)
}
} }
} }
}) )
authController.dismissed = { byOutsideTap in authController.dismissed = { byOutsideTap in
if byOutsideTap { if byOutsideTap {
if !completed { if !completed {
@ -976,6 +1047,168 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
) )
} }
func requestSaveToFiles() {
self.webView.evaluateJavaScript("document.contentType") { result, _ in
guard let contentType = result as? String else {
return
}
if #available(iOS 14.0, *), contentType == "text/html" {
self.webView.createWebArchiveData { [weak self] result in
guard let self, case let .success(data) = result else {
return
}
let path = NSTemporaryDirectory() + NSUUID().uuidString
let _ = try? data.write(to: URL(fileURLWithPath: path), options: .atomic)
let tempFile = TempBox.shared.file(path: path, fileName: "\(self._state.title).webarchive")
let url = URL(fileURLWithPath: tempFile.path)
let controller = legacyICloudFilePicker(theme: self.presentationData.theme, mode: .export, url: url, documentTypes: [], forceDarkTheme: false, dismissed: {}, completion: { _ in
})
self.present(controller, nil)
}
} else {
let s = """
var xhr = new XMLHttpRequest();
xhr.open('GET', "\(self._state.url)", true);
xhr.responseType = 'arraybuffer';
xhr.onload = function(e) {
if (this.status == 200) {
var uInt8Array = new Uint8Array(this.response);
var i = uInt8Array.length;
var binaryString = new Array(i);
while (i--){
binaryString[i] = String.fromCharCode(uInt8Array[i]);
}
var data = binaryString.join('');
var base64 = window.btoa(data);
window.webkit.messageHandlers.contentInterface.postMessage(base64);
}
};
xhr.send();
"""
self.webView.evaluateJavaScript(s)
}
}
}
struct BlobComponents: Codable {
let mimeType: String
let size: Int64
let dataString: String
}
func requestBlobSaveToFiles(url: URL) {
guard #available(iOS 14.0, *) else {
return
}
let script = """
async function createBlobFromUrl(url) {
const response = await fetch(url);
const blob = await response.blob();
return blob;
}
function blobToDataURLAsync(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
resolve(reader.result);
};
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
const url = await createBlobFromUrl(blobUrl)
return await blobToDataURLAsync(url)
"""
self.webView.callAsyncJavaScript(script,
arguments: ["blobUrl": url.absoluteString],
in: nil,
in: WKContentWorld.defaultClient) { result in
switch result {
case .success(let dataUrl):
guard let url = URL(string: dataUrl as! String) else {
print("Failed to get data")
return
}
guard let data = try? Data(contentsOf: url) else {
print("Failed to decode data URL")
return
}
print(data)
// Do anything with the data. It was a pdf on my case.
//So I used UIDocumentInteractionController to show the pdf
case .failure(let error):
print("Failed with: \(error)")
}
}
// let urlString = url.absoluteString
// let s = """
// function blobToDataURL(blob, callback) {
// var reader = new FileReader()
// reader.onload = function(e) {callback(e.target.result.split(",")[1])}
// reader.readAsDataURL(blob)
// }
// async function run() {
// const url = "\(urlString)"
// const blob = await fetch(url).then(r => r.blob())
//
// blobToDataURL(blob, datauri => {
// const responseObj = {
// mimeType: blob.type,
// size: blob.size,
// dataString: datauri
// }
// window.webkit.messageHandlers.jsListener.postMessage(JSON.stringify(responseObj))
// })
// }
// run()
// """
// self.webView.evaluateJavaScript(s)
}
private func handleBlobRequest(_ message: WKScriptMessage) {
guard let jsonString = message.body as? String, let jsonData = jsonString.data(using: .utf8) else {
return
}
let decoder = JSONDecoder()
guard let file = try? decoder.decode(BlobComponents.self, from: jsonData) else {
return
}
guard let data = Data(base64Encoded: file.dataString, options: [.ignoreUnknownCharacters]) else {
return
}
guard let url = URL(string: self._state.url) else {
return
}
let path = NSTemporaryDirectory() + NSUUID().uuidString
let _ = try? data.write(to: URL(fileURLWithPath: path), options: .atomic)
let fileName: String
if !url.lastPathComponent.isEmpty {
fileName = url.lastPathComponent
} else {
fileName = "default"
}
let tempFile = TempBox.shared.file(path: path, fileName: fileName)
let fileUrl = URL(fileURLWithPath: tempFile.path)
let controller = legacyICloudFilePicker(theme: self.presentationData.theme, mode: .export, url: fileUrl, documentTypes: [], forceDarkTheme: false, dismissed: {}, completion: { _ in
})
self.present(controller, nil)
}
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
if [-1003, -1100].contains((error as NSError).code) { if [-1003, -1100].contains((error as NSError).code) {
if let url = (error as NSError).userInfo["NSErrorFailingURLKey"] as? URL, url.absoluteString.hasPrefix("itms-appss:") { if let url = (error as NSError).userInfo["NSErrorFailingURLKey"] as? URL, url.absoluteString.hasPrefix("itms-appss:") {

View File

@ -2807,7 +2807,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
}, openStorageManagement: { }, openStorageManagement: {
}, openPasswordSetup: { }, openPasswordSetup: {
}, openPremiumIntro: { }, openPremiumIntro: {
}, openPremiumGift: { _ in }, openPremiumGift: { _, _ in
}, openPremiumManagement: { }, openPremiumManagement: {
}, openActiveSessions: { }, openActiveSessions: {
}, openBirthdaySetup: { }, openBirthdaySetup: {
@ -4645,7 +4645,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode {
let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in
gesture?.cancel() gesture?.cancel()
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: { }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: {
}, openBirthdaySetup: { }, openBirthdaySetup: {
}, performActiveSessionAction: { _, _ in }, performActiveSessionAction: { _, _ in
}, openChatFolderUpdates: {}, hideChatFolderUpdates: { }, openChatFolderUpdates: {}, hideChatFolderUpdates: {

View File

@ -156,7 +156,7 @@ public final class ChatListShimmerNode: ASDisplayNode {
let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in
gesture?.cancel() gesture?.cancel()
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, openStarsTopup: { _ in }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, openStarsTopup: { _ in
}, dismissNotice: { _ in }, dismissNotice: { _ in
}, editPeer: { _ in }, editPeer: { _ in
}) })

View File

@ -101,7 +101,7 @@ public final class ChatListNodeInteraction {
let openStorageManagement: () -> Void let openStorageManagement: () -> Void
let openPasswordSetup: () -> Void let openPasswordSetup: () -> Void
let openPremiumIntro: () -> Void let openPremiumIntro: () -> Void
let openPremiumGift: ([EnginePeer.Id: TelegramBirthday]?) -> Void let openPremiumGift: ([EnginePeer], [EnginePeer.Id: TelegramBirthday]?) -> Void
let openPremiumManagement: () -> Void let openPremiumManagement: () -> Void
let openActiveSessions: () -> Void let openActiveSessions: () -> Void
let openBirthdaySetup: () -> Void let openBirthdaySetup: () -> Void
@ -157,7 +157,7 @@ public final class ChatListNodeInteraction {
openStorageManagement: @escaping () -> Void, openStorageManagement: @escaping () -> Void,
openPasswordSetup: @escaping () -> Void, openPasswordSetup: @escaping () -> Void,
openPremiumIntro: @escaping () -> Void, openPremiumIntro: @escaping () -> Void,
openPremiumGift: @escaping ([EnginePeer.Id: TelegramBirthday]?) -> Void, openPremiumGift: @escaping ([EnginePeer], [EnginePeer.Id: TelegramBirthday]?) -> Void,
openPremiumManagement: @escaping () -> Void, openPremiumManagement: @escaping () -> Void,
openActiveSessions: @escaping () -> Void, openActiveSessions: @escaping () -> Void,
openBirthdaySetup: @escaping () -> Void, openBirthdaySetup: @escaping () -> Void,
@ -741,13 +741,13 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
case .premiumUpgrade, .premiumAnnualDiscount, .premiumRestore: case .premiumUpgrade, .premiumAnnualDiscount, .premiumRestore:
nodeInteraction?.openPremiumIntro() nodeInteraction?.openPremiumIntro()
case .xmasPremiumGift: case .xmasPremiumGift:
nodeInteraction?.openPremiumGift(nil) nodeInteraction?.openPremiumGift([], nil)
case .premiumGrace: case .premiumGrace:
nodeInteraction?.openPremiumManagement() nodeInteraction?.openPremiumManagement()
case .setupBirthday: case .setupBirthday:
nodeInteraction?.openBirthdaySetup() nodeInteraction?.openBirthdaySetup()
case let .birthdayPremiumGift(_, birthdays): case let .birthdayPremiumGift(peers, birthdays):
nodeInteraction?.openPremiumGift(birthdays) nodeInteraction?.openPremiumGift(peers, birthdays)
case .reviewLogin: case .reviewLogin:
break break
case let .starsSubscriptionLowBalance(amount, _): case let .starsSubscriptionLowBalance(amount, _):
@ -1081,13 +1081,13 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
case .premiumUpgrade, .premiumAnnualDiscount, .premiumRestore: case .premiumUpgrade, .premiumAnnualDiscount, .premiumRestore:
nodeInteraction?.openPremiumIntro() nodeInteraction?.openPremiumIntro()
case .xmasPremiumGift: case .xmasPremiumGift:
nodeInteraction?.openPremiumGift(nil) nodeInteraction?.openPremiumGift([], nil)
case .premiumGrace: case .premiumGrace:
nodeInteraction?.openPremiumManagement() nodeInteraction?.openPremiumManagement()
case .setupBirthday: case .setupBirthday:
nodeInteraction?.openBirthdaySetup() nodeInteraction?.openBirthdaySetup()
case let .birthdayPremiumGift(_, birthdays): case let .birthdayPremiumGift(peers, birthdays):
nodeInteraction?.openPremiumGift(birthdays) nodeInteraction?.openPremiumGift(peers, birthdays)
case .reviewLogin: case .reviewLogin:
break break
case let .starsSubscriptionLowBalance(amount, _): case let .starsSubscriptionLowBalance(amount, _):
@ -1710,11 +1710,11 @@ public final class ChatListNode: ListView {
} }
let controller = self.context.sharedContext.makePremiumIntroController(context: self.context, source: .ads, forceDark: false, dismissed: nil) let controller = self.context.sharedContext.makePremiumIntroController(context: self.context, source: .ads, forceDark: false, dismissed: nil)
self.push?(controller) self.push?(controller)
}, openPremiumGift: { [weak self] birthdays in }, openPremiumGift: { [weak self] peers, birthdays in
guard let self else { guard let self else {
return return
} }
if let birthdays, birthdays.count == 1, let peerId = birthdays.keys.first { if peers.count == 1, let peerId = peers.first?.id {
let _ = (self.context.engine.payments.premiumGiftCodeOptions(peerId: nil, onlyCached: true) let _ = (self.context.engine.payments.premiumGiftCodeOptions(peerId: nil, onlyCached: true)
|> filter { !$0.isEmpty } |> filter { !$0.isEmpty }
|> deliverOnMainQueue).start(next: { giftOptions in |> deliverOnMainQueue).start(next: { giftOptions in

View File

@ -30,6 +30,7 @@ public final class MultilineTextWithEntitiesComponent: Component {
public let textShadowColor: UIColor? public let textShadowColor: UIColor?
public let textStroke: (UIColor, CGFloat)? public let textStroke: (UIColor, CGFloat)?
public let highlightColor: UIColor? public let highlightColor: UIColor?
public let handleSpoilers: Bool
public let highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? public let highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)?
public let tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? public let tapAction: (([NSAttributedString.Key: Any], Int) -> Void)?
public let longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? public let longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)?
@ -50,6 +51,7 @@ public final class MultilineTextWithEntitiesComponent: Component {
textShadowColor: UIColor? = nil, textShadowColor: UIColor? = nil,
textStroke: (UIColor, CGFloat)? = nil, textStroke: (UIColor, CGFloat)? = nil,
highlightColor: UIColor? = nil, highlightColor: UIColor? = nil,
handleSpoilers: Bool = false,
highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? = nil, highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? = nil,
tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil, tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil,
longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil
@ -70,6 +72,7 @@ public final class MultilineTextWithEntitiesComponent: Component {
self.textStroke = textStroke self.textStroke = textStroke
self.highlightColor = highlightColor self.highlightColor = highlightColor
self.highlightAction = highlightAction self.highlightAction = highlightAction
self.handleSpoilers = handleSpoilers
self.tapAction = tapAction self.tapAction = tapAction
self.longTapAction = longTapAction self.longTapAction = longTapAction
} }
@ -99,7 +102,9 @@ public final class MultilineTextWithEntitiesComponent: Component {
if lhs.insets != rhs.insets { if lhs.insets != rhs.insets {
return false return false
} }
if lhs.handleSpoilers != rhs.handleSpoilers {
return false
}
if let lhsTextShadowColor = lhs.textShadowColor, let rhsTextShadowColor = rhs.textShadowColor { if let lhsTextShadowColor = lhs.textShadowColor, let rhsTextShadowColor = rhs.textShadowColor {
if !lhsTextShadowColor.isEqual(rhsTextShadowColor) { if !lhsTextShadowColor.isEqual(rhsTextShadowColor) {
return false return false
@ -131,6 +136,7 @@ public final class MultilineTextWithEntitiesComponent: Component {
} }
public final class View: UIView { public final class View: UIView {
var spoilerTextNode: ImmediateTextNodeWithEntities?
let textNode: ImmediateTextNodeWithEntities let textNode: ImmediateTextNodeWithEntities
public override init(frame: CGRect) { public override init(frame: CGRect) {
@ -197,6 +203,45 @@ public final class MultilineTextWithEntitiesComponent: Component {
let size = self.textNode.updateLayout(availableSize) let size = self.textNode.updateLayout(availableSize)
self.textNode.frame = CGRect(origin: .zero, size: size) self.textNode.frame = CGRect(origin: .zero, size: size)
if component.handleSpoilers {
let spoilerTextNode: ImmediateTextNodeWithEntities
if let current = self.spoilerTextNode {
spoilerTextNode = current
} else {
spoilerTextNode = ImmediateTextNodeWithEntities()
spoilerTextNode.alpha = 0.0
self.spoilerTextNode = spoilerTextNode
self.textNode.dustNode?.textNode = spoilerTextNode
}
spoilerTextNode.displaySpoilers = true
spoilerTextNode.displaySpoilerEffect = false
spoilerTextNode.attributedText = attributedString
spoilerTextNode.maximumNumberOfLines = component.maximumNumberOfLines
spoilerTextNode.truncationType = component.truncationType
spoilerTextNode.textAlignment = component.horizontalAlignment
spoilerTextNode.verticalAlignment = component.verticalAlignment
spoilerTextNode.lineSpacing = component.lineSpacing
spoilerTextNode.cutout = component.cutout
spoilerTextNode.insets = component.insets
spoilerTextNode.textShadowColor = component.textShadowColor
spoilerTextNode.textStroke = component.textStroke
spoilerTextNode.isUserInteractionEnabled = false
let size = spoilerTextNode.updateLayout(availableSize)
spoilerTextNode.frame = CGRect(origin: .zero, size: size)
if spoilerTextNode.view.superview == nil {
self.addSubview(spoilerTextNode.view)
}
} else if let spoilerTextNode = self.spoilerTextNode {
self.spoilerTextNode = nil
spoilerTextNode.view.removeFromSuperview()
self.textNode.dustNode?.textNode = nil
}
return size return size
} }
} }

View File

@ -366,6 +366,7 @@ public final class SheetComponent<ChildEnvironmentType: Equatable>: Component {
self.scrollView.addSubview(contentView) self.scrollView.addSubview(contentView)
} }
contentView.clipsToBounds = component.clipsContent contentView.clipsToBounds = component.clipsContent
contentView.layer.cornerRadius = self.backgroundView.layer.cornerRadius
if sheetEnvironment.isCentered { if sheetEnvironment.isCentered {
let y: CGFloat = floorToScreenPixels((availableSize.height - contentSize.height) / 2.0) let y: CGFloat = floorToScreenPixels((availableSize.height - contentSize.height) / 2.0)
transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) / 2.0), y: -y), size: contentSize), completion: nil) transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) / 2.0), y: -y), size: contentSize), completion: nil)

View File

@ -360,7 +360,7 @@ public class InvisibleInkDustNode: ASDisplayNode {
private var animColor: CGColor? private var animColor: CGColor?
private let enableAnimations: Bool private let enableAnimations: Bool
private weak var textNode: ASDisplayNode? public weak var textNode: ASDisplayNode?
private let textMaskNode: ASDisplayNode private let textMaskNode: ASDisplayNode
private let textSpotNode: ASImageNode private let textSpotNode: ASImageNode

View File

@ -222,7 +222,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, ASScrollView
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in
}, activateChatPreview: { _, _, _, gesture, _ in }, activateChatPreview: { _, _, _, gesture, _ in
gesture?.cancel() gesture?.cancel()
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: { }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: {
}, openBirthdaySetup: { }, openBirthdaySetup: {
}, performActiveSessionAction: { _, _ in }, performActiveSessionAction: { _, _ in
}, openChatFolderUpdates: {}, hideChatFolderUpdates: { }, openChatFolderUpdates: {}, hideChatFolderUpdates: {

View File

@ -371,7 +371,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, ASScrollViewDelegate {
}, activateChatPreview: { _, _, _, gesture, _ in }, activateChatPreview: { _, _, _, gesture, _ in
gesture?.cancel() gesture?.cancel()
}, present: { _ in }, present: { _ in
}, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: { }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: {
}, openBirthdaySetup: { }, openBirthdaySetup: {
}, performActiveSessionAction: { _, _ in }, performActiveSessionAction: { _, _ in
}, openChatFolderUpdates: {}, hideChatFolderUpdates: { }, openChatFolderUpdates: {}, hideChatFolderUpdates: {

View File

@ -588,6 +588,7 @@ public enum SendBotPaymentFormError {
case precheckoutFailed case precheckoutFailed
case paymentFailed case paymentFailed
case alreadyPaid case alreadyPaid
case starGiftOutOfStock
} }
public enum SendBotPaymentResult { public enum SendBotPaymentResult {

View File

@ -1313,6 +1313,8 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot
return .fail(.alreadyPaid) return .fail(.alreadyPaid)
} else if error.errorDescription == "MEDIA_ALREADY_PAID" { } else if error.errorDescription == "MEDIA_ALREADY_PAID" {
return .fail(.alreadyPaid) return .fail(.alreadyPaid)
} else if error.errorDescription == "STARGIFT_USAGE_LIMITED" {
return .fail(.starGiftOutOfStock)
} }
return .fail(.generic) return .fail(.generic)
} }

View File

@ -80,7 +80,7 @@ public final class ButtonBadgeComponent: Component {
if contentView.superview == nil { if contentView.superview == nil {
self.addSubview(contentView) self.addSubview(contentView)
} }
transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: floor((backgroundFrame.width - contentSize.width) * 0.5), y: floor((backgroundFrame.height - contentSize.height) * 0.5)), size: contentSize)) transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundFrame.width - contentSize.width) * 0.5), y: floorToScreenPixels((backgroundFrame.height - contentSize.height) * 0.5)), size: contentSize))
} }
if themeUpdated || backgroundFrame.height != self.backgroundView.image?.size.height { if themeUpdated || backgroundFrame.height != self.backgroundView.image?.size.height {
@ -264,7 +264,7 @@ public final class ButtonTextContentComponent: Component {
size.height = max(size.height, badgeSize.height) size.height = max(size.height, badgeSize.height)
} }
let contentFrame = CGRect(origin: CGPoint(x: floor((size.width - measurementSize.width) * 0.5), y: floor((size.height - measurementSize.height) * 0.5)), size: measurementSize) let contentFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - measurementSize.width) * 0.5), y: floorToScreenPixels((size.height - measurementSize.height) * 0.5)), size: measurementSize)
if let contentView = self.content.view { if let contentView = self.content.view {
if contentView.superview == nil { if contentView.superview == nil {
@ -274,7 +274,7 @@ public final class ButtonTextContentComponent: Component {
} }
if let badgeSize, let badge = self.badge { if let badgeSize, let badge = self.badge {
let badgeFrame = CGRect(origin: CGPoint(x: contentFrame.minX + contentSize.width + badgeSpacing, y: floor((size.height - badgeSize.height) * 0.5) + 1.0), size: badgeSize) let badgeFrame = CGRect(origin: CGPoint(x: contentFrame.minX + contentSize.width + badgeSpacing, y: floorToScreenPixels((size.height - badgeSize.height) * 0.5) + 1.0), size: badgeSize)
if let badgeView = badge.view { if let badgeView = badge.view {
var animateIn = false var animateIn = false
@ -490,7 +490,7 @@ public final class ButtonComponent: Component {
contentView.isUserInteractionEnabled = false contentView.isUserInteractionEnabled = false
self.addSubview(contentView) self.addSubview(contentView)
} }
let contentFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - contentSize.width) * 0.5), y: floor((availableSize.height - contentSize.height) * 0.5)), size: contentSize) let contentFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) * 0.5), y: floorToScreenPixels((availableSize.height - contentSize.height) * 0.5)), size: contentSize)
contentTransition.setFrame(view: contentView, frame: contentFrame) contentTransition.setFrame(view: contentView, frame: contentFrame)
contentTransition.setAlpha(view: contentView, alpha: contentAlpha) contentTransition.setAlpha(view: contentView, alpha: contentAlpha)
@ -528,7 +528,7 @@ public final class ButtonComponent: Component {
} }
let indicatorSize = CGSize(width: 22.0, height: 22.0) let indicatorSize = CGSize(width: 22.0, height: 22.0)
transition.setAlpha(view: activityIndicator.view, alpha: 1.0) transition.setAlpha(view: activityIndicator.view, alpha: 1.0)
activityIndicatorTransition.setFrame(view: activityIndicator.view, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - indicatorSize.width) / 2.0), y: floor((availableSize.height - indicatorSize.height) / 2.0)), size: indicatorSize)) activityIndicatorTransition.setFrame(view: activityIndicator.view, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - indicatorSize.width) / 2.0), y: floorToScreenPixels((availableSize.height - indicatorSize.height) / 2.0)), size: indicatorSize))
} else { } else {
if let activityIndicator = self.activityIndicator { if let activityIndicator = self.activityIndicator {
self.activityIndicator = nil self.activityIndicator = nil

View File

@ -648,7 +648,7 @@ public final class ChatInlineSearchResultsListComponent: Component {
}, },
openPremiumIntro: { openPremiumIntro: {
}, },
openPremiumGift: { _ in openPremiumGift: { _, _ in
}, },
openPremiumManagement: { openPremiumManagement: {
}, },

View File

@ -6224,7 +6224,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
} }
for contentNode in self.contentNodes { for contentNode in self.contentNodes {
if contentNode is ChatMessageMediaBubbleContentNode { if contentNode is ChatMessageMediaBubbleContentNode || contentNode is ChatMessageGiftBubbleContentNode {
contentNode.visibility = mapVisibility(effectiveMediaVisibility, boundsSize: self.bounds.size, insets: self.insets, to: contentNode) contentNode.visibility = mapVisibility(effectiveMediaVisibility, boundsSize: self.bounds.size, insets: self.insets, to: contentNode)
} else { } else {
contentNode.visibility = mapVisibility(effectiveVisibility, boundsSize: self.bounds.size, insets: self.insets, to: contentNode) contentNode.visibility = mapVisibility(effectiveVisibility, boundsSize: self.bounds.size, insets: self.insets, to: contentNode)

View File

@ -37,6 +37,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
private var mediaBackgroundContent: WallpaperBubbleBackgroundNode? private var mediaBackgroundContent: WallpaperBubbleBackgroundNode?
private let titleNode: TextNode private let titleNode: TextNode
private let subtitleNode: TextNodeWithEntities private let subtitleNode: TextNodeWithEntities
private var spoilerSubtitleNode: TextNodeWithEntities?
private let textClippingNode: ASDisplayNode private let textClippingNode: ASDisplayNode
private var dustNode: InvisibleInkDustNode? private var dustNode: InvisibleInkDustNode?
private let placeholderNode: StickerShimmerEffectNode private let placeholderNode: StickerShimmerEffectNode
@ -50,6 +51,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
private let buttonStarsNode: PremiumStarsNode private let buttonStarsNode: PremiumStarsNode
private let buttonTitleNode: TextNode private let buttonTitleNode: TextNode
private let moreTextNode: TextNode
private var maskView: UIImageView? private var maskView: UIImageView?
private var maskOverlayView: UIView? private var maskOverlayView: UIView?
@ -61,6 +64,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
private var isExpanded: Bool = false private var isExpanded: Bool = false
private var appliedIsExpanded: Bool = false private var appliedIsExpanded: Bool = false
private var isStarGift = false
private var currentProgressDisposable: Disposable? private var currentProgressDisposable: Disposable?
override public var visibility: ListViewItemNodeVisibility { override public var visibility: ListViewItemNodeVisibility {
@ -138,6 +143,10 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
self.ribbonTextNode.isUserInteractionEnabled = false self.ribbonTextNode.isUserInteractionEnabled = false
self.ribbonTextNode.displaysAsynchronously = false self.ribbonTextNode.displaysAsynchronously = false
self.moreTextNode = TextNode()
self.moreTextNode.isUserInteractionEnabled = false
self.moreTextNode.displaysAsynchronously = false
super.init() super.init()
self.addSubnode(self.labelNode) self.addSubnode(self.labelNode)
@ -147,10 +156,11 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
self.textClippingNode.addSubnode(self.subtitleNode.textNode) self.textClippingNode.addSubnode(self.subtitleNode.textNode)
self.addSubnode(self.placeholderNode) self.addSubnode(self.placeholderNode)
self.addSubnode(self.animationNode) self.addSubnode(self.animationNode)
self.addSubnode(self.moreTextNode)
self.addSubnode(self.buttonNode) self.addSubnode(self.buttonNode)
self.buttonNode.addSubnode(self.buttonStarsNode) self.buttonNode.addSubnode(self.buttonStarsNode)
self.addSubnode(self.buttonTitleNode) self.buttonNode.addSubnode(self.buttonTitleNode)
self.addSubnode(self.ribbonBackgroundNode) self.addSubnode(self.ribbonBackgroundNode)
self.addSubnode(self.ribbonTextNode) self.addSubnode(self.ribbonTextNode)
@ -160,13 +170,9 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
if highlighted { if highlighted {
strongSelf.buttonNode.layer.removeAnimation(forKey: "opacity") strongSelf.buttonNode.layer.removeAnimation(forKey: "opacity")
strongSelf.buttonNode.alpha = 0.4 strongSelf.buttonNode.alpha = 0.4
strongSelf.buttonTitleNode.layer.removeAnimation(forKey: "opacity")
strongSelf.buttonTitleNode.alpha = 0.4
} else { } else {
strongSelf.buttonNode.alpha = 1.0 strongSelf.buttonNode.alpha = 1.0
strongSelf.buttonNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) strongSelf.buttonNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.buttonTitleNode.alpha = 1.0
strongSelf.buttonTitleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
} }
} }
} }
@ -183,6 +189,19 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
self.currentProgressDisposable?.dispose() self.currentProgressDisposable?.dispose()
} }
override public func didLoad() {
super.didLoad()
self.maskView = UIImageView()
let maskOverlayView = UIView()
maskOverlayView.alpha = 0.0
maskOverlayView.backgroundColor = .white
self.maskOverlayView = maskOverlayView
self.maskView?.addSubview(maskOverlayView)
}
@objc private func buttonPressed() { @objc private func buttonPressed() {
guard let item = self.item else { guard let item = self.item else {
return return
@ -190,6 +209,14 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
let _ = item.controllerInteraction.openMessage(item.message, OpenMessageParams(mode: .default, progress: self.makeProgress())) let _ = item.controllerInteraction.openMessage(item.message, OpenMessageParams(mode: .default, progress: self.makeProgress()))
} }
private func expandPressed() {
self.isExpanded = !self.isExpanded
guard let item = self.item else{
return
}
let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false)
}
private func makeProgress() -> Promise<Bool> { private func makeProgress() -> Promise<Bool> {
let progress = Promise<Bool>() let progress = Promise<Bool>()
self.currentProgressDisposable?.dispose() self.currentProgressDisposable?.dispose()
@ -260,9 +287,11 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
let makeLabelLayout = TextNode.asyncLayout(self.labelNode) let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeSubtitleLayout = TextNodeWithEntities.asyncLayout(self.subtitleNode) let makeSubtitleLayout = TextNodeWithEntities.asyncLayout(self.subtitleNode)
let makeSpoilerSubtitleLayout = TextNodeWithEntities.asyncLayout(self.spoilerSubtitleNode)
let makeButtonTitleLayout = TextNode.asyncLayout(self.buttonTitleNode) let makeButtonTitleLayout = TextNode.asyncLayout(self.buttonTitleNode)
let makeRibbonTextLayout = TextNode.asyncLayout(self.ribbonTextNode) let makeRibbonTextLayout = TextNode.asyncLayout(self.ribbonTextNode)
let makeMeasureTextLayout = TextNode.asyncLayout(nil) let makeMeasureTextLayout = TextNode.asyncLayout(nil)
let makeMoreTextLayout = TextNode.asyncLayout(self.moreTextNode)
let cachedMaskBackgroundImage = self.cachedMaskBackgroundImage let cachedMaskBackgroundImage = self.cachedMaskBackgroundImage
@ -290,6 +319,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
var ribbonTitle = "" var ribbonTitle = ""
var hasServiceMessage = true var hasServiceMessage = true
var textSpacing: CGFloat = 0.0 var textSpacing: CGFloat = 0.0
var isStarGift = false
for media in item.message.media { for media in item.message.media {
if let action = media as? TelegramMediaAction { if let action = media as? TelegramMediaAction {
switch action.action { switch action.action {
@ -377,6 +407,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
hasServiceMessage = false hasServiceMessage = false
} }
case let .starGift(gift, convertStars, giftText, giftEntities, _, savedToProfile, converted): case let .starGift(gift, convertStars, giftText, giftEntities, _, savedToProfile, converted):
isStarGift = true
let authorName = item.message.author.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? "" let authorName = item.message.author.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? ""
title = item.presentationData.strings.Notification_StarGift_Title(authorName).string title = item.presentationData.strings.Notification_StarGift_Title(authorName).string
if let giftText, !giftText.isEmpty { if let giftText, !giftText.isEmpty {
@ -439,8 +470,10 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (moreLayout, moreApply) = makeMoreTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Notification_PremiumGift_More, font: Font.semibold(13.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let attributedText: NSAttributedString let attributedText: NSAttributedString
if let _ = animationFile { if !entities.isEmpty {
attributedText = stringWithAppliedEntities(text, entities: entities, baseColor: primaryTextColor, linkColor: primaryTextColor, baseFont: Font.regular(13.0), linkFont: Font.regular(13.0), boldFont: Font.semibold(13.0), italicFont: Font.italic(13.0), boldItalicFont: Font.semiboldItalic(13.0), fixedFont: Font.monospace(13.0), blockQuoteFont: Font.regular(13.0), message: nil) attributedText = stringWithAppliedEntities(text, entities: entities, baseColor: primaryTextColor, linkColor: primaryTextColor, baseFont: Font.regular(13.0), linkFont: Font.regular(13.0), boldFont: Font.semibold(13.0), italicFont: Font.italic(13.0), boldItalicFont: Font.semiboldItalic(13.0), fixedFont: Font.monospace(13.0), blockQuoteFont: Font.regular(13.0), message: nil)
} else { } else {
attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes( attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(
@ -456,6 +489,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
let textConstrainedSize = CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude) let textConstrainedSize = CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude)
let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets())) let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (_, spoilerSubtitleApply) = makeSpoilerSubtitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets(), displaySpoilers: true))
var canExpand = false var canExpand = false
var clippedTextHeight: CGFloat = subtitleLayout.size.height var clippedTextHeight: CGFloat = subtitleLayout.size.height
if subtitleLayout.numberOfLines > 4 { if subtitleLayout.numberOfLines > 4 {
@ -509,7 +544,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
backgroundMaskImage = nil backgroundMaskImage = nil
} }
var backgroundSize = CGSize(width: labelLayout.size.width + 8.0 + 8.0, height: giftSize.height) var backgroundSize = giftSize
if hasServiceMessage { if hasServiceMessage {
backgroundSize.height += labelLayout.size.height + 18.0 backgroundSize.height += labelLayout.size.height + 18.0
} else { } else {
@ -521,13 +556,14 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
if let strongSelf = self { if let strongSelf = self {
let isFirstTime = strongSelf.item == nil let isFirstTime = strongSelf.item == nil
var isExpandedUpdated = false
if strongSelf.appliedIsExpanded != currentIsExpanded { if strongSelf.appliedIsExpanded != currentIsExpanded {
strongSelf.appliedIsExpanded = currentIsExpanded strongSelf.appliedIsExpanded = currentIsExpanded
info?.setInvertOffsetDirection() info?.setInvertOffsetDirection()
isExpandedUpdated = true
if let maskOverlayView = strongSelf.maskOverlayView {
animation.transition.updateAlpha(layer: maskOverlayView.layer, alpha: currentIsExpanded ? 1.0 : 0.0)
}
} }
let _ = isExpandedUpdated
let overlayColor = item.presentationData.theme.theme.overallDarkAppearance ? UIColor(rgb: 0xffffff, alpha: 0.12) : UIColor(rgb: 0x000000, alpha: 0.12) let overlayColor = item.presentationData.theme.theme.overallDarkAppearance ? UIColor(rgb: 0xffffff, alpha: 0.12) : UIColor(rgb: 0x000000, alpha: 0.12)
@ -578,6 +614,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
} }
} }
strongSelf.item = item strongSelf.item = item
strongSelf.isStarGift = isStarGift
strongSelf.updateVisibility() strongSelf.updateVisibility()
@ -599,17 +636,18 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
)) ))
let _ = buttonTitleApply() let _ = buttonTitleApply()
let _ = ribbonTextApply() let _ = ribbonTextApply()
let _ = moreApply()
let labelFrame = CGRect(origin: CGPoint(x: 8.0, y: 2.0), size: labelLayout.size) let labelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - labelLayout.size.width) / 2.0), y: 2.0), size: labelLayout.size)
strongSelf.labelNode.frame = labelFrame strongSelf.labelNode.frame = labelFrame
let titleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - titleLayout.size.width) / 2.0) , y: mediaBackgroundFrame.minY + 151.0), size: titleLayout.size) let titleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - titleLayout.size.width) / 2.0) , y: mediaBackgroundFrame.minY + 151.0), size: titleLayout.size)
strongSelf.titleNode.frame = titleFrame strongSelf.titleNode.frame = titleFrame
let clippingTextFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: titleFrame.maxY + textSpacing), size: CGSize(width: boundingWidth, height: clippedTextHeight)) let clippingTextFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: titleFrame.maxY + textSpacing), size: CGSize(width: subtitleLayout.size.width, height: clippedTextHeight))
let subtitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: titleFrame.maxY + textSpacing), size: subtitleLayout.size) let subtitleFrame = CGRect(origin: .zero, size: subtitleLayout.size)
strongSelf.subtitleNode.textNode.frame = CGRect(origin: .zero, size: subtitleLayout.size) strongSelf.subtitleNode.textNode.frame = subtitleFrame
if isFirstTime { if isFirstTime {
strongSelf.textClippingNode.frame = clippingTextFrame strongSelf.textClippingNode.frame = clippingTextFrame
@ -617,22 +655,37 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
animation.animator.updateFrame(layer: strongSelf.textClippingNode.layer, frame: clippingTextFrame, completion: nil) animation.animator.updateFrame(layer: strongSelf.textClippingNode.layer, frame: clippingTextFrame, completion: nil)
} }
if let maskView = strongSelf.maskView, let maskOverlayView = strongSelf.maskOverlayView { if let maskView = strongSelf.maskView, let maskOverlayView = strongSelf.maskOverlayView {
animation.animator.updateFrame(layer: maskView.layer, frame: CGRect(origin: .zero, size: CGSize(width: boundingWidth, height: clippingTextFrame.size.height)), completion: nil) animation.animator.updateFrame(layer: maskView.layer, frame: CGRect(origin: .zero, size: CGSize(width: clippingTextFrame.width, height: clippingTextFrame.height)), completion: nil)
animation.animator.updateFrame(layer: maskOverlayView.layer, frame: CGRect(origin: .zero, size: CGSize(width: boundingWidth, height: clippingTextFrame.size.height)), completion: nil) animation.animator.updateFrame(layer: maskOverlayView.layer, frame: CGRect(origin: .zero, size: CGSize(width: clippingTextFrame.width, height: clippingTextFrame.height)), completion: nil)
} }
animation.animator.updateFrame(layer: strongSelf.moreTextNode.layer, frame: CGRect(origin: CGPoint(x: clippingTextFrame.maxX - moreLayout.size.width, y: clippingTextFrame.maxY - moreLayout.size.height), size: moreLayout.size), completion: nil)
if !subtitleLayout.spoilers.isEmpty { if !subtitleLayout.spoilers.isEmpty {
let spoilerSubtitleNode = spoilerSubtitleApply(TextNodeWithEntities.Arguments(
context: item.context,
cache: item.controllerInteraction.presentationContext.animationCache,
renderer: item.controllerInteraction.presentationContext.animationRenderer,
placeholderColor: item.presentationData.theme.theme.chat.message.freeform.withWallpaper.reactionInactiveBackground,
attemptSynchronous: synchronousLoads
))
if strongSelf.spoilerSubtitleNode == nil {
spoilerSubtitleNode.textNode.alpha = 0.0
spoilerSubtitleNode.textNode.isUserInteractionEnabled = false
strongSelf.spoilerSubtitleNode = spoilerSubtitleNode
strongSelf.textClippingNode.addSubnode(spoilerSubtitleNode.textNode)
}
spoilerSubtitleNode.textNode.frame = subtitleFrame
let dustColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText let dustColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText
let dustNode: InvisibleInkDustNode let dustNode: InvisibleInkDustNode
if let current = strongSelf.dustNode { if let current = strongSelf.dustNode {
dustNode = current dustNode = current
} else { } else {
dustNode = InvisibleInkDustNode(textNode: nil, enableAnimations: item.context.sharedContext.energyUsageSettings.fullTranslucency) dustNode = InvisibleInkDustNode(textNode: spoilerSubtitleNode.textNode, enableAnimations: item.context.sharedContext.energyUsageSettings.fullTranslucency)
dustNode.isUserInteractionEnabled = false
strongSelf.dustNode = dustNode strongSelf.dustNode = dustNode
strongSelf.insertSubnode(dustNode, aboveSubnode: strongSelf.subtitleNode.textNode) strongSelf.textClippingNode.insertSubnode(dustNode, aboveSubnode: strongSelf.subtitleNode.textNode)
} }
dustNode.frame = subtitleFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 1.0) dustNode.frame = subtitleFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 1.0)
dustNode.update(size: dustNode.frame.size, color: dustColor, textColor: dustColor, rects: subtitleLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: subtitleLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) dustNode.update(size: dustNode.frame.size, color: dustColor, textColor: dustColor, rects: subtitleLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: subtitleLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) })
@ -641,11 +694,10 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
strongSelf.dustNode = nil strongSelf.dustNode = nil
} }
let buttonTitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonTitleLayout.size.width) / 2.0), y: clippingTextFrame.maxY + 18.0), size: buttonTitleLayout.size)
strongSelf.buttonTitleNode.frame = buttonTitleFrame
let buttonSize = CGSize(width: buttonTitleLayout.size.width + 38.0, height: 34.0) let buttonSize = CGSize(width: buttonTitleLayout.size.width + 38.0, height: 34.0)
strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonSize.width) / 2.0), y: clippingTextFrame.maxY + 10.0), size: buttonSize) strongSelf.buttonTitleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((buttonSize.width - buttonTitleLayout.size.width) / 2.0), y: 8.0), size: buttonTitleLayout.size)
animation.animator.updateFrame(layer: strongSelf.buttonNode.layer, frame: CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonSize.width) / 2.0), y: clippingTextFrame.maxY + 10.0), size: buttonSize), completion: nil)
strongSelf.buttonStarsNode.frame = CGRect(origin: .zero, size: buttonSize) strongSelf.buttonStarsNode.frame = CGRect(origin: .zero, size: buttonSize)
if ribbonTextLayout.size.width > 0.0 { if ribbonTextLayout.size.width > 0.0 {
@ -675,6 +727,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
if ribbonTextLayout.size.width > 0.0 { if ribbonTextLayout.size.width > 0.0 {
let backgroundMaskFrame = mediaBackgroundFrame.insetBy(dx: -2.0, dy: -2.0) let backgroundMaskFrame = mediaBackgroundFrame.insetBy(dx: -2.0, dy: -2.0)
backgroundContent.frame = backgroundMaskFrame backgroundContent.frame = backgroundMaskFrame
animation.animator.updateFrame(layer: backgroundContent.layer, frame: backgroundMaskFrame, completion: nil)
backgroundContent.cornerRadius = 0.0 backgroundContent.cornerRadius = 0.0
if strongSelf.mediaBackgroundMaskNode.image?.size != mediaBackgroundFrame.size { if strongSelf.mediaBackgroundMaskNode.image?.size != mediaBackgroundFrame.size {
@ -694,7 +747,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
backgroundContent.view.mask = strongSelf.mediaBackgroundMaskNode.view backgroundContent.view.mask = strongSelf.mediaBackgroundMaskNode.view
strongSelf.mediaBackgroundMaskNode.frame = CGRect(origin: .zero, size: backgroundMaskFrame.size) strongSelf.mediaBackgroundMaskNode.frame = CGRect(origin: .zero, size: backgroundMaskFrame.size)
} else { } else {
backgroundContent.frame = mediaBackgroundFrame animation.animator.updateFrame(layer: backgroundContent.layer, frame: mediaBackgroundFrame, completion: nil)
backgroundContent.clipsToBounds = true backgroundContent.clipsToBounds = true
backgroundContent.cornerRadius = 24.0 backgroundContent.cornerRadius = 24.0
backgroundContent.view.mask = nil backgroundContent.view.mask = nil
@ -734,24 +787,16 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
strongSelf.updateAbsoluteRect(rect, within: size) strongSelf.updateAbsoluteRect(rect, within: size)
} }
if canExpand { if canExpand, let maskView = strongSelf.maskView {
if strongSelf.maskView?.image == nil { if maskView.image == nil {
strongSelf.maskView?.image = generateMaskImage() maskView.image = generateMaskImage()
} }
strongSelf.textClippingNode.view.mask = strongSelf.maskView strongSelf.textClippingNode.view.mask = strongSelf.maskView
// var expandIconFrame: CGRect = .zero animation.animator.updateAlpha(layer: strongSelf.moreTextNode.layer, alpha: strongSelf.isExpanded ? 0.0 : 1.0, completion: nil)
// if let icon = strongSelf.expandIcon.image {
// expandIconFrame = CGRect(origin: CGPoint(x: boundingWidth - icon.size.width - 19.0, y: backgroundFrame.maxY - icon.size.height - 6.0), size: icon.size)
// if wasHidden || isFirstTime {
// strongSelf.expandIcon.position = expandIconFrame.center
// } else {
// animation.animator.updatePosition(layer: strongSelf.expandIcon.layer, position: expandIconFrame.center, completion: nil)
// }
// strongSelf.expandIcon.bounds = CGRect(origin: .zero, size: expandIconFrame.size)
// }
} else { } else {
strongSelf.textClippingNode.view.mask = nil strongSelf.textClippingNode.view.mask = nil
strongSelf.moreTextNode.alpha = 0.0
} }
switch strongSelf.visibility { switch strongSelf.visibility {
@ -855,8 +900,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
} }
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
let textNodeFrame = self.labelNode.frame if let (index, attributes) = self.labelNode.attributesAtPoint(CGPoint(x: point.x - self.labelNode.frame.minX, y: point.y - self.labelNode.frame.minY - 10.0)), gesture == .tap {
if let (index, attributes) = self.labelNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY - 10.0)), gesture == .tap {
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
var concealed = true var concealed = true
if let (attributeText, fullText) = self.labelNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { if let (attributeText, fullText) = self.labelNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) {
@ -874,8 +918,18 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
} }
} }
if let (_, attributes) = self.subtitleNode.textNode.attributesAtPoint(CGPoint(x: point.x - self.textClippingNode.frame.minX, y: point.y - self.textClippingNode.frame.minY)), gesture == .tap {
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)], let dustNode = self.dustNode, !dustNode.isRevealed {
return ChatMessageBubbleContentTapAction(content: .none)
}
}
if self.buttonNode.frame.contains(point) { if self.buttonNode.frame.contains(point) {
return ChatMessageBubbleContentTapAction(content: .ignore) return ChatMessageBubbleContentTapAction(content: .ignore)
} else if self.textClippingNode.frame.contains(point) && !self.isExpanded && !self.moreTextNode.alpha.isZero {
return ChatMessageBubbleContentTapAction(content: .custom({ [weak self] in
self?.expandPressed()
}))
} else if let backgroundNode = self.backgroundNode, backgroundNode.frame.contains(point) { } else if let backgroundNode = self.backgroundNode, backgroundNode.frame.contains(point) {
return ChatMessageBubbleContentTapAction(content: .openMessage) return ChatMessageBubbleContentTapAction(content: .openMessage)
} else if self.mediaBackgroundContent?.frame.contains(point) == true { } else if self.mediaBackgroundContent?.frame.contains(point) == true {
@ -934,7 +988,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
if !alreadySeen && self.animationNode.isPlaying { if !alreadySeen && self.animationNode.isPlaying {
item.controllerInteraction.playNextOutgoingGift = false item.controllerInteraction.playNextOutgoingGift = false
Queue.mainQueue().after(1.0) {
Queue.mainQueue().after(self.isStarGift ? 0.1 : 1.0) {
item.controllerInteraction.animateDiceSuccess(false, true) item.controllerInteraction.animateDiceSuccess(false, true)
} }
} }
@ -943,7 +998,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
} }
private func generateMaskImage() -> UIImage? { private func generateMaskImage() -> UIImage? {
return generateImage(CGSize(width: 140, height: 30), rotatedContext: { size, context in return generateImage(CGSize(width: 100.0, height: 30.0), rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size)) context.clear(CGRect(origin: .zero, size: size))
context.setFillColor(UIColor.white.cgColor) context.setFillColor(UIColor.white.cgColor)
@ -956,7 +1011,7 @@ private func generateMaskImage() -> UIImage? {
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
context.setBlendMode(.copy) context.setBlendMode(.copy)
context.clip(to: CGRect(origin: CGPoint(x: 10.0, y: 8.0), size: CGSize(width: 130.0, height: 22.0))) context.clip(to: CGRect(origin: CGPoint(x: 10.0, y: 12.0), size: CGSize(width: 130.0, height: 18.0)))
context.drawLinearGradient(gradient, start: CGPoint(x: 10.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions()) context.drawLinearGradient(gradient, start: CGPoint(x: 30.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions())
})?.resizableImage(withCapInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 22.0, right: 130.0)) })?.resizableImage(withCapInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 18.0, right: 70.0))
} }

View File

@ -64,6 +64,7 @@ public final class GiftItemComponent: Component {
let ribbon: Ribbon? let ribbon: Ribbon?
let isLoading: Bool let isLoading: Bool
let isHidden: Bool let isHidden: Bool
let isSoldOut: Bool
public init( public init(
context: AccountContext, context: AccountContext,
@ -75,7 +76,8 @@ public final class GiftItemComponent: Component {
price: String, price: String,
ribbon: Ribbon? = nil, ribbon: Ribbon? = nil,
isLoading: Bool = false, isLoading: Bool = false,
isHidden: Bool = false isHidden: Bool = false,
isSoldOut: Bool = false
) { ) {
self.context = context self.context = context
self.theme = theme self.theme = theme
@ -87,6 +89,7 @@ public final class GiftItemComponent: Component {
self.ribbon = ribbon self.ribbon = ribbon
self.isLoading = isLoading self.isLoading = isLoading
self.isHidden = isHidden self.isHidden = isHidden
self.isSoldOut = isSoldOut
} }
public static func ==(lhs: GiftItemComponent, rhs: GiftItemComponent) -> Bool { public static func ==(lhs: GiftItemComponent, rhs: GiftItemComponent) -> Bool {
@ -120,6 +123,9 @@ public final class GiftItemComponent: Component {
if lhs.isHidden != rhs.isHidden { if lhs.isHidden != rhs.isHidden {
return false return false
} }
if lhs.isSoldOut != rhs.isSoldOut {
return false
}
return true return true
} }
@ -284,14 +290,26 @@ public final class GiftItemComponent: Component {
} }
} }
let buttonColor: UIColor
var isStars = false
if component.isSoldOut {
buttonColor = component.theme.list.itemDestructiveColor
} else if component.price.containsEmoji {
buttonColor = UIColor(rgb: 0xd3720a)
isStars = true
} else {
buttonColor = component.theme.list.itemAccentColor
}
let buttonSize = self.button.update( let buttonSize = self.button.update(
transition: transition, transition: transition,
component: AnyComponent( component: AnyComponent(
ButtonContentComponent( ButtonContentComponent(
context: component.context, context: component.context,
text: component.price, text: component.price,
color: component.price.containsEmoji ? UIColor(rgb: 0xd3720a) : component.theme.list.itemAccentColor, color: buttonColor,
isStars: component.price.containsEmoji) isStars: isStars
)
), ),
environment: {}, environment: {},
containerSize: availableSize containerSize: availableSize

View File

@ -25,6 +25,7 @@ import GiftItemComponent
import InAppPurchaseManager import InAppPurchaseManager
import TabSelectorComponent import TabSelectorComponent
import GiftSetupScreen import GiftSetupScreen
import UndoUI
final class GiftOptionsScreenComponent: Component { final class GiftOptionsScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -173,6 +174,20 @@ final class GiftOptionsScreenComponent: Component {
self.updateScrolling(transition: .immediate) self.updateScrolling(transition: .immediate)
} }
private func dismissAllTooltips(controller: ViewController) {
controller.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
controller.dismissWithCommitAction()
}
return true
})
controller.window?.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
controller.dismissWithCommitAction()
}
})
}
private func updateScrolling(transition: ComponentTransition) { private func updateScrolling(transition: ComponentTransition) {
guard let environment = self.environment, let component = self.component else { guard let environment = self.environment, let component = self.component else {
return return
@ -274,6 +289,7 @@ final class GiftOptionsScreenComponent: Component {
self.starsItems[itemId] = visibleItem self.starsItems[itemId] = visibleItem
} }
let isSoldOut = gift.availability?.remains == 0
let _ = visibleItem.update( let _ = visibleItem.update(
transition: itemTransition, transition: itemTransition,
component: AnyComponent( component: AnyComponent(
@ -284,13 +300,14 @@ final class GiftOptionsScreenComponent: Component {
theme: environment.theme, theme: environment.theme,
peer: nil, peer: nil,
subject: .starGift(gift.id, gift.file), subject: .starGift(gift.id, gift.file),
price: "⭐️ \(gift.price)", price: isSoldOut ? environment.strings.Gift_Options_Gift_SoldOut : "⭐️ \(gift.price)",
ribbon: gift.availability != nil ? ribbon: gift.availability != nil ?
GiftItemComponent.Ribbon( GiftItemComponent.Ribbon(
text: environment.strings.Gift_Options_Gift_Limited, text: environment.strings.Gift_Options_Gift_Limited,
color: .blue color: .blue
) )
: nil : nil,
isSoldOut: isSoldOut
) )
), ),
effectAlignment: .center, effectAlignment: .center,
@ -303,13 +320,26 @@ final class GiftOptionsScreenComponent: Component {
} else { } else {
mainController = controller mainController = controller
} }
let giftController = GiftSetupScreen( if gift.availability?.remains == 0 {
context: component.context, self.dismissAllTooltips(controller: mainController)
peerId: component.peerId, let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
subject: .starGift(gift), let resultController = UndoOverlayController(
completion: component.completion presentationData: presentationData,
) content: .sticker(context: component.context, file: gift.file, loop: false, title: nil, text: presentationData.strings.Gift_Options_SoldOut_Text, undoText: nil, customAction: nil),
mainController.push(giftController) elevatedLayout: false,
action: { _ in return true }
)
mainController.present(resultController, in: .window(.root))
HapticFeedback().error()
} else {
let giftController = GiftSetupScreen(
context: component.context,
peerId: component.peerId,
subject: .starGift(gift),
completion: component.completion
)
mainController.push(giftController)
}
} }
} }
}, },
@ -772,8 +802,6 @@ final class GiftOptionsScreenComponent: Component {
return return
} }
let introController = component.context.sharedContext.makeStarsIntroScreen(context: component.context) let introController = component.context.sharedContext.makeStarsIntroScreen(context: component.context)
introController.navigationPresentation = .modal
if let controller = environment.controller() as? GiftOptionsScreen { if let controller = environment.controller() as? GiftOptionsScreen {
let mainController: ViewController let mainController: ViewController
if let parentController = controller.parentController() { if let parentController = controller.parentController() {

View File

@ -43,6 +43,7 @@ swift_library(
"//submodules/TelegramUI/Components/EmojiSuggestionsComponent", "//submodules/TelegramUI/Components/EmojiSuggestionsComponent",
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode", "//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode",
"//submodules/InAppPurchaseManager", "//submodules/InAppPurchaseManager",
"//submodules/Components/BlurredBackgroundComponent",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -258,7 +258,7 @@ final class ChatGiftPreviewItemNode: ListViewItemNode {
apply().1(ListViewItemApply(isOnScreen: true)) apply().1(ListViewItemApply(isOnScreen: true))
}) })
itemNode!.isUserInteractionEnabled = false itemNode!.isUserInteractionEnabled = false
itemNode?.visibility = .visible(1.0, .infinite) itemNode!.visibility = .visible(1.0, .infinite)
messageNodes.append(itemNode!) messageNodes.append(itemNode!)
self.initialBubbleHeight = itemNode?.frame.height self.initialBubbleHeight = itemNode?.frame.height

View File

@ -29,6 +29,7 @@ import ChatPresentationInterfaceState
import AudioToolbox import AudioToolbox
import TextFormat import TextFormat
import InAppPurchaseManager import InAppPurchaseManager
import BlurredBackgroundComponent
final class GiftSetupScreenComponent: Component { final class GiftSetupScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -78,6 +79,9 @@ final class GiftSetupScreenComponent: Component {
private let introContent = ComponentView<Empty>() private let introContent = ComponentView<Empty>()
private let introSection = ComponentView<Empty>() private let introSection = ComponentView<Empty>()
private let hideSection = ComponentView<Empty>() private let hideSection = ComponentView<Empty>()
private let buttonBackground = ComponentView<Empty>()
private let buttonSeparator = SimpleLayer()
private let button = ComponentView<Empty>() private let button = ComponentView<Empty>()
private var ignoreScrolling: Bool = false private var ignoreScrolling: Bool = false
@ -192,6 +196,11 @@ final class GiftSetupScreenComponent: Component {
if let navigationTitleView = self.navigationTitle.view { if let navigationTitleView = self.navigationTitle.view {
transition.setAlpha(view: navigationTitleView, alpha: 1.0) transition.setAlpha(view: navigationTitleView, alpha: 1.0)
} }
let bottomContentOffset = max(0.0, self.scrollView.contentSize.height - self.scrollView.contentOffset.y - self.scrollView.frame.height)
let bottomPanelAlpha = min(16.0, bottomContentOffset) / 16.0
self.buttonBackground.view?.alpha = bottomPanelAlpha
self.buttonSeparator.opacity = Float(bottomPanelAlpha)
} }
func proceed() { func proceed() {
@ -356,6 +365,27 @@ final class GiftSetupScreenComponent: Component {
} }
starsContext.load(force: true) starsContext.load(force: true)
}, error: { [weak self] error in
guard let self, let controller = self.environment?.controller() else {
return
}
self.inProgress = false
self.state?.updated()
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
var errorText: String?
switch error {
case .starGiftOutOfStock:
errorText = presentationData.strings.Gift_Send_ErrorOutOfStock
default:
errorText = presentationData.strings.Gift_Send_ErrorUnknown
}
if let errorText = errorText {
let alertController = textAlertController(context: component.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
controller.present(alertController, in: .window(.root))
}
}) })
}) })
} }
@ -577,9 +607,10 @@ final class GiftSetupScreenComponent: Component {
if case let .starGift(starGift) = component.subject, let availability = starGift.availability { if case let .starGift(starGift) = component.subject, let availability = starGift.availability {
let remains: Int32 = availability.remains let remains: Int32 = availability.remains
let position = CGFloat(remains) / CGFloat(availability.total) let total: Int32 = availability.total
let remainsString = "\(remains)" let position = CGFloat(remains) / CGFloat(total)
let totalString = presentationStringsFormattedNumber(availability.total, environment.dateTimeFormat.groupingSeparator) let remainsString = presentationStringsFormattedNumber(remains, environment.dateTimeFormat.groupingSeparator)
let totalString = presentationStringsFormattedNumber(total, environment.dateTimeFormat.groupingSeparator)
let remainingCountSize = self.remainingCount.update( let remainingCountSize = self.remainingCount.update(
transition: transition, transition: transition,
component: AnyComponent(RemainingCountComponent( component: AnyComponent(RemainingCountComponent(
@ -594,7 +625,9 @@ final class GiftSetupScreenComponent: Component {
badgeText: "\(remainsString)", badgeText: "\(remainsString)",
badgePosition: position, badgePosition: position,
badgeGraphPosition: position, badgeGraphPosition: position,
invertProgress: true invertProgress: true,
leftString: environment.strings.Gift_Send_Remains(remains).replacingOccurrences(of: remainsString, with: "").trimmingCharacters(in: .whitespacesAndNewlines),
groupingSeparator: environment.dateTimeFormat.groupingSeparator
)), )),
environment: {}, environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
@ -826,6 +859,31 @@ final class GiftSetupScreenComponent: Component {
self.starImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: environment.theme.list.itemCheckColors.foregroundColor)!, environment.theme) self.starImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: environment.theme.list.itemCheckColors.foregroundColor)!, environment.theme)
} }
let buttonHeight: CGFloat = 50.0
let bottomPanelPadding: CGFloat = 12.0
let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding
let bottomPanelHeight = bottomPanelPadding + buttonHeight + bottomInset
let bottomPanelSize = self.buttonBackground.update(
transition: transition,
component: AnyComponent(BlurredBackgroundComponent(
color: environment.theme.rootController.tabBar.backgroundColor
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: bottomPanelHeight)
)
self.buttonSeparator.backgroundColor = environment.theme.rootController.tabBar.separatorColor.cgColor
if let view = self.buttonBackground.view {
if view.superview == nil {
self.addSubview(view)
self.layer.addSublayer(self.buttonSeparator)
}
view.frame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelSize.height), size: bottomPanelSize)
self.buttonSeparator.frame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelSize.height), size: CGSize(width: availableSize.width, height: UIScreenPixel))
}
var buttonIsEnabled = true
let buttonString: String let buttonString: String
switch component.subject { switch component.subject {
case let .premium(product): case let .premium(product):
@ -834,6 +892,9 @@ final class GiftSetupScreenComponent: Component {
case let .starGift(starGift): case let .starGift(starGift):
let amountString = presentationStringsFormattedNumber(Int32(starGift.price), presentationData.dateTimeFormat.groupingSeparator) let amountString = presentationStringsFormattedNumber(Int32(starGift.price), presentationData.dateTimeFormat.groupingSeparator)
buttonString = "\(environment.strings.Gift_Send_Send) # \(amountString)" buttonString = "\(environment.strings.Gift_Send_Send) # \(amountString)"
if let availability = starGift.availability, availability.remains == 0 {
buttonIsEnabled = false
}
} }
let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
@ -856,20 +917,20 @@ final class GiftSetupScreenComponent: Component {
id: AnyHashable(0), id: AnyHashable(0),
component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString))) component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))
), ),
isEnabled: true, isEnabled: buttonIsEnabled,
displaysProgress: self.inProgress, displaysProgress: self.inProgress,
action: { [weak self] in action: { [weak self] in
self?.proceed() self?.proceed()
} }
)), )),
environment: {}, environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50) containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: buttonHeight)
) )
if let buttonView = self.button.view { if let buttonView = self.button.view {
if buttonView.superview == nil { if buttonView.superview == nil {
self.addSubview(buttonView) self.addSubview(buttonView)
} }
buttonView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) / 2.0), y: availableSize.height - environment.safeInsets.bottom - buttonSize.height), size: buttonSize) buttonView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) / 2.0), y: availableSize.height - bottomPanelHeight + bottomPanelPadding), size: buttonSize)
} }
if self.textInputState.isEditing, let emojiSuggestion = self.textInputState.currentEmojiSuggestion, emojiSuggestion.disposable == nil { if self.textInputState.isEditing, let emojiSuggestion = self.textInputState.currentEmojiSuggestion, emojiSuggestion.disposable == nil {

View File

@ -26,6 +26,8 @@ public class RemainingCountComponent: Component {
private let badgePosition: CGFloat private let badgePosition: CGFloat
private let badgeGraphPosition: CGFloat private let badgeGraphPosition: CGFloat
private let invertProgress: Bool private let invertProgress: Bool
private let leftString: String
private let groupingSeparator: String
public init( public init(
inactiveColor: UIColor, inactiveColor: UIColor,
@ -39,7 +41,9 @@ public class RemainingCountComponent: Component {
badgeText: String?, badgeText: String?,
badgePosition: CGFloat, badgePosition: CGFloat,
badgeGraphPosition: CGFloat, badgeGraphPosition: CGFloat,
invertProgress: Bool = false invertProgress: Bool = false,
leftString: String,
groupingSeparator: String
) { ) {
self.inactiveColor = inactiveColor self.inactiveColor = inactiveColor
self.activeColors = activeColors self.activeColors = activeColors
@ -53,6 +57,8 @@ public class RemainingCountComponent: Component {
self.badgePosition = badgePosition self.badgePosition = badgePosition
self.badgeGraphPosition = badgeGraphPosition self.badgeGraphPosition = badgeGraphPosition
self.invertProgress = invertProgress self.invertProgress = invertProgress
self.leftString = leftString
self.groupingSeparator = groupingSeparator
} }
public static func ==(lhs: RemainingCountComponent, rhs: RemainingCountComponent) -> Bool { public static func ==(lhs: RemainingCountComponent, rhs: RemainingCountComponent) -> Bool {
@ -92,6 +98,12 @@ public class RemainingCountComponent: Component {
if lhs.invertProgress != rhs.invertProgress { if lhs.invertProgress != rhs.invertProgress {
return false return false
} }
if lhs.leftString != rhs.leftString {
return false
}
if lhs.groupingSeparator != rhs.groupingSeparator {
return false
}
return true return true
} }
@ -118,7 +130,8 @@ public class RemainingCountComponent: Component {
private let badgeShapeLayer = CAShapeLayer() private let badgeShapeLayer = CAShapeLayer()
private let badgeForeground: SimpleLayer private let badgeForeground: SimpleLayer
private let badgeLabel: BadgeLabelView private var badgeLabel: BadgeLabelView?
private let badgeLeftLabel = ComponentView<Empty>()
private let badgeLabelMaskView = UIImageView() private let badgeLabelMaskView = UIImageView()
private var badgeTailPosition: CGFloat = 0.0 private var badgeTailPosition: CGFloat = 0.0
@ -149,10 +162,6 @@ public class RemainingCountComponent: Component {
self.badgeForeground = SimpleLayer() self.badgeForeground = SimpleLayer()
self.badgeLabel = BadgeLabelView()
let _ = self.badgeLabel.update(value: "0", transition: .immediate)
self.badgeLabel.mask = self.badgeLabelMaskView
super.init(frame: frame) super.init(frame: frame)
self.addSubview(self.container) self.addSubview(self.container)
@ -162,7 +171,7 @@ public class RemainingCountComponent: Component {
self.addSubview(self.badgeView) self.addSubview(self.badgeView)
self.badgeView.layer.addSublayer(self.badgeForeground) self.badgeView.layer.addSublayer(self.badgeForeground)
self.badgeView.addSubview(self.badgeLabel) //self.badgeView.addSubview(self.badgeLabel)
self.badgeLabelMaskView.contentMode = .scaleToFill self.badgeLabelMaskView.contentMode = .scaleToFill
self.badgeLabelMaskView.image = generateImage(CGSize(width: 2.0, height: 30.0), rotatedContext: { size, context in self.badgeLabelMaskView.image = generateImage(CGSize(width: 2.0, height: 30.0), rotatedContext: { size, context in
@ -254,14 +263,14 @@ public class RemainingCountComponent: Component {
self.badgeView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) self.badgeView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
} }
if let badgeText = component.badgeText { if let badgeText = component.badgeText, let badgeLabel = self.badgeLabel {
let transition: ComponentTransition = .easeInOut(duration: from != nil ? 0.3 : 0.5) let transition: ComponentTransition = .easeInOut(duration: from != nil ? 0.3 : 0.5)
var frameTransition = transition var frameTransition = transition
if from == nil { if from == nil {
frameTransition = frameTransition.withAnimation(.none) frameTransition = frameTransition.withAnimation(.none)
} }
let badgeLabelSize = self.badgeLabel.update(value: badgeText, transition: transition) let badgeLabelSize = badgeLabel.update(value: badgeText, transition: transition)
frameTransition.setFrame(view: self.badgeLabel, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((badgeFullSize.width - badgeLabelSize.width) / 2.0), y: -2.0), size: badgeLabelSize)) frameTransition.setFrame(view: badgeLabel, frame: CGRect(origin: CGPoint(x: 10.0, y: -2.0), size: badgeLabelSize))
} }
} }
@ -273,7 +282,16 @@ public class RemainingCountComponent: Component {
let size = CGSize(width: availableSize.width, height: 90.0) let size = CGSize(width: availableSize.width, height: 90.0)
self.badgeLabel.color = component.activeTitleColor
if self.badgeLabel == nil {
let badgeLabel = BadgeLabelView(groupingSeparator: component.groupingSeparator)
let _ = badgeLabel.update(value: "0", transition: .immediate)
badgeLabel.mask = self.badgeLabelMaskView
self.badgeLabel = badgeLabel
self.badgeView.addSubview(badgeLabel)
}
self.badgeLabel?.color = component.activeTitleColor
let lineHeight: CGFloat = 30.0 let lineHeight: CGFloat = 30.0
let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - lineHeight), size: CGSize(width: size.width, height: lineHeight)) let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - lineHeight), size: CGSize(width: size.width, height: lineHeight))
@ -292,56 +310,8 @@ public class RemainingCountComponent: Component {
rightTextColor = component.activeTitleColor rightTextColor = component.activeTitleColor
} }
if "".isEmpty { if component.invertProgress {
if component.invertProgress { let innerLeftTitleSize = self.innerLeftTitleLabel.update(
let innerLeftTitleSize = self.innerLeftTitleLabel.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.inactiveTitle,
font: Font.semibold(15.0),
textColor: component.activeTitleColor
)
)
)
),
environment: {},
containerSize: availableSize
)
if let view = self.innerLeftTitleLabel.view {
if view.superview == nil {
self.activeContainer.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((lineHeight - innerLeftTitleSize.height) / 2.0)), size: innerLeftTitleSize)
}
let innerRightTitleSize = self.innerRightTitleLabel.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.activeValue,
font: Font.semibold(15.0),
textColor: component.activeTitleColor
)
)
)
),
environment: {},
containerSize: availableSize
)
if let view = self.innerRightTitleLabel.view {
if view.superview == nil {
self.activeContainer.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: containerFrame.width - 12.0 - innerRightTitleSize.width, y: floorToScreenPixels((lineHeight - innerRightTitleSize.height) / 2.0)), size: innerRightTitleSize)
}
}
let inactiveTitleSize = self.inactiveTitleLabel.update(
transition: .immediate, transition: .immediate,
component: AnyComponent( component: AnyComponent(
MultilineTextComponent( MultilineTextComponent(
@ -349,7 +319,7 @@ public class RemainingCountComponent: Component {
NSAttributedString( NSAttributedString(
string: component.inactiveTitle, string: component.inactiveTitle,
font: Font.semibold(15.0), font: Font.semibold(15.0),
textColor: leftTextColor textColor: component.activeTitleColor
) )
) )
) )
@ -357,60 +327,14 @@ public class RemainingCountComponent: Component {
environment: {}, environment: {},
containerSize: availableSize containerSize: availableSize
) )
if let view = self.inactiveTitleLabel.view { if let view = self.innerLeftTitleLabel.view {
if view.superview == nil { if view.superview == nil {
self.container.addSubview(view) self.activeContainer.addSubview(view)
} }
view.frame = CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((lineHeight - inactiveTitleSize.height) / 2.0)), size: inactiveTitleSize) view.frame = CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((lineHeight - innerLeftTitleSize.height) / 2.0)), size: innerLeftTitleSize)
} }
let inactiveValueSize = self.inactiveValueLabel.update( let innerRightTitleSize = self.innerRightTitleLabel.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.inactiveValue,
font: Font.semibold(15.0),
textColor: leftTextColor
)
)
)
),
environment: {},
containerSize: availableSize
)
if let view = self.inactiveValueLabel.view {
if view.superview == nil {
self.container.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: activityPosition - 12.0 - inactiveValueSize.width, y: floorToScreenPixels((lineHeight - inactiveValueSize.height) / 2.0)), size: inactiveValueSize)
}
let activeTitleSize = self.activeTitleLabel.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.activeTitle,
font: Font.semibold(15.0),
textColor: rightTextColor
)
)
)
),
environment: {},
containerSize: availableSize
)
if let view = self.activeTitleLabel.view {
if view.superview == nil {
self.container.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: activityPosition + 12.0, y: floorToScreenPixels((lineHeight - activeTitleSize.height) / 2.0)), size: activeTitleSize)
}
let activeValueSize = self.activeValueLabel.update(
transition: .immediate, transition: .immediate,
component: AnyComponent( component: AnyComponent(
MultilineTextComponent( MultilineTextComponent(
@ -418,7 +342,7 @@ public class RemainingCountComponent: Component {
NSAttributedString( NSAttributedString(
string: component.activeValue, string: component.activeValue,
font: Font.semibold(15.0), font: Font.semibold(15.0),
textColor: rightTextColor textColor: component.activeTitleColor
) )
) )
) )
@ -426,18 +350,110 @@ public class RemainingCountComponent: Component {
environment: {}, environment: {},
containerSize: availableSize containerSize: availableSize
) )
if let view = self.activeValueLabel.view { if let view = self.innerRightTitleLabel.view {
if view.superview == nil { if view.superview == nil {
self.container.addSubview(view) self.activeContainer.addSubview(view)
if component.invertProgress {
self.container.bringSubviewToFront(self.activeContainer)
}
} }
view.frame = CGRect(origin: CGPoint(x: containerFrame.width - 12.0 - activeValueSize.width, y: floorToScreenPixels((lineHeight - activeValueSize.height) / 2.0)), size: activeValueSize) view.frame = CGRect(origin: CGPoint(x: containerFrame.width - 12.0 - innerRightTitleSize.width, y: floorToScreenPixels((lineHeight - innerRightTitleSize.height) / 2.0)), size: innerRightTitleSize)
} }
} }
let inactiveTitleSize = self.inactiveTitleLabel.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.inactiveTitle,
font: Font.semibold(15.0),
textColor: leftTextColor
)
)
)
),
environment: {},
containerSize: availableSize
)
if let view = self.inactiveTitleLabel.view {
if view.superview == nil {
self.container.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((lineHeight - inactiveTitleSize.height) / 2.0)), size: inactiveTitleSize)
}
let inactiveValueSize = self.inactiveValueLabel.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.inactiveValue,
font: Font.semibold(15.0),
textColor: leftTextColor
)
)
)
),
environment: {},
containerSize: availableSize
)
if let view = self.inactiveValueLabel.view {
if view.superview == nil {
self.container.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: activityPosition - 12.0 - inactiveValueSize.width, y: floorToScreenPixels((lineHeight - inactiveValueSize.height) / 2.0)), size: inactiveValueSize)
}
let activeTitleSize = self.activeTitleLabel.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.activeTitle,
font: Font.semibold(15.0),
textColor: rightTextColor
)
)
)
),
environment: {},
containerSize: availableSize
)
if let view = self.activeTitleLabel.view {
if view.superview == nil {
self.container.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: activityPosition + 12.0, y: floorToScreenPixels((lineHeight - activeTitleSize.height) / 2.0)), size: activeTitleSize)
}
let activeValueSize = self.activeValueLabel.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.activeValue,
font: Font.semibold(15.0),
textColor: rightTextColor
)
)
)
),
environment: {},
containerSize: availableSize
)
if let view = self.activeValueLabel.view {
if view.superview == nil {
self.container.addSubview(view)
if component.invertProgress {
self.container.bringSubviewToFront(self.activeContainer)
}
}
view.frame = CGRect(origin: CGPoint(x: containerFrame.width - 12.0 - activeValueSize.width, y: floorToScreenPixels((lineHeight - activeValueSize.height) / 2.0)), size: activeValueSize)
}
var progressTransition: ComponentTransition = .immediate var progressTransition: ComponentTransition = .immediate
if !transition.animation.isImmediate { if !transition.animation.isImmediate {
progressTransition = .easeInOut(duration: 0.5) progressTransition = .easeInOut(duration: 0.5)
@ -459,14 +475,39 @@ public class RemainingCountComponent: Component {
let countWidth: CGFloat let countWidth: CGFloat
if let badgeText = component.badgeText { if let badgeText = component.badgeText {
countWidth = CGFloat(badgeText.count) * 10.0 countWidth = getLabelWidth(badgeText)
} else { } else {
countWidth = 51.0 countWidth = 51.0
} }
let badgeWidth: CGFloat = countWidth + 20.0
let badgeSpacing: CGFloat = 4.0
let badgeLeftSize = self.badgeLeftLabel.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: component.leftString,
font: Font.semibold(15.0),
textColor: component.activeTitleColor
)
)
)
),
environment: {},
containerSize: availableSize
)
if let view = self.badgeLeftLabel.view {
if view.superview == nil {
self.badgeView.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: 10.0 + countWidth + badgeSpacing, y: 4.0 + UIScreenPixel), size: badgeLeftSize)
}
let badgeWidth: CGFloat = countWidth + 20.0 + badgeSpacing + badgeLeftSize.width
let badgeSize = CGSize(width: badgeWidth, height: 30.0) let badgeSize = CGSize(width: badgeWidth, height: 30.0)
let badgeFullSize = CGSize(width: badgeWidth, height: 30.0 + 8.0) let badgeFullSize = CGSize(width: badgeWidth, height: badgeSize.height + 8.0)
let tailSize = CGSize(width: 15.0, height: 6.0) let tailSize = CGSize(width: 15.0, height: 6.0)
let tailRadius: CGFloat = 3.0 let tailRadius: CGFloat = 3.0
self.badgeMaskView.frame = CGRect(origin: .zero, size: badgeFullSize) self.badgeMaskView.frame = CGRect(origin: .zero, size: badgeFullSize)
@ -538,9 +579,9 @@ public class RemainingCountComponent: Component {
if transition.animation.isImmediate { if transition.animation.isImmediate {
if component.badgePosition < 0.1 { if component.badgePosition < 0.1 {
self.badgeView.alpha = 1.0 self.badgeView.alpha = 1.0
if let badgeText = component.badgeText { if let badgeText = component.badgeText, let badgeLabel = self.badgeLabel {
let badgeLabelSize = self.badgeLabel.update(value: badgeText, transition: .immediate) let badgeLabelSize = badgeLabel.update(value: badgeText, transition: .immediate)
transition.setFrame(view: self.badgeLabel, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((badgeFullSize.width - badgeLabelSize.width) / 2.0), y: -2.0), size: badgeLabelSize)) transition.setFrame(view: badgeLabel, frame: CGRect(origin: CGPoint(x: 10.0, y: -2.0), size: badgeLabelSize))
} }
} else { } else {
self.playAppearanceAnimation(component: component, badgeFullSize: badgeFullSize) self.playAppearanceAnimation(component: component, badgeFullSize: badgeFullSize)
@ -664,6 +705,7 @@ public class RemainingCountComponent: Component {
} }
private let spaceWidth: CGFloat = 3.0
private let labelWidth: CGFloat = 10.0 private let labelWidth: CGFloat = 10.0
private let labelHeight: CGFloat = 30.0 private let labelHeight: CGFloat = 30.0
private let labelSize = CGSize(width: labelWidth, height: labelHeight) private let labelSize = CGSize(width: labelWidth, height: labelHeight)
@ -673,7 +715,7 @@ final class BadgeLabelView: UIView {
private class StackView: UIView { private class StackView: UIView {
var labels: [UILabel] = [] var labels: [UILabel] = []
var currentValue: Int32 = 0 var currentValue: Int32?
var color: UIColor = .white { var color: UIColor = .white {
didSet { didSet {
@ -683,21 +725,27 @@ final class BadgeLabelView: UIView {
} }
} }
init() { init(groupingSeparator: String) {
super.init(frame: CGRect(origin: .zero, size: labelSize)) super.init(frame: CGRect(origin: .zero, size: labelSize))
var height: CGFloat = -labelHeight var height: CGFloat = -labelHeight * 2.0
for i in -1 ..< 10 { for i in -2 ..< 10 {
let label = UILabel() let label = UILabel()
if i == -1 { let itemWidth: CGFloat
if i == -2 {
label.text = groupingSeparator
itemWidth = spaceWidth
} else if i == -1 {
label.text = "9" label.text = "9"
itemWidth = labelWidth
} else { } else {
label.text = "\(i)" label.text = "\(i)"
itemWidth = labelWidth
} }
label.textColor = self.color label.textColor = self.color
label.font = font label.font = font
label.textAlignment = .center label.textAlignment = .center
label.frame = CGRect(x: 0, y: height, width: labelWidth, height: labelHeight) label.frame = CGRect(x: 0, y: height, width: itemWidth, height: labelHeight)
self.addSubview(label) self.addSubview(label)
self.labels.append(label) self.labels.append(label)
@ -709,37 +757,49 @@ final class BadgeLabelView: UIView {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
func update(value: Int32, isFirst: Bool, isLast: Bool, transition: ComponentTransition) { func update(value: Int32?, isFirst: Bool, isLast: Bool, transition: ComponentTransition) {
let previousValue = self.currentValue let previousValue = self.currentValue
self.currentValue = value self.currentValue = value
self.labels[1].alpha = isFirst && !isLast ? 0.0 : 1.0 self.labels[2].alpha = isFirst && !isLast ? 0.0 : 1.0
if previousValue == 9 && value < 9 { if let value {
if previousValue == 9 && value < 9 {
self.bounds = CGRect(
origin: CGPoint(
x: 0.0,
y: -1.0 * labelSize.height
),
size: labelSize
)
}
let bounds = CGRect(
origin: CGPoint(
x: 0.0,
y: CGFloat(value) * labelSize.height
),
size: labelSize
)
transition.setBounds(view: self, bounds: bounds)
} else {
self.bounds = CGRect( self.bounds = CGRect(
origin: CGPoint( origin: CGPoint(
x: 0.0, x: 0.0,
y: -1.0 * labelSize.height y: -2.0 * labelSize.height
), ),
size: labelSize size: labelSize
) )
} }
let bounds = CGRect(
origin: CGPoint(
x: 0.0,
y: CGFloat(value) * labelSize.height
),
size: labelSize
)
transition.setBounds(view: self, bounds: bounds)
} }
} }
private let groupingSeparator: String
private var itemViews: [Int: StackView] = [:] private var itemViews: [Int: StackView] = [:]
private var staticLabel = UILabel()
init() { init(groupingSeparator: String) {
self.groupingSeparator = groupingSeparator
super.init(frame: .zero) super.init(frame: .zero)
self.clipsToBounds = true self.clipsToBounds = true
@ -752,7 +812,6 @@ final class BadgeLabelView: UIView {
var color: UIColor = .white { var color: UIColor = .white {
didSet { didSet {
self.staticLabel.textColor = self.color
for (_, view) in self.itemViews { for (_, view) in self.itemViews {
view.color = self.color view.color = self.color
} }
@ -760,30 +819,12 @@ final class BadgeLabelView: UIView {
} }
func update(value: String, transition: ComponentTransition) -> CGSize { func update(value: String, transition: ComponentTransition) -> CGSize {
if value.contains(" ") {
for (_, view) in self.itemViews {
view.isHidden = true
}
if self.staticLabel.superview == nil {
self.staticLabel.textColor = self.color
self.staticLabel.font = font
self.addSubview(self.staticLabel)
}
self.staticLabel.text = value
let size = self.staticLabel.sizeThatFits(CGSize(width: 100.0, height: 100.0))
self.staticLabel.frame = CGRect(origin: .zero, size: CGSize(width: size.width, height: labelHeight))
return CGSize(width: ceil(self.staticLabel.bounds.width), height: ceil(self.staticLabel.bounds.height))
}
let string = value let string = value
let stringArray = Array(string.map { String($0) }.reversed()) let stringArray = Array(string.map { String($0) }.reversed())
let totalWidth = CGFloat(stringArray.count) * labelWidth let totalWidth: CGFloat = getLabelWidth(value)
var rightX: CGFloat = totalWidth
var validIds: [Int] = [] var validIds: [Int] = []
for i in 0 ..< stringArray.count { for i in 0 ..< stringArray.count {
validIds.append(i) validIds.append(i)
@ -794,18 +835,21 @@ final class BadgeLabelView: UIView {
itemView = current itemView = current
} else { } else {
itemTransition = transition.withAnimation(.none) itemTransition = transition.withAnimation(.none)
itemView = StackView() itemView = StackView(groupingSeparator: self.groupingSeparator)
itemView.color = self.color itemView.color = self.color
self.itemViews[i] = itemView self.itemViews[i] = itemView
self.addSubview(itemView) self.addSubview(itemView)
} }
let digit = Int32(stringArray[i]) ?? 0 let digit = Int32(stringArray[i])
itemView.update(value: digit, isFirst: i == stringArray.count - 1, isLast: i == 0, transition: transition) itemView.update(value: digit, isFirst: i == stringArray.count - 1, isLast: i == 0, transition: transition)
let itemWidth: CGFloat = digit != nil ? labelWidth : spaceWidth
rightX -= itemWidth
itemTransition.setFrame( itemTransition.setFrame(
view: itemView, view: itemView,
frame: CGRect(x: totalWidth - labelWidth * CGFloat(i + 1), y: 0.0, width: labelWidth, height: labelHeight) frame: CGRect(x: rightX, y: 0.0, width: labelWidth, height: labelHeight)
) )
} }
@ -825,3 +869,15 @@ final class BadgeLabelView: UIView {
return CGSize(width: totalWidth, height: labelHeight) return CGSize(width: totalWidth, height: labelHeight)
} }
} }
private func getLabelWidth(_ string: String) -> CGFloat {
var totalWidth: CGFloat = 0.0
for c in string {
if CharacterSet.decimalDigits.contains(c.unicodeScalars[c.unicodeScalars.startIndex]) {
totalWidth += labelWidth
} else {
totalWidth += spaceWidth
}
}
return totalWidth
}

View File

@ -374,7 +374,8 @@ private final class GiftViewSheetContent: CombinedComponent {
animationRenderer: component.context.animationRenderer, animationRenderer: component.context.animationRenderer,
placeholderColor: theme.list.mediaPlaceholderColor, placeholderColor: theme.list.mediaPlaceholderColor,
text: .plain(attributedText), text: .plain(attributedText),
maximumNumberOfLines: 0 maximumNumberOfLines: 0,
handleSpoilers: true
) )
) )
)) ))
@ -916,7 +917,6 @@ public class GiftViewScreen: ViewControllerComponentContainer {
return return
} }
let introController = context.sharedContext.makeStarsIntroScreen(context: context) let introController = context.sharedContext.makeStarsIntroScreen(context: context)
introController.navigationPresentation = .modal
self.push(introController) self.push(introController)
} }
} }

View File

@ -182,7 +182,7 @@ public final class LoadingOverlayNode: ASDisplayNode {
let interaction = ChatListNodeInteraction(context: context, animationCache: context.animationCache, animationRenderer: context.animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in let interaction = ChatListNodeInteraction(context: context, animationCache: context.animationCache, animationRenderer: context.animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in
gesture?.cancel() gesture?.cancel()
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, openStarsTopup: { _ in }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, openStarsTopup: { _ in
}, dismissNotice: { _ in }, dismissNotice: { _ in
}, editPeer: { _ in }, editPeer: { _ in
}) })
@ -507,7 +507,7 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod
}, },
openPremiumIntro: { openPremiumIntro: {
}, },
openPremiumGift: { _ in openPremiumGift: { _, _ in
}, },
openPremiumManagement: { openPremiumManagement: {
}, },

View File

@ -109,6 +109,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
public override func didLoad() { public override func didLoad() {
super.didLoad() super.didLoad()
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
self.scrollNode.view.delegate = self self.scrollNode.view.delegate = self
} }
@ -136,30 +137,32 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
let visibleBounds = self.scrollNode.bounds.insetBy(dx: 0.0, dy: -10.0) let visibleBounds = self.scrollNode.bounds.insetBy(dx: 0.0, dy: -10.0)
let topInset: CGFloat = 60.0
var validIds: [AnyHashable] = [] var validIds: [AnyHashable] = []
var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: 60.0), size: starsOptionSize) var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: topInset), size: starsOptionSize)
var index: Int32 = 0 var index: Int32 = 0
for product in starsProducts { for product in starsProducts {
let itemId = AnyHashable(index)
validIds.append(itemId)
var itemTransition = transition
let visibleItem: ComponentView<Empty>
if let current = self.starsItems[itemId] {
visibleItem = current
} else {
visibleItem = ComponentView()
self.starsItems[itemId] = visibleItem
itemTransition = .immediate
}
var isVisible = false var isVisible = false
if visibleBounds.intersects(itemFrame) { if visibleBounds.intersects(itemFrame) {
isVisible = true isVisible = true
} }
if isVisible { if isVisible {
let itemId = AnyHashable(index)
validIds.append(itemId)
var itemTransition = transition
let visibleItem: ComponentView<Empty>
if let current = self.starsItems[itemId] {
visibleItem = current
} else {
visibleItem = ComponentView()
self.starsItems[itemId] = visibleItem
itemTransition = .immediate
}
let ribbonText: String? let ribbonText: String?
if let availability = product.gift.availability { if let availability = product.gift.availability {
ribbonText = params.presentationData.strings.PeerInfo_Gifts_OneOf(compactNumericCountString(Int(availability.total))).string ribbonText = params.presentationData.strings.PeerInfo_Gifts_OneOf(compactNumericCountString(Int(availability.total))).string
@ -246,8 +249,8 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
self.starsItems.removeValue(forKey: id) self.starsItems.removeValue(forKey: id)
} }
var contentHeight = ceil(CGFloat(starsProducts.count) / 3.0) * (starsOptionSize.height + optionSpacing) - optionSpacing + 60.0 + 16.0 var bottomScrollInset: CGFloat = 0.0
var contentHeight = ceil(CGFloat(starsProducts.count) / 3.0) * (starsOptionSize.height + optionSpacing) - optionSpacing + topInset + 16.0
if self.peerId == self.context.account.peerId { if self.peerId == self.context.account.peerId {
let transition = ComponentTransition.immediate let transition = ComponentTransition.immediate
@ -321,7 +324,8 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
transition.setFrame(view: unlockButton.view, frame: CGRect(origin: CGPoint(x: buttonSideInset, y: size.height - bottomInset - buttonSize.height - scrollOffset), size: buttonSize)) transition.setFrame(view: unlockButton.view, frame: CGRect(origin: CGPoint(x: buttonSideInset, y: size.height - bottomInset - buttonSize.height - scrollOffset), size: buttonSize))
let _ = unlockButton.updateLayout(width: buttonSize.width, transition: .immediate) let _ = unlockButton.updateLayout(width: buttonSize.width, transition: .immediate)
transition.setFrame(view: unlockBackground.view, frame: CGRect(x: 0.0, y: size.height - bottomInset - buttonSize.height - 8.0 - scrollOffset, width: size.width, height: bottomInset + buttonSize.height + 8.0)) let bottomPanelHeight = bottomInset + buttonSize.height + 8.0
transition.setFrame(view: unlockBackground.view, frame: CGRect(x: 0.0, y: size.height - bottomInset - buttonSize.height - 8.0 - scrollOffset, width: size.width, height: bottomPanelHeight))
transition.setFrame(view: unlockSeparator.view, frame: CGRect(x: 0.0, y: size.height - bottomInset - buttonSize.height - 8.0 - scrollOffset, width: size.width, height: UIScreenPixel)) transition.setFrame(view: unlockSeparator.view, frame: CGRect(x: 0.0, y: size.height - bottomInset - buttonSize.height - 8.0 - scrollOffset, width: size.width, height: UIScreenPixel))
let unlockSize = unlockText.update( let unlockSize = unlockText.update(
@ -345,17 +349,22 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: floor((size.width - unlockSize.width) / 2.0), y: contentHeight), size: unlockSize)) transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: floor((size.width - unlockSize.width) / 2.0), y: contentHeight), size: unlockSize))
} }
contentHeight += unlockSize.height contentHeight += unlockSize.height
contentHeight += bottomPanelHeight
bottomScrollInset = bottomPanelHeight - 40.0
} }
contentHeight += params.bottomInset contentHeight += params.bottomInset
self.scrollNode.view.scrollIndicatorInsets = UIEdgeInsets(top: 50.0, left: 0.0, bottom: bottomScrollInset, right: 0.0)
let contentSize = CGSize(width: params.size.width, height: contentHeight) let contentSize = CGSize(width: params.size.width, height: contentHeight)
if self.scrollNode.view.contentSize != contentSize { if self.scrollNode.view.contentSize != contentSize {
self.scrollNode.view.contentSize = contentSize self.scrollNode.view.contentSize = contentSize
} }
} }
let bottomOffset = max(0.0, self.scrollNode.view.contentSize.height - self.scrollNode.view.contentOffset.y - self.scrollNode.view.frame.height) let bottomContentOffset = max(0.0, self.scrollNode.view.contentSize.height - self.scrollNode.view.contentOffset.y - self.scrollNode.view.frame.height)
if bottomOffset < 100.0 { if bottomContentOffset < 200.0 {
self.profileGifts.loadMore() self.profileGifts.loadMore()
} }
} }

View File

@ -108,11 +108,9 @@ private final class ArchiveInfoSheetContentComponent: Component {
} }
contentHeight += buttonSize.height contentHeight += buttonSize.height
if environment.safeInsets.bottom.isZero { let bottomPanelPadding: CGFloat = 12.0
contentHeight += 16.0 let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding
} else { contentHeight += bottomInset
contentHeight += environment.safeInsets.bottom + 14.0
}
return CGSize(width: availableSize.width, height: contentHeight) return CGSize(width: availableSize.width, height: contentHeight)
} }
@ -226,7 +224,7 @@ private final class ArchiveInfoScreenComponent: Component {
}) })
} }
)), )),
backgroundColor: .color(environment.theme.list.plainBackgroundColor), backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
animateOut: self.sheetAnimateOut animateOut: self.sheetAnimateOut
)), )),
environment: { environment: {

View File

@ -186,7 +186,7 @@ final class GreetingMessageListItemComponent: Component {
}, },
openPremiumIntro: { openPremiumIntro: {
}, },
openPremiumGift: { _ in openPremiumGift: { _, _ in
}, },
openPremiumManagement: { openPremiumManagement: {
}, },

View File

@ -201,7 +201,7 @@ final class QuickReplySetupScreenComponent: Component {
}, },
openPremiumIntro: { openPremiumIntro: {
}, },
openPremiumGift: { _ in openPremiumGift: { _, _ in
}, },
openPremiumManagement: { openPremiumManagement: {
}, },

View File

@ -865,7 +865,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate
}, activateChatPreview: { _, _, _, gesture, _ in }, activateChatPreview: { _, _, _, gesture, _ in
gesture?.cancel() gesture?.cancel()
}, present: { _ in }, present: { _ in
}, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: { }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: {
}, openBirthdaySetup: { }, openBirthdaySetup: {
}, performActiveSessionAction: { _, _ in }, performActiveSessionAction: { _, _ in
}, openChatFolderUpdates: {}, hideChatFolderUpdates: { }, openChatFolderUpdates: {}, hideChatFolderUpdates: {

View File

@ -31,11 +31,9 @@ swift_library(
"//submodules/TextFormat", "//submodules/TextFormat",
"//submodules/TelegramUI/Components/ListSectionComponent", "//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/TelegramUI/Components/ListActionItemComponent", "//submodules/TelegramUI/Components/ListActionItemComponent",
"//submodules/TelegramUI/Components/ScrollComponent",
"//submodules/TelegramUI/Components/Premium/PremiumStarComponent", "//submodules/TelegramUI/Components/Premium/PremiumStarComponent",
"//submodules/TelegramUI/Components/ButtonComponent", "//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/Components/BundleIconComponent", "//submodules/Components/BundleIconComponent",
"//submodules/Components/SolidRoundedButtonComponent",
"//submodules/Components/BlurredBackgroundComponent", "//submodules/Components/BlurredBackgroundComponent",
], ],
visibility = [ visibility = [

View File

@ -8,18 +8,17 @@ import Markdown
import TextFormat import TextFormat
import TelegramPresentationData import TelegramPresentationData
import ViewControllerComponent import ViewControllerComponent
import ScrollComponent
import BundleIconComponent import BundleIconComponent
import BalancedTextComponent import BalancedTextComponent
import MultilineTextComponent import MultilineTextComponent
import SolidRoundedButtonComponent import ButtonComponent
import AccountContext import AccountContext
import ScrollComponent import SheetComponent
import BlurredBackgroundComponent import BlurredBackgroundComponent
import PremiumStarComponent import PremiumStarComponent
private final class ScrollContent: CombinedComponent { private final class SheetContent: CombinedComponent {
typealias EnvironmentType = (ViewControllerComponentContainer.Environment, ScrollChildEnvironment) typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext let context: AccountContext
let openExamples: () -> Void let openExamples: () -> Void
@ -35,7 +34,7 @@ private final class ScrollContent: CombinedComponent {
self.dismiss = dismiss self.dismiss = dismiss
} }
static func ==(lhs: ScrollContent, rhs: ScrollContent) -> Bool { static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool {
if lhs.context !== rhs.context { if lhs.context !== rhs.context {
return false return false
} }
@ -44,10 +43,10 @@ private final class ScrollContent: CombinedComponent {
static var body: Body { static var body: Body {
let star = Child(PremiumStarComponent.self) let star = Child(PremiumStarComponent.self)
let title = Child(BalancedTextComponent.self) let title = Child(BalancedTextComponent.self)
let text = Child(BalancedTextComponent.self) let text = Child(BalancedTextComponent.self)
let list = Child(List<Empty>.self) let list = Child(List<Empty>.self)
let actionButton = Child(ButtonComponent.self)
return { context in return { context in
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
@ -82,13 +81,13 @@ private final class ScrollContent: CombinedComponent {
UIColor(rgb: 0xfdd219) UIColor(rgb: 0xfdd219)
], ],
particleColor: UIColor(rgb: 0xf9b004), particleColor: UIColor(rgb: 0xf9b004),
backgroundColor: environment.theme.list.plainBackgroundColor backgroundColor: environment.theme.actionSheet.opaqueItemBackgroundColor
), ),
availableSize: CGSize(width: min(414.0, context.availableSize.width), height: 220.0), availableSize: CGSize(width: min(414.0, context.availableSize.width), height: 220.0),
transition: context.transition transition: context.transition
) )
context.add(star context.add(star
.position(CGPoint(x: context.availableSize.width / 2.0, y: environment.navigationHeight + 24.0)) .position(CGPoint(x: context.availableSize.width / 2.0, y: 79.0))
) )
let title = title.update( let title = title.update(
@ -193,14 +192,39 @@ private final class ScrollContent: CombinedComponent {
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + list.size.height / 2.0)) .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + list.size.height / 2.0))
) )
contentSize.height += list.size.height contentSize.height += list.size.height
contentSize.height += spacing - 9.0 contentSize.height += spacing
contentSize.height += 12.0 + 50.0 let buttonHeight: CGFloat = 50.0
if environment.safeInsets.bottom > 0 { let bottomPanelPadding: CGFloat = 12.0
contentSize.height += environment.safeInsets.bottom + 5.0 let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding
} else {
contentSize.height += 12.0 contentSize.height += bottomPanelPadding
}
let controller = environment.controller() as? StarsIntroScreen
let actionButton = actionButton.update(
component: ButtonComponent(
background: ButtonComponent.Background(
color: environment.theme.list.itemCheckColors.fillColor,
foreground: environment.theme.list.itemCheckColors.foregroundColor,
pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8)
),
content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(
Text(text: strings.Stars_Info_Done, font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor)
)),
isEnabled: true,
displaysProgress: false,
action: {
controller?.dismissAnimated()
}
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: buttonHeight),
transition: context.transition
)
context.add(actionButton
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + actionButton.size.height / 2.0))
.cornerRadius(10.0)
)
contentSize.height += actionButton.size.height + bottomInset
return contentSize return contentSize
} }
@ -238,129 +262,58 @@ private final class ContainerComponent: CombinedComponent {
} }
static var body: Body { static var body: Body {
let background = Child(Rectangle.self) let sheet = Child(SheetComponent<(EnvironmentType)>.self)
let scroll = Child(ScrollComponent<ViewControllerComponentContainer.Environment>.self) let animateOut = StoredActionSlot(Action<Void>.self)
let bottomPanel = Child(BlurredBackgroundComponent.self)
let bottomSeparator = Child(Rectangle.self)
let actionButton = Child(SolidRoundedButtonComponent.self)
let scrollExternalState = ScrollComponent<EnvironmentType>.ExternalState()
return { context in return { context in
let environment = context.environment[EnvironmentType.self] let environment = context.environment[EnvironmentType.self]
let theme = environment.theme
let strings = environment.strings
let state = context.state
let controller = environment.controller let controller = environment.controller
let background = background.update( let sheet = sheet.update(
component: Rectangle(color: environment.theme.list.plainBackgroundColor), component: SheetComponent<EnvironmentType>(
environment: {}, content: AnyComponent(SheetContent(
availableSize: context.availableSize,
transition: context.transition
)
context.add(background
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
let scroll = scroll.update(
component: ScrollComponent<EnvironmentType>(
content: AnyComponent(ScrollContent(
context: context.component.context, context: context.component.context,
openExamples: context.component.openExamples, openExamples: context.component.openExamples,
dismiss: { dismiss: {
controller()?.dismiss() controller()?.dismiss()
} }
)), )),
externalState: scrollExternalState, backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
contentInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 1.0, right: 0.0), followContentSizeChanges: true,
contentOffsetUpdated: { [weak state] topContentOffset, bottomContentOffset in clipsContent: true,
state?.topContentOffset = topContentOffset animateOut: animateOut
state?.bottomContentOffset = bottomContentOffset
Queue.mainQueue().justDispatch {
state?.updated(transition: .immediate)
}
},
contentOffsetWillCommit: { targetContentOffset in
}
), ),
environment: { environment }, environment: {
environment
SheetComponentEnvironment(
isDisplaying: environment.value.isVisible,
isCentered: environment.metrics.widthClass == .regular,
hasInputHeight: !environment.inputHeight.isZero,
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
dismiss: { animated in
if animated {
animateOut.invoke(Action { _ in
if let controller = controller() {
controller.dismiss(completion: nil)
}
})
} else {
if let controller = controller() {
controller.dismiss(completion: nil)
}
}
}
)
},
availableSize: context.availableSize, availableSize: context.availableSize,
transition: context.transition transition: context.transition
) )
context.add(scroll context.add(sheet
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
) )
let buttonHeight: CGFloat = 50.0
let bottomPanelPadding: CGFloat = 12.0
let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding
let bottomPanelHeight = bottomPanelPadding + buttonHeight + bottomInset
let bottomPanelAlpha: CGFloat
if scrollExternalState.contentHeight > context.availableSize.height {
if let bottomContentOffset = state.bottomContentOffset {
bottomPanelAlpha = min(16.0, bottomContentOffset) / 16.0
} else {
bottomPanelAlpha = 1.0
}
} else {
bottomPanelAlpha = 0.0
}
let bottomPanel = bottomPanel.update(
component: BlurredBackgroundComponent(
color: theme.rootController.tabBar.backgroundColor
),
availableSize: CGSize(width: context.availableSize.width, height: bottomPanelHeight),
transition: context.transition
)
let bottomSeparator = bottomSeparator.update(
component: Rectangle(
color: theme.rootController.tabBar.separatorColor
),
availableSize: CGSize(width: context.availableSize.width, height: UIScreenPixel),
transition: context.transition
)
context.add(bottomPanel
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height / 2.0))
.opacity(bottomPanelAlpha)
)
context.add(bottomSeparator
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height))
.opacity(bottomPanelAlpha)
)
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let actionButton = actionButton.update(
component: SolidRoundedButtonComponent(
title: strings.Stars_Info_Done,
theme: SolidRoundedButtonComponent.Theme(
backgroundColor: theme.list.itemCheckColors.fillColor,
backgroundColors: [],
foregroundColor: theme.list.itemCheckColors.foregroundColor
),
font: .bold,
fontSize: 17.0,
height: buttonHeight,
cornerRadius: 10.0,
gloss: false,
iconName: nil,
animationName: nil,
iconPosition: .left,
action: {
controller()?.dismiss()
}
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0),
transition: context.transition
)
context.add(actionButton
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanelHeight + bottomPanelPadding + actionButton.size.height / 2.0))
)
return context.availableSize return context.availableSize
} }
} }
@ -389,7 +342,7 @@ public final class StarsIntroScreen: ViewControllerComponentContainer {
theme: forceDark ? .dark : .default theme: forceDark ? .dark : .default
) )
self.navigationPresentation = .modal self.navigationPresentation = .flatModal
openExamplesImpl = { [weak self] in openExamplesImpl = { [weak self] in
guard let self else { guard let self else {
@ -408,6 +361,12 @@ public final class StarsIntroScreen: ViewControllerComponentContainer {
required public init(coder aDecoder: NSCoder) { required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
public func dismissAnimated() {
if let view = self.node.hostView.findTaggedView(tag: SheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
}
}
} }
private final class ParagraphComponent: CombinedComponent { private final class ParagraphComponent: CombinedComponent {

View File

@ -732,7 +732,7 @@ private final class StarsTransferSheetComponent: CombinedComponent {
}) })
} }
)), )),
backgroundColor: .blur(.light), backgroundColor: .color(environment.theme.list.modalBlocksBackgroundColor),
followContentSizeChanges: true, followContentSizeChanges: true,
clipsContent: true, clipsContent: true,
animateOut: animateOut animateOut: animateOut

View File

@ -52,7 +52,6 @@ private final class SheetContent: CombinedComponent {
} }
static var body: Body { static var body: Body {
let background = Child(RoundedRectangle.self)
let closeButton = Child(Button.self) let closeButton = Child(Button.self)
let title = Child(Text.self) let title = Child(Text.self)
let amountSection = Child(ListSectionComponent.self) let amountSection = Child(ListSectionComponent.self)
@ -75,15 +74,6 @@ private final class SheetContent: CombinedComponent {
let sideInset: CGFloat = 16.0 let sideInset: CGFloat = 16.0
var contentSize = CGSize(width: context.availableSize.width, height: 18.0) var contentSize = CGSize(width: context.availableSize.width, height: 18.0)
let background = background.update(
component: RoundedRectangle(color: theme.list.blocksBackgroundColor, cornerRadius: 8.0),
availableSize: CGSize(width: context.availableSize.width, height: 1000.0),
transition: .immediate
)
context.add(background
.position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0))
)
let constrainedTitleWidth = context.availableSize.width - 16.0 * 2.0 let constrainedTitleWidth = context.availableSize.width - 16.0 * 2.0
let closeImage: UIImage let closeImage: UIImage
@ -466,7 +456,7 @@ private final class StarsWithdrawSheetComponent: CombinedComponent {
}) })
} }
)), )),
backgroundColor: .blur(.light), backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
followContentSizeChanges: false, followContentSizeChanges: false,
clipsContent: true, clipsContent: true,
isScrollEnabled: false, isScrollEnabled: false,

View File

@ -299,7 +299,7 @@ public class ImmediateTextNodeWithEntities: TextNode {
public var arguments: TextNodeWithEntities.Arguments? public var arguments: TextNodeWithEntities.Arguments?
private var inlineStickerItemLayers: [InlineStickerItemLayer.Key: InlineStickerItemLayer] = [:] private var inlineStickerItemLayers: [InlineStickerItemLayer.Key: InlineStickerItemLayer] = [:]
private var dustNode: InvisibleInkDustNode? public private(set) var dustNode: InvisibleInkDustNode?
public var visibility: Bool = false { public var visibility: Bool = false {
didSet { didSet {

View File

@ -282,7 +282,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, ASScrollViewDe
}, openStorageManagement: { }, openStorageManagement: {
}, openPasswordSetup: { }, openPasswordSetup: {
}, openPremiumIntro: { }, openPremiumIntro: {
}, openPremiumGift: { _ in }, openPremiumGift: { _, _ in
}, openPremiumManagement: { }, openPremiumManagement: {
}, openActiveSessions: { }, openActiveSessions: {
}, openBirthdaySetup: { }, openBirthdaySetup: {

View File

@ -156,7 +156,7 @@ private struct CommandChatInputContextPanelEntry: Comparable, Identifiable {
}, },
openPremiumIntro: { openPremiumIntro: {
}, },
openPremiumGift: { _ in openPremiumGift: { _, _ in
}, },
openPremiumManagement: { openPremiumManagement: {
}, },