diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 166bc5ff2c..923cb57b24 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -13008,6 +13008,8 @@ Sorry for the inconvenience."; "Gift.Options.Gift.Filter.AllGifts" = "All Gifts"; "Gift.Options.Gift.Filter.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"; @@ -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.Send" = "Send a Gift for"; "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"; "Settings.SendGift" = "Send a Gift"; @@ -13061,6 +13068,7 @@ Sorry for the inconvenience."; "Notification.PremiumGift.YearsTitle_1" = "%@ Year Premium"; "Notification.PremiumGift.YearsTitle_any" = "%@ Years Premium"; +"Notification.PremiumGift.More" = "more"; "Notification.PremiumGift.SubscriptionDescription" = "Subscription for exclusive Telegram features."; diff --git a/submodules/AvatarNode/Sources/AvatarNode.swift b/submodules/AvatarNode/Sources/AvatarNode.swift index 3fcfba0dd7..d4870d516a 100644 --- a/submodules/AvatarNode/Sources/AvatarNode.swift +++ b/submodules/AvatarNode/Sources/AvatarNode.swift @@ -931,13 +931,13 @@ public final class AvatarNode: ASDisplayNode { 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)) } - } else if case .anonymousSavedMessagesIcon = parameters.icon { + } else if case let .anonymousSavedMessagesIcon(isColored) = parameters.icon { let factor = bounds.size.width / 60.0 context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0) context.scaleBy(x: factor, y: -factor) 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 { 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)) } diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift index 6392b0de64..cbf8994a0f 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift @@ -1558,19 +1558,23 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz applePayController.presentingViewController?.dismiss(animated: true, completion: nil) } - let text: String + let text: String? switch error { - case .precheckoutFailed: - text = strongSelf.presentationData.strings.Checkout_ErrorPrecheckoutFailed - case .paymentFailed: - text = strongSelf.presentationData.strings.Checkout_ErrorPaymentFailed - case .alreadyPaid: - text = strongSelf.presentationData.strings.Checkout_ErrorInvoiceAlreadyPaid - case .generic: - text = strongSelf.presentationData.strings.Checkout_ErrorGeneric + case .precheckoutFailed: + text = strongSelf.presentationData.strings.Checkout_ErrorPrecheckoutFailed + case .paymentFailed: + text = strongSelf.presentationData.strings.Checkout_ErrorPaymentFailed + case .alreadyPaid: + text = strongSelf.presentationData.strings.Checkout_ErrorInvoiceAlreadyPaid + case .generic: + 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() } diff --git a/submodules/BrowserUI/Sources/BrowserDocumentContent.swift b/submodules/BrowserUI/Sources/BrowserDocumentContent.swift index 5932fd64de..13f76672f3 100644 --- a/submodules/BrowserUI/Sources/BrowserDocumentContent.swift +++ b/submodules/BrowserUI/Sources/BrowserDocumentContent.swift @@ -20,7 +20,7 @@ import UrlEscaping final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate, WKUIDelegate, UIScrollViewDelegate { private let context: AccountContext private var presentationData: PresentationData - let file: TelegramMediaFile + let file: FileMediaReference private let webView: WKWebView @@ -47,7 +47,7 @@ final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate private var tempFile: TempBoxFile? - init(context: AccountContext, presentationData: PresentationData, file: TelegramMediaFile) { + init(context: AccountContext, presentationData: PresentationData, file: FileMediaReference) { self.context = context self.uuid = UUID() self.presentationData = presentationData @@ -63,9 +63,9 @@ final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate var title: String = "file" var url = "" - if let path = self.context.account.postbox.mediaBox.completedResourcePath(file.resource) { + if let path = self.context.account.postbox.mediaBox.completedResourcePath(file.media.resource) { var updatedPath = path - if let fileName = file.fileName { + if let fileName = file.media.fileName { let tempFile = TempBox.shared.file(path: path, fileName: fileName) updatedPath = tempFile.path self.tempFile = tempFile diff --git a/submodules/BrowserUI/Sources/BrowserPdfContent.swift b/submodules/BrowserUI/Sources/BrowserPdfContent.swift index 2b1aec9f0f..d6d6742793 100644 --- a/submodules/BrowserUI/Sources/BrowserPdfContent.swift +++ b/submodules/BrowserUI/Sources/BrowserPdfContent.swift @@ -20,7 +20,7 @@ import PDFKit final class BrowserPdfContent: UIView, BrowserContent, UIScrollViewDelegate, PDFDocumentDelegate { private let context: AccountContext private var presentationData: PresentationData - let file: TelegramMediaFile + let file: FileMediaReference private let pdfView: PDFView private let scrollView: UIScrollView! @@ -53,7 +53,7 @@ final class BrowserPdfContent: UIView, BrowserContent, UIScrollViewDelegate, PDF private var tempFile: TempBoxFile? - init(context: AccountContext, presentationData: PresentationData, file: TelegramMediaFile) { + init(context: AccountContext, presentationData: PresentationData, file: FileMediaReference) { self.context = context self.uuid = UUID() self.presentationData = presentationData @@ -86,9 +86,9 @@ final class BrowserPdfContent: UIView, BrowserContent, UIScrollViewDelegate, PDF var title = "file" var url = "" - if let path = self.context.account.postbox.mediaBox.completedResourcePath(file.resource) { + if let path = self.context.account.postbox.mediaBox.completedResourcePath(file.media.resource) { var updatedPath = path - if let fileName = file.fileName { + if let fileName = file.media.fileName { let tempFile = TempBox.shared.file(path: path, fileName: fileName) updatedPath = tempFile.path self.tempFile = tempFile diff --git a/submodules/BrowserUI/Sources/BrowserScreen.swift b/submodules/BrowserUI/Sources/BrowserScreen.swift index 6e0d4043fc..025c2fd086 100644 --- a/submodules/BrowserUI/Sources/BrowserScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserScreen.swift @@ -500,6 +500,7 @@ public class BrowserScreen: ViewController, MinimizableController { case closeAddressBar case navigateTo(String, Bool) case expand + case saveToFiles } final class Node: ViewControllerTracingNode { @@ -568,10 +569,10 @@ public class BrowserScreen: ViewController, MinimizableController { var isDocument = false if let content = self.content.last { if let documentContent = content as? BrowserDocumentContent { - subject = .media(.standalone(media: documentContent.file)) + subject = .media(documentContent.file.abstract) isDocument = true } else if let documentContent = content as? BrowserPdfContent { - subject = .media(.standalone(media: documentContent.file)) + subject = .media(documentContent.file.abstract) isDocument = true } else { subject = .url(url) @@ -649,7 +650,7 @@ public class BrowserScreen: ViewController, MinimizableController { switch controller.subject { case let .document(file, canShare), let .pdfDocument(file, canShare): processed = true - controller.openDocument(file, canShare) + controller.openDocument(file.media, canShare) default: break } @@ -793,6 +794,10 @@ public class BrowserScreen: ViewController, MinimizableController { if let content = self.content.last { 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) 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 { 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 { @@ -1473,13 +1486,13 @@ public class BrowserScreen: ViewController, MinimizableController { public enum Subject { case webPage(url: String) case instantPage(webPage: TelegramMediaWebpage, anchor: String?, sourceLocation: InstantPageSourceLocation, preloadedResources: [Any]?) - case document(file: TelegramMediaFile, canShare: Bool) - case pdfDocument(file: TelegramMediaFile, canShare: Bool) + case document(file: FileMediaReference, canShare: Bool) + case pdfDocument(file: FileMediaReference, canShare: Bool) public var fileId: MediaId? { switch self { case let .document(file, _), let .pdfDocument(file, _): - return file.fileId + return file.media.fileId default: return nil } diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index 46311dd2ed..9835fdfe84 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -22,6 +22,7 @@ import UrlHandling import SaveProgressScreen import DeviceModel import LegacyMediaPickerUI +import PassKit private final class TonSchemeHandler: NSObject, WKURLSchemeHandler { private final class PendingTask { @@ -213,6 +214,8 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.presentationData = presentationData var handleScriptMessageImpl: ((WKScriptMessage) -> Void)? + var handleContentMessageImpl: ((WKScriptMessage) -> Void)? + var handleBlobMessageImpl: ((WKScriptMessage) -> Void)? let configuration: WKWebViewConfiguration if let preferredConfiguration { @@ -242,7 +245,12 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU contentController.add(WeakScriptMessageHandler { message in handleScriptMessageImpl?(message) }, name: "performAction") - + contentController.add(WeakScriptMessageHandler { message in + handleContentMessageImpl?(message) + }, name: "contentInterface") + contentController.add(WeakScriptMessageHandler { message in + handleBlobMessageImpl?(message) + }, name: "blobInterface") configuration.userContentController = contentController configuration.applicationNameForUserAgent = computedUserAgent() } @@ -323,6 +331,12 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU handleScriptMessageImpl = { [weak self] message in self?.handleScriptMessage(message) } + handleContentMessageImpl = { [weak self] message in + self?.handleContentRequest(message) + } + handleBlobMessageImpl = { [weak self] message in + self?.handleBlobRequest(message) + } } required init?(coder: NSCoder) { @@ -342,13 +356,9 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU } 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 } - guard let eventName = body["eventName"] as? String else { - return - } - switch eventName { case "cancellingTouch": 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) { self.presentationData = presentationData if #available(iOS 15.0, *) { @@ -735,13 +774,17 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU @available(iOS 13.0, *) func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, preferences: WKWebpagePreferences, decisionHandler: @escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void) { if #available(iOS 14.5, *), navigationAction.shouldPerformDownload { - self.presentDownloadConfirmation(fileName: navigationAction.request.mainDocumentURL?.lastPathComponent ?? "file", proceed: { download in - if download { - decisionHandler(.download, preferences) - } else { - decisionHandler(.cancel, preferences) - } - }) + if navigationAction.request.url?.scheme == "blob" { + decisionHandler(.allow, preferences) + } else { + self.presentDownloadConfirmation(fileName: navigationAction.request.mainDocumentURL?.lastPathComponent ?? "file", proceed: { download in + if download { + decisionHandler(.download, preferences) + } else { + decisionHandler(.cancel, preferences) + } + }) + } } else { 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?") { @@ -766,14 +809,22 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU if navigationResponse.canShowMIMEType { decisionHandler(.allow) } else if #available(iOS 14.5, *) { -// decisionHandler(.download) - self.presentDownloadConfirmation(fileName: navigationResponse.response.suggestedFilename ?? "file", proceed: { download in - if download { - decisionHandler(.download) - } else { + if navigationResponse.response.suggestedFilename?.lowercased().hasSuffix(".pkpass") == true { + decisionHandler(.download) + } else { + if let url = navigationResponse.response.url, url.scheme == "blob" { 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 { decisionHandler(.cancel) } @@ -838,10 +889,23 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU let tempFile = TempBox.shared.file(path: path, fileName: fileName) 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) + 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.downloadArguments = 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) { - if let url = webView.url, !url.absoluteString.contains("beatsnvibes") { + guard [NSURLAuthenticationMethodDefault, NSURLAuthenticationMethodHTTPBasic, NSURLAuthenticationMethodHTTPDigest].contains(challenge.protectionSpace.authenticationMethod) else { completionHandler(.performDefaultHandling, nil) return } var completed = false 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 { - completed = true - if let (login, password) = result { - let credential = URLCredential( - user: login, - password: password, - persistence: .permanent - ) - completionHandler(.useCredential, credential) - } else { - completionHandler(.cancelAuthenticationChallenge, nil) + + let authController = authController( + sharedContext: self.context.sharedContext, + updatedPresentationData: nil, + title: self.presentationData.strings.WebBrowser_AuthChallenge_Title(host).string, + text: self.presentationData.strings.WebBrowser_AuthChallenge_Text, + apply: { result in + if !completed { + completed = true + if let (login, password) = result { + let credential = URLCredential( + user: login, + password: password, + persistence: .permanent + ) + completionHandler(.useCredential, credential) + } else { + completionHandler(.cancelAuthenticationChallenge, nil) + } } } - }) + ) authController.dismissed = { byOutsideTap in if byOutsideTap { 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) { if [-1003, -1100].contains((error as NSError).code) { if let url = (error as NSError).userInfo["NSErrorFailingURLKey"] as? URL, url.absoluteString.hasPrefix("itms-appss:") { diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 4f211c7658..995dcb2fe9 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -2807,7 +2807,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { }, openStorageManagement: { }, openPasswordSetup: { }, openPremiumIntro: { - }, openPremiumGift: { _ in + }, openPremiumGift: { _, _ in }, openPremiumManagement: { }, openActiveSessions: { }, 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 }, 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() - }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: { + }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: { }, openBirthdaySetup: { }, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: { diff --git a/submodules/ChatListUI/Sources/ChatListShimmerNode.swift b/submodules/ChatListUI/Sources/ChatListShimmerNode.swift index 7319da6dfb..e7d6cfc338 100644 --- a/submodules/ChatListUI/Sources/ChatListShimmerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListShimmerNode.swift @@ -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 }, 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() - }, 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 }, editPeer: { _ in }) diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index fc2bb3c91e..696b5a791f 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -101,7 +101,7 @@ public final class ChatListNodeInteraction { let openStorageManagement: () -> Void let openPasswordSetup: () -> Void let openPremiumIntro: () -> Void - let openPremiumGift: ([EnginePeer.Id: TelegramBirthday]?) -> Void + let openPremiumGift: ([EnginePeer], [EnginePeer.Id: TelegramBirthday]?) -> Void let openPremiumManagement: () -> Void let openActiveSessions: () -> Void let openBirthdaySetup: () -> Void @@ -157,7 +157,7 @@ public final class ChatListNodeInteraction { openStorageManagement: @escaping () -> Void, openPasswordSetup: @escaping () -> Void, openPremiumIntro: @escaping () -> Void, - openPremiumGift: @escaping ([EnginePeer.Id: TelegramBirthday]?) -> Void, + openPremiumGift: @escaping ([EnginePeer], [EnginePeer.Id: TelegramBirthday]?) -> Void, openPremiumManagement: @escaping () -> Void, openActiveSessions: @escaping () -> Void, openBirthdaySetup: @escaping () -> Void, @@ -741,13 +741,13 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL case .premiumUpgrade, .premiumAnnualDiscount, .premiumRestore: nodeInteraction?.openPremiumIntro() case .xmasPremiumGift: - nodeInteraction?.openPremiumGift(nil) + nodeInteraction?.openPremiumGift([], nil) case .premiumGrace: nodeInteraction?.openPremiumManagement() case .setupBirthday: nodeInteraction?.openBirthdaySetup() - case let .birthdayPremiumGift(_, birthdays): - nodeInteraction?.openPremiumGift(birthdays) + case let .birthdayPremiumGift(peers, birthdays): + nodeInteraction?.openPremiumGift(peers, birthdays) case .reviewLogin: break case let .starsSubscriptionLowBalance(amount, _): @@ -1081,13 +1081,13 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL case .premiumUpgrade, .premiumAnnualDiscount, .premiumRestore: nodeInteraction?.openPremiumIntro() case .xmasPremiumGift: - nodeInteraction?.openPremiumGift(nil) + nodeInteraction?.openPremiumGift([], nil) case .premiumGrace: nodeInteraction?.openPremiumManagement() case .setupBirthday: nodeInteraction?.openBirthdaySetup() - case let .birthdayPremiumGift(_, birthdays): - nodeInteraction?.openPremiumGift(birthdays) + case let .birthdayPremiumGift(peers, birthdays): + nodeInteraction?.openPremiumGift(peers, birthdays) case .reviewLogin: break 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) self.push?(controller) - }, openPremiumGift: { [weak self] birthdays in + }, openPremiumGift: { [weak self] peers, birthdays in guard let self else { 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) |> filter { !$0.isEmpty } |> deliverOnMainQueue).start(next: { giftOptions in diff --git a/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift b/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift index f6e54bbac5..e86e95faad 100644 --- a/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift +++ b/submodules/Components/MultilineTextWithEntitiesComponent/Sources/MultilineTextWithEntitiesComponent.swift @@ -30,6 +30,7 @@ public final class MultilineTextWithEntitiesComponent: Component { public let textShadowColor: UIColor? public let textStroke: (UIColor, CGFloat)? public let highlightColor: UIColor? + public let handleSpoilers: Bool public let highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? public let tapAction: (([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, textStroke: (UIColor, CGFloat)? = nil, highlightColor: UIColor? = nil, + handleSpoilers: Bool = false, highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? = nil, tapAction: (([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.highlightColor = highlightColor self.highlightAction = highlightAction + self.handleSpoilers = handleSpoilers self.tapAction = tapAction self.longTapAction = longTapAction } @@ -99,7 +102,9 @@ public final class MultilineTextWithEntitiesComponent: Component { if lhs.insets != rhs.insets { return false } - + if lhs.handleSpoilers != rhs.handleSpoilers { + return false + } if let lhsTextShadowColor = lhs.textShadowColor, let rhsTextShadowColor = rhs.textShadowColor { if !lhsTextShadowColor.isEqual(rhsTextShadowColor) { return false @@ -131,6 +136,7 @@ public final class MultilineTextWithEntitiesComponent: Component { } public final class View: UIView { + var spoilerTextNode: ImmediateTextNodeWithEntities? let textNode: ImmediateTextNodeWithEntities public override init(frame: CGRect) { @@ -197,6 +203,45 @@ public final class MultilineTextWithEntitiesComponent: Component { let size = self.textNode.updateLayout(availableSize) 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 } } diff --git a/submodules/Components/SheetComponent/Sources/SheetComponent.swift b/submodules/Components/SheetComponent/Sources/SheetComponent.swift index 3912465594..7e61cbe1d5 100644 --- a/submodules/Components/SheetComponent/Sources/SheetComponent.swift +++ b/submodules/Components/SheetComponent/Sources/SheetComponent.swift @@ -366,6 +366,7 @@ public final class SheetComponent: Component { self.scrollView.addSubview(contentView) } contentView.clipsToBounds = component.clipsContent + contentView.layer.cornerRadius = self.backgroundView.layer.cornerRadius if sheetEnvironment.isCentered { 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) diff --git a/submodules/GraphCore/Sources/Charts/Controllers/GeneralChartComponentController.swift b/submodules/GraphCore/Sources/Charts/Controllers/GeneralChartComponentController.swift index 8d9774fa20..1b865e41ea 100644 --- a/submodules/GraphCore/Sources/Charts/Controllers/GeneralChartComponentController.swift +++ b/submodules/GraphCore/Sources/Charts/Controllers/GeneralChartComponentController.swift @@ -342,13 +342,16 @@ class GeneralChartComponentController: ChartThemeContainer { visible: chartVisibility[index]) } - if let currency, let firstValue = values.first, let color = GColor(hexString: "#dda747") { + if let currency, let firstValue = values.first, let starColor = GColor(hexString: "#dda747") { let updatedTitle: String + let color: GColor switch currency { case .ton: updatedTitle = self.strings.revenueInTon + color = firstValue.color case .xtr: updatedTitle = self.strings.revenueInStars + color = starColor } values[0] = ChartDetailsViewModel.Value( prefix: nil, diff --git a/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift b/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift index 0a88ade9d2..2a48c08549 100644 --- a/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift +++ b/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift @@ -360,7 +360,7 @@ public class InvisibleInkDustNode: ASDisplayNode { private var animColor: CGColor? private let enableAnimations: Bool - private weak var textNode: ASDisplayNode? + public weak var textNode: ASDisplayNode? private let textMaskNode: ASDisplayNode private let textSpotNode: ASImageNode diff --git a/submodules/PremiumUI/Sources/CreateGiveawayController.swift b/submodules/PremiumUI/Sources/CreateGiveawayController.swift index b878cc81f9..e77b52af64 100644 --- a/submodules/PremiumUI/Sources/CreateGiveawayController.swift +++ b/submodules/PremiumUI/Sources/CreateGiveawayController.swift @@ -784,7 +784,6 @@ private func createGiveawayControllerEntries( entries.append(.prepaid(presentationData.theme, title, text, prepaidGiveaway)) } - var starsPerUser: Int64 = 0 if case .generic = subject, case .starsGiveaway = state.mode, !starsGiveawayOptions.isEmpty { let selectedOption = starsGiveawayOptions.first(where: { $0.giveawayOption.count == state.stars })! entries.append(.starsHeader(presentationData.theme, presentationData.strings.BoostGift_Stars_Title.uppercased(), presentationData.strings.BoostGift_Stars_Boosts(selectedOption.giveawayOption.yearlyBoosts).uppercased())) @@ -795,13 +794,16 @@ private func createGiveawayControllerEntries( continue } let giftTitle: String = presentationData.strings.BoostGift_Stars_Stars(Int32(product.giveawayOption.count)) - let winners = product.giveawayOption.winners.first(where: { $0.users == state.winners }) ?? product.giveawayOption.winners.first! - let maxWinners = product.giveawayOption.winners.sorted(by: { $0.users < $1.users }).last?.users ?? 1 - - let subtitle = presentationData.strings.BoostGift_Stars_PerUser("\(winners.starsPerUser)").string + + let starsPerUser: Int64 + if let winners = product.giveawayOption.winners.first(where: { $0.users == state.winners }) { + starsPerUser = winners.starsPerUser + } else { + starsPerUser = product.giveawayOption.count / Int64(state.winners) + } + let subtitle = presentationData.strings.BoostGift_Stars_PerUser("\(starsPerUser)").string let label = product.storeProduct.price - starsPerUser = winners.starsPerUser let isSelected = product.giveawayOption.count == state.stars entries.append(.stars(i, presentationData.theme, Int32(product.giveawayOption.count), giftTitle, subtitle, label, isSelected, maxWinners)) @@ -941,7 +943,6 @@ private func createGiveawayControllerEntries( if state.mode == .starsGiveaway { let starsString = presentationData.strings.BoostGift_AdditionalPrizesInfoStars(Int32(state.stars)) if state.prizeDescription.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - let _ = starsPerUser prizeDescriptionInfoText = presentationData.strings.BoostGift_AdditionalPrizesInfoStarsOn(starsString, "").string } else { prizeDescriptionInfoText = presentationData.strings.BoostGift_AdditionalPrizesInfoStarsOn(starsString, presentationData.strings.BoostGift_AdditionalPrizesInfoStarsAndOther("\(state.winners)", state.prizeDescription).string).string diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index 0ea046eeaa..2f6c753e4a 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -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 }, activateChatPreview: { _, _, _, gesture, _ in 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: { }, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: { diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index 61fdf2c69f..1d953a2db4 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -371,7 +371,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, ASScrollViewDelegate { }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() }, present: { _ in - }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: { + }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: { }, openBirthdaySetup: { }, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: { diff --git a/submodules/StatisticsUI/Sources/ChannelStatsController.swift b/submodules/StatisticsUI/Sources/ChannelStatsController.swift index 39a97e73d5..048c815643 100644 --- a/submodules/StatisticsUI/Sources/ChannelStatsController.swift +++ b/submodules/StatisticsUI/Sources/ChannelStatsController.swift @@ -1167,7 +1167,7 @@ private enum StatsEntry: ItemListNodeEntry { detailText = stringForMediumCompactDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat) } - let label = tonAmountAttributedString(formatTonAmountText(transaction.amount, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator, showPlus: true), integralFont: font, fractionalFont: smallLabelFont, color: labelColor).mutableCopy() as! NSMutableAttributedString + let label = tonAmountAttributedString(formatTonAmountText(transaction.amount, dateTimeFormat: presentationData.dateTimeFormat, showPlus: true), integralFont: font, fractionalFont: smallLabelFont, color: labelColor).mutableCopy() as! NSMutableAttributedString label.insert(NSAttributedString(string: " $ ", font: font, textColor: labelColor), at: 1) if let range = label.string.range(of: "$"), let icon = generateTintedImage(image: UIImage(bundleImageName: "Ads/TonMedium"), color: labelColor) { diff --git a/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift b/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift index 01839afd73..02138ea79a 100644 --- a/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift +++ b/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift @@ -176,7 +176,7 @@ final class MonetizationBalanceItemNode: ListViewItemNode, ItemListItemNode { var isStars = false if let stats = item.stats as? RevenueStats { - let cryptoValue = formatTonAmountText(stats.balances.availableBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator) + let cryptoValue = formatTonAmountText(stats.balances.availableBalance, dateTimeFormat: item.presentationData.dateTimeFormat) amountString = tonAmountAttributedString(cryptoValue, integralFont: integralFont, fractionalFont: fractionalFont, color: item.presentationData.theme.list.itemPrimaryTextColor) value = stats.balances.availableBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))" } else if let stats = item.stats as? StarsRevenueStats { diff --git a/submodules/StatisticsUI/Sources/StatsOverviewItem.swift b/submodules/StatisticsUI/Sources/StatsOverviewItem.swift index 3a59f66013..168dd5459d 100644 --- a/submodules/StatisticsUI/Sources/StatsOverviewItem.swift +++ b/submodules/StatisticsUI/Sources/StatsOverviewItem.swift @@ -772,7 +772,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.context, params.width, item.presentationData, - formatTonAmountText(stats.balances.availableBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), + formatTonAmountText(stats.balances.availableBalance, dateTimeFormat: item.presentationData.dateTimeFormat), item.presentationData.strings.Monetization_StarsProceeds_Available, (stats.balances.availableBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton @@ -782,7 +782,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.context, params.width, item.presentationData, - formatTonAmountText(stats.balances.currentBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), + formatTonAmountText(stats.balances.currentBalance, dateTimeFormat: item.presentationData.dateTimeFormat), item.presentationData.strings.Monetization_StarsProceeds_Current, (stats.balances.currentBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.currentBalance, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton @@ -792,7 +792,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.context, params.width, item.presentationData, - formatTonAmountText(stats.balances.overallRevenue, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), + formatTonAmountText(stats.balances.overallRevenue, dateTimeFormat: item.presentationData.dateTimeFormat), item.presentationData.strings.Monetization_StarsProceeds_Total, (stats.balances.overallRevenue == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.overallRevenue, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton @@ -836,7 +836,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.context, params.width, item.presentationData, - formatTonAmountText(stats.balances.availableBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), + formatTonAmountText(stats.balances.availableBalance, dateTimeFormat: item.presentationData.dateTimeFormat), item.presentationData.strings.Monetization_Overview_Available, (stats.balances.availableBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton @@ -846,7 +846,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.context, params.width, item.presentationData, - formatTonAmountText(stats.balances.currentBalance, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), + formatTonAmountText(stats.balances.currentBalance, dateTimeFormat: item.presentationData.dateTimeFormat), item.presentationData.strings.Monetization_Overview_Current, (stats.balances.currentBalance == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.currentBalance, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton @@ -856,7 +856,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.context, params.width, item.presentationData, - formatTonAmountText(stats.balances.overallRevenue, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator), + formatTonAmountText(stats.balances.overallRevenue, dateTimeFormat: item.presentationData.dateTimeFormat), item.presentationData.strings.Monetization_Overview_Total, (stats.balances.overallRevenue == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.overallRevenue, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton diff --git a/submodules/StatisticsUI/Sources/TransactionInfoScreen.swift b/submodules/StatisticsUI/Sources/TransactionInfoScreen.swift index a4be4db91a..61491adb44 100644 --- a/submodules/StatisticsUI/Sources/TransactionInfoScreen.swift +++ b/submodules/StatisticsUI/Sources/TransactionInfoScreen.swift @@ -139,7 +139,7 @@ private final class SheetContent: CombinedComponent { switch component.transaction { case let .proceeds(amount, fromDate, toDate): labelColor = theme.list.itemDisclosureActions.constructive.fillColor - amountString = tonAmountAttributedString(formatTonAmountText(amount, decimalSeparator: dateTimeFormat.decimalSeparator, showPlus: true), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor).mutableCopy() as! NSMutableAttributedString + amountString = tonAmountAttributedString(formatTonAmountText(amount, dateTimeFormat: dateTimeFormat, showPlus: true), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor).mutableCopy() as! NSMutableAttributedString dateString = "\(stringForMediumCompactDate(timestamp: fromDate, strings: strings, dateTimeFormat: dateTimeFormat)) – \(stringForMediumCompactDate(timestamp: toDate, strings: strings, dateTimeFormat: dateTimeFormat))" titleString = strings.Monetization_TransactionInfo_Proceeds buttonTitle = strings.Common_OK @@ -147,7 +147,7 @@ private final class SheetContent: CombinedComponent { showPeer = true case let .withdrawal(status, amount, date, provider, _, transactionUrl): labelColor = theme.list.itemDestructiveColor - amountString = tonAmountAttributedString(formatTonAmountText(amount, decimalSeparator: dateTimeFormat.groupingSeparator), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor).mutableCopy() as! NSMutableAttributedString + amountString = tonAmountAttributedString(formatTonAmountText(amount, dateTimeFormat: dateTimeFormat), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor).mutableCopy() as! NSMutableAttributedString dateString = stringForFullDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat) switch status { @@ -166,7 +166,7 @@ private final class SheetContent: CombinedComponent { case let .refund(amount, date, _): labelColor = theme.list.itemDisclosureActions.constructive.fillColor titleString = strings.Monetization_TransactionInfo_Refund - amountString = tonAmountAttributedString(formatTonAmountText(amount, decimalSeparator: dateTimeFormat.decimalSeparator, showPlus: true), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor).mutableCopy() as! NSMutableAttributedString + amountString = tonAmountAttributedString(formatTonAmountText(amount, dateTimeFormat: dateTimeFormat, showPlus: true), integralFont: integralFont, fractionalFont: fractionalFont, color: labelColor).mutableCopy() as! NSMutableAttributedString dateString = stringForFullDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat) buttonTitle = strings.Common_OK explorerUrl = nil diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift index 1296173518..3fed134536 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift @@ -8,7 +8,7 @@ public enum BotPaymentInvoiceSource { case message(MessageId) case slug(String) case premiumGiveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32, currency: String, amount: Int64, option: PremiumGiftCodeOption) - case giftCode(users: [PeerId], currency: String, amount: Int64, option: PremiumGiftCodeOption) + case giftCode(users: [PeerId], currency: String, amount: Int64, option: PremiumGiftCodeOption, text: String?, entities: [MessageTextEntity]?) case stars(option: StarsTopUpOption) case starsGift(peerId: EnginePeer.Id, count: Int64, currency: String, amount: Int64) case starsChatSubscription(hash: String) @@ -283,7 +283,7 @@ func _internal_parseInputInvoice(transaction: Transaction, source: BotPaymentInv let option: Api.PremiumGiftCodeOption = .premiumGiftCodeOption(flags: flags, users: option.users, months: option.months, storeProduct: option.storeProductId, storeQuantity: option.storeQuantity, currency: option.currency, amount: option.amount) return .inputInvoicePremiumGiftCode(purpose: inputPurpose, option: option) - case let .giftCode(users, currency, amount, option): + case let .giftCode(users, currency, amount, option, text, entities): var inputUsers: [Api.InputUser] = [] if !users.isEmpty { for peerId in users { @@ -293,7 +293,14 @@ func _internal_parseInputInvoice(transaction: Transaction, source: BotPaymentInv } } - let inputPurpose: Api.InputStorePaymentPurpose = .inputStorePaymentPremiumGiftCode(flags: 0, users: inputUsers, boostPeer: nil, currency: currency, amount: amount, message: nil) + var inputPurposeFlags: Int32 = 0 + var textWithEntities: Api.TextWithEntities? + if let text, let entities { + inputPurposeFlags |= (1 << 1) + textWithEntities = .textWithEntities(text: text, entities: apiEntitiesFromMessageTextEntities(entities, associatedPeers: SimpleDictionary())) + } + + let inputPurpose: Api.InputStorePaymentPurpose = .inputStorePaymentPremiumGiftCode(flags: inputPurposeFlags, users: inputUsers, boostPeer: nil, currency: currency, amount: amount, message: textWithEntities) var flags: Int32 = 0 if let _ = option.storeProductId { @@ -588,6 +595,7 @@ public enum SendBotPaymentFormError { case precheckoutFailed case paymentFailed case alreadyPaid + case starGiftOutOfStock } public enum SendBotPaymentResult { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index 1aaffb2ea5..b3041b1984 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -45,7 +45,7 @@ public struct StarGift: Equatable, Codable, PostboxCoding { public let remains: Int32 public let total: Int32 - init(remains: Int32, total: Int32) { + public init(remains: Int32, total: Int32) { self.remains = remains self.total = total } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index 6ae977572f..3e0ed0eab8 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -1313,6 +1313,8 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot return .fail(.alreadyPaid) } else if error.errorDescription == "MEDIA_ALREADY_PAID" { return .fail(.alreadyPaid) + } else if error.errorDescription == "STARGIFT_USAGE_LIMITED" { + return .fail(.starGiftOutOfStock) } return .fail(.generic) } diff --git a/submodules/TelegramStringFormatting/Sources/TonFormat.swift b/submodules/TelegramStringFormatting/Sources/TonFormat.swift index c337430784..7f3f05d663 100644 --- a/submodules/TelegramStringFormatting/Sources/TonFormat.swift +++ b/submodules/TelegramStringFormatting/Sources/TonFormat.swift @@ -10,7 +10,7 @@ public func formatTonAddress(_ address: String) -> String { return address } -public func formatTonUsdValue(_ value: Int64, divide: Bool = true, rate: Double, dateTimeFormat: PresentationDateTimeFormat) -> String { +public func formatTonUsdValue(_ value: Int64, divide: Bool = true, rate: Double = 1.0, dateTimeFormat: PresentationDateTimeFormat) -> String { let decimalSeparator = dateTimeFormat.decimalSeparator let normalizedValue: Double = divide ? Double(value) / 1000000000 : Double(value) var formattedValue = String(format: "%0.2f", normalizedValue * rate) @@ -27,15 +27,15 @@ public func formatTonUsdValue(_ value: Int64, divide: Bool = true, rate: Double, return "$\(formattedValue)" } -public func formatTonAmountText(_ value: Int64, decimalSeparator: String, showPlus: Bool = false) -> String { +public func formatTonAmountText(_ value: Int64, dateTimeFormat: PresentationDateTimeFormat, showPlus: Bool = false) -> String { var balanceText = "\(abs(value))" while balanceText.count < 10 { balanceText.insert("0", at: balanceText.startIndex) } - balanceText.insert(contentsOf: decimalSeparator, at: balanceText.index(balanceText.endIndex, offsetBy: -9)) + balanceText.insert(contentsOf: dateTimeFormat.decimalSeparator, at: balanceText.index(balanceText.endIndex, offsetBy: -9)) while true { if balanceText.hasSuffix("0") { - if balanceText.hasSuffix("\(decimalSeparator)0") { + if balanceText.hasSuffix("\(dateTimeFormat.decimalSeparator)0") { balanceText.removeLast() balanceText.removeLast() break @@ -52,8 +52,18 @@ public func formatTonAmountText(_ value: Int64, decimalSeparator: String, showPl balanceText.insert("+", at: balanceText.startIndex) } - if let dec = balanceText.range(of: decimalSeparator) { - balanceText = String(balanceText[balanceText.startIndex ..< min(balanceText.endIndex, balanceText.index(dec.upperBound, offsetBy: 2))]) + if let dotIndex = balanceText.range(of: dateTimeFormat.decimalSeparator) { + balanceText = String(balanceText[balanceText.startIndex ..< min(balanceText.endIndex, balanceText.index(dotIndex.upperBound, offsetBy: 2))]) + + let integerPartString = balanceText[.. Promise { let progress = Promise() self.currentProgressDisposable?.dispose() @@ -260,9 +287,11 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { let makeLabelLayout = TextNode.asyncLayout(self.labelNode) let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeSubtitleLayout = TextNodeWithEntities.asyncLayout(self.subtitleNode) + let makeSpoilerSubtitleLayout = TextNodeWithEntities.asyncLayout(self.spoilerSubtitleNode) let makeButtonTitleLayout = TextNode.asyncLayout(self.buttonTitleNode) let makeRibbonTextLayout = TextNode.asyncLayout(self.ribbonTextNode) let makeMeasureTextLayout = TextNode.asyncLayout(nil) + let makeMoreTextLayout = TextNode.asyncLayout(self.moreTextNode) let cachedMaskBackgroundImage = self.cachedMaskBackgroundImage @@ -290,6 +319,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { var ribbonTitle = "" var hasServiceMessage = true var textSpacing: CGFloat = 0.0 + var isStarGift = false for media in item.message.media { if let action = media as? TelegramMediaAction { switch action.action { @@ -377,6 +407,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { hasServiceMessage = false } case let .starGift(gift, convertStars, giftText, giftEntities, _, savedToProfile, converted): + isStarGift = true let authorName = item.message.author.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? "" title = item.presentationData.strings.Notification_StarGift_Title(authorName).string 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 (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 - 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) } else { 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 (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 clippedTextHeight: CGFloat = subtitleLayout.size.height if subtitleLayout.numberOfLines > 4 { @@ -509,7 +544,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { backgroundMaskImage = nil } - var backgroundSize = CGSize(width: labelLayout.size.width + 8.0 + 8.0, height: giftSize.height) + var backgroundSize = giftSize if hasServiceMessage { backgroundSize.height += labelLayout.size.height + 18.0 } else { @@ -521,13 +556,14 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { if let strongSelf = self { let isFirstTime = strongSelf.item == nil - var isExpandedUpdated = false if strongSelf.appliedIsExpanded != currentIsExpanded { strongSelf.appliedIsExpanded = currentIsExpanded 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) @@ -578,6 +614,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } } strongSelf.item = item + strongSelf.isStarGift = isStarGift strongSelf.updateVisibility() @@ -599,17 +636,18 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { )) let _ = buttonTitleApply() let _ = ribbonTextApply() - - let labelFrame = CGRect(origin: CGPoint(x: 8.0, y: 2.0), size: labelLayout.size) + let _ = moreApply() + + let labelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - labelLayout.size.width) / 2.0), y: 2.0), size: labelLayout.size) 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) 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) - strongSelf.subtitleNode.textNode.frame = CGRect(origin: .zero, size: subtitleLayout.size) + let subtitleFrame = CGRect(origin: .zero, size: subtitleLayout.size) + strongSelf.subtitleNode.textNode.frame = subtitleFrame if isFirstTime { strongSelf.textClippingNode.frame = clippingTextFrame @@ -617,22 +655,37 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { animation.animator.updateFrame(layer: strongSelf.textClippingNode.layer, frame: clippingTextFrame, completion: nil) } 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: maskOverlayView.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: 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 { + 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 dustNode: InvisibleInkDustNode if let current = strongSelf.dustNode { dustNode = current } else { - dustNode = InvisibleInkDustNode(textNode: nil, enableAnimations: item.context.sharedContext.energyUsageSettings.fullTranslucency) - dustNode.isUserInteractionEnabled = false + dustNode = InvisibleInkDustNode(textNode: spoilerSubtitleNode.textNode, enableAnimations: item.context.sharedContext.energyUsageSettings.fullTranslucency) 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.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 } - 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) - 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) if ribbonTextLayout.size.width > 0.0 { @@ -675,6 +727,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { if ribbonTextLayout.size.width > 0.0 { let backgroundMaskFrame = mediaBackgroundFrame.insetBy(dx: -2.0, dy: -2.0) backgroundContent.frame = backgroundMaskFrame + animation.animator.updateFrame(layer: backgroundContent.layer, frame: backgroundMaskFrame, completion: nil) backgroundContent.cornerRadius = 0.0 if strongSelf.mediaBackgroundMaskNode.image?.size != mediaBackgroundFrame.size { @@ -694,7 +747,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { backgroundContent.view.mask = strongSelf.mediaBackgroundMaskNode.view strongSelf.mediaBackgroundMaskNode.frame = CGRect(origin: .zero, size: backgroundMaskFrame.size) } else { - backgroundContent.frame = mediaBackgroundFrame + animation.animator.updateFrame(layer: backgroundContent.layer, frame: mediaBackgroundFrame, completion: nil) backgroundContent.clipsToBounds = true backgroundContent.cornerRadius = 24.0 backgroundContent.view.mask = nil @@ -734,24 +787,16 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.updateAbsoluteRect(rect, within: size) } - if canExpand { - if strongSelf.maskView?.image == nil { - strongSelf.maskView?.image = generateMaskImage() + if canExpand, let maskView = strongSelf.maskView { + if maskView.image == nil { + maskView.image = generateMaskImage() } strongSelf.textClippingNode.view.mask = strongSelf.maskView -// var expandIconFrame: CGRect = .zero -// 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) -// } + animation.animator.updateAlpha(layer: strongSelf.moreTextNode.layer, alpha: strongSelf.isExpanded ? 0.0 : 1.0, completion: nil) } else { strongSelf.textClippingNode.view.mask = nil + strongSelf.moreTextNode.alpha = 0.0 } switch strongSelf.visibility { @@ -855,8 +900,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } 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 - textNodeFrame.minX, y: point.y - textNodeFrame.minY - 10.0)), gesture == .tap { + 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 url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { var concealed = true 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) { 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) { return ChatMessageBubbleContentTapAction(content: .openMessage) } else if self.mediaBackgroundContent?.frame.contains(point) == true { @@ -934,7 +988,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { if !alreadySeen && self.animationNode.isPlaying { item.controllerInteraction.playNextOutgoingGift = false - Queue.mainQueue().after(1.0) { + + Queue.mainQueue().after(self.isStarGift ? 0.1 : 1.0) { item.controllerInteraction.animateDiceSuccess(false, true) } } @@ -943,7 +998,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } 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.setFillColor(UIColor.white.cgColor) @@ -956,7 +1011,7 @@ private func generateMaskImage() -> UIImage? { let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! context.setBlendMode(.copy) - context.clip(to: CGRect(origin: CGPoint(x: 10.0, y: 8.0), size: CGSize(width: 130.0, height: 22.0))) - context.drawLinearGradient(gradient, start: CGPoint(x: 10.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)) + 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: 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: 18.0, right: 70.0)) } diff --git a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift index 2f1b3612be..fda1f35734 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift @@ -64,6 +64,7 @@ public final class GiftItemComponent: Component { let ribbon: Ribbon? let isLoading: Bool let isHidden: Bool + let isSoldOut: Bool public init( context: AccountContext, @@ -75,7 +76,8 @@ public final class GiftItemComponent: Component { price: String, ribbon: Ribbon? = nil, isLoading: Bool = false, - isHidden: Bool = false + isHidden: Bool = false, + isSoldOut: Bool = false ) { self.context = context self.theme = theme @@ -87,6 +89,7 @@ public final class GiftItemComponent: Component { self.ribbon = ribbon self.isLoading = isLoading self.isHidden = isHidden + self.isSoldOut = isSoldOut } public static func ==(lhs: GiftItemComponent, rhs: GiftItemComponent) -> Bool { @@ -120,6 +123,9 @@ public final class GiftItemComponent: Component { if lhs.isHidden != rhs.isHidden { return false } + if lhs.isSoldOut != rhs.isSoldOut { + return false + } 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( transition: transition, component: AnyComponent( ButtonContentComponent( context: component.context, text: component.price, - color: component.price.containsEmoji ? UIColor(rgb: 0xd3720a) : component.theme.list.itemAccentColor, - isStars: component.price.containsEmoji) + color: buttonColor, + isStars: isStars + ) ), environment: {}, containerSize: availableSize diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift index f7c9b5873b..869d9a61c0 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift @@ -25,6 +25,7 @@ import GiftItemComponent import InAppPurchaseManager import TabSelectorComponent import GiftSetupScreen +import UndoUI final class GiftOptionsScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -173,6 +174,20 @@ final class GiftOptionsScreenComponent: Component { 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) { guard let environment = self.environment, let component = self.component else { return @@ -274,6 +289,7 @@ final class GiftOptionsScreenComponent: Component { self.starsItems[itemId] = visibleItem } + let isSoldOut = gift.availability?.remains == 0 let _ = visibleItem.update( transition: itemTransition, component: AnyComponent( @@ -284,13 +300,14 @@ final class GiftOptionsScreenComponent: Component { theme: environment.theme, peer: nil, subject: .starGift(gift.id, gift.file), - price: "⭐️ \(gift.price)", + price: isSoldOut ? environment.strings.Gift_Options_Gift_SoldOut : "⭐️ \(gift.price)", ribbon: gift.availability != nil ? GiftItemComponent.Ribbon( text: environment.strings.Gift_Options_Gift_Limited, color: .blue ) - : nil + : nil, + isSoldOut: isSoldOut ) ), effectAlignment: .center, @@ -303,13 +320,26 @@ final class GiftOptionsScreenComponent: Component { } else { mainController = controller } - let giftController = GiftSetupScreen( - context: component.context, - peerId: component.peerId, - subject: .starGift(gift), - completion: component.completion - ) - mainController.push(giftController) + if gift.availability?.remains == 0 { + self.dismissAllTooltips(controller: mainController) + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let resultController = UndoOverlayController( + 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), + 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) + } } } }, @@ -771,9 +801,7 @@ final class GiftOptionsScreenComponent: Component { guard let self, let component = self.component, let environment = self.environment else { return } - let introController = component.context.sharedContext.makeStarsIntroScreen(context: component.context) - introController.navigationPresentation = .modal - + let introController = component.context.sharedContext.makeStarsIntroScreen(context: component.context) if let controller = environment.controller() as? GiftOptionsScreen { let mainController: ViewController if let parentController = controller.parentController() { diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD index a5a8867320..82507f195a 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD @@ -43,6 +43,7 @@ swift_library( "//submodules/TelegramUI/Components/EmojiSuggestionsComponent", "//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode", "//submodules/InAppPurchaseManager", + "//submodules/Components/BlurredBackgroundComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift index 12a43a1017..4ec4cfbe93 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift @@ -258,7 +258,7 @@ final class ChatGiftPreviewItemNode: ListViewItemNode { apply().1(ListViewItemApply(isOnScreen: true)) }) itemNode!.isUserInteractionEnabled = false - itemNode?.visibility = .visible(1.0, .infinite) + itemNode!.visibility = .visible(1.0, .infinite) messageNodes.append(itemNode!) self.initialBubbleHeight = itemNode?.frame.height diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index 713deadceb..e503f27700 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -29,6 +29,7 @@ import ChatPresentationInterfaceState import AudioToolbox import TextFormat import InAppPurchaseManager +import BlurredBackgroundComponent final class GiftSetupScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -78,6 +79,9 @@ final class GiftSetupScreenComponent: Component { private let introContent = ComponentView() private let introSection = ComponentView() private let hideSection = ComponentView() + + private let buttonBackground = ComponentView() + private let buttonSeparator = SimpleLayer() private let button = ComponentView() private var ignoreScrolling: Bool = false @@ -192,6 +196,11 @@ final class GiftSetupScreenComponent: Component { if let navigationTitleView = self.navigationTitle.view { 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() { @@ -356,6 +365,27 @@ final class GiftSetupScreenComponent: Component { } 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 { let remains: Int32 = availability.remains - let position = CGFloat(remains) / CGFloat(availability.total) - let remainsString = "\(remains)" - let totalString = presentationStringsFormattedNumber(availability.total, environment.dateTimeFormat.groupingSeparator) + let total: Int32 = availability.total + let position = CGFloat(remains) / CGFloat(total) + let remainsString = presentationStringsFormattedNumber(remains, environment.dateTimeFormat.groupingSeparator) + let totalString = presentationStringsFormattedNumber(total, environment.dateTimeFormat.groupingSeparator) let remainingCountSize = self.remainingCount.update( transition: transition, component: AnyComponent(RemainingCountComponent( @@ -594,7 +625,9 @@ final class GiftSetupScreenComponent: Component { badgeText: "\(remainsString)", badgePosition: 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: {}, 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) } + 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 switch component.subject { case let .premium(product): @@ -834,6 +892,9 @@ final class GiftSetupScreenComponent: Component { case let .starGift(starGift): let amountString = presentationStringsFormattedNumber(Int32(starGift.price), presentationData.dateTimeFormat.groupingSeparator) 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) @@ -856,20 +917,20 @@ final class GiftSetupScreenComponent: Component { id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString))) ), - isEnabled: true, + isEnabled: buttonIsEnabled, displaysProgress: self.inProgress, action: { [weak self] in self?.proceed() } )), 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 buttonView.superview == nil { 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 { diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/RemainingCountComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/RemainingCountComponent.swift index 267382cf3e..bd1e466aba 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/RemainingCountComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/RemainingCountComponent.swift @@ -26,6 +26,8 @@ public class RemainingCountComponent: Component { private let badgePosition: CGFloat private let badgeGraphPosition: CGFloat private let invertProgress: Bool + private let leftString: String + private let groupingSeparator: String public init( inactiveColor: UIColor, @@ -39,7 +41,9 @@ public class RemainingCountComponent: Component { badgeText: String?, badgePosition: CGFloat, badgeGraphPosition: CGFloat, - invertProgress: Bool = false + invertProgress: Bool = false, + leftString: String, + groupingSeparator: String ) { self.inactiveColor = inactiveColor self.activeColors = activeColors @@ -53,6 +57,8 @@ public class RemainingCountComponent: Component { self.badgePosition = badgePosition self.badgeGraphPosition = badgeGraphPosition self.invertProgress = invertProgress + self.leftString = leftString + self.groupingSeparator = groupingSeparator } public static func ==(lhs: RemainingCountComponent, rhs: RemainingCountComponent) -> Bool { @@ -92,6 +98,12 @@ public class RemainingCountComponent: Component { if lhs.invertProgress != rhs.invertProgress { return false } + if lhs.leftString != rhs.leftString { + return false + } + if lhs.groupingSeparator != rhs.groupingSeparator { + return false + } return true } @@ -118,7 +130,8 @@ public class RemainingCountComponent: Component { private let badgeShapeLayer = CAShapeLayer() private let badgeForeground: SimpleLayer - private let badgeLabel: BadgeLabelView + private var badgeLabel: BadgeLabelView? + private let badgeLeftLabel = ComponentView() private let badgeLabelMaskView = UIImageView() private var badgeTailPosition: CGFloat = 0.0 @@ -148,11 +161,7 @@ public class RemainingCountComponent: Component { self.badgeView.mask = self.badgeMaskView self.badgeForeground = SimpleLayer() - - self.badgeLabel = BadgeLabelView() - let _ = self.badgeLabel.update(value: "0", transition: .immediate) - self.badgeLabel.mask = self.badgeLabelMaskView - + super.init(frame: frame) self.addSubview(self.container) @@ -162,7 +171,7 @@ public class RemainingCountComponent: Component { self.addSubview(self.badgeView) self.badgeView.layer.addSublayer(self.badgeForeground) - self.badgeView.addSubview(self.badgeLabel) + //self.badgeView.addSubview(self.badgeLabel) self.badgeLabelMaskView.contentMode = .scaleToFill 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) } - 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) var frameTransition = transition if from == nil { frameTransition = frameTransition.withAnimation(.none) } - let badgeLabelSize = self.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)) + let badgeLabelSize = badgeLabel.update(value: badgeText, transition: transition) + 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) - 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 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 } - if "".isEmpty { - if component.invertProgress { - 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( + if component.invertProgress { + let innerLeftTitleSize = self.innerLeftTitleLabel.update( transition: .immediate, component: AnyComponent( MultilineTextComponent( @@ -349,7 +319,7 @@ public class RemainingCountComponent: Component { NSAttributedString( string: component.inactiveTitle, font: Font.semibold(15.0), - textColor: leftTextColor + textColor: component.activeTitleColor ) ) ) @@ -357,60 +327,14 @@ public class RemainingCountComponent: Component { environment: {}, containerSize: availableSize ) - if let view = self.inactiveTitleLabel.view { + if let view = self.innerLeftTitleLabel.view { 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( - 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( + let innerRightTitleSize = self.innerRightTitleLabel.update( transition: .immediate, component: AnyComponent( MultilineTextComponent( @@ -418,7 +342,7 @@ public class RemainingCountComponent: Component { NSAttributedString( string: component.activeValue, font: Font.semibold(15.0), - textColor: rightTextColor + textColor: component.activeTitleColor ) ) ) @@ -426,17 +350,109 @@ public class RemainingCountComponent: Component { environment: {}, containerSize: availableSize ) - if let view = self.activeValueLabel.view { + if let view = self.innerRightTitleLabel.view { if view.superview == nil { - self.container.addSubview(view) - - if component.invertProgress { - self.container.bringSubviewToFront(self.activeContainer) - } + self.activeContainer.addSubview(view) } - 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 if !transition.animation.isImmediate { @@ -459,14 +475,39 @@ public class RemainingCountComponent: Component { let countWidth: CGFloat if let badgeText = component.badgeText { - countWidth = CGFloat(badgeText.count) * 10.0 + countWidth = getLabelWidth(badgeText) } else { 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 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 tailRadius: CGFloat = 3.0 self.badgeMaskView.frame = CGRect(origin: .zero, size: badgeFullSize) @@ -538,9 +579,9 @@ public class RemainingCountComponent: Component { if transition.animation.isImmediate { if component.badgePosition < 0.1 { self.badgeView.alpha = 1.0 - if let badgeText = component.badgeText { - let badgeLabelSize = self.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)) + if let badgeText = component.badgeText, let badgeLabel = self.badgeLabel { + let badgeLabelSize = badgeLabel.update(value: badgeText, transition: .immediate) + transition.setFrame(view: badgeLabel, frame: CGRect(origin: CGPoint(x: 10.0, y: -2.0), size: badgeLabelSize)) } } else { 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 labelHeight: CGFloat = 30.0 private let labelSize = CGSize(width: labelWidth, height: labelHeight) @@ -673,7 +715,7 @@ final class BadgeLabelView: UIView { private class StackView: UIView { var labels: [UILabel] = [] - var currentValue: Int32 = 0 + var currentValue: Int32? var color: UIColor = .white { didSet { @@ -683,21 +725,27 @@ final class BadgeLabelView: UIView { } } - init() { + init(groupingSeparator: String) { super.init(frame: CGRect(origin: .zero, size: labelSize)) - var height: CGFloat = -labelHeight - for i in -1 ..< 10 { + var height: CGFloat = -labelHeight * 2.0 + for i in -2 ..< 10 { let label = UILabel() - if i == -1 { + let itemWidth: CGFloat + if i == -2 { + label.text = groupingSeparator + itemWidth = spaceWidth + } else if i == -1 { label.text = "9" + itemWidth = labelWidth } else { label.text = "\(i)" + itemWidth = labelWidth } label.textColor = self.color label.font = font 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.labels.append(label) @@ -709,37 +757,49 @@ final class BadgeLabelView: UIView { 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 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( origin: CGPoint( x: 0.0, - y: -1.0 * labelSize.height + y: -2.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) } } + private let groupingSeparator: String private var itemViews: [Int: StackView] = [:] - private var staticLabel = UILabel() - init() { + init(groupingSeparator: String) { + self.groupingSeparator = groupingSeparator + super.init(frame: .zero) self.clipsToBounds = true @@ -752,7 +812,6 @@ final class BadgeLabelView: UIView { var color: UIColor = .white { didSet { - self.staticLabel.textColor = self.color for (_, view) in self.itemViews { view.color = self.color } @@ -760,30 +819,12 @@ final class BadgeLabelView: UIView { } 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 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] = [] for i in 0 ..< stringArray.count { validIds.append(i) @@ -794,18 +835,21 @@ final class BadgeLabelView: UIView { itemView = current } else { itemTransition = transition.withAnimation(.none) - itemView = StackView() + itemView = StackView(groupingSeparator: self.groupingSeparator) itemView.color = self.color self.itemViews[i] = 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) + let itemWidth: CGFloat = digit != nil ? labelWidth : spaceWidth + rightX -= itemWidth + itemTransition.setFrame( 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) } } + +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 +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index ff09e4d3a4..6872f69960 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -374,7 +374,8 @@ private final class GiftViewSheetContent: CombinedComponent { animationRenderer: component.context.animationRenderer, placeholderColor: theme.list.mediaPlaceholderColor, text: .plain(attributedText), - maximumNumberOfLines: 0 + maximumNumberOfLines: 0, + handleSpoilers: true ) ) )) @@ -916,7 +917,6 @@ public class GiftViewScreen: ViewControllerComponentContainer { return } let introController = context.sharedContext.makeStarsIntroScreen(context: context) - introController.navigationPresentation = .modal self.push(introController) } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift index 27bcb14a8a..1759c73b7f 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift @@ -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 }, 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() - }, 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 }, editPeer: { _ in }) @@ -507,7 +507,7 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod }, openPremiumIntro: { }, - openPremiumGift: { _ in + openPremiumGift: { _, _ in }, openPremiumManagement: { }, diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift index 15b5abe989..1793c5f6fb 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift @@ -1619,7 +1619,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { controlsClippingFrame = apparentAvatarFrame } - let avatarClipOffset: CGFloat = !self.isAvatarExpanded && deviceMetrics.hasDynamicIsland && self.avatarClippingNode.clipsToBounds && !isLandscape ? 48.0 : 0.0 + let avatarClipOffset: CGFloat = !self.isAvatarExpanded && deviceMetrics.hasDynamicIsland && self.avatarClippingNode.clipsToBounds && !isLandscape ? 47.0 : 0.0 let clippingNodeTransition = ContainedViewLayoutTransition.immediate clippingNodeTransition.updateFrame(layer: self.avatarClippingNode.layer, frame: CGRect(origin: CGPoint(x: 0.0, y: avatarClipOffset), size: CGSize(width: width, height: 1000.0))) clippingNodeTransition.updateSublayerTransformOffset(layer: self.avatarClippingNode.layer, offset: CGPoint(x: 0.0, y: -avatarClipOffset)) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index f499db410e..6a2eebbb92 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -1715,7 +1715,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese if overallRevenueBalance > 0 || overallStarsBalance > 0 { var string = "" if overallRevenueBalance > 0 { - string.append("#\(formatTonAmountText(revenueBalance, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))") + string.append("#\(formatTonAmountText(revenueBalance, dateTimeFormat: presentationData.dateTimeFormat))") } if overallStarsBalance > 0 { if !string.isEmpty { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index 4becea5f6d..e05bcfec47 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -109,6 +109,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr public override func didLoad() { super.didLoad() + self.scrollNode.view.contentInsetAdjustmentBehavior = .never 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 topInset: CGFloat = 60.0 + 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 for product in starsProducts { - let itemId = AnyHashable(index) - validIds.append(itemId) - - var itemTransition = transition - let visibleItem: ComponentView - if let current = self.starsItems[itemId] { - visibleItem = current - } else { - visibleItem = ComponentView() - self.starsItems[itemId] = visibleItem - itemTransition = .immediate - } - var isVisible = false if visibleBounds.intersects(itemFrame) { isVisible = true } if isVisible { + let itemId = AnyHashable(index) + validIds.append(itemId) + + var itemTransition = transition + let visibleItem: ComponentView + if let current = self.starsItems[itemId] { + visibleItem = current + } else { + visibleItem = ComponentView() + self.starsItems[itemId] = visibleItem + itemTransition = .immediate + } + let ribbonText: String? if let availability = product.gift.availability { 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) } - 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 { 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)) 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)) 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)) } contentHeight += unlockSize.height + contentHeight += bottomPanelHeight + + bottomScrollInset = bottomPanelHeight - 40.0 } 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) if 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) - if bottomOffset < 100.0 { + let bottomContentOffset = max(0.0, self.scrollNode.view.contentSize.height - self.scrollNode.view.contentOffset.y - self.scrollNode.view.frame.height) + if bottomContentOffset < 200.0 { self.profileGifts.loadMore() } } diff --git a/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoScreen.swift b/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoScreen.swift index 0bee05ace1..5bc9281d03 100644 --- a/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoScreen.swift +++ b/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/Sources/ArchiveInfoScreen.swift @@ -108,11 +108,9 @@ private final class ArchiveInfoSheetContentComponent: Component { } contentHeight += buttonSize.height - if environment.safeInsets.bottom.isZero { - contentHeight += 16.0 - } else { - contentHeight += environment.safeInsets.bottom + 14.0 - } + let bottomPanelPadding: CGFloat = 12.0 + let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding + contentHeight += bottomInset 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 )), environment: { diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift index 6aeef906f7..a038c5413b 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift @@ -186,7 +186,7 @@ final class GreetingMessageListItemComponent: Component { }, openPremiumIntro: { }, - openPremiumGift: { _ in + openPremiumGift: { _, _ in }, openPremiumManagement: { }, diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift index bf1e17537d..1475fb6bbb 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift @@ -201,7 +201,7 @@ final class QuickReplySetupScreenComponent: Component { }, openPremiumIntro: { }, - openPremiumGift: { _ in + openPremiumGift: { _, _ in }, openPremiumManagement: { }, diff --git a/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/Sources/CollectibleItemInfoScreen.swift b/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/Sources/CollectibleItemInfoScreen.swift index de5d8b52b9..1d385f0986 100644 --- a/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/Sources/CollectibleItemInfoScreen.swift +++ b/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/Sources/CollectibleItemInfoScreen.swift @@ -108,7 +108,7 @@ private final class PeerBadgeComponent: Component { let _ = self.background.update( transition: transition, - component: AnyComponent(RoundedRectangle(color: component.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.1), cornerRadius: nil)), + component: AnyComponent(RoundedRectangle(color: component.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3), cornerRadius: nil)), environment: {}, containerSize: size ) @@ -246,25 +246,10 @@ private final class CollectibleItemInfoScreenContentComponent: Component { let dateText = stringForDate(timestamp: username.info.purchaseDate, strings: environment.strings) - let (rawCryptoCurrencyText, cryptoCurrencySign, _) = formatCurrencyAmountCustom(username.info.cryptoCurrencyAmount, currency: username.info.cryptoCurrency, customFormat: CurrencyFormatterEntry( - symbol: "~", - thousandsSeparator: ",", - decimalSeparator: ".", - symbolOnLeft: true, - spaceBetweenAmountAndSymbol: false, - decimalDigits: 9 - )) - var cryptoCurrencyText = rawCryptoCurrencyText - while cryptoCurrencyText.hasSuffix("0") { - cryptoCurrencyText = String(cryptoCurrencyText[cryptoCurrencyText.startIndex ..< cryptoCurrencyText.index(before: cryptoCurrencyText.endIndex)]) - } - if cryptoCurrencyText.hasSuffix(".") { - cryptoCurrencyText = String(cryptoCurrencyText[cryptoCurrencyText.startIndex ..< cryptoCurrencyText.index(before: cryptoCurrencyText.endIndex)]) - } + let cryptoCurrencyText = formatTonAmountText(username.info.cryptoCurrencyAmount, dateTimeFormat: environment.dateTimeFormat) + let currencyText = formatTonUsdValue(username.info.currencyAmount, divide: false, rate: 0.01, dateTimeFormat: environment.dateTimeFormat) - let (currencyText, currencySign, _) = formatCurrencyAmountCustom(username.info.currencyAmount, currency: username.info.currency) - - let rawTextString = environment.strings.CollectibleItemInfo_UsernameText("@\(username.username)", environment.strings.CollectibleItemInfo_StoreName, dateText, "\(cryptoCurrencySign)\(cryptoCurrencyText)", "\(currencySign)\(currencyText)") + let rawTextString = environment.strings.CollectibleItemInfo_UsernameText("@\(username.username)", environment.strings.CollectibleItemInfo_StoreName, dateText, "~\(cryptoCurrencyText)", currencyText) textText.append(NSAttributedString(string: rawTextString.string, font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor)) for range in rawTextString.ranges { switch range.index { @@ -292,25 +277,10 @@ private final class CollectibleItemInfoScreenContentComponent: Component { let dateText = stringForDate(timestamp: phoneNumber.info.purchaseDate, strings: environment.strings) - let (rawCryptoCurrencyText, cryptoCurrencySign, _) = formatCurrencyAmountCustom(phoneNumber.info.cryptoCurrencyAmount, currency: phoneNumber.info.cryptoCurrency, customFormat: CurrencyFormatterEntry( - symbol: "~", - thousandsSeparator: ",", - decimalSeparator: ".", - symbolOnLeft: true, - spaceBetweenAmountAndSymbol: false, - decimalDigits: 9 - )) - var cryptoCurrencyText = rawCryptoCurrencyText - while cryptoCurrencyText.hasSuffix("0") { - cryptoCurrencyText = String(cryptoCurrencyText[cryptoCurrencyText.startIndex ..< cryptoCurrencyText.index(before: cryptoCurrencyText.endIndex)]) - } - if cryptoCurrencyText.hasSuffix(".") { - cryptoCurrencyText = String(cryptoCurrencyText[cryptoCurrencyText.startIndex ..< cryptoCurrencyText.index(before: cryptoCurrencyText.endIndex)]) - } + let cryptoCurrencyText = formatTonAmountText(phoneNumber.info.cryptoCurrencyAmount, dateTimeFormat: environment.dateTimeFormat) + let currencyText = formatTonUsdValue(phoneNumber.info.currencyAmount, divide: false, rate: 0.01, dateTimeFormat: environment.dateTimeFormat) - let (currencyText, currencySign, _) = formatCurrencyAmountCustom(phoneNumber.info.currencyAmount, currency: phoneNumber.info.currency) - - let rawTextString = environment.strings.CollectibleItemInfo_PhoneText("\(formattedPhoneNumber)", environment.strings.CollectibleItemInfo_StoreName, dateText, "\(cryptoCurrencySign)\(cryptoCurrencyText)", "\(currencySign)\(currencyText)") + let rawTextString = environment.strings.CollectibleItemInfo_PhoneText("\(formattedPhoneNumber)", environment.strings.CollectibleItemInfo_StoreName, dateText, "~\(cryptoCurrencyText)", currencyText) textText.append(NSAttributedString(string: rawTextString.string, font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor)) for range in rawTextString.ranges { switch range.index { diff --git a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift index 6eeecc59bd..2955564155 100644 --- a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift +++ b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift @@ -865,7 +865,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() }, present: { _ in - }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: { + }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: { }, openBirthdaySetup: { }, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: { diff --git a/submodules/TelegramUI/Components/Stars/StarsIntroScreen/BUILD b/submodules/TelegramUI/Components/Stars/StarsIntroScreen/BUILD index 598da6fc38..61bc2e9d45 100644 --- a/submodules/TelegramUI/Components/Stars/StarsIntroScreen/BUILD +++ b/submodules/TelegramUI/Components/Stars/StarsIntroScreen/BUILD @@ -31,11 +31,9 @@ swift_library( "//submodules/TextFormat", "//submodules/TelegramUI/Components/ListSectionComponent", "//submodules/TelegramUI/Components/ListActionItemComponent", - "//submodules/TelegramUI/Components/ScrollComponent", "//submodules/TelegramUI/Components/Premium/PremiumStarComponent", "//submodules/TelegramUI/Components/ButtonComponent", "//submodules/Components/BundleIconComponent", - "//submodules/Components/SolidRoundedButtonComponent", "//submodules/Components/BlurredBackgroundComponent", ], visibility = [ diff --git a/submodules/TelegramUI/Components/Stars/StarsIntroScreen/Sources/StarsIntroScreen.swift b/submodules/TelegramUI/Components/Stars/StarsIntroScreen/Sources/StarsIntroScreen.swift index 6789d813fb..fb4e0e9d03 100644 --- a/submodules/TelegramUI/Components/Stars/StarsIntroScreen/Sources/StarsIntroScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsIntroScreen/Sources/StarsIntroScreen.swift @@ -8,18 +8,17 @@ import Markdown import TextFormat import TelegramPresentationData import ViewControllerComponent -import ScrollComponent import BundleIconComponent import BalancedTextComponent import MultilineTextComponent -import SolidRoundedButtonComponent +import ButtonComponent import AccountContext -import ScrollComponent +import SheetComponent import BlurredBackgroundComponent import PremiumStarComponent -private final class ScrollContent: CombinedComponent { - typealias EnvironmentType = (ViewControllerComponentContainer.Environment, ScrollChildEnvironment) +private final class SheetContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let openExamples: () -> Void @@ -35,7 +34,7 @@ private final class ScrollContent: CombinedComponent { self.dismiss = dismiss } - static func ==(lhs: ScrollContent, rhs: ScrollContent) -> Bool { + static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool { if lhs.context !== rhs.context { return false } @@ -44,10 +43,10 @@ private final class ScrollContent: CombinedComponent { static var body: Body { let star = Child(PremiumStarComponent.self) - let title = Child(BalancedTextComponent.self) let text = Child(BalancedTextComponent.self) let list = Child(List.self) + let actionButton = Child(ButtonComponent.self) return { context in let environment = context.environment[ViewControllerComponentContainer.Environment.self].value @@ -82,13 +81,13 @@ private final class ScrollContent: CombinedComponent { UIColor(rgb: 0xfdd219) ], 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), transition: context.transition ) 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( @@ -193,15 +192,40 @@ private final class ScrollContent: CombinedComponent { .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + list.size.height / 2.0)) ) contentSize.height += list.size.height - contentSize.height += spacing - 9.0 + contentSize.height += spacing - contentSize.height += 12.0 + 50.0 - if environment.safeInsets.bottom > 0 { - contentSize.height += environment.safeInsets.bottom + 5.0 - } else { - contentSize.height += 12.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 + + 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 } } @@ -238,129 +262,58 @@ private final class ContainerComponent: CombinedComponent { } static var body: Body { - let background = Child(Rectangle.self) - let scroll = Child(ScrollComponent.self) - let bottomPanel = Child(BlurredBackgroundComponent.self) - let bottomSeparator = Child(Rectangle.self) - let actionButton = Child(SolidRoundedButtonComponent.self) - let scrollExternalState = ScrollComponent.ExternalState() - + let sheet = Child(SheetComponent<(EnvironmentType)>.self) + let animateOut = StoredActionSlot(Action.self) + return { context in let environment = context.environment[EnvironmentType.self] - let theme = environment.theme - let strings = environment.strings - let state = context.state let controller = environment.controller - let background = background.update( - component: Rectangle(color: environment.theme.list.plainBackgroundColor), - environment: {}, - 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( - content: AnyComponent(ScrollContent( + let sheet = sheet.update( + component: SheetComponent( + content: AnyComponent(SheetContent( context: context.component.context, openExamples: context.component.openExamples, dismiss: { controller()?.dismiss() } )), - externalState: scrollExternalState, - contentInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 1.0, right: 0.0), - contentOffsetUpdated: { [weak state] topContentOffset, bottomContentOffset in - state?.topContentOffset = topContentOffset - state?.bottomContentOffset = bottomContentOffset - Queue.mainQueue().justDispatch { - state?.updated(transition: .immediate) - } - }, - contentOffsetWillCommit: { targetContentOffset in - } + backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), + followContentSizeChanges: true, + clipsContent: true, + animateOut: animateOut ), - 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, transition: context.transition ) - context.add(scroll + context.add(sheet .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 } } @@ -389,7 +342,7 @@ public final class StarsIntroScreen: ViewControllerComponentContainer { theme: forceDark ? .dark : .default ) - self.navigationPresentation = .modal + self.navigationPresentation = .flatModal openExamplesImpl = { [weak self] in guard let self else { @@ -408,6 +361,12 @@ public final class StarsIntroScreen: ViewControllerComponentContainer { required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + public func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { + view.dismissAnimated() + } + } } private final class ParagraphComponent: CombinedComponent { diff --git a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift index 62a93d5716..ed5fd15396 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift @@ -732,7 +732,7 @@ private final class StarsTransferSheetComponent: CombinedComponent { }) } )), - backgroundColor: .blur(.light), + backgroundColor: .color(environment.theme.list.modalBlocksBackgroundColor), followContentSizeChanges: true, clipsContent: true, animateOut: animateOut diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift index fcf4852d38..7b9c5ce639 100644 --- a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift @@ -52,7 +52,6 @@ private final class SheetContent: CombinedComponent { } static var body: Body { - let background = Child(RoundedRectangle.self) let closeButton = Child(Button.self) let title = Child(Text.self) let amountSection = Child(ListSectionComponent.self) @@ -74,15 +73,6 @@ private final class SheetContent: CombinedComponent { let sideInset: CGFloat = 16.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 @@ -466,7 +456,7 @@ private final class StarsWithdrawSheetComponent: CombinedComponent { }) } )), - backgroundColor: .blur(.light), + backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), followContentSizeChanges: false, clipsContent: true, isScrollEnabled: false, diff --git a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift index 1419496161..dc3db47119 100644 --- a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift +++ b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift @@ -299,7 +299,7 @@ public class ImmediateTextNodeWithEntities: TextNode { public var arguments: TextNodeWithEntities.Arguments? private var inlineStickerItemLayers: [InlineStickerItemLayer.Key: InlineStickerItemLayer] = [:] - private var dustNode: InvisibleInkDustNode? + public private(set) var dustNode: InvisibleInkDustNode? public var visibility: Bool = false { didSet { diff --git a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift index ebf77e9191..21892f43af 100644 --- a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift @@ -282,7 +282,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, ASScrollViewDe }, openStorageManagement: { }, openPasswordSetup: { }, openPremiumIntro: { - }, openPremiumGift: { _ in + }, openPremiumGift: { _, _ in }, openPremiumManagement: { }, openActiveSessions: { }, openBirthdaySetup: { diff --git a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift index e5741bad8a..719a632d4e 100644 --- a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift @@ -156,7 +156,7 @@ private struct CommandChatInputContextPanelEntry: Comparable, Identifiable { }, openPremiumIntro: { }, - openPremiumGift: { _ in + openPremiumGift: { _, _ in }, openPremiumManagement: { }, diff --git a/submodules/TelegramUI/Sources/OpenChatMessage.swift b/submodules/TelegramUI/Sources/OpenChatMessage.swift index a7712b527b..889f521c1d 100644 --- a/submodules/TelegramUI/Sources/OpenChatMessage.swift +++ b/submodules/TelegramUI/Sources/OpenChatMessage.swift @@ -250,9 +250,9 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { let subject: BrowserScreen.Subject if file.mimeType == "application/pdf" { - subject = .pdfDocument(file: file, canShare: canShare) + subject = .pdfDocument(file: .message(message: MessageReference(params.message), media: file), canShare: canShare) } else { - subject = .document(file: file, canShare: canShare) + subject = .document(file: .message(message: MessageReference(params.message), media: file), canShare: canShare) } let controller = BrowserScreen(context: params.context, subject: subject) controller.openDocument = { file, canShare in