mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 13:35:19 +00:00
Various fixes
This commit is contained in:
parent
aa23e95384
commit
d47bbbeea0
@ -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 {
|
||||
|
@ -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:") {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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<Empty>()
|
||||
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
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
)
|
||||
))
|
||||
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user