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

This commit is contained in:
Mikhail Filimonov 2024-10-07 09:56:10 +04:00
commit 56b7092fef
62 changed files with 1525 additions and 657 deletions

View File

@ -13008,6 +13008,8 @@ Sorry for the inconvenience.";
"Gift.Options.Gift.Filter.AllGifts" = "All Gifts"; "Gift.Options.Gift.Filter.AllGifts" = "All Gifts";
"Gift.Options.Gift.Filter.Limited" = "Limited"; "Gift.Options.Gift.Filter.Limited" = "Limited";
"Gift.Options.Gift.Limited" = "Limited"; "Gift.Options.Gift.Limited" = "Limited";
"Gift.Options.Gift.SoldOut" = "Sold Out";
"Gift.Options.SoldOut.Text" = "Sorry, this gift is sold out.";
"PeerInfo.PaneGifts" = "Gifts"; "PeerInfo.PaneGifts" = "Gifts";
@ -13038,6 +13040,12 @@ Sorry for the inconvenience.";
"Gift.Send.HideMyName" = "Hide My Name"; "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.HideMyName.Info" = "Hide my name and message from visitors to %1$@'s profile. %2$@ will still see your name and message.";
"Gift.Send.Send" = "Send a Gift for"; "Gift.Send.Send" = "Send a Gift for";
"Gift.Send.Limited" = "Limited";
"Gift.Send.Remains_1" = "%@ left";
"Gift.Send.Remains_any" = "%@ left";
"Gift.Send.ErrorUnknown" = "An error occurred. Please try again.";
"Gift.Send.ErrorOutOfStock" = "Sorry, this gift is sold out. Please choose another gift.";
"Profile.SendGift" = "Send a Gift"; "Profile.SendGift" = "Send a Gift";
"Settings.SendGift" = "Send a Gift"; "Settings.SendGift" = "Send a Gift";
@ -13049,6 +13057,7 @@ Sorry for the inconvenience.";
"Report.Title.User" = "Report User"; "Report.Title.User" = "Report User";
"Report.Title.Group" = "Report Group"; "Report.Title.Group" = "Report Group";
"Report.Title.Channel" = "Report Channel"; "Report.Title.Channel" = "Report Channel";
"Report.Title.Bot" = "Report Bot";
"Report.Comment.Placeholder" = "Add Comment"; "Report.Comment.Placeholder" = "Add Comment";
"Report.Comment.Placeholder.Optional" = "Add Comment (Optional)"; "Report.Comment.Placeholder.Optional" = "Add Comment (Optional)";
"Report.Comment.Info" = "Please help us by telling what is wrong with the message you have selected."; "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_1" = "%@ Year Premium";
"Notification.PremiumGift.YearsTitle_any" = "%@ Years Premium"; "Notification.PremiumGift.YearsTitle_any" = "%@ Years Premium";
"Notification.PremiumGift.More" = "more";
"Notification.PremiumGift.SubscriptionDescription" = "Subscription for exclusive Telegram features."; "Notification.PremiumGift.SubscriptionDescription" = "Subscription for exclusive Telegram features.";
"Notification.StarsGift.Stars_1" = "%@ Star"; "Notification.StarsGift.Stars_1" = "%@ Star";
"Notification.StarsGift.Stars_any" = "%@ Stars"; "Notification.StarsGift.Stars_any" = "%@ Stars";
"WebBrowser.AuthChallenge.Title" = "Sign in to %@";
"WebBrowser.AuthChallenge.Text" = "Your login information will be sent securely.";

View File

@ -148,18 +148,7 @@ class BazelCommandLine:
self.disable_provisioning_profiles = True self.disable_provisioning_profiles = True
def set_configuration(self, configuration): def set_configuration(self, configuration):
if configuration == 'debug_universal': if configuration == 'debug_arm64':
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':
self.configuration_args = [ self.configuration_args = [
# bazel debug build configuration # bazel debug build configuration
'-c', 'dbg', '-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. # Build single-architecture binaries. It is almost 2 times faster is 32-bit support is not required.
'--ios_multi_cpus=sim_arm64', '--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. # Always build universal Watch binaries.
'--watchos_cpus=arm64_32' '--watchos_cpus=arm64_32'
] + self.common_debug_args ] + self.common_debug_args
@ -217,41 +196,10 @@ class BazelCommandLine:
'--apple_generate_dsym', '--apple_generate_dsym',
# Require DSYM files as build output. # Require DSYM files as build output.
'--output_groups=+dsyms' '--output_groups=+dsyms',
] + self.common_release_args
elif configuration == 'release_armv7':
self.configuration_args = [
# bazel optimized build configuration
'-c', 'opt',
# Build single-architecture binaries. It is almost 2 times faster is 32-bit support is not required. '--swiftcopt=-num-threads',
'--ios_multi_cpus=armv7', '--swiftcopt=0',
# 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'
] + self.common_release_args ] + self.common_release_args
else: else:
raise Exception('Unknown configuration {}'.format(configuration)) raise Exception('Unknown configuration {}'.format(configuration))

View File

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

View File

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

View File

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

View File

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

View File

