diff --git a/submodules/BrowserUI/Sources/BrowserScreen.swift b/submodules/BrowserUI/Sources/BrowserScreen.swift index 6e0d4043fc..a6023a90d4 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 { @@ -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 { 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/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/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/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index becb0dffaf..fc09e566ff 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -37,6 +37,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { private var mediaBackgroundContent: WallpaperBubbleBackgroundNode? private let titleNode: TextNode private let subtitleNode: TextNodeWithEntities + private var spoilerSubtitleNode: TextNodeWithEntities? private let textClippingNode: ASDisplayNode private var dustNode: InvisibleInkDustNode? private let placeholderNode: StickerShimmerEffectNode @@ -286,6 +287,7 @@ 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) @@ -487,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 { @@ -633,7 +637,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { let _ = buttonTitleApply() let _ = ribbonTextApply() 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 @@ -642,8 +646,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { 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 @@ -657,16 +661,31 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { 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) }) @@ -881,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) { @@ -900,6 +918,12 @@ 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 { diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index 510cf6b4dd..027d10a67c 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -607,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( @@ -624,7 +625,9 @@ final class GiftSetupScreenComponent: Component { badgeText: "\(remainsString)", badgePosition: position, badgeGraphPosition: position, - invertProgress: true + invertProgress: true, + leftString: environment.strings.Gift_Send_Left, + groupingSeparator: environment.dateTimeFormat.groupingSeparator )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/RemainingCountComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/RemainingCountComponent.swift index bb1ab4199c..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,7 @@ 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() @@ -149,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) @@ -163,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 @@ -255,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)) } } @@ -274,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)) @@ -293,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( @@ -350,7 +319,7 @@ public class RemainingCountComponent: Component { NSAttributedString( string: component.inactiveTitle, font: Font.semibold(15.0), - textColor: leftTextColor + textColor: component.activeTitleColor ) ) ) @@ -358,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( @@ -419,7 +342,7 @@ public class RemainingCountComponent: Component { NSAttributedString( string: component.activeValue, font: Font.semibold(15.0), - textColor: rightTextColor + textColor: component.activeTitleColor ) ) ) @@ -427,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 { @@ -460,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) @@ -539,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) @@ -665,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) @@ -674,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 { @@ -684,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) @@ -710,36 +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] = [:] - init() { + init(groupingSeparator: String) { + self.groupingSeparator = groupingSeparator + super.init(frame: .zero) self.clipsToBounds = true @@ -762,8 +822,9 @@ final class BadgeLabelView: UIView { 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) @@ -774,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) ) } @@ -805,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 039a81c650..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 ) ) )) 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 {