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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -399,7 +399,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
case topic(EnginePeer, ChatListItemContent.ThreadInfo, Int, PresentationTheme, PresentationStrings, ChatListSearchSectionExpandType)
case recentlySearchedPeer(EnginePeer, EnginePeer?, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, PeerStoryStats?, Bool)
case localPeer(EnginePeer, EnginePeer?, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType, PeerStoryStats?, Bool)
case globalPeer(FoundPeer, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType, PeerStoryStats?, Bool)
case globalPeer(FoundPeer, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType, PeerStoryStats?, Bool, String?)
case message(EngineMessage, EngineRenderedPeer, EnginePeerReadCounters?, EngineMessageHistoryThread.Info?, ChatListPresentationData, Int32, Bool?, Bool, MessageOrderingKey, (id: String, size: Int64, isFirstInList: Bool)?, MessageSection, Bool, PeerStoryStats?, Bool)
case addContact(String, PresentationTheme, PresentationStrings)
@ -411,7 +411,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
return .localPeerId(peer.id)
case let .localPeer(peer, _, _, _, _, _, _, _, _, _, _):
return .localPeerId(peer.id)
case let .globalPeer(peer, _, _, _, _, _, _, _, _, _):
case let .globalPeer(peer, _, _, _, _, _, _, _, _, _, _):
return .globalPeerId(peer.peer.id)
case let .message(message, _, _, _, _, _, _, _, _, _, section, _, _, _):
return .messageId(message.id, section)
@ -440,8 +440,8 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
} else {
return false
}
case let .globalPeer(lhsPeer, lhsUnreadBadge, lhsIndex, lhsTheme, lhsStrings, lhsSortOrder, lhsDisplayOrder, lhsExpandType, lhsStoryStats, lhsRequiresPremiumForMessaging):
if case let .globalPeer(rhsPeer, rhsUnreadBadge, rhsIndex, rhsTheme, rhsStrings, rhsSortOrder, rhsDisplayOrder, rhsExpandType, rhsStoryStats, rhsRequiresPremiumForMessaging) = rhs, lhsPeer == rhsPeer && lhsIndex == rhsIndex && lhsTheme === rhsTheme && lhsStrings === rhsStrings && lhsSortOrder == rhsSortOrder && lhsDisplayOrder == rhsDisplayOrder && lhsUnreadBadge?.0 == rhsUnreadBadge?.0 && lhsUnreadBadge?.1 == rhsUnreadBadge?.1 && lhsExpandType == rhsExpandType && lhsStoryStats == rhsStoryStats && lhsRequiresPremiumForMessaging == rhsRequiresPremiumForMessaging {
case let .globalPeer(lhsPeer, lhsUnreadBadge, lhsIndex, lhsTheme, lhsStrings, lhsSortOrder, lhsDisplayOrder, lhsExpandType, lhsStoryStats, lhsRequiresPremiumForMessaging, lhsQuery):
if case let .globalPeer(rhsPeer, rhsUnreadBadge, rhsIndex, rhsTheme, rhsStrings, rhsSortOrder, rhsDisplayOrder, rhsExpandType, rhsStoryStats, rhsRequiresPremiumForMessaging, rhsQuery) = rhs, lhsPeer == rhsPeer && lhsIndex == rhsIndex && lhsTheme === rhsTheme && lhsStrings === rhsStrings && lhsSortOrder == rhsSortOrder && lhsDisplayOrder == rhsDisplayOrder && lhsUnreadBadge?.0 == rhsUnreadBadge?.0 && lhsUnreadBadge?.1 == rhsUnreadBadge?.1 && lhsExpandType == rhsExpandType && lhsStoryStats == rhsStoryStats && lhsRequiresPremiumForMessaging == rhsRequiresPremiumForMessaging, lhsQuery == rhsQuery {
return true
} else {
return false
@ -543,11 +543,11 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
case .globalPeer, .message, .addContact:
return true
}
case let .globalPeer(_, _, lhsIndex, _, _, _, _, _, _, _):
case let .globalPeer(_, _, lhsIndex, _, _, _, _, _, _, _, _):
switch rhs {
case .topic, .recentlySearchedPeer, .localPeer:
return false
case let .globalPeer(_, _, rhsIndex, _, _, _, _, _, _, _):
case let .globalPeer(_, _, rhsIndex, _, _, _, _, _, _, _, _):
return lhsIndex <= rhsIndex
case .message, .addContact:
return true
@ -798,7 +798,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
openStories(peer.id, sourceNode.avatarNode)
}
})
case let .globalPeer(peer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType, storyStats, requiresPremiumForMessaging):
case let .globalPeer(peer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType, storyStats, requiresPremiumForMessaging, query):
var enabled = true
if filter.contains(.onlyWriteable) {
enabled = canSendMessagesToPeer(peer.peer)
@ -822,7 +822,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
var suffixString = ""
if let subscribers = peer.subscribers, subscribers != 0 {
if peer.peer is TelegramUser {
suffixString = ", \(strings.Conversation_StatusSubscribers(subscribers))"
suffixString = ", \(strings.Conversation_StatusBotSubscribers(subscribers))"
} else if let channel = peer.peer as? TelegramChannel, case .broadcast = channel.info {
suffixString = ", \(strings.Conversation_StatusSubscribers(subscribers))"
} else {
@ -858,7 +858,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
isSavedMessages = true
}
return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch(isSavedMessages: isSavedMessages), peer: .peer(peer: EnginePeer(peer.peer), chatPeer: EnginePeer(peer.peer)), status: .addressName(suffixString), badge: badge, requiresPremiumForMessaging: requiresPremiumForMessaging, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in
return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch(isSavedMessages: isSavedMessages), peer: .peer(peer: EnginePeer(peer.peer), chatPeer: EnginePeer(peer.peer)), status: .addressName(suffixString), badge: badge, requiresPremiumForMessaging: requiresPremiumForMessaging, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, searchQuery: query, action: { _ in
interaction.peerSelected(EnginePeer(peer.peer), nil, nil, nil)
}, disabledAction: { _ in
interaction.disabledPeerSelected(EnginePeer(peer.peer), nil, requiresPremiumForMessaging ? .premiumRequired : .generic)
@ -2607,7 +2607,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
if !existingPeerIds.contains(peer.peer.id), filteredPeer(EnginePeer(peer.peer), EnginePeer(accountPeer)) {
existingPeerIds.insert(peer.peer.id)
entries.append(.globalPeer(peer, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, globalExpandType, nil, false))
entries.append(.globalPeer(peer, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, globalExpandType, nil, false, finalQuery))
index += 1
numberOfGlobalPeers += 1
}
@ -2807,7 +2807,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
}, openStorageManagement: {
}, openPasswordSetup: {
}, openPremiumIntro: {
}, openPremiumGift: { _ in
}, openPremiumGift: { _, _ in
}, openPremiumManagement: {
}, openActiveSessions: {
}, openBirthdaySetup: {
@ -2953,7 +2953,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
if case let .user(user) = peer, user.flags.contains(.requirePremium) {
requiresPremiumForMessagingPeerIds.append(peer.id)
}
case let .globalPeer(foundPeer, _, _, _, _, _, _, _, _, _):
case let .globalPeer(foundPeer, _, _, _, _, _, _, _, _, _, _):
storyStatsIds.append(foundPeer.peer.id)
if let user = foundPeer.peer as? TelegramUser, user.flags.contains(.requirePremium) {
requiresPremiumForMessagingPeerIds.append(foundPeer.peer.id)
@ -2994,8 +2994,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
mappedItems[i] = .recentlySearchedPeer(peer, associatedPeer, unreadBadge, index, theme, strings, sortOrder, displayOrder, stats[peer.id] ?? nil, requiresPremiumForMessaging[peer.id] ?? false)
case let .localPeer(peer, associatedPeer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, _, _):
mappedItems[i] = .localPeer(peer, associatedPeer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, stats[peer.id] ?? nil, requiresPremiumForMessaging[peer.id] ?? false)
case let .globalPeer(peer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, _, _):
mappedItems[i] = .globalPeer(peer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, stats[peer.peer.id] ?? nil, requiresPremiumForMessaging[peer.peer.id] ?? false)
case let .globalPeer(peer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, _, _, searchQuery):
mappedItems[i] = .globalPeer(peer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, stats[peer.peer.id] ?? nil, requiresPremiumForMessaging[peer.peer.id] ?? false, searchQuery)
case let .message(message, peer, combinedPeerReadState, threadInfo, presentationData, totalCount, selected, displayCustomHeader, key, resourceId, section, allPaused, _, _):
mappedItems[i] = .message(message, peer, combinedPeerReadState, threadInfo, presentationData, totalCount, selected, displayCustomHeader, key, resourceId, section, allPaused, stats[peer.peerId] ?? nil, requiresPremiumForMessaging[peer.peerId] ?? false)
default:
@ -4645,7 +4645,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode {
let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in
gesture?.cancel()
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: {
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: {
}, openBirthdaySetup: {
}, performActiveSessionAction: { _, _ in
}, openChatFolderUpdates: {}, hideChatFolderUpdates: {

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
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in
gesture?.cancel()
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, openStarsTopup: { _ in
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, openStarsTopup: { _ in
}, dismissNotice: { _ in
}, editPeer: { _ in
})

View File

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

View File

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

View File

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

View File

@ -175,6 +175,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
let options: [ItemListPeerItemRevealOption]
let additionalActions: [ContactsPeerItemAction]
let actionIcon: ContactsPeerItemActionIcon
let searchQuery: String?
let action: ((ContactsPeerItemPeer) -> Void)?
let disabledAction: ((ContactsPeerItemPeer) -> Void)?
let setPeerIdWithRevealedOptions: ((EnginePeer.Id?, EnginePeer.Id?) -> Void)?
@ -215,6 +216,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
actionIcon: ContactsPeerItemActionIcon = .none,
index: SortIndex?,
header: ListViewItemHeader?,
searchQuery: String? = nil,
action: ((ContactsPeerItemPeer) -> Void)?,
disabledAction: ((ContactsPeerItemPeer) -> Void)? = nil,
setPeerIdWithRevealedOptions: ((EnginePeer.Id?, EnginePeer.Id?) -> Void)? = nil,
@ -245,6 +247,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
self.options = options
self.additionalActions = additionalActions
self.actionIcon = actionIcon
self.searchQuery = searchQuery
self.action = action
self.disabledAction = disabledAction
self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions
@ -880,7 +883,16 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
statusAttributedString = NSAttributedString(string: string, font: statusFont, textColor: activity ? item.presentationData.theme.list.itemAccentColor : item.presentationData.theme.list.itemSecondaryTextColor)
}
case let .addressName(suffix):
if let addressName = peer.addressName {
var addressName = peer.addressName
if let currentAddressName = addressName, let searchQuery = item.searchQuery?.lowercased(), !peer.usernames.isEmpty && !currentAddressName.lowercased().contains(searchQuery) {
for username in peer.usernames {
if username.username.lowercased().contains(searchQuery) {
addressName = username.username
break
}
}
}
if let addressName {
let addressNameString = NSAttributedString(string: "@" + addressName, font: statusFont, textColor: item.presentationData.theme.list.itemAccentColor)
if !suffix.isEmpty {
let suffixString = NSAttributedString(string: suffix, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)

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?._hiddenMedia.set(.single(nil))
self.galleryNode.completeCustomDismiss = { [weak self] isPictureInPicture in
if isPictureInPicture {
if let chatController = self?.baseNavigationController?.topViewController as? ChatController {
chatController.updatePushedTransition(0.0, transition: .animated(duration: 0.45, curve: .customSpring(damping: 180.0, initialVelocity: 0.0)))
}
} else {
self?._hiddenMedia.set(.single(nil))
}
self?.presentingViewController?.dismiss(animated: false, completion: nil)
}

View File

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

View File

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

View File

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

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)))
Queue.mainQueue().after(0.3) {
self.completeCustomDismiss()
self.completeCustomDismiss(false)
}
}
f(.default)
@ -719,7 +719,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: true)))
Queue.mainQueue().after(0.3) {
self.completeCustomDismiss()
self.completeCustomDismiss(false)
}
}
f(.default)

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 {
private let context: AccountContext
private let presentationData: PresentationData
@ -875,12 +1114,17 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
private let isShowingContextMenuPromise = ValuePromise<Bool>(false, ignoreRepeated: true)
private let hasExpandedCaptionPromise = Promise<Bool>()
private var hideControlsDisposable: Disposable?
private var automaticPictureInPictureDisposable: Disposable?
var playbackCompleted: (() -> Void)?
private var customUnembedWhenPortrait: ((OverlayMediaItemNode) -> Bool)?
private var pictureInPictureContent: AnyObject?
private var nativePictureInPictureContent: AnyObject?
private var activePictureInPictureNavigationController: NavigationController?
private var activePictureInPictureController: ViewController?
init(context: AccountContext, presentationData: PresentationData, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction, Message) -> Void, present: @escaping (ViewController, Any?) -> Void) {
self.context = context
@ -1064,6 +1308,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
self.mediaPlaybackStateDisposable.dispose()
self.scrubbingFrameDisposable?.dispose()
self.hideControlsDisposable?.dispose()
self.automaticPictureInPictureDisposable?.dispose()
}
override func ready() -> Signal<Void, NoError> {
@ -1259,6 +1504,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
strongSelf.videoNode?.setBaseRate(playbackRate)
}
}
if strongSelf.nativePictureInPictureContent == nil {
strongSelf.setupNativePictureInPicture()
}
}
}
self.videoNode = videoNode
@ -1749,6 +1998,12 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
}
}
}
if #available(iOS 15.0, *) {
if let nativePictureInPictureContent = self.nativePictureInPictureContent as? NativePictureInPictureContentImpl {
nativePictureInPictureContent.updateIsCentral(isCentral: isCentral)
}
}
}
}
@ -2330,13 +2585,91 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
self.beginCustomDismiss(false)
self.statusNode.isHidden = true
self.animateOut(toOverlay: overlayNode, completion: { [weak self] in
self?.completeCustomDismiss()
self?.completeCustomDismiss(false)
})
}
}
}
private func setupNativePictureInPicture() {
guard let item = self.item, let videoNode = self.videoNode else {
return
}
if !(item.content is NativeVideoContent) {
return
}
var useNative = true
if let data = self.context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_native_pip"] {
useNative = false
}
if !useNative {
return
}
var hiddenMedia: (MessageId, Media)? = nil
switch item.contentInfo {
case let .message(message, _):
for media in message.media {
if let media = media as? TelegramMediaImage {
hiddenMedia = (message.id, media)
} else if let media = media as? TelegramMediaFile, media.isVideo {
hiddenMedia = (message.id, media)
}
}
default:
break
}
if #available(iOS 15.0, *) {
let content = NativePictureInPictureContentImpl(context: self.context, mediaManager: self.context.sharedContext.mediaManager, accountId: self.context.account.id, hiddenMedia: hiddenMedia, videoNode: videoNode, canSkip: true, willBegin: { [weak self] content in
guard let self, let controller = self.galleryController(), let navigationController = self.baseNavigationController() else {
return
}
self.activePictureInPictureNavigationController = navigationController
self.activePictureInPictureController = controller
controller.view.alpha = 0.0
controller.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in
self?.completeCustomDismiss(true)
})
}, didBegin: { [weak self] _ in
guard let self else {
return
}
let _ = self
}, expand: { [weak self] completion in
guard let self, let activePictureInPictureController = self.activePictureInPictureController, let activePictureInPictureNavigationController = self.activePictureInPictureNavigationController else {
completion()
return
}
self.activePictureInPictureController = nil
self.activePictureInPictureNavigationController = nil
activePictureInPictureController.presentationArguments = nil
activePictureInPictureNavigationController.currentWindow?.present(activePictureInPictureController, on: .root, blockInteraction: false, completion: {
})
activePictureInPictureController.view.alpha = 1.0
activePictureInPictureController.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { _ in
completion()
})
})
self.nativePictureInPictureContent = content
}
}
@objc func pictureInPictureButtonPressed() {
if #available(iOS 15.0, *) {
if let nativePictureInPictureContent = self.nativePictureInPictureContent as? NativePictureInPictureContentImpl {
nativePictureInPictureContent.beginPictureInPicture()
return
}
}
var isNativePictureInPictureSupported = false
switch self.item?.contentInfo {
case let .message(message, _):
@ -2391,7 +2724,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
guard let strongSelf = self else {
return
}
strongSelf.completeCustomDismiss()
strongSelf.completeCustomDismiss(false)
}, expand: { [weak baseNavigationController] completion in
guard let contentInfo = item.contentInfo else {
return
@ -2524,7 +2857,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
self.beginCustomDismiss(false)
self.statusNode.isHidden = true
self.animateOut(toOverlay: overlayNode, completion: { [weak self] in
self?.completeCustomDismiss()
self?.completeCustomDismiss(false)
})
}
}
@ -2847,7 +3180,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false)))
Queue.mainQueue().after(0.3) {
strongSelf.completeCustomDismiss()
strongSelf.completeCustomDismiss(false)
}
}
f(.default)
@ -2928,7 +3261,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: true)))
Queue.mainQueue().after(0.3) {
self.completeCustomDismiss()
self.completeCustomDismiss(false)
}
}
f(.default)

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?.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.galleryNode.completeCustomDismiss = { [weak self] in
self.galleryNode.completeCustomDismiss = { [weak self] _ in
self?._hiddenMedia.set(.single(nil))
self?.presentingViewController?.dismiss(animated: false, completion: nil)
}

View File

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

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
}, activateChatPreview: { _, _, _, gesture, _ in
gesture?.cancel()
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: {
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: {
}, openBirthdaySetup: {
}, performActiveSessionAction: { _, _ in
}, openChatFolderUpdates: {}, hideChatFolderUpdates: {

View File

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

View File

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

View File

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

View File

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

View File

@ -123,7 +123,7 @@ final class VideoChatScheduledInfoComponent: Component {
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "Starts in", font: Font.with(size: 23.0, design: .round, weight: .semibold), textColor: .white))
text: .plain(NSAttributedString(string: component.strings.VoiceChat_StartsIn, font: Font.with(size: 23.0, design: .round, weight: .semibold), textColor: .white))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: 200.0)

View File

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

View File

@ -7098,7 +7098,10 @@ final class VoiceChatContextReferenceContentSource: ContextReferenceContentSourc
}
public func shouldUseV2VideoChatImpl(context: AccountContext) -> Bool {
var useV2 = true
var useV2 = false
if let data = context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_enable_videochatui_v2"] {
useV2 = true
}
if context.sharedContext.immediateExperimentalUISettings.disableCallV2 {
useV2 = false
}