@ -399,7 +399,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
case topic(EnginePeer, ChatListItemContent.ThreadInfo, Int, PresentationTheme, PresentationStrings, ChatListSearchSectionExpandType) case topic(EnginePeer, ChatListItemContent.ThreadInfo, Int, PresentationTheme, PresentationStrings, ChatListSearchSectionExpandType)
case recentlySearchedPeer(EnginePeer, EnginePeer?, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, PeerStoryStats?, Bool) 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 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 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) case addContact(String, PresentationTheme, PresentationStrings)
@ -411,7 +411,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
return .localPeerId(peer.id) return .localPeerId(peer.id)
case let .localPeer(peer, _, _, _, _, _, _, _, _, _, _): case let .localPeer(peer, _, _, _, _, _, _, _, _, _, _):
return .localPeerId(peer.id) return .localPeerId(peer.id)
case let .globalPeer(peer, _, _, _, _, _, _, _, _, _): case let .globalPeer(peer, _, _, _, _, _, _, _, _, _, _):
return .globalPeerId(peer.peer.id) return .globalPeerId(peer.peer.id)
case let .message(message, _, _, _, _, _, _, _, _, _, section, _, _, _): case let .message(message, _, _, _, _, _, _, _, _, _, section, _, _, _):
return .messageId(message.id, section) return .messageId(message.id, section)
@ -440,8 +440,8 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
} else { } else {
return false return false
} }
case let .globalPeer(lhsPeer, lhsUnreadBadge, lhsIndex, lhsTheme, lhsStrings, lhsSortOrder, lhsDisplayOrder, lhsExpandType, lhsStoryStats, lhsRequiresPremiumForMessaging): 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) = 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 { 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 return true
} else { } else {
return false return false
@ -543,11 +543,11 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
case .globalPeer, .message, .addContact: case .globalPeer, .message, .addContact:
return true return true
} }
case let .globalPeer(_, _, lhsIndex, _, _, _, _, _, _, _): case let .globalPeer(_, _, lhsIndex, _, _, _, _, _, _, _, _):
switch rhs { switch rhs {
case .topic, .recentlySearchedPeer, .localPeer: case .topic, .recentlySearchedPeer, .localPeer:
return false return false
case let .globalPeer(_, _, rhsIndex, _, _, _, _, _, _, _): case let .globalPeer(_, _, rhsIndex, _, _, _, _, _, _, _, _):
return lhsIndex <= rhsIndex return lhsIndex <= rhsIndex
case .message, .addContact: case .message, .addContact:
return true return true
@ -798,7 +798,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
openStories(peer.id, sourceNode.avatarNode) 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 var enabled = true
if filter.contains(.onlyWriteable) { if filter.contains(.onlyWriteable) {
enabled = canSendMessagesToPeer(peer.peer) enabled = canSendMessagesToPeer(peer.peer)
@ -822,7 +822,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
var suffixString = "" var suffixString = ""
if let subscribers = peer.subscribers, subscribers != 0 { if let subscribers = peer.subscribers, subscribers != 0 {
if peer.peer is TelegramUser { 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 { } else if let channel = peer.peer as? TelegramChannel, case .broadcast = channel.info {
suffixString = ", \(strings.Conversation_StatusSubscribers(subscribers))" suffixString = ", \(strings.Conversation_StatusSubscribers(subscribers))"
} else { } else {
@ -858,7 +858,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
isSavedMessages = true 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) interaction.peerSelected(EnginePeer(peer.peer), nil, nil, nil)
}, disabledAction: { _ in }, disabledAction: { _ in
interaction.disabledPeerSelected(EnginePeer(peer.peer), nil, requiresPremiumForMessaging ? .premiumRequired : .generic) 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)) { if !existingPeerIds.contains(peer.peer.id), filteredPeer(EnginePeer(peer.peer), EnginePeer(accountPeer)) {
existingPeerIds.insert(peer.peer.id) 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 index += 1
numberOfGlobalPeers += 1 numberOfGlobalPeers += 1
} }
@ -2807,7 +2807,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
}, openStorageManagement: { }, openStorageManagement: {
}, openPasswordSetup: { }, openPasswordSetup: {
}, openPremiumIntro: { }, openPremiumIntro: {
}, openPremiumGift: { _ in }, openPremiumGift: { _, _ in
}, openPremiumManagement: { }, openPremiumManagement: {
}, openActiveSessions: { }, openActiveSessions: {
}, openBirthdaySetup: { }, openBirthdaySetup: {
@ -2953,7 +2953,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
if case let .user(user) = peer, user.flags.contains(.requirePremium) { if case let .user(user) = peer, user.flags.contains(.requirePremium) {
requiresPremiumForMessagingPeerIds.append(peer.id) requiresPremiumForMessagingPeerIds.append(peer.id)
} }
case let .globalPeer(foundPeer, _, _, _, _, _, _, _, _, _): case let .globalPeer(foundPeer, _, _, _, _, _, _, _, _, _, _):
storyStatsIds.append(foundPeer.peer.id) storyStatsIds.append(foundPeer.peer.id)
if let user = foundPeer.peer as? TelegramUser, user.flags.contains(.requirePremium) { if let user = foundPeer.peer as? TelegramUser, user.flags.contains(.requirePremium) {
requiresPremiumForMessagingPeerIds.append(foundPeer.peer.id) 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) 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, _, _): 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) 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, _, _): 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) 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, _, _): 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) mappedItems[i] = .message(message, peer, combinedPeerReadState, threadInfo, presentationData, totalCount, selected, displayCustomHeader, key, resourceId, section, allPaused, stats[peer.peerId] ?? nil, requiresPremiumForMessaging[peer.peerId] ?? false)
default: 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 let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in
gesture?.cancel() gesture?.cancel()
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: { }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: {
}, openBirthdaySetup: { }, openBirthdaySetup: {
}, performActiveSessionAction: { _, _ in }, performActiveSessionAction: { _, _ in
}, openChatFolderUpdates: {}, hideChatFolderUpdates: { }, openChatFolderUpdates: {}, hideChatFolderUpdates: {

View File

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

View File

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

View File

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

View File

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

View File

@ -175,6 +175,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
let options: [ItemListPeerItemRevealOption] let options: [ItemListPeerItemRevealOption]
let additionalActions: [ContactsPeerItemAction] let additionalActions: [ContactsPeerItemAction]
let actionIcon: ContactsPeerItemActionIcon let actionIcon: ContactsPeerItemActionIcon
let searchQuery: String?
let action: ((ContactsPeerItemPeer) -> Void)? let action: ((ContactsPeerItemPeer) -> Void)?
let disabledAction: ((ContactsPeerItemPeer) -> Void)? let disabledAction: ((ContactsPeerItemPeer) -> Void)?
let setPeerIdWithRevealedOptions: ((EnginePeer.Id?, EnginePeer.Id?) -> Void)? let setPeerIdWithRevealedOptions: ((EnginePeer.Id?, EnginePeer.Id?) -> Void)?
@ -215,6 +216,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
actionIcon: ContactsPeerItemActionIcon = .none, actionIcon: ContactsPeerItemActionIcon = .none,
index: SortIndex?, index: SortIndex?,
header: ListViewItemHeader?, header: ListViewItemHeader?,
searchQuery: String? = nil,
action: ((ContactsPeerItemPeer) -> Void)?, action: ((ContactsPeerItemPeer) -> Void)?,
disabledAction: ((ContactsPeerItemPeer) -> Void)? = nil, disabledAction: ((ContactsPeerItemPeer) -> Void)? = nil,
setPeerIdWithRevealedOptions: ((EnginePeer.Id?, EnginePeer.Id?) -> Void)? = nil, setPeerIdWithRevealedOptions: ((EnginePeer.Id?, EnginePeer.Id?) -> Void)? = nil,
@ -245,6 +247,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
self.options = options self.options = options
self.additionalActions = additionalActions self.additionalActions = additionalActions
self.actionIcon = actionIcon self.actionIcon = actionIcon
self.searchQuery = searchQuery
self.action = action self.action = action
self.disabledAction = disabledAction self.disabledAction = disabledAction
self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions 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) statusAttributedString = NSAttributedString(string: string, font: statusFont, textColor: activity ? item.presentationData.theme.list.itemAccentColor : item.presentationData.theme.list.itemSecondaryTextColor)
} }
case let .addressName(suffix): 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) let addressNameString = NSAttributedString(string: "@" + addressName, font: statusFont, textColor: item.presentationData.theme.list.itemAccentColor)
if !suffix.isEmpty { if !suffix.isEmpty {
let suffixString = NSAttributedString(string: suffix, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) let suffixString = NSAttributedString(string: suffix, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)

View File

@ -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
}
}

