mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-08 08:31:13 +00:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
56b7092fef
@ -13008,6 +13008,8 @@ Sorry for the inconvenience.";
|
||||
"Gift.Options.Gift.Filter.AllGifts" = "All Gifts";
|
||||
"Gift.Options.Gift.Filter.Limited" = "Limited";
|
||||
"Gift.Options.Gift.Limited" = "Limited";
|
||||
"Gift.Options.Gift.SoldOut" = "Sold Out";
|
||||
"Gift.Options.SoldOut.Text" = "Sorry, this gift is sold out.";
|
||||
|
||||
"PeerInfo.PaneGifts" = "Gifts";
|
||||
|
||||
@ -13038,6 +13040,12 @@ Sorry for the inconvenience.";
|
||||
"Gift.Send.HideMyName" = "Hide My Name";
|
||||
"Gift.Send.HideMyName.Info" = "Hide my name and message from visitors to %1$@'s profile. %2$@ will still see your name and message.";
|
||||
"Gift.Send.Send" = "Send a Gift for";
|
||||
"Gift.Send.Limited" = "Limited";
|
||||
"Gift.Send.Remains_1" = "%@ left";
|
||||
"Gift.Send.Remains_any" = "%@ left";
|
||||
|
||||
"Gift.Send.ErrorUnknown" = "An error occurred. Please try again.";
|
||||
"Gift.Send.ErrorOutOfStock" = "Sorry, this gift is sold out. Please choose another gift.";
|
||||
|
||||
"Profile.SendGift" = "Send a Gift";
|
||||
"Settings.SendGift" = "Send a Gift";
|
||||
@ -13049,6 +13057,7 @@ Sorry for the inconvenience.";
|
||||
"Report.Title.User" = "Report User";
|
||||
"Report.Title.Group" = "Report Group";
|
||||
"Report.Title.Channel" = "Report Channel";
|
||||
"Report.Title.Bot" = "Report Bot";
|
||||
"Report.Comment.Placeholder" = "Add Comment";
|
||||
"Report.Comment.Placeholder.Optional" = "Add Comment (Optional)";
|
||||
"Report.Comment.Info" = "Please help us by telling what is wrong with the message you have selected.";
|
||||
@ -13059,8 +13068,12 @@ Sorry for the inconvenience.";
|
||||
|
||||
"Notification.PremiumGift.YearsTitle_1" = "%@ Year Premium";
|
||||
"Notification.PremiumGift.YearsTitle_any" = "%@ Years Premium";
|
||||
"Notification.PremiumGift.More" = "more";
|
||||
|
||||
"Notification.PremiumGift.SubscriptionDescription" = "Subscription for exclusive Telegram features.";
|
||||
|
||||
"Notification.StarsGift.Stars_1" = "%@ Star";
|
||||
"Notification.StarsGift.Stars_any" = "%@ Stars";
|
||||
|
||||
"WebBrowser.AuthChallenge.Title" = "Sign in to %@";
|
||||
"WebBrowser.AuthChallenge.Text" = "Your login information will be sent securely.";
|
||||
|
@ -148,18 +148,7 @@ class BazelCommandLine:
|
||||
self.disable_provisioning_profiles = True
|
||||
|
||||
def set_configuration(self, configuration):
|
||||
if configuration == 'debug_universal':
|
||||
self.configuration_args = [
|
||||
# bazel debug build configuration
|
||||
'-c', 'dbg',
|
||||
|
||||
# Build universal binaries.
|
||||
'--ios_multi_cpus=armv7,arm64',
|
||||
|
||||
# Always build universal Watch binaries.
|
||||
'--watchos_cpus=arm64_32'
|
||||
] + self.common_debug_args
|
||||
elif configuration == 'debug_arm64':
|
||||
if configuration == 'debug_arm64':
|
||||
self.configuration_args = [
|
||||
# bazel debug build configuration
|
||||
'-c', 'dbg',
|
||||
@ -189,16 +178,6 @@ class BazelCommandLine:
|
||||
# Build single-architecture binaries. It is almost 2 times faster is 32-bit support is not required.
|
||||
'--ios_multi_cpus=sim_arm64',
|
||||
|
||||
# Always build universal Watch binaries.
|
||||
'--watchos_cpus=arm64_32'
|
||||
] + self.common_debug_args
|
||||
elif configuration == 'debug_armv7':
|
||||
self.configuration_args = [
|
||||
# bazel debug build configuration
|
||||
'-c', 'dbg',
|
||||
|
||||
'--ios_multi_cpus=armv7',
|
||||
|
||||
# Always build universal Watch binaries.
|
||||
'--watchos_cpus=arm64_32'
|
||||
] + self.common_debug_args
|
||||
@ -217,41 +196,10 @@ class BazelCommandLine:
|
||||
'--apple_generate_dsym',
|
||||
|
||||
# Require DSYM files as build output.
|
||||
'--output_groups=+dsyms'
|
||||
] + self.common_release_args
|
||||
elif configuration == 'release_armv7':
|
||||
self.configuration_args = [
|
||||
# bazel optimized build configuration
|
||||
'-c', 'opt',
|
||||
'--output_groups=+dsyms',
|
||||
|
||||
# Build single-architecture binaries. It is almost 2 times faster is 32-bit support is not required.
|
||||
'--ios_multi_cpus=armv7',
|
||||
|
||||
# Always build universal Watch binaries.
|
||||
'--watchos_cpus=arm64_32',
|
||||
|
||||
# Generate DSYM files when building.
|
||||
'--apple_generate_dsym',
|
||||
|
||||
# Require DSYM files as build output.
|
||||
'--output_groups=+dsyms'
|
||||
] + self.common_release_args
|
||||
elif configuration == 'release_universal':
|
||||
self.configuration_args = [
|
||||
# bazel optimized build configuration
|
||||
'-c', 'opt',
|
||||
|
||||
# Build universal binaries.
|
||||
'--ios_multi_cpus=armv7,arm64',
|
||||
|
||||
# Always build universal Watch binaries.
|
||||
'--watchos_cpus=arm64_32',
|
||||
|
||||
# Generate DSYM files when building.
|
||||
'--apple_generate_dsym',
|
||||
|
||||
# Require DSYM files as build output.
|
||||
'--output_groups=+dsyms'
|
||||
'--swiftcopt=-num-threads',
|
||||
'--swiftcopt=0',
|
||||
] + self.common_release_args
|
||||
else:
|
||||
raise Exception('Unknown configuration {}'.format(configuration))
|
||||
|
@ -931,13 +931,13 @@ public final class AvatarNode: ASDisplayNode {
|
||||
if let repliesIcon = repliesIcon {
|
||||
context.draw(repliesIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - repliesIcon.size.width) / 2.0), y: floor((bounds.size.height - repliesIcon.size.height) / 2.0)), size: repliesIcon.size))
|
||||
}
|
||||
} else if case .anonymousSavedMessagesIcon = parameters.icon {
|
||||
} else if case let .anonymousSavedMessagesIcon(isColored) = parameters.icon {
|
||||
let factor = bounds.size.width / 60.0
|
||||
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
|
||||
context.scaleBy(x: factor, y: -factor)
|
||||
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
|
||||
|
||||
if let theme = parameters.theme, theme.overallDarkAppearance {
|
||||
if let theme = parameters.theme, theme.overallDarkAppearance, !isColored {
|
||||
if let anonymousSavedMessagesDarkIcon = anonymousSavedMessagesDarkIcon {
|
||||
context.draw(anonymousSavedMessagesDarkIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - anonymousSavedMessagesDarkIcon.size.width) / 2.0), y: floor((bounds.size.height - anonymousSavedMessagesDarkIcon.size.height) / 2.0)), size: anonymousSavedMessagesDarkIcon.size))
|
||||
}
|
||||
|
@ -1558,19 +1558,23 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
|
||||
applePayController.presentingViewController?.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
let text: String
|
||||
let text: String?
|
||||
switch error {
|
||||
case .precheckoutFailed:
|
||||
text = strongSelf.presentationData.strings.Checkout_ErrorPrecheckoutFailed
|
||||
case .paymentFailed:
|
||||
text = strongSelf.presentationData.strings.Checkout_ErrorPaymentFailed
|
||||
case .alreadyPaid:
|
||||
text = strongSelf.presentationData.strings.Checkout_ErrorInvoiceAlreadyPaid
|
||||
case .generic:
|
||||
text = strongSelf.presentationData.strings.Checkout_ErrorGeneric
|
||||
case .precheckoutFailed:
|
||||
text = strongSelf.presentationData.strings.Checkout_ErrorPrecheckoutFailed
|
||||
case .paymentFailed:
|
||||
text = strongSelf.presentationData.strings.Checkout_ErrorPaymentFailed
|
||||
case .alreadyPaid:
|
||||
text = strongSelf.presentationData.strings.Checkout_ErrorInvoiceAlreadyPaid
|
||||
case .generic:
|
||||
text = strongSelf.presentationData.strings.Checkout_ErrorGeneric
|
||||
default:
|
||||
text = nil
|
||||
}
|
||||
|
||||
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil)
|
||||
if let text {
|
||||
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil)
|
||||
}
|
||||
|
||||
strongSelf.failed()
|
||||
}
|
||||
|
@ -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:") {
|
||||
|
@ -399,7 +399,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
|
||||
case topic(EnginePeer, ChatListItemContent.ThreadInfo, Int, PresentationTheme, PresentationStrings, ChatListSearchSectionExpandType)
|
||||
case recentlySearchedPeer(EnginePeer, EnginePeer?, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, PeerStoryStats?, Bool)
|
||||
case localPeer(EnginePeer, EnginePeer?, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType, PeerStoryStats?, Bool)
|
||||
case globalPeer(FoundPeer, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType, PeerStoryStats?, Bool)
|
||||
case globalPeer(FoundPeer, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType, PeerStoryStats?, Bool, String?)
|
||||
case message(EngineMessage, EngineRenderedPeer, EnginePeerReadCounters?, EngineMessageHistoryThread.Info?, ChatListPresentationData, Int32, Bool?, Bool, MessageOrderingKey, (id: String, size: Int64, isFirstInList: Bool)?, MessageSection, Bool, PeerStoryStats?, Bool)
|
||||
case addContact(String, PresentationTheme, PresentationStrings)
|
||||
|
||||
@ -411,7 +411,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
|
||||
return .localPeerId(peer.id)
|
||||
case let .localPeer(peer, _, _, _, _, _, _, _, _, _, _):
|
||||
return .localPeerId(peer.id)
|
||||
case let .globalPeer(peer, _, _, _, _, _, _, _, _, _):
|
||||
case let .globalPeer(peer, _, _, _, _, _, _, _, _, _, _):
|
||||
return .globalPeerId(peer.peer.id)
|
||||
case let .message(message, _, _, _, _, _, _, _, _, _, section, _, _, _):
|
||||
return .messageId(message.id, section)
|
||||
@ -440,8 +440,8 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .globalPeer(lhsPeer, lhsUnreadBadge, lhsIndex, lhsTheme, lhsStrings, lhsSortOrder, lhsDisplayOrder, lhsExpandType, lhsStoryStats, lhsRequiresPremiumForMessaging):
|
||||
if case let .globalPeer(rhsPeer, rhsUnreadBadge, rhsIndex, rhsTheme, rhsStrings, rhsSortOrder, rhsDisplayOrder, rhsExpandType, rhsStoryStats, rhsRequiresPremiumForMessaging) = rhs, lhsPeer == rhsPeer && lhsIndex == rhsIndex && lhsTheme === rhsTheme && lhsStrings === rhsStrings && lhsSortOrder == rhsSortOrder && lhsDisplayOrder == rhsDisplayOrder && lhsUnreadBadge?.0 == rhsUnreadBadge?.0 && lhsUnreadBadge?.1 == rhsUnreadBadge?.1 && lhsExpandType == rhsExpandType && lhsStoryStats == rhsStoryStats && lhsRequiresPremiumForMessaging == rhsRequiresPremiumForMessaging {
|
||||
case let .globalPeer(lhsPeer, lhsUnreadBadge, lhsIndex, lhsTheme, lhsStrings, lhsSortOrder, lhsDisplayOrder, lhsExpandType, lhsStoryStats, lhsRequiresPremiumForMessaging, lhsQuery):
|
||||
if case let .globalPeer(rhsPeer, rhsUnreadBadge, rhsIndex, rhsTheme, rhsStrings, rhsSortOrder, rhsDisplayOrder, rhsExpandType, rhsStoryStats, rhsRequiresPremiumForMessaging, rhsQuery) = rhs, lhsPeer == rhsPeer && lhsIndex == rhsIndex && lhsTheme === rhsTheme && lhsStrings === rhsStrings && lhsSortOrder == rhsSortOrder && lhsDisplayOrder == rhsDisplayOrder && lhsUnreadBadge?.0 == rhsUnreadBadge?.0 && lhsUnreadBadge?.1 == rhsUnreadBadge?.1 && lhsExpandType == rhsExpandType && lhsStoryStats == rhsStoryStats && lhsRequiresPremiumForMessaging == rhsRequiresPremiumForMessaging, lhsQuery == rhsQuery {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
@ -543,11 +543,11 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
|
||||
case .globalPeer, .message, .addContact:
|
||||
return true
|
||||
}
|
||||
case let .globalPeer(_, _, lhsIndex, _, _, _, _, _, _, _):
|
||||
case let .globalPeer(_, _, lhsIndex, _, _, _, _, _, _, _, _):
|
||||
switch rhs {
|
||||
case .topic, .recentlySearchedPeer, .localPeer:
|
||||
return false
|
||||
case let .globalPeer(_, _, rhsIndex, _, _, _, _, _, _, _):
|
||||
case let .globalPeer(_, _, rhsIndex, _, _, _, _, _, _, _, _):
|
||||
return lhsIndex <= rhsIndex
|
||||
case .message, .addContact:
|
||||
return true
|
||||
@ -798,7 +798,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
|
||||
openStories(peer.id, sourceNode.avatarNode)
|
||||
}
|
||||
})
|
||||
case let .globalPeer(peer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType, storyStats, requiresPremiumForMessaging):
|
||||
case let .globalPeer(peer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType, storyStats, requiresPremiumForMessaging, query):
|
||||
var enabled = true
|
||||
if filter.contains(.onlyWriteable) {
|
||||
enabled = canSendMessagesToPeer(peer.peer)
|
||||
@ -822,7 +822,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
|
||||
var suffixString = ""
|
||||
if let subscribers = peer.subscribers, subscribers != 0 {
|
||||
if peer.peer is TelegramUser {
|
||||
suffixString = ", \(strings.Conversation_StatusSubscribers(subscribers))"
|
||||
suffixString = ", \(strings.Conversation_StatusBotSubscribers(subscribers))"
|
||||
} else if let channel = peer.peer as? TelegramChannel, case .broadcast = channel.info {
|
||||
suffixString = ", \(strings.Conversation_StatusSubscribers(subscribers))"
|
||||
} else {
|
||||
@ -858,7 +858,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
|
||||
isSavedMessages = true
|
||||
}
|
||||
|
||||
return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch(isSavedMessages: isSavedMessages), peer: .peer(peer: EnginePeer(peer.peer), chatPeer: EnginePeer(peer.peer)), status: .addressName(suffixString), badge: badge, requiresPremiumForMessaging: requiresPremiumForMessaging, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in
|
||||
return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch(isSavedMessages: isSavedMessages), peer: .peer(peer: EnginePeer(peer.peer), chatPeer: EnginePeer(peer.peer)), status: .addressName(suffixString), badge: badge, requiresPremiumForMessaging: requiresPremiumForMessaging, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, searchQuery: query, action: { _ in
|
||||
interaction.peerSelected(EnginePeer(peer.peer), nil, nil, nil)
|
||||
}, disabledAction: { _ in
|
||||
interaction.disabledPeerSelected(EnginePeer(peer.peer), nil, requiresPremiumForMessaging ? .premiumRequired : .generic)
|
||||
@ -2607,7 +2607,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
|
||||
if !existingPeerIds.contains(peer.peer.id), filteredPeer(EnginePeer(peer.peer), EnginePeer(accountPeer)) {
|
||||
existingPeerIds.insert(peer.peer.id)
|
||||
entries.append(.globalPeer(peer, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, globalExpandType, nil, false))
|
||||
entries.append(.globalPeer(peer, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, globalExpandType, nil, false, finalQuery))
|
||||
index += 1
|
||||
numberOfGlobalPeers += 1
|
||||
}
|
||||
@ -2807,7 +2807,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
}, openStorageManagement: {
|
||||
}, openPasswordSetup: {
|
||||
}, openPremiumIntro: {
|
||||
}, openPremiumGift: { _ in
|
||||
}, openPremiumGift: { _, _ in
|
||||
}, openPremiumManagement: {
|
||||
}, openActiveSessions: {
|
||||
}, openBirthdaySetup: {
|
||||
@ -2953,7 +2953,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
if case let .user(user) = peer, user.flags.contains(.requirePremium) {
|
||||
requiresPremiumForMessagingPeerIds.append(peer.id)
|
||||
}
|
||||
case let .globalPeer(foundPeer, _, _, _, _, _, _, _, _, _):
|
||||
case let .globalPeer(foundPeer, _, _, _, _, _, _, _, _, _, _):
|
||||
storyStatsIds.append(foundPeer.peer.id)
|
||||
if let user = foundPeer.peer as? TelegramUser, user.flags.contains(.requirePremium) {
|
||||
requiresPremiumForMessagingPeerIds.append(foundPeer.peer.id)
|
||||
@ -2994,8 +2994,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
mappedItems[i] = .recentlySearchedPeer(peer, associatedPeer, unreadBadge, index, theme, strings, sortOrder, displayOrder, stats[peer.id] ?? nil, requiresPremiumForMessaging[peer.id] ?? false)
|
||||
case let .localPeer(peer, associatedPeer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, _, _):
|
||||
mappedItems[i] = .localPeer(peer, associatedPeer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, stats[peer.id] ?? nil, requiresPremiumForMessaging[peer.id] ?? false)
|
||||
case let .globalPeer(peer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, _, _):
|
||||
mappedItems[i] = .globalPeer(peer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, stats[peer.peer.id] ?? nil, requiresPremiumForMessaging[peer.peer.id] ?? false)
|
||||
case let .globalPeer(peer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, _, _, searchQuery):
|
||||
mappedItems[i] = .globalPeer(peer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, stats[peer.peer.id] ?? nil, requiresPremiumForMessaging[peer.peer.id] ?? false, searchQuery)
|
||||
case let .message(message, peer, combinedPeerReadState, threadInfo, presentationData, totalCount, selected, displayCustomHeader, key, resourceId, section, allPaused, _, _):
|
||||
mappedItems[i] = .message(message, peer, combinedPeerReadState, threadInfo, presentationData, totalCount, selected, displayCustomHeader, key, resourceId, section, allPaused, stats[peer.peerId] ?? nil, requiresPremiumForMessaging[peer.peerId] ?? false)
|
||||
default:
|
||||
@ -4645,7 +4645,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode {
|
||||
let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in
|
||||
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in
|
||||
gesture?.cancel()
|
||||
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: {
|
||||
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: {
|
||||
}, openBirthdaySetup: {
|
||||
}, performActiveSessionAction: { _, _ in
|
||||
}, openChatFolderUpdates: {}, hideChatFolderUpdates: {
|
||||
|
@ -156,7 +156,7 @@ public final class ChatListShimmerNode: ASDisplayNode {
|
||||
let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in
|
||||
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in
|
||||
gesture?.cancel()
|
||||
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, openStarsTopup: { _ in
|
||||
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, openStarsTopup: { _ in
|
||||
}, dismissNotice: { _ in
|
||||
}, editPeer: { _ in
|
||||
})
|
||||
|
@ -101,7 +101,7 @@ public final class ChatListNodeInteraction {
|
||||
let openStorageManagement: () -> Void
|
||||
let openPasswordSetup: () -> Void
|
||||
let openPremiumIntro: () -> Void
|
||||
let openPremiumGift: ([EnginePeer.Id: TelegramBirthday]?) -> Void
|
||||
let openPremiumGift: ([EnginePeer], [EnginePeer.Id: TelegramBirthday]?) -> Void
|
||||
let openPremiumManagement: () -> Void
|
||||
let openActiveSessions: () -> Void
|
||||
let openBirthdaySetup: () -> Void
|
||||
@ -157,7 +157,7 @@ public final class ChatListNodeInteraction {
|
||||
openStorageManagement: @escaping () -> Void,
|
||||
openPasswordSetup: @escaping () -> Void,
|
||||
openPremiumIntro: @escaping () -> Void,
|
||||
openPremiumGift: @escaping ([EnginePeer.Id: TelegramBirthday]?) -> Void,
|
||||
openPremiumGift: @escaping ([EnginePeer], [EnginePeer.Id: TelegramBirthday]?) -> Void,
|
||||
openPremiumManagement: @escaping () -> Void,
|
||||
openActiveSessions: @escaping () -> Void,
|
||||
openBirthdaySetup: @escaping () -> Void,
|
||||
@ -741,13 +741,13 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
|
||||
case .premiumUpgrade, .premiumAnnualDiscount, .premiumRestore:
|
||||
nodeInteraction?.openPremiumIntro()
|
||||
case .xmasPremiumGift:
|
||||
nodeInteraction?.openPremiumGift(nil)
|
||||
nodeInteraction?.openPremiumGift([], nil)
|
||||
case .premiumGrace:
|
||||
nodeInteraction?.openPremiumManagement()
|
||||
case .setupBirthday:
|
||||
nodeInteraction?.openBirthdaySetup()
|
||||
case let .birthdayPremiumGift(_, birthdays):
|
||||
nodeInteraction?.openPremiumGift(birthdays)
|
||||
case let .birthdayPremiumGift(peers, birthdays):
|
||||
nodeInteraction?.openPremiumGift(peers, birthdays)
|
||||
case .reviewLogin:
|
||||
break
|
||||
case let .starsSubscriptionLowBalance(amount, _):
|
||||
@ -1081,13 +1081,13 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
|
||||
case .premiumUpgrade, .premiumAnnualDiscount, .premiumRestore:
|
||||
nodeInteraction?.openPremiumIntro()
|
||||
case .xmasPremiumGift:
|
||||
nodeInteraction?.openPremiumGift(nil)
|
||||
nodeInteraction?.openPremiumGift([], nil)
|
||||
case .premiumGrace:
|
||||
nodeInteraction?.openPremiumManagement()
|
||||
case .setupBirthday:
|
||||
nodeInteraction?.openBirthdaySetup()
|
||||
case let .birthdayPremiumGift(_, birthdays):
|
||||
nodeInteraction?.openPremiumGift(birthdays)
|
||||
case let .birthdayPremiumGift(peers, birthdays):
|
||||
nodeInteraction?.openPremiumGift(peers, birthdays)
|
||||
case .reviewLogin:
|
||||
break
|
||||
case let .starsSubscriptionLowBalance(amount, _):
|
||||
@ -1710,13 +1710,24 @@ public final class ChatListNode: ListView {
|
||||
}
|
||||
let controller = self.context.sharedContext.makePremiumIntroController(context: self.context, source: .ads, forceDark: false, dismissed: nil)
|
||||
self.push?(controller)
|
||||
}, openPremiumGift: { [weak self] birthdays in
|
||||
}, openPremiumGift: { [weak self] peers, birthdays in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let controller = self.context.sharedContext.makePremiumGiftController(context: self.context, source: .chatList(birthdays), completion: nil)
|
||||
controller.navigationPresentation = .modal
|
||||
self.push?(controller)
|
||||
if peers.count == 1, let peerId = peers.first?.id {
|
||||
let _ = (self.context.engine.payments.premiumGiftCodeOptions(peerId: nil, onlyCached: true)
|
||||
|> filter { !$0.isEmpty }
|
||||
|> deliverOnMainQueue).start(next: { giftOptions in
|
||||
let premiumOptions = giftOptions.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) }
|
||||
let controller = self.context.sharedContext.makeGiftOptionsController(context: self.context, peerId: peerId, premiumOptions: premiumOptions)
|
||||
controller.navigationPresentation = .modal
|
||||
self.push?(controller)
|
||||
})
|
||||
} else {
|
||||
let controller = self.context.sharedContext.makePremiumGiftController(context: self.context, source: .chatList(birthdays), completion: nil)
|
||||
controller.navigationPresentation = .modal
|
||||
self.push?(controller)
|
||||
}
|
||||
}, openPremiumManagement: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -366,6 +366,7 @@ public final class SheetComponent<ChildEnvironmentType: Equatable>: Component {
|
||||
self.scrollView.addSubview(contentView)
|
||||
}
|
||||
contentView.clipsToBounds = component.clipsContent
|
||||
contentView.layer.cornerRadius = self.backgroundView.layer.cornerRadius
|
||||
if sheetEnvironment.isCentered {
|
||||
let y: CGFloat = floorToScreenPixels((availableSize.height - contentSize.height) / 2.0)
|
||||
transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) / 2.0), y: -y), size: contentSize), completion: nil)
|
||||
|
@ -175,6 +175,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
|
||||
let options: [ItemListPeerItemRevealOption]
|
||||
let additionalActions: [ContactsPeerItemAction]
|
||||
let actionIcon: ContactsPeerItemActionIcon
|
||||
let searchQuery: String?
|
||||
let action: ((ContactsPeerItemPeer) -> Void)?
|
||||
let disabledAction: ((ContactsPeerItemPeer) -> Void)?
|
||||
let setPeerIdWithRevealedOptions: ((EnginePeer.Id?, EnginePeer.Id?) -> Void)?
|
||||
@ -215,6 +216,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
|
||||
actionIcon: ContactsPeerItemActionIcon = .none,
|
||||
index: SortIndex?,
|
||||
header: ListViewItemHeader?,
|
||||
searchQuery: String? = nil,
|
||||
action: ((ContactsPeerItemPeer) -> Void)?,
|
||||
disabledAction: ((ContactsPeerItemPeer) -> Void)? = nil,
|
||||
setPeerIdWithRevealedOptions: ((EnginePeer.Id?, EnginePeer.Id?) -> Void)? = nil,
|
||||
@ -245,6 +247,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
|
||||
self.options = options
|
||||
self.additionalActions = additionalActions
|
||||
self.actionIcon = actionIcon
|
||||
self.searchQuery = searchQuery
|
||||
self.action = action
|
||||
self.disabledAction = disabledAction
|
||||
self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions
|
||||
@ -880,7 +883,16 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
||||
statusAttributedString = NSAttributedString(string: string, font: statusFont, textColor: activity ? item.presentationData.theme.list.itemAccentColor : item.presentationData.theme.list.itemSecondaryTextColor)
|
||||
}
|
||||
case let .addressName(suffix):
|
||||
if let addressName = peer.addressName {
|
||||
var addressName = peer.addressName
|
||||
if let currentAddressName = addressName, let searchQuery = item.searchQuery?.lowercased(), !peer.usernames.isEmpty && !currentAddressName.lowercased().contains(searchQuery) {
|
||||
for username in peer.usernames {
|
||||
if username.username.lowercased().contains(searchQuery) {
|
||||
addressName = username.username
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if let addressName {
|
||||
let addressNameString = NSAttributedString(string: "@" + addressName, font: statusFont, textColor: item.presentationData.theme.list.itemAccentColor)
|
||||
if !suffix.isEmpty {
|
||||
let suffixString = NSAttributedString(string: suffix, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
|
||||
|
@ -1,20 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
final class MinimizeKeyboardGestureRecognizer: UISwipeGestureRecognizer, UIGestureRecognizerDelegate {
|
||||
override init(target: Any?, action: Selector?) {
|
||||
super.init(target: target, action: action)
|
||||
|
||||
self.cancelsTouchesInView = false
|
||||
self.delaysTouchesBegan = false
|
||||
self.delaysTouchesEnded = false
|
||||
self.delegate = self
|
||||
|
||||
self.direction = [.left, .right]
|
||||
self.numberOfTouchesRequired = 2
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
@ -1405,8 +1405,15 @@ public class GalleryController: ViewController, StandalonePresentableController,
|
||||
}
|
||||
}
|
||||
|
||||
self.galleryNode.completeCustomDismiss = { [weak self] in
|
||||
self?._hiddenMedia.set(.single(nil))
|
||||
self.galleryNode.completeCustomDismiss = { [weak self] isPictureInPicture in
|
||||
if isPictureInPicture {
|
||||
if let chatController = self?.baseNavigationController?.topViewController as? ChatController {
|
||||
chatController.updatePushedTransition(0.0, transition: .animated(duration: 0.45, curve: .customSpring(damping: 180.0, initialVelocity: 0.0)))
|
||||
}
|
||||
} else {
|
||||
self?._hiddenMedia.set(.single(nil))
|
||||
}
|
||||
|
||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,7 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture
|
||||
public var pager: GalleryPagerNode
|
||||
|
||||
public var beginCustomDismiss: (Bool) -> Void = { _ in }
|
||||
public var completeCustomDismiss: () -> Void = { }
|
||||
public var completeCustomDismiss: (Bool) -> Void = { _ in }
|
||||
public var baseNavigationController: () -> NavigationController? = { return nil }
|
||||
public var galleryController: () -> ViewController? = { return nil }
|
||||
|
||||
@ -123,9 +123,9 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture
|
||||
}
|
||||
}
|
||||
|
||||
self.pager.completeCustomDismiss = { [weak self] in
|
||||
self.pager.completeCustomDismiss = { [weak self] isPictureInPicture in
|
||||
if let strongSelf = self {
|
||||
strongSelf.completeCustomDismiss()
|
||||
strongSelf.completeCustomDismiss(isPictureInPicture)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,7 @@ open class GalleryItemNode: ASDisplayNode {
|
||||
public var updateOrientation: (UIInterfaceOrientation) -> Void = { _ in }
|
||||
public var dismiss: () -> Void = { }
|
||||
public var beginCustomDismiss: (Bool) -> Void = { _ in }
|
||||
public var completeCustomDismiss: () -> Void = { }
|
||||
public var completeCustomDismiss: (Bool) -> Void = { _ in }
|
||||
public var baseNavigationController: () -> NavigationController? = { return nil }
|
||||
public var galleryController: () -> ViewController? = { return nil }
|
||||
public var alternativeDismiss: () -> Bool = { return false }
|
||||
|
@ -110,7 +110,7 @@ public final class GalleryPagerNode: ASDisplayNode, ASScrollViewDelegate, ASGest
|
||||
public var updateOrientation: (UIInterfaceOrientation) -> Void = { _ in }
|
||||
public var dismiss: () -> Void = { }
|
||||
public var beginCustomDismiss: (Bool) -> Void = { _ in }
|
||||
public var completeCustomDismiss: () -> Void = { }
|
||||
public var completeCustomDismiss: (Bool) -> Void = { _ in }
|
||||
public var baseNavigationController: () -> NavigationController? = { return nil }
|
||||
public var galleryController: () -> ViewController? = { return nil }
|
||||
|
||||
|
@ -687,7 +687,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false)))
|
||||
|
||||
Queue.mainQueue().after(0.3) {
|
||||
self.completeCustomDismiss()
|
||||
self.completeCustomDismiss(false)
|
||||
}
|
||||
}
|
||||
f(.default)
|
||||
@ -719,7 +719,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: true)))
|
||||
|
||||
Queue.mainQueue().after(0.3) {
|
||||
self.completeCustomDismiss()
|
||||
self.completeCustomDismiss(false)
|
||||
}
|
||||
}
|
||||
f(.default)
|
||||
|
@ -804,6 +804,245 @@ private final class PictureInPictureContentImpl: NSObject, PictureInPictureConte
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 15.0, *)
|
||||
private final class NativePictureInPictureContentImpl: NSObject, AVPictureInPictureControllerDelegate {
|
||||
private final class PlaybackDelegate: NSObject, AVPictureInPictureSampleBufferPlaybackDelegate {
|
||||
private let node: UniversalVideoNode
|
||||
private var statusDisposable: Disposable?
|
||||
private var status: MediaPlayerStatus?
|
||||
weak var pictureInPictureController: AVPictureInPictureController?
|
||||
|
||||
private var previousIsPlaying = false
|
||||
init(node: UniversalVideoNode) {
|
||||
self.node = node
|
||||
|
||||
super.init()
|
||||
|
||||
var invalidatedStateOnce = false
|
||||
self.statusDisposable = (self.node.status
|
||||
|> deliverOnMainQueue).start(next: { [weak self] status in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.status = status
|
||||
if let status {
|
||||
let isPlaying = status.status == .playing
|
||||
if !invalidatedStateOnce {
|
||||
invalidatedStateOnce = true
|
||||
strongSelf.pictureInPictureController?.invalidatePlaybackState()
|
||||
} else if strongSelf.previousIsPlaying != isPlaying {
|
||||
strongSelf.previousIsPlaying = isPlaying
|
||||
strongSelf.pictureInPictureController?.invalidatePlaybackState()
|
||||
}
|
||||
}
|
||||
}).strict()
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.statusDisposable?.dispose()
|
||||
}
|
||||
|
||||
public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, setPlaying playing: Bool) {
|
||||
self.node.togglePlayPause()
|
||||
}
|
||||
|
||||
public func pictureInPictureControllerTimeRangeForPlayback(_ pictureInPictureController: AVPictureInPictureController) -> CMTimeRange {
|
||||
guard let status = self.status else {
|
||||
return CMTimeRange(start: CMTime(seconds: 0.0, preferredTimescale: CMTimeScale(30.0)), duration: CMTime(seconds: 0.0, preferredTimescale: CMTimeScale(30.0)))
|
||||
}
|
||||
return CMTimeRange(start: CMTime(seconds: 0.0, preferredTimescale: CMTimeScale(30.0)), duration: CMTime(seconds: status.duration - status.timestamp, preferredTimescale: CMTimeScale(30.0)))
|
||||
}
|
||||
|
||||
public func pictureInPictureControllerIsPlaybackPaused(_ pictureInPictureController: AVPictureInPictureController) -> Bool {
|
||||
guard let status = self.status else {
|
||||
return false
|
||||
}
|
||||
switch status.status {
|
||||
case .playing:
|
||||
return false
|
||||
case .buffering, .paused:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, didTransitionToRenderSize newRenderSize: CMVideoDimensions) {
|
||||
}
|
||||
|
||||
public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, skipByInterval skipInterval: CMTime, completion completionHandler: @escaping () -> Void) {
|
||||
let node = self.node
|
||||
let _ = (self.node.status
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak node] status in
|
||||
if let node = node, let timestamp = status?.timestamp, let duration = status?.duration {
|
||||
let nextTimestamp = timestamp + skipInterval.seconds
|
||||
if nextTimestamp > duration {
|
||||
node.seek(0.0)
|
||||
node.pause()
|
||||
} else {
|
||||
node.seek(min(duration, nextTimestamp))
|
||||
}
|
||||
}
|
||||
|
||||
completionHandler()
|
||||
})
|
||||
}
|
||||
|
||||
public func pictureInPictureControllerShouldProhibitBackgroundAudioPlayback(_ pictureInPictureController: AVPictureInPictureController) -> Bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private let context: AccountContext
|
||||
private let accountId: AccountRecordId
|
||||
private let hiddenMedia: (MessageId, Media)?
|
||||
private weak var mediaManager: MediaManager?
|
||||
private var pictureInPictureController: AVPictureInPictureController?
|
||||
private var contentDelegate: PlaybackDelegate?
|
||||
private let node: UniversalVideoNode
|
||||
private let willBegin: (NativePictureInPictureContentImpl) -> Void
|
||||
private let didBegin: (NativePictureInPictureContentImpl) -> Void
|
||||
private let expand: (@escaping () -> Void) -> Void
|
||||
private var pictureInPictureTimer: SwiftSignalKit.Timer?
|
||||
private var didExpand: Bool = false
|
||||
|
||||
private var hiddenMediaManagerIndex: Int?
|
||||
|
||||
private var messageRemovedDisposable: Disposable?
|
||||
|
||||
private var isNativePictureInPictureActiveDisposable: Disposable?
|
||||
|
||||
init(context: AccountContext, mediaManager: MediaManager, accountId: AccountRecordId, hiddenMedia: (MessageId, Media)?, videoNode: UniversalVideoNode, canSkip: Bool, willBegin: @escaping (NativePictureInPictureContentImpl) -> Void, didBegin: @escaping (NativePictureInPictureContentImpl) -> Void, expand: @escaping (@escaping () -> Void) -> Void) {
|
||||
self.context = context
|
||||
self.mediaManager = mediaManager
|
||||
self.accountId = accountId
|
||||
self.hiddenMedia = hiddenMedia
|
||||
self.node = videoNode
|
||||
self.willBegin = willBegin
|
||||
self.didBegin = didBegin
|
||||
self.expand = expand
|
||||
|
||||
super.init()
|
||||
|
||||
if let videoLayer = videoNode.getVideoLayer() {
|
||||
let contentDelegate = PlaybackDelegate(node: self.node)
|
||||
self.contentDelegate = contentDelegate
|
||||
|
||||
let pictureInPictureController = AVPictureInPictureController(contentSource: AVPictureInPictureController.ContentSource(sampleBufferDisplayLayer: videoLayer, playbackDelegate: contentDelegate))
|
||||
self.pictureInPictureController = pictureInPictureController
|
||||
contentDelegate.pictureInPictureController = pictureInPictureController
|
||||
|
||||
pictureInPictureController.canStartPictureInPictureAutomaticallyFromInline = false
|
||||
pictureInPictureController.requiresLinearPlayback = !canSkip
|
||||
pictureInPictureController.delegate = self
|
||||
self.pictureInPictureController = pictureInPictureController
|
||||
}
|
||||
|
||||
if let (messageId, _) = hiddenMedia {
|
||||
self.messageRemovedDisposable = (context.engine.data.subscribe(TelegramEngine.EngineData.Item.Messages.Message(id: messageId))
|
||||
|> map { message -> Bool in
|
||||
if let _ = message {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|> filter { $0 }
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.node.canAttachContent = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.messageRemovedDisposable?.dispose()
|
||||
self.isNativePictureInPictureActiveDisposable?.dispose()
|
||||
self.pictureInPictureTimer?.invalidate()
|
||||
self.node.setCanPlaybackWithoutHierarchy(false)
|
||||
|
||||
if let hiddenMediaManagerIndex = self.hiddenMediaManagerIndex, let mediaManager = self.mediaManager {
|
||||
mediaManager.galleryHiddenMediaManager.removeSource(hiddenMediaManagerIndex)
|
||||
}
|
||||
}
|
||||
|
||||
func updateIsCentral(isCentral: Bool) {
|
||||
guard let pictureInPictureController = self.pictureInPictureController else {
|
||||
return
|
||||
}
|
||||
|
||||
if isCentral {
|
||||
pictureInPictureController.canStartPictureInPictureAutomaticallyFromInline = true
|
||||
} else {
|
||||
pictureInPictureController.canStartPictureInPictureAutomaticallyFromInline = false
|
||||
}
|
||||
}
|
||||
|
||||
func beginPictureInPicture() {
|
||||
guard let pictureInPictureController = self.pictureInPictureController else {
|
||||
return
|
||||
}
|
||||
if pictureInPictureController.isPictureInPicturePossible {
|
||||
pictureInPictureController.startPictureInPicture()
|
||||
}
|
||||
}
|
||||
|
||||
func invalidatePlaybackState() {
|
||||
self.pictureInPictureController?.invalidatePlaybackState()
|
||||
}
|
||||
|
||||
public func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
|
||||
self.node.setCanPlaybackWithoutHierarchy(true)
|
||||
|
||||
if let hiddenMedia = self.hiddenMedia, let mediaManager = self.mediaManager {
|
||||
let accountId = self.accountId
|
||||
self.hiddenMediaManagerIndex = mediaManager.galleryHiddenMediaManager.addSource(Signal<(MessageId, Media)?, NoError>.single(hiddenMedia)
|
||||
|> map { messageIdAndMedia in
|
||||
if let (messageId, media) = messageIdAndMedia {
|
||||
return .chat(accountId, messageId, media)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
self.willBegin(self)
|
||||
}
|
||||
|
||||
public func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
|
||||
self.didBegin(self)
|
||||
}
|
||||
|
||||
public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, failedToStartPictureInPictureWithError error: Error) {
|
||||
print(error)
|
||||
}
|
||||
|
||||
public func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
|
||||
}
|
||||
|
||||
public func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
|
||||
self.node.setCanPlaybackWithoutHierarchy(false)
|
||||
if let hiddenMediaManagerIndex = self.hiddenMediaManagerIndex, let mediaManager = self.mediaManager {
|
||||
mediaManager.galleryHiddenMediaManager.removeSource(hiddenMediaManagerIndex)
|
||||
self.hiddenMediaManagerIndex = nil
|
||||
}
|
||||
}
|
||||
|
||||
public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {
|
||||
self.expand { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.didExpand = true
|
||||
|
||||
completionHandler(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
private let context: AccountContext
|
||||
private let presentationData: PresentationData
|
||||
@ -875,12 +1114,17 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
private let isShowingContextMenuPromise = ValuePromise<Bool>(false, ignoreRepeated: true)
|
||||
private let hasExpandedCaptionPromise = Promise<Bool>()
|
||||
private var hideControlsDisposable: Disposable?
|
||||
private var automaticPictureInPictureDisposable: Disposable?
|
||||
|
||||
var playbackCompleted: (() -> Void)?
|
||||
|
||||
private var customUnembedWhenPortrait: ((OverlayMediaItemNode) -> Bool)?
|
||||
|
||||
private var pictureInPictureContent: AnyObject?
|
||||
private var nativePictureInPictureContent: AnyObject?
|
||||
|
||||
private var activePictureInPictureNavigationController: NavigationController?
|
||||
private var activePictureInPictureController: ViewController?
|
||||
|
||||
init(context: AccountContext, presentationData: PresentationData, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction, Message) -> Void, present: @escaping (ViewController, Any?) -> Void) {
|
||||
self.context = context
|
||||
@ -1064,6 +1308,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
self.mediaPlaybackStateDisposable.dispose()
|
||||
self.scrubbingFrameDisposable?.dispose()
|
||||
self.hideControlsDisposable?.dispose()
|
||||
self.automaticPictureInPictureDisposable?.dispose()
|
||||
}
|
||||
|
||||
override func ready() -> Signal<Void, NoError> {
|
||||
@ -1259,6 +1504,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
strongSelf.videoNode?.setBaseRate(playbackRate)
|
||||
}
|
||||
}
|
||||
|
||||
if strongSelf.nativePictureInPictureContent == nil {
|
||||
strongSelf.setupNativePictureInPicture()
|
||||
}
|
||||
}
|
||||
}
|
||||
self.videoNode = videoNode
|
||||
@ -1749,6 +1998,12 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if #available(iOS 15.0, *) {
|
||||
if let nativePictureInPictureContent = self.nativePictureInPictureContent as? NativePictureInPictureContentImpl {
|
||||
nativePictureInPictureContent.updateIsCentral(isCentral: isCentral)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2330,13 +2585,91 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
self.beginCustomDismiss(false)
|
||||
self.statusNode.isHidden = true
|
||||
self.animateOut(toOverlay: overlayNode, completion: { [weak self] in
|
||||
self?.completeCustomDismiss()
|
||||
self?.completeCustomDismiss(false)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setupNativePictureInPicture() {
|
||||
guard let item = self.item, let videoNode = self.videoNode else {
|
||||
return
|
||||
}
|
||||
|
||||
if !(item.content is NativeVideoContent) {
|
||||
return
|
||||
}
|
||||
|
||||
var useNative = true
|
||||
if let data = self.context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_native_pip"] {
|
||||
useNative = false
|
||||
}
|
||||
if !useNative {
|
||||
return
|
||||
}
|
||||
|
||||
var hiddenMedia: (MessageId, Media)? = nil
|
||||
switch item.contentInfo {
|
||||
case let .message(message, _):
|
||||
for media in message.media {
|
||||
if let media = media as? TelegramMediaImage {
|
||||
hiddenMedia = (message.id, media)
|
||||
} else if let media = media as? TelegramMediaFile, media.isVideo {
|
||||
hiddenMedia = (message.id, media)
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if #available(iOS 15.0, *) {
|
||||
let content = NativePictureInPictureContentImpl(context: self.context, mediaManager: self.context.sharedContext.mediaManager, accountId: self.context.account.id, hiddenMedia: hiddenMedia, videoNode: videoNode, canSkip: true, willBegin: { [weak self] content in
|
||||
guard let self, let controller = self.galleryController(), let navigationController = self.baseNavigationController() else {
|
||||
return
|
||||
}
|
||||
self.activePictureInPictureNavigationController = navigationController
|
||||
self.activePictureInPictureController = controller
|
||||
|
||||
controller.view.alpha = 0.0
|
||||
controller.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in
|
||||
self?.completeCustomDismiss(true)
|
||||
})
|
||||
}, didBegin: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let _ = self
|
||||
}, expand: { [weak self] completion in
|
||||
guard let self, let activePictureInPictureController = self.activePictureInPictureController, let activePictureInPictureNavigationController = self.activePictureInPictureNavigationController else {
|
||||
completion()
|
||||
return
|
||||
}
|
||||
|
||||
self.activePictureInPictureController = nil
|
||||
self.activePictureInPictureNavigationController = nil
|
||||
|
||||
activePictureInPictureController.presentationArguments = nil
|
||||
activePictureInPictureNavigationController.currentWindow?.present(activePictureInPictureController, on: .root, blockInteraction: false, completion: {
|
||||
})
|
||||
|
||||
activePictureInPictureController.view.alpha = 1.0
|
||||
activePictureInPictureController.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
})
|
||||
|
||||
self.nativePictureInPictureContent = content
|
||||
}
|
||||
}
|
||||
|
||||
@objc func pictureInPictureButtonPressed() {
|
||||
if #available(iOS 15.0, *) {
|
||||
if let nativePictureInPictureContent = self.nativePictureInPictureContent as? NativePictureInPictureContentImpl {
|
||||
nativePictureInPictureContent.beginPictureInPicture()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var isNativePictureInPictureSupported = false
|
||||
switch self.item?.contentInfo {
|
||||
case let .message(message, _):
|
||||
@ -2391,7 +2724,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.completeCustomDismiss()
|
||||
strongSelf.completeCustomDismiss(false)
|
||||
}, expand: { [weak baseNavigationController] completion in
|
||||
guard let contentInfo = item.contentInfo else {
|
||||
return
|
||||
@ -2524,7 +2857,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
self.beginCustomDismiss(false)
|
||||
self.statusNode.isHidden = true
|
||||
self.animateOut(toOverlay: overlayNode, completion: { [weak self] in
|
||||
self?.completeCustomDismiss()
|
||||
self?.completeCustomDismiss(false)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -2847,7 +3180,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false)))
|
||||
|
||||
Queue.mainQueue().after(0.3) {
|
||||
strongSelf.completeCustomDismiss()
|
||||
strongSelf.completeCustomDismiss(false)
|
||||
}
|
||||
}
|
||||
f(.default)
|
||||
@ -2928,7 +3261,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: true)))
|
||||
|
||||
Queue.mainQueue().after(0.3) {
|
||||
self.completeCustomDismiss()
|
||||
self.completeCustomDismiss(false)
|
||||
}
|
||||
}
|
||||
f(.default)
|
||||
|
@ -300,7 +300,7 @@ public final class SecretMediaPreviewController: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
self.controllerNode.completeCustomDismiss = { [weak self] in
|
||||
self.controllerNode.completeCustomDismiss = { [weak self] _ in
|
||||
self?._hiddenMedia.set(.single(nil))
|
||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||
}
|
||||
|
@ -398,7 +398,7 @@ public class InstantPageGalleryController: ViewController, StandalonePresentable
|
||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||
}
|
||||
|
||||
self.galleryNode.completeCustomDismiss = { [weak self] in
|
||||
self.galleryNode.completeCustomDismiss = { [weak self] _ in
|
||||
self?._hiddenMedia.set(.single(nil))
|
||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -222,7 +222,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, ASScrollView
|
||||
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in
|
||||
}, activateChatPreview: { _, _, _, gesture, _ in
|
||||
gesture?.cancel()
|
||||
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: {
|
||||
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: {
|
||||
}, openBirthdaySetup: {
|
||||
}, performActiveSessionAction: { _, _ in
|
||||
}, openChatFolderUpdates: {}, hideChatFolderUpdates: {
|
||||
|
@ -371,7 +371,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
||||
}, activateChatPreview: { _, _, _, gesture, _ in
|
||||
gesture?.cancel()
|
||||
}, present: { _ in
|
||||
}, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: {
|
||||
}, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: {
|
||||
}, openBirthdaySetup: {
|
||||
}, performActiveSessionAction: { _, _ in
|
||||
}, openChatFolderUpdates: {}, hideChatFolderUpdates: {
|
||||
|
@ -160,11 +160,11 @@ final class VideoChatActionButtonComponent: Component {
|
||||
case .unmuted:
|
||||
backgroundColor = !isActive ? UIColor(rgb: 0x124B21) : UIColor(rgb: 0x34C659)
|
||||
case .raiseHand, .scheduled:
|
||||
backgroundColor = UIColor(rgb: 0x3252EF)
|
||||
backgroundColor = !isActive ? UIColor(rgb: 0x23306B) : UIColor(rgb: 0x3252EF)
|
||||
}
|
||||
iconDiameter = 60.0
|
||||
case let .video(isActive):
|
||||
titleText = "video"
|
||||
titleText = component.strings.VoiceChat_Video
|
||||
switch component.microphoneState {
|
||||
case .connecting:
|
||||
backgroundColor = UIColor(white: 0.1, alpha: 1.0)
|
||||
@ -177,7 +177,7 @@ final class VideoChatActionButtonComponent: Component {
|
||||
}
|
||||
iconDiameter = 60.0
|
||||
case .leave:
|
||||
titleText = "leave"
|
||||
titleText = component.strings.VoiceChat_Leave
|
||||
backgroundColor = UIColor(rgb: 0x47191E)
|
||||
iconDiameter = 22.0
|
||||
}
|
||||
@ -282,6 +282,7 @@ final class VideoChatActionButtonComponent: Component {
|
||||
}
|
||||
|
||||
self.isEnabled = isEnabled
|
||||
self.isUserInteractionEnabled = isEnabled
|
||||
|
||||
return size
|
||||
}
|
||||
|
@ -189,6 +189,7 @@ final class VideoChatMicButtonComponent: Component {
|
||||
}
|
||||
|
||||
let call: PresentationGroupCall
|
||||
let strings: PresentationStrings
|
||||
let content: Content
|
||||
let isCollapsed: Bool
|
||||
let updateUnmutedStateIsPushToTalk: (Bool?) -> Void
|
||||
@ -197,6 +198,7 @@ final class VideoChatMicButtonComponent: Component {
|
||||
|
||||
init(
|
||||
call: PresentationGroupCall,
|
||||
strings: PresentationStrings,
|
||||
content: Content,
|
||||
isCollapsed: Bool,
|
||||
updateUnmutedStateIsPushToTalk: @escaping (Bool?) -> Void,
|
||||
@ -204,6 +206,7 @@ final class VideoChatMicButtonComponent: Component {
|
||||
scheduleAction: @escaping () -> Void
|
||||
) {
|
||||
self.call = call
|
||||
self.strings = strings
|
||||
self.content = content
|
||||
self.isCollapsed = isCollapsed
|
||||
self.updateUnmutedStateIsPushToTalk = updateUnmutedStateIsPushToTalk
|
||||
@ -327,29 +330,29 @@ final class VideoChatMicButtonComponent: Component {
|
||||
var isEnabled = true
|
||||
switch component.content {
|
||||
case .connecting:
|
||||
titleText = "Connecting..."
|
||||
titleText = component.strings.VoiceChat_Connecting
|
||||
isEnabled = false
|
||||
case .muted:
|
||||
titleText = "Unmute"
|
||||
titleText = component.strings.VoiceChat_Unmute
|
||||
case let .unmuted(isPushToTalk):
|
||||
titleText = isPushToTalk ? "You are Live" : "Tap to Mute"
|
||||
titleText = isPushToTalk ? component.strings.VoiceChat_Live : component.strings.VoiceChat_Mute
|
||||
case let .raiseHand(isRaised):
|
||||
if isRaised {
|
||||
titleText = "You asked to speak"
|
||||
subtitleText = "We let the speakers know"
|
||||
titleText = component.strings.VoiceChat_AskedToSpeak
|
||||
subtitleText = component.strings.VoiceChat_AskedToSpeakHelp
|
||||
} else {
|
||||
titleText = "Muted by Admin"
|
||||
subtitleText = "Tap if you want to speak"
|
||||
titleText = component.strings.VoiceChat_MutedByAdmin
|
||||
subtitleText = component.strings.VoiceChat_MutedByAdminHelp
|
||||
}
|
||||
case let .scheduled(state):
|
||||
switch state {
|
||||
case .start:
|
||||
titleText = "Start Now"
|
||||
titleText = component.strings.VoiceChat_StartNow
|
||||
case let .toggleSubscription(isSubscribed):
|
||||
if isSubscribed {
|
||||
titleText = "Clear Reminder"
|
||||
titleText = component.strings.VoiceChat_CancelReminder
|
||||
} else {
|
||||
titleText = "Set Reminder"
|
||||
titleText = component.strings.VoiceChat_SetReminder
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1193,17 +1193,17 @@ final class VideoChatParticipantsComponent: Component {
|
||||
|
||||
let subtitle: PeerListItemComponent.Subtitle
|
||||
if participant.peer.id == component.call.accountContext.account.peerId {
|
||||
subtitle = PeerListItemComponent.Subtitle(text: "this is you", color: .accent)
|
||||
subtitle = PeerListItemComponent.Subtitle(text: component.strings.VoiceChat_You, color: .accent)
|
||||
} else if component.speakingParticipants.contains(participant.peer.id) {
|
||||
if let volume = participant.volume, volume / 100 != 100 {
|
||||
subtitle = PeerListItemComponent.Subtitle(text: "\(volume / 100)% speaking", color: .constructive)
|
||||
subtitle = PeerListItemComponent.Subtitle(text: component.strings.VoiceChat_StatusSpeakingVolume("\(volume / 100)%").string, color: .constructive)
|
||||
} else {
|
||||
subtitle = PeerListItemComponent.Subtitle(text: "speaking", color: .constructive)
|
||||
subtitle = PeerListItemComponent.Subtitle(text: component.strings.VoiceChat_StatusSpeaking, color: .constructive)
|
||||
}
|
||||
} else if let about = participant.about, !about.isEmpty {
|
||||
subtitle = PeerListItemComponent.Subtitle(text: about, color: .neutral)
|
||||
} else {
|
||||
subtitle = PeerListItemComponent.Subtitle(text: "listening", color: .neutral)
|
||||
subtitle = PeerListItemComponent.Subtitle(text: component.strings.VoiceChat_StatusListening, color: .neutral)
|
||||
}
|
||||
|
||||
let rightAccessoryComponent: AnyComponent<Empty> = AnyComponent(VideoChatParticipantStatusComponent(
|
||||
@ -1667,12 +1667,12 @@ final class VideoChatParticipantsComponent: Component {
|
||||
if let participants = component.participants, let inviteType = participants.inviteType {
|
||||
switch inviteType {
|
||||
case .invite:
|
||||
inviteText = "Invite Members"
|
||||
inviteText = component.strings.VoiceChat_InviteMember
|
||||
case .shareLink:
|
||||
inviteText = "Share Invite Link"
|
||||
inviteText = component.strings.VoiceChat_Share
|
||||
}
|
||||
} else {
|
||||
inviteText = "Invite Members"
|
||||
inviteText = component.strings.VoiceChat_InviteMember
|
||||
}
|
||||
let inviteListItemSize = self.inviteListItemView.update(
|
||||
transition: transition,
|
||||
|
@ -123,7 +123,7 @@ final class VideoChatScheduledInfoComponent: Component {
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: "Starts in", font: Font.with(size: 23.0, design: .round, weight: .semibold), textColor: .white))
|
||||
text: .plain(NSAttributedString(string: component.strings.VoiceChat_StartsIn, font: Font.with(size: 23.0, design: .round, weight: .semibold), textColor: .white))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: 200.0)
|
||||
|
@ -1022,7 +1022,7 @@ final class VideoChatScreenComponent: Component {
|
||||
if case let .channel(channel) = self.peer, case .broadcast = channel.info {
|
||||
displayEvent = false
|
||||
}
|
||||
if members.totalCount < 250 {
|
||||
if members.totalCount < 40 {
|
||||
displayEvent = true
|
||||
} else if event.peer.isVerified {
|
||||
displayEvent = true
|
||||
@ -1270,9 +1270,9 @@ final class VideoChatScreenComponent: Component {
|
||||
if callState.networkState == .connected, let members = self.members {
|
||||
idleTitleStatusText = environment.strings.VoiceChat_Panel_Members(Int32(max(1, members.totalCount)))
|
||||
} else if callState.scheduleTimestamp != nil {
|
||||
idleTitleStatusText = "scheduled"
|
||||
idleTitleStatusText = environment.strings.VoiceChat_Scheduled
|
||||
} else {
|
||||
idleTitleStatusText = "connecting..."
|
||||
idleTitleStatusText = environment.strings.VoiceChat_Connecting
|
||||
}
|
||||
} else {
|
||||
idleTitleStatusText = " "
|
||||
@ -1686,6 +1686,7 @@ final class VideoChatScreenComponent: Component {
|
||||
transition: transition,
|
||||
component: AnyComponent(VideoChatMicButtonComponent(
|
||||
call: component.call,
|
||||
strings: environment.strings,
|
||||
content: micButtonContent,
|
||||
isCollapsed: areButtonsCollapsed,
|
||||
updateUnmutedStateIsPushToTalk: { [weak self] unmutedStateIsPushToTalk in
|
||||
|
@ -7098,7 +7098,10 @@ final class VoiceChatContextReferenceContentSource: ContextReferenceContentSourc
|
||||
}
|
||||
|
||||
public func shouldUseV2VideoChatImpl(context: AccountContext) -> Bool {
|
||||
var useV2 = true
|
||||
var useV2 = false
|
||||
if let data = context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_enable_videochatui_v2"] {
|
||||
useV2 = true
|
||||
}
|
||||
if context.sharedContext.immediateExperimentalUISettings.disableCallV2 {
|
||||
useV2 = false
|
||||
}
|
||||
|
@ -595,6 +595,7 @@ public enum SendBotPaymentFormError {
|
||||
case precheckoutFailed
|
||||
case paymentFailed
|
||||
case alreadyPaid
|
||||
case starGiftOutOfStock
|
||||
}
|
||||
|
||||
public enum SendBotPaymentResult {
|
||||
|
@ -1313,6 +1313,8 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot
|
||||
return .fail(.alreadyPaid)
|
||||
} else if error.errorDescription == "MEDIA_ALREADY_PAID" {
|
||||
return .fail(.alreadyPaid)
|
||||
} else if error.errorDescription == "STARGIFT_USAGE_LIMITED" {
|
||||
return .fail(.starGiftOutOfStock)
|
||||
}
|
||||
return .fail(.generic)
|
||||
}
|
||||
|
@ -462,6 +462,10 @@ public extension EnginePeer {
|
||||
var addressName: String? {
|
||||
return self._asPeer().addressName
|
||||
}
|
||||
|
||||
var usernames: [TelegramPeerUsername] {
|
||||
return self._asPeer().usernames
|
||||
}
|
||||
|
||||
var indexName: EnginePeer.IndexName {
|
||||
return EnginePeer.IndexName(self._asPeer().indexName)
|
||||
|
@ -51,7 +51,7 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
|
||||
|
||||
var renderedMyPeers: [FoundPeer] = []
|
||||
@ -61,7 +61,11 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo
|
||||
if let group = peer as? TelegramGroup, group.migrationReference != nil {
|
||||
continue
|
||||
}
|
||||
renderedMyPeers.append(FoundPeer(peer: peer, subscribers: subscribers[peerId]))
|
||||
if let user = peer as? TelegramUser {
|
||||
renderedMyPeers.append(FoundPeer(peer: peer, subscribers: user.subscriberCount))
|
||||
} else {
|
||||
renderedMyPeers.append(FoundPeer(peer: peer, subscribers: subscribers[peerId]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,7 +76,11 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo
|
||||
if let group = peer as? TelegramGroup, group.migrationReference != nil {
|
||||
continue
|
||||
}
|
||||
renderedPeers.append(FoundPeer(peer: peer, subscribers: subscribers[peerId]))
|
||||
if let user = peer as? TelegramUser {
|
||||
renderedPeers.append(FoundPeer(peer: peer, subscribers: user.subscriberCount))
|
||||
} else {
|
||||
renderedPeers.append(FoundPeer(peer: peer, subscribers: subscribers[peerId]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -80,7 +80,7 @@ public final class ButtonBadgeComponent: Component {
|
||||
if contentView.superview == nil {
|
||||
self.addSubview(contentView)
|
||||
}
|
||||
transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: floor((backgroundFrame.width - contentSize.width) * 0.5), y: floor((backgroundFrame.height - contentSize.height) * 0.5)), size: contentSize))
|
||||
transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundFrame.width - contentSize.width) * 0.5), y: floorToScreenPixels((backgroundFrame.height - contentSize.height) * 0.5)), size: contentSize))
|
||||
}
|
||||
|
||||
if themeUpdated || backgroundFrame.height != self.backgroundView.image?.size.height {
|
||||
@ -264,7 +264,7 @@ public final class ButtonTextContentComponent: Component {
|
||||
size.height = max(size.height, badgeSize.height)
|
||||
}
|
||||
|
||||
let contentFrame = CGRect(origin: CGPoint(x: floor((size.width - measurementSize.width) * 0.5), y: floor((size.height - measurementSize.height) * 0.5)), size: measurementSize)
|
||||
let contentFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - measurementSize.width) * 0.5), y: floorToScreenPixels((size.height - measurementSize.height) * 0.5)), size: measurementSize)
|
||||
|
||||
if let contentView = self.content.view {
|
||||
if contentView.superview == nil {
|
||||
@ -274,7 +274,7 @@ public final class ButtonTextContentComponent: Component {
|
||||
}
|
||||
|
||||
if let badgeSize, let badge = self.badge {
|
||||
let badgeFrame = CGRect(origin: CGPoint(x: contentFrame.minX + contentSize.width + badgeSpacing, y: floor((size.height - badgeSize.height) * 0.5) + 1.0), size: badgeSize)
|
||||
let badgeFrame = CGRect(origin: CGPoint(x: contentFrame.minX + contentSize.width + badgeSpacing, y: floorToScreenPixels((size.height - badgeSize.height) * 0.5) + 1.0), size: badgeSize)
|
||||
|
||||
if let badgeView = badge.view {
|
||||
var animateIn = false
|
||||
@ -490,7 +490,7 @@ public final class ButtonComponent: Component {
|
||||
contentView.isUserInteractionEnabled = false
|
||||
self.addSubview(contentView)
|
||||
}
|
||||
let contentFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - contentSize.width) * 0.5), y: floor((availableSize.height - contentSize.height) * 0.5)), size: contentSize)
|
||||
let contentFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) * 0.5), y: floorToScreenPixels((availableSize.height - contentSize.height) * 0.5)), size: contentSize)
|
||||
|
||||
contentTransition.setFrame(view: contentView, frame: contentFrame)
|
||||
contentTransition.setAlpha(view: contentView, alpha: contentAlpha)
|
||||
@ -528,7 +528,7 @@ public final class ButtonComponent: Component {
|
||||
}
|
||||
let indicatorSize = CGSize(width: 22.0, height: 22.0)
|
||||
transition.setAlpha(view: activityIndicator.view, alpha: 1.0)
|
||||
activityIndicatorTransition.setFrame(view: activityIndicator.view, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - indicatorSize.width) / 2.0), y: floor((availableSize.height - indicatorSize.height) / 2.0)), size: indicatorSize))
|
||||
activityIndicatorTransition.setFrame(view: activityIndicator.view, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - indicatorSize.width) / 2.0), y: floorToScreenPixels((availableSize.height - indicatorSize.height) / 2.0)), size: indicatorSize))
|
||||
} else {
|
||||
if let activityIndicator = self.activityIndicator {
|
||||
self.activityIndicator = nil
|
||||
|
@ -648,7 +648,7 @@ public final class ChatInlineSearchResultsListComponent: Component {
|
||||
},
|
||||
openPremiumIntro: {
|
||||
},
|
||||
openPremiumGift: { _ in
|
||||
openPremiumGift: { _, _ in
|
||||
},
|
||||
openPremiumManagement: {
|
||||
},
|
||||
|
@ -6203,17 +6203,13 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
return
|
||||
}
|
||||
|
||||
let effectiveMediaVisibility = self.visibility
|
||||
|
||||
var isPlaying = true
|
||||
if case let .visible(_, subRect) = self.visibility {
|
||||
if subRect.minY > 32.0 {
|
||||
isPlaying = false
|
||||
}
|
||||
} else {
|
||||
isPlaying = false
|
||||
}
|
||||
if !item.controllerInteraction.canReadHistory {
|
||||
isPlaying = false
|
||||
}
|
||||
|
||||
if self.forceStopAnimations {
|
||||
isPlaying = false
|
||||
}
|
||||
@ -6228,7 +6224,19 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
}
|
||||
|
||||
for contentNode in self.contentNodes {
|
||||
contentNode.visibility = mapVisibility(effectiveVisibility, boundsSize: self.bounds.size, insets: self.insets, to: contentNode)
|
||||
if contentNode is ChatMessageMediaBubbleContentNode || contentNode is ChatMessageGiftBubbleContentNode {
|
||||
contentNode.visibility = mapVisibility(effectiveMediaVisibility, boundsSize: self.bounds.size, insets: self.insets, to: contentNode)
|
||||
} else {
|
||||
contentNode.visibility = mapVisibility(effectiveVisibility, boundsSize: self.bounds.size, insets: self.insets, to: contentNode)
|
||||
}
|
||||
}
|
||||
|
||||
if case let .visible(_, subRect) = self.visibility {
|
||||
if subRect.minY > 32.0 {
|
||||
isPlaying = false
|
||||
}
|
||||
} else {
|
||||
isPlaying = false
|
||||
}
|
||||
|
||||
if let threadInfoNode = self.threadInfoNode {
|
||||
|
@ -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
|
||||
@ -50,6 +51,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
private let buttonStarsNode: PremiumStarsNode
|
||||
private let buttonTitleNode: TextNode
|
||||
|
||||
private let moreTextNode: TextNode
|
||||
|
||||
private var maskView: UIImageView?
|
||||
private var maskOverlayView: UIView?
|
||||
|
||||
@ -61,6 +64,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
private var isExpanded: Bool = false
|
||||
private var appliedIsExpanded: Bool = false
|
||||
|
||||
private var isStarGift = false
|
||||
|
||||
private var currentProgressDisposable: Disposable?
|
||||
|
||||
override public var visibility: ListViewItemNodeVisibility {
|
||||
@ -138,6 +143,10 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
self.ribbonTextNode.isUserInteractionEnabled = false
|
||||
self.ribbonTextNode.displaysAsynchronously = false
|
||||
|
||||
self.moreTextNode = TextNode()
|
||||
self.moreTextNode.isUserInteractionEnabled = false
|
||||
self.moreTextNode.displaysAsynchronously = false
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.labelNode)
|
||||
@ -147,10 +156,11 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
self.textClippingNode.addSubnode(self.subtitleNode.textNode)
|
||||
self.addSubnode(self.placeholderNode)
|
||||
self.addSubnode(self.animationNode)
|
||||
self.addSubnode(self.moreTextNode)
|
||||
|
||||
self.addSubnode(self.buttonNode)
|
||||
self.buttonNode.addSubnode(self.buttonStarsNode)
|
||||
self.addSubnode(self.buttonTitleNode)
|
||||
self.buttonNode.addSubnode(self.buttonTitleNode)
|
||||
|
||||
self.addSubnode(self.ribbonBackgroundNode)
|
||||
self.addSubnode(self.ribbonTextNode)
|
||||
@ -160,13 +170,9 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
if highlighted {
|
||||
strongSelf.buttonNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.buttonNode.alpha = 0.4
|
||||
strongSelf.buttonTitleNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.buttonTitleNode.alpha = 0.4
|
||||
} else {
|
||||
strongSelf.buttonNode.alpha = 1.0
|
||||
strongSelf.buttonNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
strongSelf.buttonTitleNode.alpha = 1.0
|
||||
strongSelf.buttonTitleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -183,6 +189,19 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
self.currentProgressDisposable?.dispose()
|
||||
}
|
||||
|
||||
override public func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.maskView = UIImageView()
|
||||
|
||||
let maskOverlayView = UIView()
|
||||
maskOverlayView.alpha = 0.0
|
||||
maskOverlayView.backgroundColor = .white
|
||||
self.maskOverlayView = maskOverlayView
|
||||
|
||||
self.maskView?.addSubview(maskOverlayView)
|
||||
}
|
||||
|
||||
@objc private func buttonPressed() {
|
||||
guard let item = self.item else {
|
||||
return
|
||||
@ -190,6 +209,14 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
let _ = item.controllerInteraction.openMessage(item.message, OpenMessageParams(mode: .default, progress: self.makeProgress()))
|
||||
}
|
||||
|
||||
private func expandPressed() {
|
||||
self.isExpanded = !self.isExpanded
|
||||
guard let item = self.item else{
|
||||
return
|
||||
}
|
||||
let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false)
|
||||
}
|
||||
|
||||
private func makeProgress() -> Promise<Bool> {
|
||||
let progress = Promise<Bool>()
|
||||
self.currentProgressDisposable?.dispose()
|
||||
@ -260,9 +287,11 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
let makeSubtitleLayout = TextNodeWithEntities.asyncLayout(self.subtitleNode)
|
||||
let makeSpoilerSubtitleLayout = TextNodeWithEntities.asyncLayout(self.spoilerSubtitleNode)
|
||||
let makeButtonTitleLayout = TextNode.asyncLayout(self.buttonTitleNode)
|
||||
let makeRibbonTextLayout = TextNode.asyncLayout(self.ribbonTextNode)
|
||||
let makeMeasureTextLayout = TextNode.asyncLayout(nil)
|
||||
let makeMoreTextLayout = TextNode.asyncLayout(self.moreTextNode)
|
||||
|
||||
let cachedMaskBackgroundImage = self.cachedMaskBackgroundImage
|
||||
|
||||
@ -290,6 +319,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
var ribbonTitle = ""
|
||||
var hasServiceMessage = true
|
||||
var textSpacing: CGFloat = 0.0
|
||||
var isStarGift = false
|
||||
for media in item.message.media {
|
||||
if let action = media as? TelegramMediaAction {
|
||||
switch action.action {
|
||||
@ -377,6 +407,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
hasServiceMessage = false
|
||||
}
|
||||
case let .starGift(gift, convertStars, giftText, giftEntities, _, savedToProfile, converted):
|
||||
isStarGift = true
|
||||
let authorName = item.message.author.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? ""
|
||||
title = item.presentationData.strings.Notification_StarGift_Title(authorName).string
|
||||
if let giftText, !giftText.isEmpty {
|
||||
@ -439,8 +470,10 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let (moreLayout, moreApply) = makeMoreTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Notification_PremiumGift_More, font: Font.semibold(13.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let attributedText: NSAttributedString
|
||||
if let _ = animationFile {
|
||||
if !entities.isEmpty {
|
||||
attributedText = stringWithAppliedEntities(text, entities: entities, baseColor: primaryTextColor, linkColor: primaryTextColor, baseFont: Font.regular(13.0), linkFont: Font.regular(13.0), boldFont: Font.semibold(13.0), italicFont: Font.italic(13.0), boldItalicFont: Font.semiboldItalic(13.0), fixedFont: Font.monospace(13.0), blockQuoteFont: Font.regular(13.0), message: nil)
|
||||
} else {
|
||||
attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(
|
||||
@ -456,6 +489,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
let textConstrainedSize = CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude)
|
||||
let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let (_, spoilerSubtitleApply) = makeSpoilerSubtitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets(), displaySpoilers: true))
|
||||
|
||||
var canExpand = false
|
||||
var clippedTextHeight: CGFloat = subtitleLayout.size.height
|
||||
if subtitleLayout.numberOfLines > 4 {
|
||||
@ -509,7 +544,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
backgroundMaskImage = nil
|
||||
}
|
||||
|
||||
var backgroundSize = CGSize(width: labelLayout.size.width + 8.0 + 8.0, height: giftSize.height)
|
||||
var backgroundSize = giftSize
|
||||
if hasServiceMessage {
|
||||
backgroundSize.height += labelLayout.size.height + 18.0
|
||||
} else {
|
||||
@ -521,13 +556,14 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
if let strongSelf = self {
|
||||
let isFirstTime = strongSelf.item == nil
|
||||
|
||||
var isExpandedUpdated = false
|
||||
if strongSelf.appliedIsExpanded != currentIsExpanded {
|
||||
strongSelf.appliedIsExpanded = currentIsExpanded
|
||||
info?.setInvertOffsetDirection()
|
||||
isExpandedUpdated = true
|
||||
|
||||
if let maskOverlayView = strongSelf.maskOverlayView {
|
||||
animation.transition.updateAlpha(layer: maskOverlayView.layer, alpha: currentIsExpanded ? 1.0 : 0.0)
|
||||
}
|
||||
}
|
||||
let _ = isExpandedUpdated
|
||||
|
||||
let overlayColor = item.presentationData.theme.theme.overallDarkAppearance ? UIColor(rgb: 0xffffff, alpha: 0.12) : UIColor(rgb: 0x000000, alpha: 0.12)
|
||||
|
||||
@ -578,6 +614,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
}
|
||||
strongSelf.item = item
|
||||
strongSelf.isStarGift = isStarGift
|
||||
|
||||
strongSelf.updateVisibility()
|
||||
|
||||
@ -599,17 +636,18 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
))
|
||||
let _ = buttonTitleApply()
|
||||
let _ = ribbonTextApply()
|
||||
|
||||
let labelFrame = CGRect(origin: CGPoint(x: 8.0, y: 2.0), size: labelLayout.size)
|
||||
let _ = moreApply()
|
||||
|
||||
let labelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - labelLayout.size.width) / 2.0), y: 2.0), size: labelLayout.size)
|
||||
strongSelf.labelNode.frame = labelFrame
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - titleLayout.size.width) / 2.0) , y: mediaBackgroundFrame.minY + 151.0), size: titleLayout.size)
|
||||
strongSelf.titleNode.frame = titleFrame
|
||||
|
||||
let clippingTextFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: titleFrame.maxY + textSpacing), size: CGSize(width: boundingWidth, height: clippedTextHeight))
|
||||
let clippingTextFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: titleFrame.maxY + textSpacing), size: CGSize(width: subtitleLayout.size.width, height: clippedTextHeight))
|
||||
|
||||
let subtitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: titleFrame.maxY + textSpacing), size: subtitleLayout.size)
|
||||
strongSelf.subtitleNode.textNode.frame = CGRect(origin: .zero, size: subtitleLayout.size)
|
||||
let subtitleFrame = CGRect(origin: .zero, size: subtitleLayout.size)
|
||||
strongSelf.subtitleNode.textNode.frame = subtitleFrame
|
||||
|
||||
if isFirstTime {
|
||||
strongSelf.textClippingNode.frame = clippingTextFrame
|
||||
@ -617,22 +655,37 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
animation.animator.updateFrame(layer: strongSelf.textClippingNode.layer, frame: clippingTextFrame, completion: nil)
|
||||
}
|
||||
if let maskView = strongSelf.maskView, let maskOverlayView = strongSelf.maskOverlayView {
|
||||
animation.animator.updateFrame(layer: maskView.layer, frame: CGRect(origin: .zero, size: CGSize(width: boundingWidth, height: clippingTextFrame.size.height)), completion: nil)
|
||||
animation.animator.updateFrame(layer: maskOverlayView.layer, frame: CGRect(origin: .zero, size: CGSize(width: boundingWidth, height: clippingTextFrame.size.height)), completion: nil)
|
||||
animation.animator.updateFrame(layer: maskView.layer, frame: CGRect(origin: .zero, size: CGSize(width: clippingTextFrame.width, height: clippingTextFrame.height)), completion: nil)
|
||||
animation.animator.updateFrame(layer: maskOverlayView.layer, frame: CGRect(origin: .zero, size: CGSize(width: clippingTextFrame.width, height: clippingTextFrame.height)), completion: nil)
|
||||
}
|
||||
|
||||
animation.animator.updateFrame(layer: strongSelf.moreTextNode.layer, frame: CGRect(origin: CGPoint(x: clippingTextFrame.maxX - moreLayout.size.width, y: clippingTextFrame.maxY - moreLayout.size.height), size: moreLayout.size), completion: nil)
|
||||
|
||||
if !subtitleLayout.spoilers.isEmpty {
|
||||
let spoilerSubtitleNode = spoilerSubtitleApply(TextNodeWithEntities.Arguments(
|
||||
context: item.context,
|
||||
cache: item.controllerInteraction.presentationContext.animationCache,
|
||||
renderer: item.controllerInteraction.presentationContext.animationRenderer,
|
||||
placeholderColor: item.presentationData.theme.theme.chat.message.freeform.withWallpaper.reactionInactiveBackground,
|
||||
attemptSynchronous: synchronousLoads
|
||||
))
|
||||
if strongSelf.spoilerSubtitleNode == nil {
|
||||
spoilerSubtitleNode.textNode.alpha = 0.0
|
||||
spoilerSubtitleNode.textNode.isUserInteractionEnabled = false
|
||||
strongSelf.spoilerSubtitleNode = spoilerSubtitleNode
|
||||
|
||||
strongSelf.textClippingNode.addSubnode(spoilerSubtitleNode.textNode)
|
||||
}
|
||||
spoilerSubtitleNode.textNode.frame = subtitleFrame
|
||||
|
||||
let dustColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText
|
||||
|
||||
let dustNode: InvisibleInkDustNode
|
||||
if let current = strongSelf.dustNode {
|
||||
dustNode = current
|
||||
} else {
|
||||
dustNode = InvisibleInkDustNode(textNode: nil, enableAnimations: item.context.sharedContext.energyUsageSettings.fullTranslucency)
|
||||
dustNode.isUserInteractionEnabled = false
|
||||
dustNode = InvisibleInkDustNode(textNode: spoilerSubtitleNode.textNode, enableAnimations: item.context.sharedContext.energyUsageSettings.fullTranslucency)
|
||||
strongSelf.dustNode = dustNode
|
||||
strongSelf.insertSubnode(dustNode, aboveSubnode: strongSelf.subtitleNode.textNode)
|
||||
strongSelf.textClippingNode.insertSubnode(dustNode, aboveSubnode: strongSelf.subtitleNode.textNode)
|
||||
}
|
||||
dustNode.frame = subtitleFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 1.0)
|
||||
dustNode.update(size: dustNode.frame.size, color: dustColor, textColor: dustColor, rects: subtitleLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: subtitleLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) })
|
||||
@ -641,11 +694,10 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
strongSelf.dustNode = nil
|
||||
}
|
||||
|
||||
let buttonTitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonTitleLayout.size.width) / 2.0), y: clippingTextFrame.maxY + 18.0), size: buttonTitleLayout.size)
|
||||
strongSelf.buttonTitleNode.frame = buttonTitleFrame
|
||||
|
||||
let buttonSize = CGSize(width: buttonTitleLayout.size.width + 38.0, height: 34.0)
|
||||
strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonSize.width) / 2.0), y: clippingTextFrame.maxY + 10.0), size: buttonSize)
|
||||
strongSelf.buttonTitleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((buttonSize.width - buttonTitleLayout.size.width) / 2.0), y: 8.0), size: buttonTitleLayout.size)
|
||||
|
||||
animation.animator.updateFrame(layer: strongSelf.buttonNode.layer, frame: CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonSize.width) / 2.0), y: clippingTextFrame.maxY + 10.0), size: buttonSize), completion: nil)
|
||||
strongSelf.buttonStarsNode.frame = CGRect(origin: .zero, size: buttonSize)
|
||||
|
||||
if ribbonTextLayout.size.width > 0.0 {
|
||||
@ -675,6 +727,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
if ribbonTextLayout.size.width > 0.0 {
|
||||
let backgroundMaskFrame = mediaBackgroundFrame.insetBy(dx: -2.0, dy: -2.0)
|
||||
backgroundContent.frame = backgroundMaskFrame
|
||||
animation.animator.updateFrame(layer: backgroundContent.layer, frame: backgroundMaskFrame, completion: nil)
|
||||
backgroundContent.cornerRadius = 0.0
|
||||
|
||||
if strongSelf.mediaBackgroundMaskNode.image?.size != mediaBackgroundFrame.size {
|
||||
@ -694,7 +747,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
backgroundContent.view.mask = strongSelf.mediaBackgroundMaskNode.view
|
||||
strongSelf.mediaBackgroundMaskNode.frame = CGRect(origin: .zero, size: backgroundMaskFrame.size)
|
||||
} else {
|
||||
backgroundContent.frame = mediaBackgroundFrame
|
||||
animation.animator.updateFrame(layer: backgroundContent.layer, frame: mediaBackgroundFrame, completion: nil)
|
||||
backgroundContent.clipsToBounds = true
|
||||
backgroundContent.cornerRadius = 24.0
|
||||
backgroundContent.view.mask = nil
|
||||
@ -734,24 +787,16 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
strongSelf.updateAbsoluteRect(rect, within: size)
|
||||
}
|
||||
|
||||
if canExpand {
|
||||
if strongSelf.maskView?.image == nil {
|
||||
strongSelf.maskView?.image = generateMaskImage()
|
||||
if canExpand, let maskView = strongSelf.maskView {
|
||||
if maskView.image == nil {
|
||||
maskView.image = generateMaskImage()
|
||||
}
|
||||
strongSelf.textClippingNode.view.mask = strongSelf.maskView
|
||||
|
||||
// var expandIconFrame: CGRect = .zero
|
||||
// if let icon = strongSelf.expandIcon.image {
|
||||
// expandIconFrame = CGRect(origin: CGPoint(x: boundingWidth - icon.size.width - 19.0, y: backgroundFrame.maxY - icon.size.height - 6.0), size: icon.size)
|
||||
// if wasHidden || isFirstTime {
|
||||
// strongSelf.expandIcon.position = expandIconFrame.center
|
||||
// } else {
|
||||
// animation.animator.updatePosition(layer: strongSelf.expandIcon.layer, position: expandIconFrame.center, completion: nil)
|
||||
// }
|
||||
// strongSelf.expandIcon.bounds = CGRect(origin: .zero, size: expandIconFrame.size)
|
||||
// }
|
||||
animation.animator.updateAlpha(layer: strongSelf.moreTextNode.layer, alpha: strongSelf.isExpanded ? 0.0 : 1.0, completion: nil)
|
||||
} else {
|
||||
strongSelf.textClippingNode.view.mask = nil
|
||||
strongSelf.moreTextNode.alpha = 0.0
|
||||
}
|
||||
|
||||
switch strongSelf.visibility {
|
||||
@ -855,8 +900,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
|
||||
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
|
||||
let textNodeFrame = self.labelNode.frame
|
||||
if let (index, attributes) = self.labelNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY - 10.0)), gesture == .tap {
|
||||
if let (index, attributes) = self.labelNode.attributesAtPoint(CGPoint(x: point.x - self.labelNode.frame.minX, y: point.y - self.labelNode.frame.minY - 10.0)), gesture == .tap {
|
||||
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
|
||||
var concealed = true
|
||||
if let (attributeText, fullText) = self.labelNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) {
|
||||
@ -874,8 +918,18 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
}
|
||||
|
||||
if let (_, attributes) = self.subtitleNode.textNode.attributesAtPoint(CGPoint(x: point.x - self.textClippingNode.frame.minX, y: point.y - self.textClippingNode.frame.minY)), gesture == .tap {
|
||||
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)], let dustNode = self.dustNode, !dustNode.isRevealed {
|
||||
return ChatMessageBubbleContentTapAction(content: .none)
|
||||
}
|
||||
}
|
||||
|
||||
if self.buttonNode.frame.contains(point) {
|
||||
return ChatMessageBubbleContentTapAction(content: .ignore)
|
||||
} else if self.textClippingNode.frame.contains(point) && !self.isExpanded && !self.moreTextNode.alpha.isZero {
|
||||
return ChatMessageBubbleContentTapAction(content: .custom({ [weak self] in
|
||||
self?.expandPressed()
|
||||
}))
|
||||
} else if let backgroundNode = self.backgroundNode, backgroundNode.frame.contains(point) {
|
||||
return ChatMessageBubbleContentTapAction(content: .openMessage)
|
||||
} else if self.mediaBackgroundContent?.frame.contains(point) == true {
|
||||
@ -934,7 +988,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
|
||||
if !alreadySeen && self.animationNode.isPlaying {
|
||||
item.controllerInteraction.playNextOutgoingGift = false
|
||||
Queue.mainQueue().after(1.0) {
|
||||
|
||||
Queue.mainQueue().after(self.isStarGift ? 0.1 : 1.0) {
|
||||
item.controllerInteraction.animateDiceSuccess(false, true)
|
||||
}
|
||||
}
|
||||
@ -943,7 +998,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
|
||||
private func generateMaskImage() -> UIImage? {
|
||||
return generateImage(CGSize(width: 140, height: 30), rotatedContext: { size, context in
|
||||
return generateImage(CGSize(width: 100.0, height: 30.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: .zero, size: size))
|
||||
|
||||
context.setFillColor(UIColor.white.cgColor)
|
||||
@ -956,7 +1011,7 @@ private func generateMaskImage() -> UIImage? {
|
||||
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
|
||||
|
||||
context.setBlendMode(.copy)
|
||||
context.clip(to: CGRect(origin: CGPoint(x: 10.0, y: 8.0), size: CGSize(width: 130.0, height: 22.0)))
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: 10.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions())
|
||||
})?.resizableImage(withCapInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 22.0, right: 130.0))
|
||||
context.clip(to: CGRect(origin: CGPoint(x: 10.0, y: 12.0), size: CGSize(width: 130.0, height: 18.0)))
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: 30.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions())
|
||||
})?.resizableImage(withCapInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 18.0, right: 70.0))
|
||||
}
|
||||
|
@ -468,8 +468,12 @@ private final class SheetContent: CombinedComponent {
|
||||
switch component.subject {
|
||||
case .peer:
|
||||
if let peer = state.peer {
|
||||
if case .user = peer {
|
||||
mainTitle = environment.strings.Report_Title_User
|
||||
if case let .user(user) = peer {
|
||||
if let _ = user.botInfo {
|
||||
mainTitle = environment.strings.Report_Title_Bot
|
||||
} else {
|
||||
mainTitle = environment.strings.Report_Title_User
|
||||
}
|
||||
} else if case let .channel(channel) = peer, case .broadcast = channel.info {
|
||||
mainTitle = environment.strings.Report_Title_Channel
|
||||
} else {
|
||||
|
@ -64,6 +64,7 @@ public final class GiftItemComponent: Component {
|
||||
let ribbon: Ribbon?
|
||||
let isLoading: Bool
|
||||
let isHidden: Bool
|
||||
let isSoldOut: Bool
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
@ -75,7 +76,8 @@ public final class GiftItemComponent: Component {
|
||||
price: String,
|
||||
ribbon: Ribbon? = nil,
|
||||
isLoading: Bool = false,
|
||||
isHidden: Bool = false
|
||||
isHidden: Bool = false,
|
||||
isSoldOut: Bool = false
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
@ -87,6 +89,7 @@ public final class GiftItemComponent: Component {
|
||||
self.ribbon = ribbon
|
||||
self.isLoading = isLoading
|
||||
self.isHidden = isHidden
|
||||
self.isSoldOut = isSoldOut
|
||||
}
|
||||
|
||||
public static func ==(lhs: GiftItemComponent, rhs: GiftItemComponent) -> Bool {
|
||||
@ -120,6 +123,9 @@ public final class GiftItemComponent: Component {
|
||||
if lhs.isHidden != rhs.isHidden {
|
||||
return false
|
||||
}
|
||||
if lhs.isSoldOut != rhs.isSoldOut {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -284,14 +290,26 @@ public final class GiftItemComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
let buttonColor: UIColor
|
||||
var isStars = false
|
||||
if component.isSoldOut {
|
||||
buttonColor = component.theme.list.itemDestructiveColor
|
||||
} else if component.price.containsEmoji {
|
||||
buttonColor = UIColor(rgb: 0xd3720a)
|
||||
isStars = true
|
||||
} else {
|
||||
buttonColor = component.theme.list.itemAccentColor
|
||||
}
|
||||
|
||||
let buttonSize = self.button.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
ButtonContentComponent(
|
||||
context: component.context,
|
||||
text: component.price,
|
||||
color: component.price.containsEmoji ? UIColor(rgb: 0xd3720a) : component.theme.list.itemAccentColor,
|
||||
isStars: component.price.containsEmoji)
|
||||
color: buttonColor,
|
||||
isStars: isStars
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
|
@ -25,6 +25,7 @@ import GiftItemComponent
|
||||
import InAppPurchaseManager
|
||||
import TabSelectorComponent
|
||||
import GiftSetupScreen
|
||||
import UndoUI
|
||||
|
||||
final class GiftOptionsScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
@ -173,6 +174,20 @@ final class GiftOptionsScreenComponent: Component {
|
||||
self.updateScrolling(transition: .immediate)
|
||||
}
|
||||
|
||||
private func dismissAllTooltips(controller: ViewController) {
|
||||
controller.forEachController({ controller in
|
||||
if let controller = controller as? UndoOverlayController {
|
||||
controller.dismissWithCommitAction()
|
||||
}
|
||||
return true
|
||||
})
|
||||
controller.window?.forEachController({ controller in
|
||||
if let controller = controller as? UndoOverlayController {
|
||||
controller.dismissWithCommitAction()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func updateScrolling(transition: ComponentTransition) {
|
||||
guard let environment = self.environment, let component = self.component else {
|
||||
return
|
||||
@ -274,6 +289,7 @@ final class GiftOptionsScreenComponent: Component {
|
||||
self.starsItems[itemId] = visibleItem
|
||||
}
|
||||
|
||||
let isSoldOut = gift.availability?.remains == 0
|
||||
let _ = visibleItem.update(
|
||||
transition: itemTransition,
|
||||
component: AnyComponent(
|
||||
@ -284,13 +300,14 @@ final class GiftOptionsScreenComponent: Component {
|
||||
theme: environment.theme,
|
||||
peer: nil,
|
||||
subject: .starGift(gift.id, gift.file),
|
||||
price: "⭐️ \(gift.price)",
|
||||
price: isSoldOut ? environment.strings.Gift_Options_Gift_SoldOut : "⭐️ \(gift.price)",
|
||||
ribbon: gift.availability != nil ?
|
||||
GiftItemComponent.Ribbon(
|
||||
text: environment.strings.Gift_Options_Gift_Limited,
|
||||
color: .blue
|
||||
)
|
||||
: nil
|
||||
: nil,
|
||||
isSoldOut: isSoldOut
|
||||
)
|
||||
),
|
||||
effectAlignment: .center,
|
||||
@ -303,13 +320,26 @@ final class GiftOptionsScreenComponent: Component {
|
||||
} else {
|
||||
mainController = controller
|
||||
}
|
||||
let giftController = GiftSetupScreen(
|
||||
context: component.context,
|
||||
peerId: component.peerId,
|
||||
subject: .starGift(gift),
|
||||
completion: component.completion
|
||||
)
|
||||
mainController.push(giftController)
|
||||
if gift.availability?.remains == 0 {
|
||||
self.dismissAllTooltips(controller: mainController)
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let resultController = UndoOverlayController(
|
||||
presentationData: presentationData,
|
||||
content: .sticker(context: component.context, file: gift.file, loop: false, title: nil, text: presentationData.strings.Gift_Options_SoldOut_Text, undoText: nil, customAction: nil),
|
||||
elevatedLayout: false,
|
||||
action: { _ in return true }
|
||||
)
|
||||
mainController.present(resultController, in: .window(.root))
|
||||
HapticFeedback().error()
|
||||
} else {
|
||||
let giftController = GiftSetupScreen(
|
||||
context: component.context,
|
||||
peerId: component.peerId,
|
||||
subject: .starGift(gift),
|
||||
completion: component.completion
|
||||
)
|
||||
mainController.push(giftController)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -771,9 +801,7 @@ final class GiftOptionsScreenComponent: Component {
|
||||
guard let self, let component = self.component, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
let introController = component.context.sharedContext.makeStarsIntroScreen(context: component.context)
|
||||
introController.navigationPresentation = .modal
|
||||
|
||||
let introController = component.context.sharedContext.makeStarsIntroScreen(context: component.context)
|
||||
if let controller = environment.controller() as? GiftOptionsScreen {
|
||||
let mainController: ViewController
|
||||
if let parentController = controller.parentController() {
|
||||
|
@ -43,6 +43,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/EmojiSuggestionsComponent",
|
||||
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode",
|
||||
"//submodules/InAppPurchaseManager",
|
||||
"//submodules/Components/BlurredBackgroundComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -258,7 +258,7 @@ final class ChatGiftPreviewItemNode: ListViewItemNode {
|
||||
apply().1(ListViewItemApply(isOnScreen: true))
|
||||
})
|
||||
itemNode!.isUserInteractionEnabled = false
|
||||
itemNode?.visibility = .visible(1.0, .infinite)
|
||||
itemNode!.visibility = .visible(1.0, .infinite)
|
||||
messageNodes.append(itemNode!)
|
||||
|
||||
self.initialBubbleHeight = itemNode?.frame.height
|
||||
|
@ -29,6 +29,7 @@ import ChatPresentationInterfaceState
|
||||
import AudioToolbox
|
||||
import TextFormat
|
||||
import InAppPurchaseManager
|
||||
import BlurredBackgroundComponent
|
||||
|
||||
final class GiftSetupScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
@ -78,6 +79,9 @@ final class GiftSetupScreenComponent: Component {
|
||||
private let introContent = ComponentView<Empty>()
|
||||
private let introSection = ComponentView<Empty>()
|
||||
private let hideSection = ComponentView<Empty>()
|
||||
|
||||
private let buttonBackground = ComponentView<Empty>()
|
||||
private let buttonSeparator = SimpleLayer()
|
||||
private let button = ComponentView<Empty>()
|
||||
|
||||
private var ignoreScrolling: Bool = false
|
||||
@ -143,6 +147,8 @@ final class GiftSetupScreenComponent: Component {
|
||||
self.addSubview(self.scrollView)
|
||||
|
||||
self.scrollView.layer.addSublayer(self.topOverscrollLayer)
|
||||
|
||||
self.disablesInteractiveKeyboardGestureRecognizer = true
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@ -190,6 +196,11 @@ final class GiftSetupScreenComponent: Component {
|
||||
if let navigationTitleView = self.navigationTitle.view {
|
||||
transition.setAlpha(view: navigationTitleView, alpha: 1.0)
|
||||
}
|
||||
|
||||
let bottomContentOffset = max(0.0, self.scrollView.contentSize.height - self.scrollView.contentOffset.y - self.scrollView.frame.height)
|
||||
let bottomPanelAlpha = min(16.0, bottomContentOffset) / 16.0
|
||||
self.buttonBackground.view?.alpha = bottomPanelAlpha
|
||||
self.buttonSeparator.opacity = Float(bottomPanelAlpha)
|
||||
}
|
||||
|
||||
func proceed() {
|
||||
@ -352,6 +363,29 @@ final class GiftSetupScreenComponent: Component {
|
||||
}
|
||||
navigationController.setViewControllers(controllers, animated: true)
|
||||
}
|
||||
|
||||
starsContext.load(force: true)
|
||||
}, error: { [weak self] error in
|
||||
guard let self, let controller = self.environment?.controller() else {
|
||||
return
|
||||
}
|
||||
|
||||
self.inProgress = false
|
||||
self.state?.updated()
|
||||
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
var errorText: String?
|
||||
switch error {
|
||||
case .starGiftOutOfStock:
|
||||
errorText = presentationData.strings.Gift_Send_ErrorOutOfStock
|
||||
default:
|
||||
errorText = presentationData.strings.Gift_Send_ErrorUnknown
|
||||
}
|
||||
|
||||
if let errorText = errorText {
|
||||
let alertController = textAlertController(context: component.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
|
||||
controller.present(alertController, in: .window(.root))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -459,9 +493,7 @@ final class GiftSetupScreenComponent: Component {
|
||||
return
|
||||
}
|
||||
if let textInputView = self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View {
|
||||
if self.textInputState.isEditing {
|
||||
textInputView.insertText(text: text)
|
||||
}
|
||||
textInputView.insertText(text: text)
|
||||
}
|
||||
},
|
||||
backwardsDeleteText: { [weak self] in
|
||||
@ -510,6 +542,16 @@ final class GiftSetupScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if case .starGift = component.subject {
|
||||
self.optionsDisposable = (component.context.engine.payments.starsTopUpOptions()
|
||||
|> deliverOnMainQueue).start(next: { [weak self] options in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.options = options
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let environment = environment[EnvironmentType.self].value
|
||||
@ -564,17 +606,17 @@ final class GiftSetupScreenComponent: Component {
|
||||
contentHeight += 26.0
|
||||
|
||||
if case let .starGift(starGift) = component.subject, let availability = starGift.availability {
|
||||
//TODO:localize
|
||||
let remains: Int32 = Int32(CGFloat(availability.remains) * 0.66)
|
||||
let position = CGFloat(remains) / CGFloat(availability.total)
|
||||
let remainsString = "\(remains)" //presentationStringsFormattedNumber(remains, environment.dateTimeFormat.groupingSeparator)
|
||||
let totalString = presentationStringsFormattedNumber(availability.total, environment.dateTimeFormat.groupingSeparator)
|
||||
let remains: Int32 = availability.remains
|
||||
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(
|
||||
inactiveColor: environment.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3),
|
||||
activeColors: [UIColor(rgb: 0x5bc2ff), UIColor(rgb: 0x2d9eff)],
|
||||
inactiveTitle: "Limited",
|
||||
inactiveTitle: environment.strings.Gift_Send_Limited,
|
||||
inactiveValue: "",
|
||||
inactiveTitleColor: environment.theme.list.itemSecondaryTextColor,
|
||||
activeTitle: "",
|
||||
@ -583,7 +625,9 @@ final class GiftSetupScreenComponent: Component {
|
||||
badgeText: "\(remainsString)",
|
||||
badgePosition: position,
|
||||
badgeGraphPosition: position,
|
||||
invertProgress: true
|
||||
invertProgress: true,
|
||||
leftString: environment.strings.Gift_Send_Remains(remains).replacingOccurrences(of: remainsString, with: "").trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
groupingSeparator: environment.dateTimeFormat.groupingSeparator
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
|
||||
@ -815,6 +859,31 @@ final class GiftSetupScreenComponent: Component {
|
||||
self.starImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: environment.theme.list.itemCheckColors.foregroundColor)!, environment.theme)
|
||||
}
|
||||
|
||||
let buttonHeight: CGFloat = 50.0
|
||||
let bottomPanelPadding: CGFloat = 12.0
|
||||
let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding
|
||||
let bottomPanelHeight = bottomPanelPadding + buttonHeight + bottomInset
|
||||
|
||||
let bottomPanelSize = self.buttonBackground.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(BlurredBackgroundComponent(
|
||||
color: environment.theme.rootController.tabBar.backgroundColor
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: bottomPanelHeight)
|
||||
)
|
||||
self.buttonSeparator.backgroundColor = environment.theme.rootController.tabBar.separatorColor.cgColor
|
||||
|
||||
if let view = self.buttonBackground.view {
|
||||
if view.superview == nil {
|
||||
self.addSubview(view)
|
||||
self.layer.addSublayer(self.buttonSeparator)
|
||||
}
|
||||
view.frame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelSize.height), size: bottomPanelSize)
|
||||
self.buttonSeparator.frame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelSize.height), size: CGSize(width: availableSize.width, height: UIScreenPixel))
|
||||
}
|
||||
|
||||
var buttonIsEnabled = true
|
||||
let buttonString: String
|
||||
switch component.subject {
|
||||
case let .premium(product):
|
||||
@ -823,6 +892,9 @@ final class GiftSetupScreenComponent: Component {
|
||||
case let .starGift(starGift):
|
||||
let amountString = presentationStringsFormattedNumber(Int32(starGift.price), presentationData.dateTimeFormat.groupingSeparator)
|
||||
buttonString = "\(environment.strings.Gift_Send_Send) # \(amountString)"
|
||||
if let availability = starGift.availability, availability.remains == 0 {
|
||||
buttonIsEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
|
||||
@ -845,20 +917,20 @@ final class GiftSetupScreenComponent: Component {
|
||||
id: AnyHashable(0),
|
||||
component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))
|
||||
),
|
||||
isEnabled: true,
|
||||
isEnabled: buttonIsEnabled,
|
||||
displaysProgress: self.inProgress,
|
||||
action: { [weak self] in
|
||||
self?.proceed()
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50)
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: buttonHeight)
|
||||
)
|
||||
if let buttonView = self.button.view {
|
||||
if buttonView.superview == nil {
|
||||
self.addSubview(buttonView)
|
||||
}
|
||||
buttonView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) / 2.0), y: availableSize.height - environment.safeInsets.bottom - buttonSize.height), size: buttonSize)
|
||||
buttonView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) / 2.0), y: availableSize.height - bottomPanelHeight + bottomPanelPadding), size: buttonSize)
|
||||
}
|
||||
|
||||
if self.textInputState.isEditing, let emojiSuggestion = self.textInputState.currentEmojiSuggestion, emojiSuggestion.disposable == nil {
|
||||
@ -1002,7 +1074,7 @@ final class GiftSetupScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.recenterOnTag == nil && self.previousHadInputHeight != (environment.inputHeight > 0.0) {
|
||||
if self.recenterOnTag == nil && self.previousHadInputHeight != (environment.inputHeight > 0.0), case .keyboard = self.currentInputMode {
|
||||
if self.textInputState.isEditing {
|
||||
self.recenterOnTag = self.textInputTag
|
||||
}
|
||||
@ -1252,9 +1324,6 @@ public final class GiftSetupScreen: ViewControllerComponentContainer {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
}
|
||||
|
||||
@objc private func cancelPressed() {
|
||||
self.dismiss()
|
||||
}
|
||||
|
@ -26,6 +26,8 @@ public class RemainingCountComponent: Component {
|
||||
private let badgePosition: CGFloat
|
||||
private let badgeGraphPosition: CGFloat
|
||||
private let invertProgress: Bool
|
||||
private let leftString: String
|
||||
private let groupingSeparator: String
|
||||
|
||||
public init(
|
||||
inactiveColor: UIColor,
|
||||
@ -39,7 +41,9 @@ public class RemainingCountComponent: Component {
|
||||
badgeText: String?,
|
||||
badgePosition: CGFloat,
|
||||
badgeGraphPosition: CGFloat,
|
||||
invertProgress: Bool = false
|
||||
invertProgress: Bool = false,
|
||||
leftString: String,
|
||||
groupingSeparator: String
|
||||
) {
|
||||
self.inactiveColor = inactiveColor
|
||||
self.activeColors = activeColors
|
||||
@ -53,6 +57,8 @@ public class RemainingCountComponent: Component {
|
||||
self.badgePosition = badgePosition
|
||||
self.badgeGraphPosition = badgeGraphPosition
|
||||
self.invertProgress = invertProgress
|
||||
self.leftString = leftString
|
||||
self.groupingSeparator = groupingSeparator
|
||||
}
|
||||
|
||||
public static func ==(lhs: RemainingCountComponent, rhs: RemainingCountComponent) -> Bool {
|
||||
@ -92,6 +98,12 @@ public class RemainingCountComponent: Component {
|
||||
if lhs.invertProgress != rhs.invertProgress {
|
||||
return false
|
||||
}
|
||||
if lhs.leftString != rhs.leftString {
|
||||
return false
|
||||
}
|
||||
if lhs.groupingSeparator != rhs.groupingSeparator {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -118,7 +130,8 @@ public class RemainingCountComponent: Component {
|
||||
private let badgeShapeLayer = CAShapeLayer()
|
||||
|
||||
private let badgeForeground: SimpleLayer
|
||||
private let badgeLabel: BadgeLabelView
|
||||
private var badgeLabel: BadgeLabelView?
|
||||
private let badgeLeftLabel = ComponentView<Empty>()
|
||||
private let badgeLabelMaskView = UIImageView()
|
||||
|
||||
private var badgeTailPosition: CGFloat = 0.0
|
||||
@ -148,11 +161,7 @@ public class RemainingCountComponent: Component {
|
||||
self.badgeView.mask = self.badgeMaskView
|
||||
|
||||
self.badgeForeground = SimpleLayer()
|
||||
|
||||
self.badgeLabel = BadgeLabelView()
|
||||
let _ = self.badgeLabel.update(value: "0", transition: .immediate)
|
||||
self.badgeLabel.mask = self.badgeLabelMaskView
|
||||
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.container)
|
||||
@ -162,7 +171,7 @@ public class RemainingCountComponent: Component {
|
||||
|
||||
self.addSubview(self.badgeView)
|
||||
self.badgeView.layer.addSublayer(self.badgeForeground)
|
||||
self.badgeView.addSubview(self.badgeLabel)
|
||||
//self.badgeView.addSubview(self.badgeLabel)
|
||||
|
||||
self.badgeLabelMaskView.contentMode = .scaleToFill
|
||||
self.badgeLabelMaskView.image = generateImage(CGSize(width: 2.0, height: 30.0), rotatedContext: { size, context in
|
||||
@ -254,14 +263,14 @@ public class RemainingCountComponent: Component {
|
||||
self.badgeView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
|
||||
}
|
||||
|
||||
if let badgeText = component.badgeText {
|
||||
if let badgeText = component.badgeText, let badgeLabel = self.badgeLabel {
|
||||
let transition: ComponentTransition = .easeInOut(duration: from != nil ? 0.3 : 0.5)
|
||||
var frameTransition = transition
|
||||
if from == nil {
|
||||
frameTransition = frameTransition.withAnimation(.none)
|
||||
}
|
||||
let badgeLabelSize = self.badgeLabel.update(value: badgeText, transition: transition)
|
||||
frameTransition.setFrame(view: self.badgeLabel, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((badgeFullSize.width - badgeLabelSize.width) / 2.0), y: -2.0), size: badgeLabelSize))
|
||||
let badgeLabelSize = badgeLabel.update(value: badgeText, transition: transition)
|
||||
frameTransition.setFrame(view: badgeLabel, frame: CGRect(origin: CGPoint(x: 10.0, y: -2.0), size: badgeLabelSize))
|
||||
}
|
||||
}
|
||||
|
||||
@ -273,7 +282,16 @@ public class RemainingCountComponent: Component {
|
||||
|
||||
let size = CGSize(width: availableSize.width, height: 90.0)
|
||||
|
||||
self.badgeLabel.color = component.activeTitleColor
|
||||
|
||||
if self.badgeLabel == nil {
|
||||
let badgeLabel = BadgeLabelView(groupingSeparator: component.groupingSeparator)
|
||||
let _ = badgeLabel.update(value: "0", transition: .immediate)
|
||||
badgeLabel.mask = self.badgeLabelMaskView
|
||||
self.badgeLabel = badgeLabel
|
||||
self.badgeView.addSubview(badgeLabel)
|
||||
}
|
||||
|
||||
self.badgeLabel?.color = component.activeTitleColor
|
||||
|
||||
let lineHeight: CGFloat = 30.0
|
||||
let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - lineHeight), size: CGSize(width: size.width, height: lineHeight))
|
||||
@ -292,56 +310,8 @@ public class RemainingCountComponent: Component {
|
||||
rightTextColor = component.activeTitleColor
|
||||
}
|
||||
|
||||
if "".isEmpty {
|
||||
if component.invertProgress {
|
||||
let innerLeftTitleSize = self.innerLeftTitleLabel.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(
|
||||
NSAttributedString(
|
||||
string: component.inactiveTitle,
|
||||
font: Font.semibold(15.0),
|
||||
textColor: component.activeTitleColor
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
if let view = self.innerLeftTitleLabel.view {
|
||||
if view.superview == nil {
|
||||
self.activeContainer.addSubview(view)
|
||||
}
|
||||
view.frame = CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((lineHeight - innerLeftTitleSize.height) / 2.0)), size: innerLeftTitleSize)
|
||||
}
|
||||
|
||||
let innerRightTitleSize = self.innerRightTitleLabel.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(
|
||||
NSAttributedString(
|
||||
string: component.activeValue,
|
||||
font: Font.semibold(15.0),
|
||||
textColor: component.activeTitleColor
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
if let view = self.innerRightTitleLabel.view {
|
||||
if view.superview == nil {
|
||||
self.activeContainer.addSubview(view)
|
||||
}
|
||||
view.frame = CGRect(origin: CGPoint(x: containerFrame.width - 12.0 - innerRightTitleSize.width, y: floorToScreenPixels((lineHeight - innerRightTitleSize.height) / 2.0)), size: innerRightTitleSize)
|
||||
}
|
||||
}
|
||||
|
||||
let inactiveTitleSize = self.inactiveTitleLabel.update(
|
||||
if component.invertProgress {
|
||||
let innerLeftTitleSize = self.innerLeftTitleLabel.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
@ -349,7 +319,7 @@ public class RemainingCountComponent: Component {
|
||||
NSAttributedString(
|
||||
string: component.inactiveTitle,
|
||||
font: Font.semibold(15.0),
|
||||
textColor: leftTextColor
|
||||
textColor: component.activeTitleColor
|
||||
)
|
||||
)
|
||||
)
|
||||
@ -357,60 +327,14 @@ public class RemainingCountComponent: Component {
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
if let view = self.inactiveTitleLabel.view {
|
||||
if let view = self.innerLeftTitleLabel.view {
|
||||
if view.superview == nil {
|
||||
self.container.addSubview(view)
|
||||
self.activeContainer.addSubview(view)
|
||||
}
|
||||
view.frame = CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((lineHeight - inactiveTitleSize.height) / 2.0)), size: inactiveTitleSize)
|
||||
view.frame = CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((lineHeight - innerLeftTitleSize.height) / 2.0)), size: innerLeftTitleSize)
|
||||
}
|
||||
|
||||
let inactiveValueSize = self.inactiveValueLabel.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(
|
||||
NSAttributedString(
|
||||
string: component.inactiveValue,
|
||||
font: Font.semibold(15.0),
|
||||
textColor: leftTextColor
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
if let view = self.inactiveValueLabel.view {
|
||||
if view.superview == nil {
|
||||
self.container.addSubview(view)
|
||||
}
|
||||
view.frame = CGRect(origin: CGPoint(x: activityPosition - 12.0 - inactiveValueSize.width, y: floorToScreenPixels((lineHeight - inactiveValueSize.height) / 2.0)), size: inactiveValueSize)
|
||||
}
|
||||
|
||||
let activeTitleSize = self.activeTitleLabel.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(
|
||||
NSAttributedString(
|
||||
string: component.activeTitle,
|
||||
font: Font.semibold(15.0),
|
||||
textColor: rightTextColor
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
if let view = self.activeTitleLabel.view {
|
||||
if view.superview == nil {
|
||||
self.container.addSubview(view)
|
||||
}
|
||||
view.frame = CGRect(origin: CGPoint(x: activityPosition + 12.0, y: floorToScreenPixels((lineHeight - activeTitleSize.height) / 2.0)), size: activeTitleSize)
|
||||
}
|
||||
|
||||
let activeValueSize = self.activeValueLabel.update(
|
||||
let innerRightTitleSize = self.innerRightTitleLabel.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
@ -418,7 +342,7 @@ public class RemainingCountComponent: Component {
|
||||
NSAttributedString(
|
||||
string: component.activeValue,
|
||||
font: Font.semibold(15.0),
|
||||
textColor: rightTextColor
|
||||
textColor: component.activeTitleColor
|
||||
)
|
||||
)
|
||||
)
|
||||
@ -426,17 +350,109 @@ public class RemainingCountComponent: Component {
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
if let view = self.activeValueLabel.view {
|
||||
if let view = self.innerRightTitleLabel.view {
|
||||
if view.superview == nil {
|
||||
self.container.addSubview(view)
|
||||
|
||||
if component.invertProgress {
|
||||
self.container.bringSubviewToFront(self.activeContainer)
|
||||
}
|
||||
self.activeContainer.addSubview(view)
|
||||
}
|
||||
view.frame = CGRect(origin: CGPoint(x: containerFrame.width - 12.0 - activeValueSize.width, y: floorToScreenPixels((lineHeight - activeValueSize.height) / 2.0)), size: activeValueSize)
|
||||
view.frame = CGRect(origin: CGPoint(x: containerFrame.width - 12.0 - innerRightTitleSize.width, y: floorToScreenPixels((lineHeight - innerRightTitleSize.height) / 2.0)), size: innerRightTitleSize)
|
||||
}
|
||||
}
|
||||
|
||||
let inactiveTitleSize = self.inactiveTitleLabel.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(
|
||||
NSAttributedString(
|
||||
string: component.inactiveTitle,
|
||||
font: Font.semibold(15.0),
|
||||
textColor: leftTextColor
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
if let view = self.inactiveTitleLabel.view {
|
||||
if view.superview == nil {
|
||||
self.container.addSubview(view)
|
||||
}
|
||||
view.frame = CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((lineHeight - inactiveTitleSize.height) / 2.0)), size: inactiveTitleSize)
|
||||
}
|
||||
|
||||
let inactiveValueSize = self.inactiveValueLabel.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(
|
||||
NSAttributedString(
|
||||
string: component.inactiveValue,
|
||||
font: Font.semibold(15.0),
|
||||
textColor: leftTextColor
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
if let view = self.inactiveValueLabel.view {
|
||||
if view.superview == nil {
|
||||
self.container.addSubview(view)
|
||||
}
|
||||
view.frame = CGRect(origin: CGPoint(x: activityPosition - 12.0 - inactiveValueSize.width, y: floorToScreenPixels((lineHeight - inactiveValueSize.height) / 2.0)), size: inactiveValueSize)
|
||||
}
|
||||
|
||||
let activeTitleSize = self.activeTitleLabel.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(
|
||||
NSAttributedString(
|
||||
string: component.activeTitle,
|
||||
font: Font.semibold(15.0),
|
||||
textColor: rightTextColor
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
if let view = self.activeTitleLabel.view {
|
||||
if view.superview == nil {
|
||||
self.container.addSubview(view)
|
||||
}
|
||||
view.frame = CGRect(origin: CGPoint(x: activityPosition + 12.0, y: floorToScreenPixels((lineHeight - activeTitleSize.height) / 2.0)), size: activeTitleSize)
|
||||
}
|
||||
|
||||
let activeValueSize = self.activeValueLabel.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(
|
||||
NSAttributedString(
|
||||
string: component.activeValue,
|
||||
font: Font.semibold(15.0),
|
||||
textColor: rightTextColor
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
if let view = self.activeValueLabel.view {
|
||||
if view.superview == nil {
|
||||
self.container.addSubview(view)
|
||||
|
||||
if component.invertProgress {
|
||||
self.container.bringSubviewToFront(self.activeContainer)
|
||||
}
|
||||
}
|
||||
view.frame = CGRect(origin: CGPoint(x: containerFrame.width - 12.0 - activeValueSize.width, y: floorToScreenPixels((lineHeight - activeValueSize.height) / 2.0)), size: activeValueSize)
|
||||
}
|
||||
|
||||
var progressTransition: ComponentTransition = .immediate
|
||||
if !transition.animation.isImmediate {
|
||||
@ -459,14 +475,39 @@ public class RemainingCountComponent: Component {
|
||||
|
||||
let countWidth: CGFloat
|
||||
if let badgeText = component.badgeText {
|
||||
countWidth = CGFloat(badgeText.count) * 10.0
|
||||
countWidth = getLabelWidth(badgeText)
|
||||
} else {
|
||||
countWidth = 51.0
|
||||
}
|
||||
let badgeWidth: CGFloat = countWidth + 20.0
|
||||
|
||||
let badgeSpacing: CGFloat = 4.0
|
||||
|
||||
let badgeLeftSize = self.badgeLeftLabel.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(
|
||||
NSAttributedString(
|
||||
string: component.leftString,
|
||||
font: Font.semibold(15.0),
|
||||
textColor: component.activeTitleColor
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
if let view = self.badgeLeftLabel.view {
|
||||
if view.superview == nil {
|
||||
self.badgeView.addSubview(view)
|
||||
}
|
||||
view.frame = CGRect(origin: CGPoint(x: 10.0 + countWidth + badgeSpacing, y: 4.0 + UIScreenPixel), size: badgeLeftSize)
|
||||
}
|
||||
|
||||
let badgeWidth: CGFloat = countWidth + 20.0 + badgeSpacing + badgeLeftSize.width
|
||||
let badgeSize = CGSize(width: badgeWidth, height: 30.0)
|
||||
let badgeFullSize = CGSize(width: badgeWidth, height: 30.0 + 8.0)
|
||||
let badgeFullSize = CGSize(width: badgeWidth, height: badgeSize.height + 8.0)
|
||||
let tailSize = CGSize(width: 15.0, height: 6.0)
|
||||
let tailRadius: CGFloat = 3.0
|
||||
self.badgeMaskView.frame = CGRect(origin: .zero, size: badgeFullSize)
|
||||
@ -538,9 +579,9 @@ public class RemainingCountComponent: Component {
|
||||
if transition.animation.isImmediate {
|
||||
if component.badgePosition < 0.1 {
|
||||
self.badgeView.alpha = 1.0
|
||||
if let badgeText = component.badgeText {
|
||||
let badgeLabelSize = self.badgeLabel.update(value: badgeText, transition: .immediate)
|
||||
transition.setFrame(view: self.badgeLabel, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((badgeFullSize.width - badgeLabelSize.width) / 2.0), y: -2.0), size: badgeLabelSize))
|
||||
if let badgeText = component.badgeText, let badgeLabel = self.badgeLabel {
|
||||
let badgeLabelSize = badgeLabel.update(value: badgeText, transition: .immediate)
|
||||
transition.setFrame(view: badgeLabel, frame: CGRect(origin: CGPoint(x: 10.0, y: -2.0), size: badgeLabelSize))
|
||||
}
|
||||
} else {
|
||||
self.playAppearanceAnimation(component: component, badgeFullSize: badgeFullSize)
|
||||
@ -664,6 +705,7 @@ public class RemainingCountComponent: Component {
|
||||
}
|
||||
|
||||
|
||||
private let spaceWidth: CGFloat = 3.0
|
||||
private let labelWidth: CGFloat = 10.0
|
||||
private let labelHeight: CGFloat = 30.0
|
||||
private let labelSize = CGSize(width: labelWidth, height: labelHeight)
|
||||
@ -673,7 +715,7 @@ final class BadgeLabelView: UIView {
|
||||
private class StackView: UIView {
|
||||
var labels: [UILabel] = []
|
||||
|
||||
var currentValue: Int32 = 0
|
||||
var currentValue: Int32?
|
||||
|
||||
var color: UIColor = .white {
|
||||
didSet {
|
||||
@ -683,21 +725,27 @@ final class BadgeLabelView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
init(groupingSeparator: String) {
|
||||
super.init(frame: CGRect(origin: .zero, size: labelSize))
|
||||
|
||||
var height: CGFloat = -labelHeight
|
||||
for i in -1 ..< 10 {
|
||||
var height: CGFloat = -labelHeight * 2.0
|
||||
for i in -2 ..< 10 {
|
||||
let label = UILabel()
|
||||
if i == -1 {
|
||||
let itemWidth: CGFloat
|
||||
if i == -2 {
|
||||
label.text = groupingSeparator
|
||||
itemWidth = spaceWidth
|
||||
} else if i == -1 {
|
||||
label.text = "9"
|
||||
itemWidth = labelWidth
|
||||
} else {
|
||||
label.text = "\(i)"
|
||||
itemWidth = labelWidth
|
||||
}
|
||||
label.textColor = self.color
|
||||
label.font = font
|
||||
label.textAlignment = .center
|
||||
label.frame = CGRect(x: 0, y: height, width: labelWidth, height: labelHeight)
|
||||
label.frame = CGRect(x: 0, y: height, width: itemWidth, height: labelHeight)
|
||||
self.addSubview(label)
|
||||
self.labels.append(label)
|
||||
|
||||
@ -709,37 +757,49 @@ final class BadgeLabelView: UIView {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(value: Int32, isFirst: Bool, isLast: Bool, transition: ComponentTransition) {
|
||||
func update(value: Int32?, isFirst: Bool, isLast: Bool, transition: ComponentTransition) {
|
||||
let previousValue = self.currentValue
|
||||
self.currentValue = value
|
||||
|
||||
self.labels[1].alpha = isFirst && !isLast ? 0.0 : 1.0
|
||||
self.labels[2].alpha = isFirst && !isLast ? 0.0 : 1.0
|
||||
|
||||
if previousValue == 9 && value < 9 {
|
||||
if let value {
|
||||
if previousValue == 9 && value < 9 {
|
||||
self.bounds = CGRect(
|
||||
origin: CGPoint(
|
||||
x: 0.0,
|
||||
y: -1.0 * labelSize.height
|
||||
),
|
||||
size: labelSize
|
||||
)
|
||||
}
|
||||
|
||||
let bounds = CGRect(
|
||||
origin: CGPoint(
|
||||
x: 0.0,
|
||||
y: CGFloat(value) * labelSize.height
|
||||
),
|
||||
size: labelSize
|
||||
)
|
||||
transition.setBounds(view: self, bounds: bounds)
|
||||
} else {
|
||||
self.bounds = CGRect(
|
||||
origin: CGPoint(
|
||||
x: 0.0,
|
||||
y: -1.0 * labelSize.height
|
||||
y: -2.0 * labelSize.height
|
||||
),
|
||||
size: labelSize
|
||||
)
|
||||
}
|
||||
|
||||
let bounds = CGRect(
|
||||
origin: CGPoint(
|
||||
x: 0.0,
|
||||
y: CGFloat(value) * labelSize.height
|
||||
),
|
||||
size: labelSize
|
||||
)
|
||||
transition.setBounds(view: self, bounds: bounds)
|
||||
}
|
||||
}
|
||||
|
||||
private let groupingSeparator: String
|
||||
private var itemViews: [Int: StackView] = [:]
|
||||
private var staticLabel = UILabel()
|
||||
|
||||
init() {
|
||||
init(groupingSeparator: String) {
|
||||
self.groupingSeparator = groupingSeparator
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
self.clipsToBounds = true
|
||||
@ -752,7 +812,6 @@ final class BadgeLabelView: UIView {
|
||||
|
||||
var color: UIColor = .white {
|
||||
didSet {
|
||||
self.staticLabel.textColor = self.color
|
||||
for (_, view) in self.itemViews {
|
||||
view.color = self.color
|
||||
}
|
||||
@ -760,30 +819,12 @@ final class BadgeLabelView: UIView {
|
||||
}
|
||||
|
||||
func update(value: String, transition: ComponentTransition) -> CGSize {
|
||||
if value.contains(" ") {
|
||||
for (_, view) in self.itemViews {
|
||||
view.isHidden = true
|
||||
}
|
||||
|
||||
if self.staticLabel.superview == nil {
|
||||
self.staticLabel.textColor = self.color
|
||||
self.staticLabel.font = font
|
||||
|
||||
self.addSubview(self.staticLabel)
|
||||
}
|
||||
|
||||
self.staticLabel.text = value
|
||||
let size = self.staticLabel.sizeThatFits(CGSize(width: 100.0, height: 100.0))
|
||||
self.staticLabel.frame = CGRect(origin: .zero, size: CGSize(width: size.width, height: labelHeight))
|
||||
|
||||
return CGSize(width: ceil(self.staticLabel.bounds.width), height: ceil(self.staticLabel.bounds.height))
|
||||
}
|
||||
|
||||
let string = value
|
||||
let stringArray = Array(string.map { String($0) }.reversed())
|
||||
|
||||
let totalWidth = CGFloat(stringArray.count) * labelWidth
|
||||
let totalWidth: CGFloat = getLabelWidth(value)
|
||||
|
||||
var rightX: CGFloat = totalWidth
|
||||
var validIds: [Int] = []
|
||||
for i in 0 ..< stringArray.count {
|
||||
validIds.append(i)
|
||||
@ -794,18 +835,21 @@ final class BadgeLabelView: UIView {
|
||||
itemView = current
|
||||
} else {
|
||||
itemTransition = transition.withAnimation(.none)
|
||||
itemView = StackView()
|
||||
itemView = StackView(groupingSeparator: self.groupingSeparator)
|
||||
itemView.color = self.color
|
||||
self.itemViews[i] = itemView
|
||||
self.addSubview(itemView)
|
||||
}
|
||||
|
||||
let digit = Int32(stringArray[i]) ?? 0
|
||||
let digit = Int32(stringArray[i])
|
||||
itemView.update(value: digit, isFirst: i == stringArray.count - 1, isLast: i == 0, transition: transition)
|
||||
|
||||
let itemWidth: CGFloat = digit != nil ? labelWidth : spaceWidth
|
||||
rightX -= itemWidth
|
||||
|
||||
itemTransition.setFrame(
|
||||
view: itemView,
|
||||
frame: CGRect(x: totalWidth - labelWidth * CGFloat(i + 1), y: 0.0, width: labelWidth, height: labelHeight)
|
||||
frame: CGRect(x: rightX, y: 0.0, width: labelWidth, height: labelHeight)
|
||||
)
|
||||
}
|
||||
|
||||
@ -825,3 +869,15 @@ final class BadgeLabelView: UIView {
|
||||
return CGSize(width: totalWidth, height: labelHeight)
|
||||
}
|
||||
}
|
||||
|
||||
private func getLabelWidth(_ string: String) -> CGFloat {
|
||||
var totalWidth: CGFloat = 0.0
|
||||
for c in string {
|
||||
if CharacterSet.decimalDigits.contains(c.unicodeScalars[c.unicodeScalars.startIndex]) {
|
||||
totalWidth += labelWidth
|
||||
} else {
|
||||
totalWidth += spaceWidth
|
||||
}
|
||||
}
|
||||
return totalWidth
|
||||
}
|
||||
|
@ -181,10 +181,12 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
let text: String?
|
||||
let entities: [MessageTextEntity]?
|
||||
let limitTotal: Int32?
|
||||
var outgoing = false
|
||||
var incoming = false
|
||||
var savedToProfile = false
|
||||
var converted = false
|
||||
var giftId: Int64 = 0
|
||||
var date: Int32 = 0
|
||||
if let arguments = component.subject.arguments {
|
||||
animationFile = arguments.gift.file
|
||||
stars = arguments.gift.price
|
||||
@ -192,10 +194,16 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
entities = arguments.entities
|
||||
limitTotal = arguments.gift.availability?.total
|
||||
convertStars = arguments.convertStars
|
||||
if case .message = component.subject {
|
||||
outgoing = !arguments.incoming
|
||||
} else {
|
||||
outgoing = false
|
||||
}
|
||||
incoming = arguments.incoming || arguments.peerId == component.context.account.peerId
|
||||
savedToProfile = arguments.savedToProfile
|
||||
converted = arguments.converted
|
||||
giftId = arguments.gift.id
|
||||
date = arguments.date
|
||||
} else {
|
||||
animationFile = nil
|
||||
stars = 0
|
||||
@ -236,12 +244,12 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
}
|
||||
|
||||
var formattedAmount = presentationStringsFormattedNumber(abs(Int32(stars)), dateTimeFormat.groupingSeparator)
|
||||
if !incoming && stars > 0 {
|
||||
if outgoing {
|
||||
formattedAmount = "- \(formattedAmount)"
|
||||
}
|
||||
let countFont: UIFont = Font.semibold(17.0)
|
||||
let amountText = formattedAmount
|
||||
let countColor = incoming ? theme.list.itemDisclosureActions.constructive.fillColor : theme.list.itemDestructiveColor
|
||||
let countColor = outgoing ? theme.list.itemDestructiveColor : theme.list.itemDisclosureActions.constructive.fillColor
|
||||
|
||||
let title = title.update(
|
||||
component: MultilineTextComponent(
|
||||
@ -333,7 +341,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
id: "date",
|
||||
title: strings.Gift_View_Date,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: Int32(Date().timeIntervalSince1970), strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor)))
|
||||
MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor)))
|
||||
)
|
||||
))
|
||||
|
||||
@ -342,11 +350,13 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
if let gift = state.starGiftsMap[giftId], let availability = gift.availability {
|
||||
remains = availability.remains
|
||||
}
|
||||
let remainsString = presentationStringsFormattedNumber(remains, environment.dateTimeFormat.groupingSeparator)
|
||||
let totalString = presentationStringsFormattedNumber(limitTotal, environment.dateTimeFormat.groupingSeparator)
|
||||
tableItems.append(.init(
|
||||
id: "availability",
|
||||
title: strings.Gift_View_Availability,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_View_Availability_Of("\(remains)", "\(limitTotal)").string, font: tableFont, textColor: tableTextColor)))
|
||||
MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_View_Availability_Of("\(remainsString)", "\(totalString)").string, font: tableFont, textColor: tableTextColor)))
|
||||
)
|
||||
))
|
||||
}
|
||||
@ -363,7 +373,9 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
animationCache: component.context.animationCache,
|
||||
animationRenderer: component.context.animationRenderer,
|
||||
placeholderColor: theme.list.mediaPlaceholderColor,
|
||||
text: .plain(attributedText)
|
||||
text: .plain(attributedText),
|
||||
maximumNumberOfLines: 0,
|
||||
handleSpoilers: true
|
||||
)
|
||||
)
|
||||
))
|
||||
@ -719,14 +731,14 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
case message(EngineMessage)
|
||||
case profileGift(EnginePeer.Id, ProfileGiftsContext.State.StarGift)
|
||||
|
||||
var arguments: (peerId: EnginePeer.Id, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, incoming: Bool, gift: StarGift, convertStars: Int64, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool)? {
|
||||
var arguments: (peerId: EnginePeer.Id, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, incoming: Bool, gift: StarGift, date: Int32, convertStars: Int64, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool)? {
|
||||
switch self {
|
||||
case let .message(message):
|
||||
if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted) = action.action {
|
||||
return (message.id.peerId, message.author?.id, message.author?.compactDisplayTitle, message.id, message.flags.contains(.Incoming), gift, convertStars, text, entities, nameHidden, savedToProfile, converted)
|
||||
return (message.id.peerId, message.author?.id, message.author?.compactDisplayTitle, message.id, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, converted)
|
||||
}
|
||||
case let .profileGift(peerId, gift):
|
||||
return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, gift.messageId, false, gift.gift, gift.convertStars ?? 0, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, false)
|
||||
return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, gift.messageId, false, gift.gift, gift.date, gift.convertStars ?? 0, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, false)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -905,7 +917,6 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
return
|
||||
}
|
||||
let introController = context.sharedContext.makeStarsIntroScreen(context: context)
|
||||
introController.navigationPresentation = .modal
|
||||
self.push(introController)
|
||||
}
|
||||
}
|
||||
@ -1067,17 +1078,26 @@ private final class TableComponent: CombinedComponent {
|
||||
} else {
|
||||
insets = UIEdgeInsets(top: 0.0, left: horizontalPadding, bottom: 0.0, right: horizontalPadding)
|
||||
}
|
||||
let valueChild = valueChildren[item.id].update(
|
||||
component: item.component,
|
||||
availableSize: CGSize(width: rightColumnWidth - insets.left - insets.right, height: context.availableSize.height),
|
||||
transition: context.transition
|
||||
)
|
||||
updatedValueChildren.append((valueChild, insets))
|
||||
|
||||
var titleHeight: CGFloat = 0.0
|
||||
if let titleChild = updatedTitleChildren[i] {
|
||||
titleHeight = titleChild.size.height
|
||||
}
|
||||
|
||||
let availableValueWidth: CGFloat
|
||||
if titleHeight > 0.0 {
|
||||
availableValueWidth = rightColumnWidth
|
||||
} else {
|
||||
availableValueWidth = context.availableSize.width
|
||||
}
|
||||
|
||||
let valueChild = valueChildren[item.id].update(
|
||||
component: item.component,
|
||||
availableSize: CGSize(width: availableValueWidth - insets.left - insets.right, height: context.availableSize.height),
|
||||
transition: context.transition
|
||||
)
|
||||
updatedValueChildren.append((valueChild, insets))
|
||||
|
||||
let rowHeight = max(40.0, max(titleHeight, valueChild.size.height) + verticalPadding * 2.0)
|
||||
rowHeights[i] = rowHeight
|
||||
totalHeight += rowHeight
|
||||
|
@ -182,7 +182,7 @@ public final class LoadingOverlayNode: ASDisplayNode {
|
||||
let interaction = ChatListNodeInteraction(context: context, animationCache: context.animationCache, animationRenderer: context.animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in
|
||||
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in
|
||||
gesture?.cancel()
|
||||
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, openStarsTopup: { _ in
|
||||
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, openStarsTopup: { _ in
|
||||
}, dismissNotice: { _ in
|
||||
}, editPeer: { _ in
|
||||
})
|
||||
@ -507,7 +507,7 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod
|
||||
},
|
||||
openPremiumIntro: {
|
||||
},
|
||||
openPremiumGift: { _ in
|
||||
openPremiumGift: { _, _ in
|
||||
},
|
||||
openPremiumManagement: {
|
||||
},
|
||||
|
@ -109,6 +109,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
public override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
|
||||
self.scrollNode.view.delegate = self
|
||||
}
|
||||
|
||||
@ -136,28 +137,32 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
|
||||
let visibleBounds = self.scrollNode.bounds.insetBy(dx: 0.0, dy: -10.0)
|
||||
|
||||
let topInset: CGFloat = 60.0
|
||||
|
||||
var validIds: [AnyHashable] = []
|
||||
var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: 60.0), size: starsOptionSize)
|
||||
var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: topInset), size: starsOptionSize)
|
||||
|
||||
var index: Int32 = 0
|
||||
for product in starsProducts {
|
||||
let itemId = AnyHashable(product.date)
|
||||
validIds.append(itemId)
|
||||
|
||||
var itemTransition = transition
|
||||
let visibleItem: ComponentView<Empty>
|
||||
if let current = self.starsItems[itemId] {
|
||||
visibleItem = current
|
||||
} else {
|
||||
visibleItem = ComponentView()
|
||||
self.starsItems[itemId] = visibleItem
|
||||
itemTransition = .immediate
|
||||
}
|
||||
|
||||
var isVisible = false
|
||||
if visibleBounds.intersects(itemFrame) {
|
||||
isVisible = true
|
||||
}
|
||||
|
||||
if isVisible {
|
||||
let itemId = AnyHashable(index)
|
||||
validIds.append(itemId)
|
||||
|
||||
var itemTransition = transition
|
||||
let visibleItem: ComponentView<Empty>
|
||||
if let current = self.starsItems[itemId] {
|
||||
visibleItem = current
|
||||
} else {
|
||||
visibleItem = ComponentView()
|
||||
self.starsItems[itemId] = visibleItem
|
||||
itemTransition = .immediate
|
||||
}
|
||||
|
||||
let ribbonText: String?
|
||||
if let availability = product.gift.availability {
|
||||
ribbonText = params.presentationData.strings.PeerInfo_Gifts_OneOf(compactNumericCountString(Int(availability.total))).string
|
||||
@ -221,6 +226,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
itemFrame.origin.x = sideInset
|
||||
itemFrame.origin.y += starsOptionSize.height + optionSpacing
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
|
||||
var removeIds: [AnyHashable] = []
|
||||
@ -243,8 +249,8 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
self.starsItems.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
var contentHeight = ceil(CGFloat(starsProducts.count) / 3.0) * starsOptionSize.height + 60.0 + 16.0
|
||||
|
||||
var bottomScrollInset: CGFloat = 0.0
|
||||
var contentHeight = ceil(CGFloat(starsProducts.count) / 3.0) * (starsOptionSize.height + optionSpacing) - optionSpacing + topInset + 16.0
|
||||
if self.peerId == self.context.account.peerId {
|
||||
let transition = ComponentTransition.immediate
|
||||
|
||||
@ -318,7 +324,8 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
transition.setFrame(view: unlockButton.view, frame: CGRect(origin: CGPoint(x: buttonSideInset, y: size.height - bottomInset - buttonSize.height - scrollOffset), size: buttonSize))
|
||||
let _ = unlockButton.updateLayout(width: buttonSize.width, transition: .immediate)
|
||||
|
||||
transition.setFrame(view: unlockBackground.view, frame: CGRect(x: 0.0, y: size.height - bottomInset - buttonSize.height - 8.0 - scrollOffset, width: size.width, height: bottomInset + buttonSize.height + 8.0))
|
||||
let bottomPanelHeight = bottomInset + buttonSize.height + 8.0
|
||||
transition.setFrame(view: unlockBackground.view, frame: CGRect(x: 0.0, y: size.height - bottomInset - buttonSize.height - 8.0 - scrollOffset, width: size.width, height: bottomPanelHeight))
|
||||
transition.setFrame(view: unlockSeparator.view, frame: CGRect(x: 0.0, y: size.height - bottomInset - buttonSize.height - 8.0 - scrollOffset, width: size.width, height: UIScreenPixel))
|
||||
|
||||
let unlockSize = unlockText.update(
|
||||
@ -342,17 +349,22 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: floor((size.width - unlockSize.width) / 2.0), y: contentHeight), size: unlockSize))
|
||||
}
|
||||
contentHeight += unlockSize.height
|
||||
contentHeight += bottomPanelHeight
|
||||
|
||||
bottomScrollInset = bottomPanelHeight - 40.0
|
||||
}
|
||||
contentHeight += params.bottomInset
|
||||
|
||||
self.scrollNode.view.scrollIndicatorInsets = UIEdgeInsets(top: 50.0, left: 0.0, bottom: bottomScrollInset, right: 0.0)
|
||||
|
||||
let contentSize = CGSize(width: params.size.width, height: contentHeight)
|
||||
if self.scrollNode.view.contentSize != contentSize {
|
||||
self.scrollNode.view.contentSize = contentSize
|
||||
}
|
||||
}
|
||||
|
||||
let bottomOffset = max(0.0, self.scrollNode.view.contentSize.height - self.scrollNode.view.contentOffset.y - self.scrollNode.view.frame.height)
|
||||
if bottomOffset < 100.0 {
|
||||
let bottomContentOffset = max(0.0, self.scrollNode.view.contentSize.height - self.scrollNode.view.contentOffset.y - self.scrollNode.view.frame.height)
|
||||
if bottomContentOffset < 200.0 {
|
||||
self.profileGifts.loadMore()
|
||||
}
|
||||
}
|
||||
|
@ -108,11 +108,9 @@ private final class ArchiveInfoSheetContentComponent: Component {
|
||||
}
|
||||
contentHeight += buttonSize.height
|
||||
|
||||
if environment.safeInsets.bottom.isZero {
|
||||
contentHeight += 16.0
|
||||
} else {
|
||||
contentHeight += environment.safeInsets.bottom + 14.0
|
||||
}
|
||||
let bottomPanelPadding: CGFloat = 12.0
|
||||
let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding
|
||||
contentHeight += bottomInset
|
||||
|
||||
return CGSize(width: availableSize.width, height: contentHeight)
|
||||
}
|
||||
@ -226,7 +224,7 @@ private final class ArchiveInfoScreenComponent: Component {
|
||||
})
|
||||
}
|
||||
)),
|
||||
backgroundColor: .color(environment.theme.list.plainBackgroundColor),
|
||||
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
|
||||
animateOut: self.sheetAnimateOut
|
||||
)),
|
||||
environment: {
|
||||
|
@ -186,7 +186,7 @@ final class GreetingMessageListItemComponent: Component {
|
||||
},
|
||||
openPremiumIntro: {
|
||||
},
|
||||
openPremiumGift: { _ in
|
||||
openPremiumGift: { _, _ in
|
||||
},
|
||||
openPremiumManagement: {
|
||||
},
|
||||
|
@ -201,7 +201,7 @@ final class QuickReplySetupScreenComponent: Component {
|
||||
},
|
||||
openPremiumIntro: {
|
||||
},
|
||||
openPremiumGift: { _ in
|
||||
openPremiumGift: { _, _ in
|
||||
},
|
||||
openPremiumManagement: {
|
||||
},
|
||||
|
@ -865,7 +865,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate
|
||||
}, activateChatPreview: { _, _, _, gesture, _ in
|
||||
gesture?.cancel()
|
||||
}, present: { _ in
|
||||
}, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: {
|
||||
}, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: {
|
||||
}, openBirthdaySetup: {
|
||||
}, performActiveSessionAction: { _, _ in
|
||||
}, openChatFolderUpdates: {}, hideChatFolderUpdates: {
|
||||
|
@ -31,11 +31,9 @@ swift_library(
|
||||
"//submodules/TextFormat",
|
||||
"//submodules/TelegramUI/Components/ListSectionComponent",
|
||||
"//submodules/TelegramUI/Components/ListActionItemComponent",
|
||||
"//submodules/TelegramUI/Components/ScrollComponent",
|
||||
"//submodules/TelegramUI/Components/Premium/PremiumStarComponent",
|
||||
"//submodules/TelegramUI/Components/ButtonComponent",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/Components/SolidRoundedButtonComponent",
|
||||
"//submodules/Components/BlurredBackgroundComponent",
|
||||
],
|
||||
visibility = [
|
||||
|
@ -8,18 +8,17 @@ import Markdown
|
||||
import TextFormat
|
||||
import TelegramPresentationData
|
||||
import ViewControllerComponent
|
||||
import ScrollComponent
|
||||
import BundleIconComponent
|
||||
import BalancedTextComponent
|
||||
import MultilineTextComponent
|
||||
import SolidRoundedButtonComponent
|
||||
import ButtonComponent
|
||||
import AccountContext
|
||||
import ScrollComponent
|
||||
import SheetComponent
|
||||
import BlurredBackgroundComponent
|
||||
import PremiumStarComponent
|
||||
|
||||
private final class ScrollContent: CombinedComponent {
|
||||
typealias EnvironmentType = (ViewControllerComponentContainer.Environment, ScrollChildEnvironment)
|
||||
private final class SheetContent: CombinedComponent {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let context: AccountContext
|
||||
let openExamples: () -> Void
|
||||
@ -35,7 +34,7 @@ private final class ScrollContent: CombinedComponent {
|
||||
self.dismiss = dismiss
|
||||
}
|
||||
|
||||
static func ==(lhs: ScrollContent, rhs: ScrollContent) -> Bool {
|
||||
static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
@ -44,10 +43,10 @@ private final class ScrollContent: CombinedComponent {
|
||||
|
||||
static var body: Body {
|
||||
let star = Child(PremiumStarComponent.self)
|
||||
|
||||
let title = Child(BalancedTextComponent.self)
|
||||
let text = Child(BalancedTextComponent.self)
|
||||
let list = Child(List<Empty>.self)
|
||||
let actionButton = Child(ButtonComponent.self)
|
||||
|
||||
return { context in
|
||||
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
|
||||
@ -82,13 +81,13 @@ private final class ScrollContent: CombinedComponent {
|
||||
UIColor(rgb: 0xfdd219)
|
||||
],
|
||||
particleColor: UIColor(rgb: 0xf9b004),
|
||||
backgroundColor: environment.theme.list.plainBackgroundColor
|
||||
backgroundColor: environment.theme.actionSheet.opaqueItemBackgroundColor
|
||||
),
|
||||
availableSize: CGSize(width: min(414.0, context.availableSize.width), height: 220.0),
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(star
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: environment.navigationHeight + 24.0))
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: 79.0))
|
||||
)
|
||||
|
||||
let title = title.update(
|
||||
@ -193,15 +192,40 @@ private final class ScrollContent: CombinedComponent {
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + list.size.height / 2.0))
|
||||
)
|
||||
contentSize.height += list.size.height
|
||||
contentSize.height += spacing - 9.0
|
||||
contentSize.height += spacing
|
||||
|
||||
contentSize.height += 12.0 + 50.0
|
||||
if environment.safeInsets.bottom > 0 {
|
||||
contentSize.height += environment.safeInsets.bottom + 5.0
|
||||
} else {
|
||||
contentSize.height += 12.0
|
||||
}
|
||||
|
||||
let buttonHeight: CGFloat = 50.0
|
||||
let bottomPanelPadding: CGFloat = 12.0
|
||||
let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding
|
||||
|
||||
contentSize.height += bottomPanelPadding
|
||||
|
||||
let controller = environment.controller() as? StarsIntroScreen
|
||||
let actionButton = actionButton.update(
|
||||
component: ButtonComponent(
|
||||
background: ButtonComponent.Background(
|
||||
color: environment.theme.list.itemCheckColors.fillColor,
|
||||
foreground: environment.theme.list.itemCheckColors.foregroundColor,
|
||||
pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8)
|
||||
),
|
||||
content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(
|
||||
Text(text: strings.Stars_Info_Done, font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor)
|
||||
)),
|
||||
isEnabled: true,
|
||||
displaysProgress: false,
|
||||
action: {
|
||||
controller?.dismissAnimated()
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: buttonHeight),
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(actionButton
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + actionButton.size.height / 2.0))
|
||||
.cornerRadius(10.0)
|
||||
)
|
||||
contentSize.height += actionButton.size.height + bottomInset
|
||||
|
||||
return contentSize
|
||||
}
|
||||
}
|
||||
@ -238,129 +262,58 @@ private final class ContainerComponent: CombinedComponent {
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let background = Child(Rectangle.self)
|
||||
let scroll = Child(ScrollComponent<ViewControllerComponentContainer.Environment>.self)
|
||||
let bottomPanel = Child(BlurredBackgroundComponent.self)
|
||||
let bottomSeparator = Child(Rectangle.self)
|
||||
let actionButton = Child(SolidRoundedButtonComponent.self)
|
||||
let scrollExternalState = ScrollComponent<EnvironmentType>.ExternalState()
|
||||
|
||||
let sheet = Child(SheetComponent<(EnvironmentType)>.self)
|
||||
let animateOut = StoredActionSlot(Action<Void>.self)
|
||||
|
||||
return { context in
|
||||
let environment = context.environment[EnvironmentType.self]
|
||||
let theme = environment.theme
|
||||
let strings = environment.strings
|
||||
let state = context.state
|
||||
|
||||
let controller = environment.controller
|
||||
|
||||
let background = background.update(
|
||||
component: Rectangle(color: environment.theme.list.plainBackgroundColor),
|
||||
environment: {},
|
||||
availableSize: context.availableSize,
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(background
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
|
||||
)
|
||||
|
||||
let scroll = scroll.update(
|
||||
component: ScrollComponent<EnvironmentType>(
|
||||
content: AnyComponent(ScrollContent(
|
||||
let sheet = sheet.update(
|
||||
component: SheetComponent<EnvironmentType>(
|
||||
content: AnyComponent(SheetContent(
|
||||
context: context.component.context,
|
||||
openExamples: context.component.openExamples,
|
||||
dismiss: {
|
||||
controller()?.dismiss()
|
||||
}
|
||||
)),
|
||||
externalState: scrollExternalState,
|
||||
contentInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 1.0, right: 0.0),
|
||||
contentOffsetUpdated: { [weak state] topContentOffset, bottomContentOffset in
|
||||
state?.topContentOffset = topContentOffset
|
||||
state?.bottomContentOffset = bottomContentOffset
|
||||
Queue.mainQueue().justDispatch {
|
||||
state?.updated(transition: .immediate)
|
||||
}
|
||||
},
|
||||
contentOffsetWillCommit: { targetContentOffset in
|
||||
}
|
||||
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
|
||||
followContentSizeChanges: true,
|
||||
clipsContent: true,
|
||||
animateOut: animateOut
|
||||
),
|
||||
environment: { environment },
|
||||
environment: {
|
||||
environment
|
||||
SheetComponentEnvironment(
|
||||
isDisplaying: environment.value.isVisible,
|
||||
isCentered: environment.metrics.widthClass == .regular,
|
||||
hasInputHeight: !environment.inputHeight.isZero,
|
||||
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
|
||||
dismiss: { animated in
|
||||
if animated {
|
||||
animateOut.invoke(Action { _ in
|
||||
if let controller = controller() {
|
||||
controller.dismiss(completion: nil)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
if let controller = controller() {
|
||||
controller.dismiss(completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
availableSize: context.availableSize,
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
context.add(scroll
|
||||
context.add(sheet
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
|
||||
)
|
||||
|
||||
let buttonHeight: CGFloat = 50.0
|
||||
let bottomPanelPadding: CGFloat = 12.0
|
||||
let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding
|
||||
let bottomPanelHeight = bottomPanelPadding + buttonHeight + bottomInset
|
||||
|
||||
let bottomPanelAlpha: CGFloat
|
||||
if scrollExternalState.contentHeight > context.availableSize.height {
|
||||
if let bottomContentOffset = state.bottomContentOffset {
|
||||
bottomPanelAlpha = min(16.0, bottomContentOffset) / 16.0
|
||||
} else {
|
||||
bottomPanelAlpha = 1.0
|
||||
}
|
||||
} else {
|
||||
bottomPanelAlpha = 0.0
|
||||
}
|
||||
|
||||
let bottomPanel = bottomPanel.update(
|
||||
component: BlurredBackgroundComponent(
|
||||
color: theme.rootController.tabBar.backgroundColor
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width, height: bottomPanelHeight),
|
||||
transition: context.transition
|
||||
)
|
||||
let bottomSeparator = bottomSeparator.update(
|
||||
component: Rectangle(
|
||||
color: theme.rootController.tabBar.separatorColor
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width, height: UIScreenPixel),
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
context.add(bottomPanel
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height / 2.0))
|
||||
.opacity(bottomPanelAlpha)
|
||||
)
|
||||
context.add(bottomSeparator
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height))
|
||||
.opacity(bottomPanelAlpha)
|
||||
)
|
||||
|
||||
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
|
||||
let actionButton = actionButton.update(
|
||||
component: SolidRoundedButtonComponent(
|
||||
title: strings.Stars_Info_Done,
|
||||
theme: SolidRoundedButtonComponent.Theme(
|
||||
backgroundColor: theme.list.itemCheckColors.fillColor,
|
||||
backgroundColors: [],
|
||||
foregroundColor: theme.list.itemCheckColors.foregroundColor
|
||||
),
|
||||
font: .bold,
|
||||
fontSize: 17.0,
|
||||
height: buttonHeight,
|
||||
cornerRadius: 10.0,
|
||||
gloss: false,
|
||||
iconName: nil,
|
||||
animationName: nil,
|
||||
iconPosition: .left,
|
||||
action: {
|
||||
controller()?.dismiss()
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0),
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(actionButton
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanelHeight + bottomPanelPadding + actionButton.size.height / 2.0))
|
||||
)
|
||||
|
||||
return context.availableSize
|
||||
}
|
||||
}
|
||||
@ -389,7 +342,7 @@ public final class StarsIntroScreen: ViewControllerComponentContainer {
|
||||
theme: forceDark ? .dark : .default
|
||||
)
|
||||
|
||||
self.navigationPresentation = .modal
|
||||
self.navigationPresentation = .flatModal
|
||||
|
||||
openExamplesImpl = { [weak self] in
|
||||
guard let self else {
|
||||
@ -408,6 +361,12 @@ public final class StarsIntroScreen: ViewControllerComponentContainer {
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public func dismissAnimated() {
|
||||
if let view = self.node.hostView.findTaggedView(tag: SheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
|
||||
view.dismissAnimated()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class ParagraphComponent: CombinedComponent {
|
||||
|
@ -732,7 +732,7 @@ private final class StarsTransferSheetComponent: CombinedComponent {
|
||||
})
|
||||
}
|
||||
)),
|
||||
backgroundColor: .blur(.light),
|
||||
backgroundColor: .color(environment.theme.list.modalBlocksBackgroundColor),
|
||||
followContentSizeChanges: true,
|
||||
clipsContent: true,
|
||||
animateOut: animateOut
|
||||
|
@ -52,7 +52,6 @@ private final class SheetContent: CombinedComponent {
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let background = Child(RoundedRectangle.self)
|
||||
let closeButton = Child(Button.self)
|
||||
let title = Child(Text.self)
|
||||
let amountSection = Child(ListSectionComponent.self)
|
||||
@ -74,15 +73,6 @@ private final class SheetContent: CombinedComponent {
|
||||
|
||||
let sideInset: CGFloat = 16.0
|
||||
var contentSize = CGSize(width: context.availableSize.width, height: 18.0)
|
||||
|
||||
let background = background.update(
|
||||
component: RoundedRectangle(color: theme.list.blocksBackgroundColor, cornerRadius: 8.0),
|
||||
availableSize: CGSize(width: context.availableSize.width, height: 1000.0),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(background
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0))
|
||||
)
|
||||
|
||||
let constrainedTitleWidth = context.availableSize.width - 16.0 * 2.0
|
||||
|
||||
@ -466,7 +456,7 @@ private final class StarsWithdrawSheetComponent: CombinedComponent {
|
||||
})
|
||||
}
|
||||
)),
|
||||
backgroundColor: .blur(.light),
|
||||
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
|
||||
followContentSizeChanges: false,
|
||||
clipsContent: true,
|
||||
isScrollEnabled: false,
|
||||
|
@ -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 {
|
||||
|
@ -282,7 +282,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, ASScrollViewDe
|
||||
}, openStorageManagement: {
|
||||
}, openPasswordSetup: {
|
||||
}, openPremiumIntro: {
|
||||
}, openPremiumGift: { _ in
|
||||
}, openPremiumGift: { _, _ in
|
||||
}, openPremiumManagement: {
|
||||
}, openActiveSessions: {
|
||||
}, openBirthdaySetup: {
|
||||
|
@ -156,7 +156,7 @@ private struct CommandChatInputContextPanelEntry: Comparable, Identifiable {
|
||||
},
|
||||
openPremiumIntro: {
|
||||
},
|
||||
openPremiumGift: { _ in
|
||||
openPremiumGift: { _, _ in
|
||||
},
|
||||
openPremiumManagement: {
|
||||
},
|
||||
|
@ -25,9 +25,22 @@ public final class HLSQualitySet {
|
||||
if let alternativeFile = alternativeRepresentation as? TelegramMediaFile {
|
||||
for attribute in alternativeFile.attributes {
|
||||
if case let .Video(_, size, _, _, _, videoCodec) = attribute {
|
||||
let _ = size
|
||||
if let videoCodec, NativeVideoContent.isVideoCodecSupported(videoCodec: videoCodec) {
|
||||
qualityFiles[Int(size.height)] = baseFile.withMedia(alternativeFile)
|
||||
let key = Int(size.height)
|
||||
if let currentFile = qualityFiles[key] {
|
||||
var currentCodec: String?
|
||||
for attribute in currentFile.media.attributes {
|
||||
if case let .Video(_, _, _, _, _, videoCodec) = attribute {
|
||||
currentCodec = videoCodec
|
||||
}
|
||||
}
|
||||
if let currentCodec, currentCodec == "av1" {
|
||||
} else {
|
||||
qualityFiles[key] = baseFile.withMedia(alternativeFile)
|
||||
}
|
||||
} else {
|
||||
qualityFiles[key] = baseFile.withMedia(alternativeFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import AccountContext
|
||||
import PhotoResources
|
||||
import UIKitRuntimeUtils
|
||||
import RangeSet
|
||||
import VideoToolbox
|
||||
|
||||
private extension CGRect {
|
||||
var center: CGPoint {
|
||||
@ -25,6 +26,11 @@ public enum NativeVideoContentId: Hashable {
|
||||
case profileVideo(Int64, String?)
|
||||
}
|
||||
|
||||
private let isAv1Supported: Bool = {
|
||||
let value = VTIsHardwareDecodeSupported(kCMVideoCodecType_AV1)
|
||||
return value
|
||||
}()
|
||||
|
||||
public final class NativeVideoContent: UniversalVideoContent {
|
||||
public let id: AnyHashable
|
||||
public let nativeId: NativeVideoContentId
|
||||
@ -58,7 +64,17 @@ public final class NativeVideoContent: UniversalVideoContent {
|
||||
let hasSentFramesToDisplay: (() -> Void)?
|
||||
|
||||
public static func isVideoCodecSupported(videoCodec: String) -> Bool {
|
||||
return videoCodec == "h264" || videoCodec == "h265" || videoCodec == "avc" || videoCodec == "hevc"
|
||||
if videoCodec == "h264" || videoCodec == "h265" || videoCodec == "avc" || videoCodec == "hevc" {
|
||||
return true
|
||||
}
|
||||
|
||||
if videoCodec == "av1" {
|
||||
if isAv1Supported {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
public static func isHLSVideo(file: TelegramMediaFile) -> Bool {
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"app": "11.2",
|
||||
"app": "11.2.1",
|
||||
"xcode": "16.0",
|
||||
"bazel": "7.3.1",
|
||||
"macos": "15.0"
|
||||
|
Loading…
x
Reference in New Issue
Block a user