View File

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

View File

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

View File

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

View File

@ -51,7 +51,7 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo
}
}
}
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
var renderedMyPeers: [FoundPeer] = []
@ -61,7 +61,11 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo
if let group = peer as? TelegramGroup, group.migrationReference != nil {
continue
}
renderedMyPeers.append(FoundPeer(peer: peer, subscribers: subscribers[peerId]))
if let user = peer as? TelegramUser {
renderedMyPeers.append(FoundPeer(peer: peer, subscribers: user.subscriberCount))
} else {
renderedMyPeers.append(FoundPeer(peer: peer, subscribers: subscribers[peerId]))
}
}
}
@ -72,7 +76,11 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo
if let group = peer as? TelegramGroup, group.migrationReference != nil {
continue
}
renderedPeers.append(FoundPeer(peer: peer, subscribers: subscribers[peerId]))
if let user = peer as? TelegramUser {
renderedPeers.append(FoundPeer(peer: peer, subscribers: user.subscriberCount))
} else {
renderedPeers.append(FoundPeer(peer: peer, subscribers: subscribers[peerId]))
}
}
}

View File

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

View File

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

View File

@ -6203,17 +6203,13 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
return
}
let effectiveMediaVisibility = self.visibility
var isPlaying = true
if case let .visible(_, subRect) = self.visibility {
if subRect.minY > 32.0 {
isPlaying = false
}
} else {
isPlaying = false
}
if !item.controllerInteraction.canReadHistory {
isPlaying = false
}
if self.forceStopAnimations {
isPlaying = false
}
@ -6228,7 +6224,19 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
}
for contentNode in self.contentNodes {
contentNode.visibility = mapVisibility(effectiveVisibility, boundsSize: self.bounds.size, insets: self.insets, to: contentNode)
if contentNode is ChatMessageMediaBubbleContentNode || contentNode is ChatMessageGiftBubbleContentNode {
contentNode.visibility = mapVisibility(effectiveMediaVisibility, boundsSize: self.bounds.size, insets: self.insets, to: contentNode)
} else {
contentNode.visibility = mapVisibility(effectiveVisibility, boundsSize: self.bounds.size, insets: self.insets, to: contentNode)
}
}
if case let .visible(_, subRect) = self.visibility {
if subRect.minY > 32.0 {
isPlaying = false
}
} else {
isPlaying = false
}
if let threadInfoNode = self.threadInfoNode {

View File

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

View File

@ -468,8 +468,12 @@ private final class SheetContent: CombinedComponent {
switch component.subject {
case .peer:
if let peer = state.peer {
if case .user = peer {
mainTitle = environment.strings.Report_Title_User
if case let .user(user) = peer {
if let _ = user.botInfo {
mainTitle = environment.strings.Report_Title_Bot
} else {
mainTitle = environment.strings.Report_Title_User
}
} else if case let .channel(channel) = peer, case .broadcast = channel.info {
mainTitle = environment.strings.Report_Title_Channel
} else {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -181,10 +181,12 @@ private final class GiftViewSheetContent: CombinedComponent {
let text: String?
let entities: [MessageTextEntity]?
let limitTotal: Int32?
var outgoing = false
var incoming = false
var savedToProfile = false
var converted = false
var giftId: Int64 = 0
var date: Int32 = 0
if let arguments = component.subject.arguments {
animationFile = arguments.gift.file
stars = arguments.gift.price
@ -192,10 +194,16 @@ private final class GiftViewSheetContent: CombinedComponent {
entities = arguments.entities
limitTotal = arguments.gift.availability?.total
convertStars = arguments.convertStars
if case .message = component.subject {
outgoing = !arguments.incoming
} else {
outgoing = false
}
incoming = arguments.incoming || arguments.peerId == component.context.account.peerId
savedToProfile = arguments.savedToProfile
converted = arguments.converted
giftId = arguments.gift.id
date = arguments.date
} else {
animationFile = nil
stars = 0
@ -236,12 +244,12 @@ private final class GiftViewSheetContent: CombinedComponent {
}
var formattedAmount = presentationStringsFormattedNumber(abs(Int32(stars)), dateTimeFormat.groupingSeparator)
if !incoming && stars > 0 {
if outgoing {
formattedAmount = "- \(formattedAmount)"
}
let countFont: UIFont = Font.semibold(17.0)
let amountText = formattedAmount
let countColor = incoming ? theme.list.itemDisclosureActions.constructive.fillColor : theme.list.itemDestructiveColor
let countColor = outgoing ? theme.list.itemDestructiveColor : theme.list.itemDisclosureActions.constructive.fillColor
let title = title.update(
component: MultilineTextComponent(
@ -333,7 +341,7 @@ private final class GiftViewSheetContent: CombinedComponent {
id: "date",
title: strings.Gift_View_Date,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: Int32(Date().timeIntervalSince1970), strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor)))
MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor)))
)
))
@ -342,11 +350,13 @@ private final class GiftViewSheetContent: CombinedComponent {
if let gift = state.starGiftsMap[giftId], let availability = gift.availability {
remains = availability.remains
}
let remainsString = presentationStringsFormattedNumber(remains, environment.dateTimeFormat.groupingSeparator)
let totalString = presentationStringsFormattedNumber(limitTotal, environment.dateTimeFormat.groupingSeparator)
tableItems.append(.init(
id: "availability",
title: strings.Gift_View_Availability,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_View_Availability_Of("\(remains)", "\(limitTotal)").string, font: tableFont, textColor: tableTextColor)))
MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_View_Availability_Of("\(remainsString)", "\(totalString)").string, font: tableFont, textColor: tableTextColor)))
)
))
}
@ -363,7 +373,9 @@ private final class GiftViewSheetContent: CombinedComponent {
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
placeholderColor: theme.list.mediaPlaceholderColor,
text: .plain(attributedText)
text: .plain(attributedText),
maximumNumberOfLines: 0,
handleSpoilers: true
)
)
))
@ -719,14 +731,14 @@ public class GiftViewScreen: ViewControllerComponentContainer {
case message(EngineMessage)
case profileGift(EnginePeer.Id, ProfileGiftsContext.State.StarGift)
var arguments: (peerId: EnginePeer.Id, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, incoming: Bool, gift: StarGift, convertStars: Int64, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool)? {
var arguments: (peerId: EnginePeer.Id, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, incoming: Bool, gift: StarGift, date: Int32, convertStars: Int64, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool)? {
switch self {
case let .message(message):
if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted) = action.action {
return (message.id.peerId, message.author?.id, message.author?.compactDisplayTitle, message.id, message.flags.contains(.Incoming), gift, convertStars, text, entities, nameHidden, savedToProfile, converted)
return (message.id.peerId, message.author?.id, message.author?.compactDisplayTitle, message.id, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, converted)
}
case let .profileGift(peerId, gift):
return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, gift.messageId, false, gift.gift, gift.convertStars ?? 0, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, false)
return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, gift.messageId, false, gift.gift, gift.date, gift.convertStars ?? 0, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, false)
}
return nil
}
@ -905,7 +917,6 @@ public class GiftViewScreen: ViewControllerComponentContainer {
return
}
let introController = context.sharedContext.makeStarsIntroScreen(context: context)
introController.navigationPresentation = .modal
self.push(introController)
}
}
@ -1067,17 +1078,26 @@ private final class TableComponent: CombinedComponent {
} else {
insets = UIEdgeInsets(top: 0.0, left: horizontalPadding, bottom: 0.0, right: horizontalPadding)
}
let valueChild = valueChildren[item.id].update(
component: item.component,
availableSize: CGSize(width: rightColumnWidth - insets.left - insets.right, height: context.availableSize.height),
transition: context.transition
)
updatedValueChildren.append((valueChild, insets))
var titleHeight: CGFloat = 0.0
if let titleChild = updatedTitleChildren[i] {
titleHeight = titleChild.size.height
}
let availableValueWidth: CGFloat
if titleHeight > 0.0 {
availableValueWidth = rightColumnWidth
} else {
availableValueWidth = context.availableSize.width
}
let valueChild = valueChildren[item.id].update(
component: item.component,
availableSize: CGSize(width: availableValueWidth - insets.left - insets.right, height: context.availableSize.height),
transition: context.transition
)
updatedValueChildren.append((valueChild, insets))
let rowHeight = max(40.0, max(titleHeight, valueChild.size.height) + verticalPadding * 2.0)
rowHeights[i] = rowHeight
totalHeight += rowHeight

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
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in
gesture?.cancel()
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, openStarsTopup: { _ in
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, openStarsTopup: { _ in
}, dismissNotice: { _ in
}, editPeer: { _ in
})
@ -507,7 +507,7 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod
},
openPremiumIntro: {
},
openPremiumGift: { _ in
openPremiumGift: { _, _ in
},
openPremiumManagement: {
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -25,9 +25,22 @@ public final class HLSQualitySet {
if let alternativeFile = alternativeRepresentation as? TelegramMediaFile {
for attribute in alternativeFile.attributes {
if case let .Video(_, size, _, _, _, videoCodec) = attribute {
let _ = size
if let videoCodec, NativeVideoContent.isVideoCodecSupported(videoCodec: videoCodec) {
qualityFiles[Int(size.height)] = baseFile.withMedia(alternativeFile)
let key = Int(size.height)
if let currentFile = qualityFiles[key] {
var currentCodec: String?
for attribute in currentFile.media.attributes {
if case let .Video(_, _, _, _, _, videoCodec) = attribute {
currentCodec = videoCodec
}
}
if let currentCodec, currentCodec == "av1" {
} else {
qualityFiles[key] = baseFile.withMedia(alternativeFile)
}
} else {
qualityFiles[key] = baseFile.withMedia(alternativeFile)
}
}
}
}

View File

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

View File

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