View File

@ -1405,8 +1405,15 @@ public class GalleryController: ViewController, StandalonePresentableController,
} }
} }
self.galleryNode.completeCustomDismiss = { [weak self] in self.galleryNode.completeCustomDismiss = { [weak self] isPictureInPicture in
self?._hiddenMedia.set(.single(nil)) 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) self?.presentingViewController?.dismiss(animated: false, completion: nil)
} }

View File

@ -25,7 +25,7 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture
public var pager: GalleryPagerNode public var pager: GalleryPagerNode
public var beginCustomDismiss: (Bool) -> Void = { _ in } public var beginCustomDismiss: (Bool) -> Void = { _ in }
public var completeCustomDismiss: () -> Void = { } public var completeCustomDismiss: (Bool) -> Void = { _ in }
public var baseNavigationController: () -> NavigationController? = { return nil } public var baseNavigationController: () -> NavigationController? = { return nil }
public var galleryController: () -> ViewController? = { 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 { if let strongSelf = self {
strongSelf.completeCustomDismiss() strongSelf.completeCustomDismiss(isPictureInPicture)
} }
} }

View File

@ -25,7 +25,7 @@ open class GalleryItemNode: ASDisplayNode {
public var updateOrientation: (UIInterfaceOrientation) -> Void = { _ in } public var updateOrientation: (UIInterfaceOrientation) -> Void = { _ in }
public var dismiss: () -> Void = { } public var dismiss: () -> Void = { }
public var beginCustomDismiss: (Bool) -> Void = { _ in } public var beginCustomDismiss: (Bool) -> Void = { _ in }
public var completeCustomDismiss: () -> Void = { } public var completeCustomDismiss: (Bool) -> Void = { _ in }
public var baseNavigationController: () -> NavigationController? = { return nil } public var baseNavigationController: () -> NavigationController? = { return nil }
public var galleryController: () -> ViewController? = { return nil } public var galleryController: () -> ViewController? = { return nil }
public var alternativeDismiss: () -> Bool = { return false } public var alternativeDismiss: () -> Bool = { return false }

View File

@ -110,7 +110,7 @@ public final class GalleryPagerNode: ASDisplayNode, ASScrollViewDelegate, ASGest
public var updateOrientation: (UIInterfaceOrientation) -> Void = { _ in } public var updateOrientation: (UIInterfaceOrientation) -> Void = { _ in }
public var dismiss: () -> Void = { } public var dismiss: () -> Void = { }
public var beginCustomDismiss: (Bool) -> Void = { _ in } public var beginCustomDismiss: (Bool) -> Void = { _ in }
public var completeCustomDismiss: () -> Void = { } public var completeCustomDismiss: (Bool) -> Void = { _ in }
public var baseNavigationController: () -> NavigationController? = { return nil } public var baseNavigationController: () -> NavigationController? = { return nil }
public var galleryController: () -> ViewController? = { return nil } public var galleryController: () -> ViewController? = { return nil }

View File

@ -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))) 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) { Queue.mainQueue().after(0.3) {
self.completeCustomDismiss() self.completeCustomDismiss(false)
} }
} }
f(.default) 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))) 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) { Queue.mainQueue().after(0.3) {
self.completeCustomDismiss() self.completeCustomDismiss(false)
} }
} }
f(.default) f(.default)

View File

@ -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 { final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
private let context: AccountContext private let context: AccountContext
private let presentationData: PresentationData private let presentationData: PresentationData
@ -875,12 +1114,17 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
private let isShowingContextMenuPromise = ValuePromise<Bool>(false, ignoreRepeated: true) private let isShowingContextMenuPromise = ValuePromise<Bool>(false, ignoreRepeated: true)
private let hasExpandedCaptionPromise = Promise<Bool>() private let hasExpandedCaptionPromise = Promise<Bool>()
private var hideControlsDisposable: Disposable? private var hideControlsDisposable: Disposable?
private var automaticPictureInPictureDisposable: Disposable?
var playbackCompleted: (() -> Void)? var playbackCompleted: (() -> Void)?
private var customUnembedWhenPortrait: ((OverlayMediaItemNode) -> Bool)? private var customUnembedWhenPortrait: ((OverlayMediaItemNode) -> Bool)?
private var pictureInPictureContent: AnyObject? 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) { init(context: AccountContext, presentationData: PresentationData, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction, Message) -> Void, present: @escaping (ViewController, Any?) -> Void) {
self.context = context self.context = context
@ -1064,6 +1308,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
self.mediaPlaybackStateDisposable.dispose() self.mediaPlaybackStateDisposable.dispose()
self.scrubbingFrameDisposable?.dispose() self.scrubbingFrameDisposable?.dispose()
self.hideControlsDisposable?.dispose() self.hideControlsDisposable?.dispose()
self.automaticPictureInPictureDisposable?.dispose()
} }
override func ready() -> Signal<Void, NoError> { override func ready() -> Signal<Void, NoError> {
@ -1259,6 +1504,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
strongSelf.videoNode?.setBaseRate(playbackRate) strongSelf.videoNode?.setBaseRate(playbackRate)
} }
} }
if strongSelf.nativePictureInPictureContent == nil {
strongSelf.setupNativePictureInPicture()
}
} }
} }
self.videoNode = videoNode 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.beginCustomDismiss(false)
self.statusNode.isHidden = true self.statusNode.isHidden = true
self.animateOut(toOverlay: overlayNode, completion: { [weak self] in 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() { @objc func pictureInPictureButtonPressed() {
if #available(iOS 15.0, *) {
if let nativePictureInPictureContent = self.nativePictureInPictureContent as? NativePictureInPictureContentImpl {
nativePictureInPictureContent.beginPictureInPicture()
return
}
}
var isNativePictureInPictureSupported = false var isNativePictureInPictureSupported = false
switch self.item?.contentInfo { switch self.item?.contentInfo {
case let .message(message, _): case let .message(message, _):
@ -2391,7 +2724,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
strongSelf.completeCustomDismiss() strongSelf.completeCustomDismiss(false)
}, expand: { [weak baseNavigationController] completion in }, expand: { [weak baseNavigationController] completion in
guard let contentInfo = item.contentInfo else { guard let contentInfo = item.contentInfo else {
return return
@ -2524,7 +2857,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
self.beginCustomDismiss(false) self.beginCustomDismiss(false)
self.statusNode.isHidden = true self.statusNode.isHidden = true
self.animateOut(toOverlay: overlayNode, completion: { [weak self] in 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))) 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) { Queue.mainQueue().after(0.3) {
strongSelf.completeCustomDismiss() strongSelf.completeCustomDismiss(false)
} }
} }
f(.default) 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))) 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) { Queue.mainQueue().after(0.3) {
self.completeCustomDismiss() self.completeCustomDismiss(false)
} }
} }
f(.default) f(.default)

View File

@ -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?._hiddenMedia.set(.single(nil))
self?.presentingViewController?.dismiss(animated: false, completion: nil) self?.presentingViewController?.dismiss(animated: false, completion: nil)
} }

View File

@ -398,7 +398,7 @@ public class InstantPageGalleryController: ViewController, StandalonePresentable
self?.presentingViewController?.dismiss(animated: false, completion: nil) 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?._hiddenMedia.set(.single(nil))
self?.presentingViewController?.dismiss(animated: false, completion: nil) self?.presentingViewController?.dismiss(animated: false, completion: nil)
} }

View File

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

View File

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

View File

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

View File

@ -160,11 +160,11 @@ final class VideoChatActionButtonComponent: Component {
case .unmuted: case .unmuted:
backgroundColor = !isActive ? UIColor(rgb: 0x124B21) : UIColor(rgb: 0x34C659) backgroundColor = !isActive ? UIColor(rgb: 0x124B21) : UIColor(rgb: 0x34C659)
case .raiseHand, .scheduled: case .raiseHand, .scheduled:
backgroundColor = UIColor(rgb: 0x3252EF) backgroundColor = !isActive ? UIColor(rgb: 0x23306B) : UIColor(rgb: 0x3252EF)
} }
iconDiameter = 60.0 iconDiameter = 60.0
case let .video(isActive): case let .video(isActive):
titleText = "video" titleText = component.strings.VoiceChat_Video
switch component.microphoneState { switch component.microphoneState {
case .connecting: case .connecting:
backgroundColor = UIColor(white: 0.1, alpha: 1.0) backgroundColor = UIColor(white: 0.1, alpha: 1.0)
@ -177,7 +177,7 @@ final class VideoChatActionButtonComponent: Component {
} }
iconDiameter = 60.0 iconDiameter = 60.0
case .leave: case .leave:
titleText = "leave" titleText = component.strings.VoiceChat_Leave
backgroundColor = UIColor(rgb: 0x47191E) backgroundColor = UIColor(rgb: 0x47191E)
iconDiameter = 22.0 iconDiameter = 22.0
} }
@ -282,6 +282,7 @@ final class VideoChatActionButtonComponent: Component {
} }
self.isEnabled = isEnabled self.isEnabled = isEnabled
self.isUserInteractionEnabled = isEnabled
return size return size
} }

View File

@ -189,6 +189,7 @@ final class VideoChatMicButtonComponent: Component {
} }
let call: PresentationGroupCall let call: PresentationGroupCall
let strings: PresentationStrings
let content: Content let content: Content
let isCollapsed: Bool let isCollapsed: Bool
let updateUnmutedStateIsPushToTalk: (Bool?) -> Void let updateUnmutedStateIsPushToTalk: (Bool?) -> Void
@ -197,6 +198,7 @@ final class VideoChatMicButtonComponent: Component {
init( init(
call: PresentationGroupCall, call: PresentationGroupCall,
strings: PresentationStrings,
content: Content, content: Content,
isCollapsed: Bool, isCollapsed: Bool,
updateUnmutedStateIsPushToTalk: @escaping (Bool?) -> Void, updateUnmutedStateIsPushToTalk: @escaping (Bool?) -> Void,
@ -204,6 +206,7 @@ final class VideoChatMicButtonComponent: Component {
scheduleAction: @escaping () -> Void scheduleAction: @escaping () -> Void
) { ) {
self.call = call self.call = call
self.strings = strings
self.content = content self.content = content
self.isCollapsed = isCollapsed self.isCollapsed = isCollapsed
self.updateUnmutedStateIsPushToTalk = updateUnmutedStateIsPushToTalk self.updateUnmutedStateIsPushToTalk = updateUnmutedStateIsPushToTalk
@ -327,29 +330,29 @@ final class VideoChatMicButtonComponent: Component {
var isEnabled = true var isEnabled = true
switch component.content { switch component.content {
case .connecting: case .connecting:
titleText = "Connecting..." titleText = component.strings.VoiceChat_Connecting
isEnabled = false isEnabled = false
case .muted: case .muted:
titleText = "Unmute" titleText = component.strings.VoiceChat_Unmute
case let .unmuted(isPushToTalk): 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): case let .raiseHand(isRaised):
if isRaised { if isRaised {
titleText = "You asked to speak" titleText = component.strings.VoiceChat_AskedToSpeak
subtitleText = "We let the speakers know" subtitleText = component.strings.VoiceChat_AskedToSpeakHelp
} else { } else {
titleText = "Muted by Admin" titleText = component.strings.VoiceChat_MutedByAdmin
subtitleText = "Tap if you want to speak" subtitleText = component.strings.VoiceChat_MutedByAdminHelp
} }
case let .scheduled(state): case let .scheduled(state):
switch state { switch state {
case .start: case .start:
titleText = "Start Now" titleText = component.strings.VoiceChat_StartNow
case let .toggleSubscription(isSubscribed): case let .toggleSubscription(isSubscribed):
if isSubscribed { if isSubscribed {
titleText = "Clear Reminder" titleText = component.strings.VoiceChat_CancelReminder
} else { } else {
titleText = "Set Reminder" titleText = component.strings.VoiceChat_SetReminder
} }
} }
} }

View File

@ -1193,17 +1193,17 @@ final class VideoChatParticipantsComponent: Component {
let subtitle: PeerListItemComponent.Subtitle let subtitle: PeerListItemComponent.Subtitle
if participant.peer.id == component.call.accountContext.account.peerId { 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) { } else if component.speakingParticipants.contains(participant.peer.id) {
if let volume = participant.volume, volume / 100 != 100 { 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 { } 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 { } else if let about = participant.about, !about.isEmpty {
subtitle = PeerListItemComponent.Subtitle(text: about, color: .neutral) subtitle = PeerListItemComponent.Subtitle(text: about, color: .neutral)
} else { } else {
subtitle = PeerListItemComponent.Subtitle(text: "listening", color: .neutral) subtitle = PeerListItemComponent.Subtitle(text: component.strings.VoiceChat_StatusListening, color: .neutral)
} }
let rightAccessoryComponent: AnyComponent<Empty> = AnyComponent(VideoChatParticipantStatusComponent( let rightAccessoryComponent: AnyComponent<Empty> = AnyComponent(VideoChatParticipantStatusComponent(
@ -1667,12 +1667,12 @@ final class VideoChatParticipantsComponent: Component {
if let participants = component.participants, let inviteType = participants.inviteType { if let participants = component.participants, let inviteType = participants.inviteType {
switch inviteType { switch inviteType {
case .invite: case .invite:
inviteText = "Invite Members" inviteText = component.strings.VoiceChat_InviteMember
case .shareLink: case .shareLink:
inviteText = "Share Invite Link" inviteText = component.strings.VoiceChat_Share
} }
} else { } else {
inviteText = "Invite Members" inviteText = component.strings.VoiceChat_InviteMember
} }
let inviteListItemSize = self.inviteListItemView.update( let inviteListItemSize = self.inviteListItemView.update(
transition: transition, transition: transition,

View File

@ -123,7 +123,7 @@ final class VideoChatScheduledInfoComponent: Component {
let titleSize = self.title.update( let titleSize = self.title.update(
transition: .immediate, transition: .immediate,
component: AnyComponent(MultilineTextComponent( 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: {}, environment: {},
containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: 200.0) containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: 200.0)

View File

@ -1022,7 +1022,7 @@ final class VideoChatScreenComponent: Component {
if case let .channel(channel) = self.peer, case .broadcast = channel.info { if case let .channel(channel) = self.peer, case .broadcast = channel.info {
displayEvent = false displayEvent = false
} }
if members.totalCount < 250 { if members.totalCount < 40 {
displayEvent = true displayEvent = true
} else if event.peer.isVerified { } else if event.peer.isVerified {
displayEvent = true displayEvent = true
@ -1270,9 +1270,9 @@ final class VideoChatScreenComponent: Component {
if callState.networkState == .connected, let members = self.members { if callState.networkState == .connected, let members = self.members {
idleTitleStatusText = environment.strings.VoiceChat_Panel_Members(Int32(max(1, members.totalCount))) idleTitleStatusText = environment.strings.VoiceChat_Panel_Members(Int32(max(1, members.totalCount)))
} else if callState.scheduleTimestamp != nil { } else if callState.scheduleTimestamp != nil {
idleTitleStatusText = "scheduled" idleTitleStatusText = environment.strings.VoiceChat_Scheduled
} else { } else {
idleTitleStatusText = "connecting..." idleTitleStatusText = environment.strings.VoiceChat_Connecting
} }
} else { } else {
idleTitleStatusText = " " idleTitleStatusText = " "
@ -1686,6 +1686,7 @@ final class VideoChatScreenComponent: Component {
transition: transition, transition: transition,
component: AnyComponent(VideoChatMicButtonComponent( component: AnyComponent(VideoChatMicButtonComponent(
call: component.call, call: component.call,
strings: environment.strings,
content: micButtonContent, content: micButtonContent,
isCollapsed: areButtonsCollapsed, isCollapsed: areButtonsCollapsed,
updateUnmutedStateIsPushToTalk: { [weak self] unmutedStateIsPushToTalk in updateUnmutedStateIsPushToTalk: { [weak self] unmutedStateIsPushToTalk in

View File

@ -7098,7 +7098,10 @@ final class VoiceChatContextReferenceContentSource: ContextReferenceContentSourc
} }
public func shouldUseV2VideoChatImpl(context: AccountContext) -> Bool { 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 { if context.sharedContext.immediateExperimentalUISettings.disableCallV2 {
useV2 = false useV2 = false
} }

View File

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

View File

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

View File

@ -463,6 +463,10 @@ public extension EnginePeer {
return self._asPeer().addressName return self._asPeer().addressName
} }
var usernames: [TelegramPeerUsername] {
return self._asPeer().usernames
}
var indexName: EnginePeer.IndexName { var indexName: EnginePeer.IndexName {
return EnginePeer.IndexName(self._asPeer().indexName) return EnginePeer.IndexName(self._asPeer().indexName)
} }

View File

@ -61,7 +61,11 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo
if let group = peer as? TelegramGroup, group.migrationReference != nil { if let group = peer as? TelegramGroup, group.migrationReference != nil {
continue 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 { if let group = peer as? TelegramGroup, group.migrationReference != nil {
continue 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]))
}
} }
} }

View File

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

View File

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

View File

@ -6203,17 +6203,13 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
return return
} }
let effectiveMediaVisibility = self.visibility
var isPlaying = true var isPlaying = true
if case let .visible(_, subRect) = self.visibility {
if subRect.minY > 32.0 {
isPlaying = false
}
} else {
isPlaying = false
}
if !item.controllerInteraction.canReadHistory { if !item.controllerInteraction.canReadHistory {
isPlaying = false isPlaying = false
} }
if self.forceStopAnimations { if self.forceStopAnimations {
isPlaying = false isPlaying = false
} }
@ -6228,7 +6224,19 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
} }
for contentNode in self.contentNodes { 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 { if let threadInfoNode = self.threadInfoNode {

View File

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

View File

@ -468,8 +468,12 @@ private final class SheetContent: CombinedComponent {
switch component.subject { switch component.subject {
case .peer: case .peer:
if let peer = state.peer { if let peer = state.peer {
if case .user = peer { if case let .user(user) = peer {
mainTitle = environment.strings.Report_Title_User 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 { } else if case let .channel(channel) = peer, case .broadcast = channel.info {
mainTitle = environment.strings.Report_Title_Channel mainTitle = environment.strings.Report_Title_Channel
} else { } else {

View File

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

View File

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

View File

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

View File

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

View File

@ -29,6 +29,7 @@ import ChatPresentationInterfaceState
import AudioToolbox import AudioToolbox
import TextFormat import TextFormat
import InAppPurchaseManager import InAppPurchaseManager
import BlurredBackgroundComponent
final class GiftSetupScreenComponent: Component { final class GiftSetupScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -78,6 +79,9 @@ final class GiftSetupScreenComponent: Component {
private let introContent = ComponentView<Empty>() private let introContent = ComponentView<Empty>()
private let introSection = ComponentView<Empty>() private let introSection = ComponentView<Empty>()
private let hideSection = ComponentView<Empty>() private let hideSection = ComponentView<Empty>()
private let buttonBackground = ComponentView<Empty>()
private let buttonSeparator = SimpleLayer()
private let button = ComponentView<Empty>() private let button = ComponentView<Empty>()
private var ignoreScrolling: Bool = false private var ignoreScrolling: Bool = false
@ -143,6 +147,8 @@ final class GiftSetupScreenComponent: Component {
self.addSubview(self.scrollView) self.addSubview(self.scrollView)
self.scrollView.layer.addSublayer(self.topOverscrollLayer) self.scrollView.layer.addSublayer(self.topOverscrollLayer)
self.disablesInteractiveKeyboardGestureRecognizer = true
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
@ -190,6 +196,11 @@ final class GiftSetupScreenComponent: Component {
if let navigationTitleView = self.navigationTitle.view { if let navigationTitleView = self.navigationTitle.view {
transition.setAlpha(view: navigationTitleView, alpha: 1.0) transition.setAlpha(view: navigationTitleView, alpha: 1.0)
} }
let bottomContentOffset = max(0.0, self.scrollView.contentSize.height - self.scrollView.contentOffset.y - self.scrollView.frame.height)
let bottomPanelAlpha = min(16.0, bottomContentOffset) / 16.0
self.buttonBackground.view?.alpha = bottomPanelAlpha
self.buttonSeparator.opacity = Float(bottomPanelAlpha)
} }
func proceed() { func proceed() {
@ -352,6 +363,29 @@ final class GiftSetupScreenComponent: Component {
} }
navigationController.setViewControllers(controllers, animated: true) 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 return
} }
if let textInputView = self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View { 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 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 let environment = environment[EnvironmentType.self].value
@ -564,17 +606,17 @@ final class GiftSetupScreenComponent: Component {
contentHeight += 26.0 contentHeight += 26.0
if case let .starGift(starGift) = component.subject, let availability = starGift.availability { if case let .starGift(starGift) = component.subject, let availability = starGift.availability {
//TODO:localize let remains: Int32 = availability.remains
let remains: Int32 = Int32(CGFloat(availability.remains) * 0.66) let total: Int32 = availability.total
let position = CGFloat(remains) / CGFloat(availability.total) let position = CGFloat(remains) / CGFloat(total)
let remainsString = "\(remains)" //presentationStringsFormattedNumber(remains, environment.dateTimeFormat.groupingSeparator) let remainsString = presentationStringsFormattedNumber(remains, environment.dateTimeFormat.groupingSeparator)
let totalString = presentationStringsFormattedNumber(availability.total, environment.dateTimeFormat.groupingSeparator) let totalString = presentationStringsFormattedNumber(total, environment.dateTimeFormat.groupingSeparator)
let remainingCountSize = self.remainingCount.update( let remainingCountSize = self.remainingCount.update(
transition: transition, transition: transition,
component: AnyComponent(RemainingCountComponent( component: AnyComponent(RemainingCountComponent(
inactiveColor: environment.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3), inactiveColor: environment.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3),
activeColors: [UIColor(rgb: 0x5bc2ff), UIColor(rgb: 0x2d9eff)], activeColors: [UIColor(rgb: 0x5bc2ff), UIColor(rgb: 0x2d9eff)],
inactiveTitle: "Limited", inactiveTitle: environment.strings.Gift_Send_Limited,
inactiveValue: "", inactiveValue: "",
inactiveTitleColor: environment.theme.list.itemSecondaryTextColor, inactiveTitleColor: environment.theme.list.itemSecondaryTextColor,
activeTitle: "", activeTitle: "",
@ -583,7 +625,9 @@ final class GiftSetupScreenComponent: Component {
badgeText: "\(remainsString)", badgeText: "\(remainsString)",
badgePosition: position, badgePosition: position,
badgeGraphPosition: position, badgeGraphPosition: position,
invertProgress: true invertProgress: true,
leftString: environment.strings.Gift_Send_Remains(remains).replacingOccurrences(of: remainsString, with: "").trimmingCharacters(in: .whitespacesAndNewlines),
groupingSeparator: environment.dateTimeFormat.groupingSeparator
)), )),
environment: {}, environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
@ -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) self.starImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: environment.theme.list.itemCheckColors.foregroundColor)!, environment.theme)
} }
let buttonHeight: CGFloat = 50.0
let bottomPanelPadding: CGFloat = 12.0
let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding
let bottomPanelHeight = bottomPanelPadding + buttonHeight + bottomInset
let bottomPanelSize = self.buttonBackground.update(
transition: transition,
component: AnyComponent(BlurredBackgroundComponent(
color: environment.theme.rootController.tabBar.backgroundColor
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: bottomPanelHeight)
)
self.buttonSeparator.backgroundColor = environment.theme.rootController.tabBar.separatorColor.cgColor
if let view = self.buttonBackground.view {
if view.superview == nil {
self.addSubview(view)
self.layer.addSublayer(self.buttonSeparator)
}
view.frame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelSize.height), size: bottomPanelSize)
self.buttonSeparator.frame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelSize.height), size: CGSize(width: availableSize.width, height: UIScreenPixel))
}
var buttonIsEnabled = true
let buttonString: String let buttonString: String
switch component.subject { switch component.subject {
case let .premium(product): case let .premium(product):
@ -823,6 +892,9 @@ final class GiftSetupScreenComponent: Component {
case let .starGift(starGift): case let .starGift(starGift):
let amountString = presentationStringsFormattedNumber(Int32(starGift.price), presentationData.dateTimeFormat.groupingSeparator) let amountString = presentationStringsFormattedNumber(Int32(starGift.price), presentationData.dateTimeFormat.groupingSeparator)
buttonString = "\(environment.strings.Gift_Send_Send) # \(amountString)" buttonString = "\(environment.strings.Gift_Send_Send) # \(amountString)"
if let availability = starGift.availability, availability.remains == 0 {
buttonIsEnabled = false
}
} }
let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
@ -845,20 +917,20 @@ final class GiftSetupScreenComponent: Component {
id: AnyHashable(0), id: AnyHashable(0),
component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString))) component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))
), ),
isEnabled: true, isEnabled: buttonIsEnabled,
displaysProgress: self.inProgress, displaysProgress: self.inProgress,
action: { [weak self] in action: { [weak self] in
self?.proceed() self?.proceed()
} }
)), )),
environment: {}, environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50) containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: buttonHeight)
) )
if let buttonView = self.button.view { if let buttonView = self.button.view {
if buttonView.superview == nil { if buttonView.superview == nil {
self.addSubview(buttonView) self.addSubview(buttonView)
} }
buttonView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) / 2.0), y: availableSize.height - environment.safeInsets.bottom - buttonSize.height), size: buttonSize) buttonView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) / 2.0), y: availableSize.height - bottomPanelHeight + bottomPanelPadding), size: buttonSize)
} }
if self.textInputState.isEditing, let emojiSuggestion = self.textInputState.currentEmojiSuggestion, emojiSuggestion.disposable == nil { if self.textInputState.isEditing, let emojiSuggestion = self.textInputState.currentEmojiSuggestion, emojiSuggestion.disposable == nil {
@ -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 { if self.textInputState.isEditing {
self.recenterOnTag = self.textInputTag self.recenterOnTag = self.textInputTag
} }
@ -1252,9 +1324,6 @@ public final class GiftSetupScreen: ViewControllerComponentContainer {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
deinit {
}
@objc private func cancelPressed() { @objc private func cancelPressed() {
self.dismiss() self.dismiss()
} }

View File

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

View File

@ -181,10 +181,12 @@ private final class GiftViewSheetContent: CombinedComponent {
let text: String? let text: String?
let entities: [MessageTextEntity]? let entities: [MessageTextEntity]?
let limitTotal: Int32? let limitTotal: Int32?
var outgoing = false
var incoming = false var incoming = false
var savedToProfile = false var savedToProfile = false
var converted = false var converted = false
var giftId: Int64 = 0 var giftId: Int64 = 0
var date: Int32 = 0
if let arguments = component.subject.arguments { if let arguments = component.subject.arguments {
animationFile = arguments.gift.file animationFile = arguments.gift.file
stars = arguments.gift.price stars = arguments.gift.price
@ -192,10 +194,16 @@ private final class GiftViewSheetContent: CombinedComponent {
entities = arguments.entities entities = arguments.entities
limitTotal = arguments.gift.availability?.total limitTotal = arguments.gift.availability?.total
convertStars = arguments.convertStars convertStars = arguments.convertStars
if case .message = component.subject {
outgoing = !arguments.incoming
} else {
outgoing = false
}
incoming = arguments.incoming || arguments.peerId == component.context.account.peerId incoming = arguments.incoming || arguments.peerId == component.context.account.peerId
savedToProfile = arguments.savedToProfile savedToProfile = arguments.savedToProfile
converted = arguments.converted converted = arguments.converted
giftId = arguments.gift.id giftId = arguments.gift.id
date = arguments.date
} else { } else {
animationFile = nil animationFile = nil
stars = 0 stars = 0
@ -236,12 +244,12 @@ private final class GiftViewSheetContent: CombinedComponent {
} }
var formattedAmount = presentationStringsFormattedNumber(abs(Int32(stars)), dateTimeFormat.groupingSeparator) var formattedAmount = presentationStringsFormattedNumber(abs(Int32(stars)), dateTimeFormat.groupingSeparator)
if !incoming && stars > 0 { if outgoing {
formattedAmount = "- \(formattedAmount)" formattedAmount = "- \(formattedAmount)"
} }
let countFont: UIFont = Font.semibold(17.0) let countFont: UIFont = Font.semibold(17.0)
let amountText = formattedAmount 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( let title = title.update(
component: MultilineTextComponent( component: MultilineTextComponent(
@ -333,7 +341,7 @@ private final class GiftViewSheetContent: CombinedComponent {
id: "date", id: "date",
title: strings.Gift_View_Date, title: strings.Gift_View_Date,
component: AnyComponent( 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 { if let gift = state.starGiftsMap[giftId], let availability = gift.availability {
remains = availability.remains remains = availability.remains
} }
let remainsString = presentationStringsFormattedNumber(remains, environment.dateTimeFormat.groupingSeparator)
let totalString = presentationStringsFormattedNumber(limitTotal, environment.dateTimeFormat.groupingSeparator)
tableItems.append(.init( tableItems.append(.init(
id: "availability", id: "availability",
title: strings.Gift_View_Availability, title: strings.Gift_View_Availability,
component: AnyComponent( 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, animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer, animationRenderer: component.context.animationRenderer,
placeholderColor: theme.list.mediaPlaceholderColor, 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 message(EngineMessage)
case profileGift(EnginePeer.Id, ProfileGiftsContext.State.StarGift) 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 { switch self {
case let .message(message): 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 { 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): 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 return nil
} }
@ -905,7 +917,6 @@ public class GiftViewScreen: ViewControllerComponentContainer {
return return
} }
let introController = context.sharedContext.makeStarsIntroScreen(context: context) let introController = context.sharedContext.makeStarsIntroScreen(context: context)
introController.navigationPresentation = .modal
self.push(introController) self.push(introController)
} }
} }
@ -1067,17 +1078,26 @@ private final class TableComponent: CombinedComponent {
} else { } else {
insets = UIEdgeInsets(top: 0.0, left: horizontalPadding, bottom: 0.0, right: horizontalPadding) 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 var titleHeight: CGFloat = 0.0
if let titleChild = updatedTitleChildren[i] { if let titleChild = updatedTitleChildren[i] {
titleHeight = titleChild.size.height 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) let rowHeight = max(40.0, max(titleHeight, valueChild.size.height) + verticalPadding * 2.0)
rowHeights[i] = rowHeight rowHeights[i] = rowHeight
totalHeight += rowHeight totalHeight += rowHeight

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -25,9 +25,22 @@ public final class HLSQualitySet {
if let alternativeFile = alternativeRepresentation as? TelegramMediaFile { if let alternativeFile = alternativeRepresentation as? TelegramMediaFile {
for attribute in alternativeFile.attributes { for attribute in alternativeFile.attributes {
if case let .Video(_, size, _, _, _, videoCodec) = attribute { if case let .Video(_, size, _, _, _, videoCodec) = attribute {
let _ = size
if let videoCodec, NativeVideoContent.isVideoCodecSupported(videoCodec: videoCodec) { 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)
}
} }
} }
} }

View File

@ -11,6 +11,7 @@ import AccountContext
import PhotoResources import PhotoResources
import UIKitRuntimeUtils import UIKitRuntimeUtils
import RangeSet import RangeSet
import VideoToolbox
private extension CGRect { private extension CGRect {
var center: CGPoint { var center: CGPoint {
@ -25,6 +26,11 @@ public enum NativeVideoContentId: Hashable {
case profileVideo(Int64, String?) case profileVideo(Int64, String?)
} }
private let isAv1Supported: Bool = {
let value = VTIsHardwareDecodeSupported(kCMVideoCodecType_AV1)
return value
}()
public final class NativeVideoContent: UniversalVideoContent { public final class NativeVideoContent: UniversalVideoContent {
public let id: AnyHashable public let id: AnyHashable
public let nativeId: NativeVideoContentId public let nativeId: NativeVideoContentId
@ -58,7 +64,17 @@ public final class NativeVideoContent: UniversalVideoContent {
let hasSentFramesToDisplay: (() -> Void)? let hasSentFramesToDisplay: (() -> Void)?
public static func isVideoCodecSupported(videoCodec: String) -> Bool { 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 { public static func isHLSVideo(file: TelegramMediaFile) -> Bool {

View File

@ -1,5 +1,5 @@
{ {
"app": "11.2", "app": "11.2.1",
"xcode": "16.0", "xcode": "16.0",
"bazel": "7.3.1", "bazel": "7.3.1",
"macos": "15.0" "macos": "15.0"