mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 13:35:19 +00:00
Update API
This commit is contained in:
parent
588cdeee7a
commit
f7e6755a39
@ -1017,6 +1017,7 @@ public protocol SharedAccountContext: AnyObject {
|
||||
func makeStarsWithdrawalScreen(context: AccountContext, stats: StarsRevenueStats, completion: @escaping (Int64) -> Void) -> ViewController
|
||||
func makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController
|
||||
func makeStarsGiveawayBoostScreen(context: AccountContext, peerId: EnginePeer.Id, boost: ChannelBoostersContext.State.Boost) -> ViewController
|
||||
func makeGiftViewScreen(context: AccountContext, message: EngineMessage) -> ViewController
|
||||
|
||||
func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal<MiniAppListScreenInitialData, NoError>
|
||||
func makeMiniAppListScreen(context: AccountContext, initialData: MiniAppListScreenInitialData) -> ViewController
|
||||
|
@ -63,6 +63,7 @@ public final class ChatMessageItemAssociatedData: Equatable {
|
||||
public let isStandalone: Bool
|
||||
public let isInline: Bool
|
||||
public let showSensitiveContent: Bool
|
||||
public let starGifts: [Int64: TelegramMediaFile]
|
||||
|
||||
public init(
|
||||
automaticDownloadPeerType: MediaAutoDownloadPeerType,
|
||||
@ -96,7 +97,8 @@ public final class ChatMessageItemAssociatedData: Equatable {
|
||||
deviceContactsNumbers: Set<String> = Set(),
|
||||
isStandalone: Bool = false,
|
||||
isInline: Bool = false,
|
||||
showSensitiveContent: Bool = false
|
||||
showSensitiveContent: Bool = false,
|
||||
starGifts: [Int64: TelegramMediaFile] = [:]
|
||||
) {
|
||||
self.automaticDownloadPeerType = automaticDownloadPeerType
|
||||
self.automaticDownloadPeerId = automaticDownloadPeerId
|
||||
@ -130,6 +132,7 @@ public final class ChatMessageItemAssociatedData: Equatable {
|
||||
self.isStandalone = isStandalone
|
||||
self.isInline = isInline
|
||||
self.showSensitiveContent = showSensitiveContent
|
||||
self.starGifts = starGifts
|
||||
}
|
||||
|
||||
public static func == (lhs: ChatMessageItemAssociatedData, rhs: ChatMessageItemAssociatedData) -> Bool {
|
||||
@ -223,6 +226,9 @@ public final class ChatMessageItemAssociatedData: Equatable {
|
||||
if lhs.showSensitiveContent != rhs.showSensitiveContent {
|
||||
return false
|
||||
}
|
||||
if lhs.starGifts != rhs.starGifts {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -276,3 +276,7 @@ public struct PremiumConfiguration {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public protocol GiftOptionsScreenProtocol {
|
||||
|
||||
}
|
||||
|
@ -992,7 +992,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
|
||||
sendWhenOnlineAvailable = true
|
||||
}
|
||||
}
|
||||
if peer.id.namespace == Namespaces.Peer.CloudUser && peer.id.id._internalGetInt64Value() == 777000 {
|
||||
if peer.id.isTelegramNotifications {
|
||||
sendWhenOnlineAvailable = false
|
||||
}
|
||||
|
||||
|
@ -29,22 +29,27 @@ public final class BotCheckoutController: ViewController {
|
||||
|
||||
public static func fetch(context: AccountContext, source: BotPaymentInvoiceSource) -> Signal<InputData, FetchError> {
|
||||
let theme = context.sharedContext.currentPresentationData.with { $0 }.theme
|
||||
let themeParams: [String: Any] = [
|
||||
"bg_color": Int32(bitPattern: theme.list.plainBackgroundColor.rgb),
|
||||
"secondary_bg_color": Int32(bitPattern: theme.list.blocksBackgroundColor.rgb),
|
||||
"text_color": Int32(bitPattern: theme.list.itemPrimaryTextColor.rgb),
|
||||
"hint_color": Int32(bitPattern: theme.list.itemSecondaryTextColor.rgb),
|
||||
"link_color": Int32(bitPattern: theme.list.itemAccentColor.rgb),
|
||||
"button_color": Int32(bitPattern: theme.list.itemCheckColors.fillColor.rgb),
|
||||
"button_text_color": Int32(bitPattern: theme.list.itemCheckColors.foregroundColor.rgb),
|
||||
"header_bg_color": Int32(bitPattern: theme.rootController.navigationBar.opaqueBackgroundColor.rgb),
|
||||
"accent_text_color": Int32(bitPattern: theme.list.itemAccentColor.rgb),
|
||||
"section_bg_color": Int32(bitPattern: theme.list.itemBlocksBackgroundColor.rgb),
|
||||
"section_header_text_color": Int32(bitPattern: theme.list.freeTextColor.rgb),
|
||||
"subtitle_text_color": Int32(bitPattern: theme.list.itemSecondaryTextColor.rgb),
|
||||
"destructive_text_color": Int32(bitPattern: theme.list.itemDestructiveColor.rgb),
|
||||
"section_separator_color": Int32(bitPattern: theme.list.itemBlocksSeparatorColor.rgb)
|
||||
]
|
||||
let themeParams: [String: Any]?
|
||||
if case .starGift = source {
|
||||
themeParams = nil
|
||||
} else {
|
||||
themeParams = [
|
||||
"bg_color": Int32(bitPattern: theme.list.plainBackgroundColor.rgb),
|
||||
"secondary_bg_color": Int32(bitPattern: theme.list.blocksBackgroundColor.rgb),
|
||||
"text_color": Int32(bitPattern: theme.list.itemPrimaryTextColor.rgb),
|
||||
"hint_color": Int32(bitPattern: theme.list.itemSecondaryTextColor.rgb),
|
||||
"link_color": Int32(bitPattern: theme.list.itemAccentColor.rgb),
|
||||
"button_color": Int32(bitPattern: theme.list.itemCheckColors.fillColor.rgb),
|
||||
"button_text_color": Int32(bitPattern: theme.list.itemCheckColors.foregroundColor.rgb),
|
||||
"header_bg_color": Int32(bitPattern: theme.rootController.navigationBar.opaqueBackgroundColor.rgb),
|
||||
"accent_text_color": Int32(bitPattern: theme.list.itemAccentColor.rgb),
|
||||
"section_bg_color": Int32(bitPattern: theme.list.itemBlocksBackgroundColor.rgb),
|
||||
"section_header_text_color": Int32(bitPattern: theme.list.freeTextColor.rgb),
|
||||
"subtitle_text_color": Int32(bitPattern: theme.list.itemSecondaryTextColor.rgb),
|
||||
"destructive_text_color": Int32(bitPattern: theme.list.itemDestructiveColor.rgb),
|
||||
"section_separator_color": Int32(bitPattern: theme.list.itemBlocksSeparatorColor.rgb)
|
||||
]
|
||||
}
|
||||
|
||||
return context.engine.payments.fetchBotPaymentForm(source: source, themeParams: themeParams)
|
||||
|> mapError { _ -> FetchError in
|
||||
|
@ -49,6 +49,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/SaveProgressScreen",
|
||||
"//submodules/TelegramUI/Components/ListActionItemComponent",
|
||||
"//submodules/Utils/DeviceModel",
|
||||
"//submodules/LegacyMediaPickerUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -147,7 +147,7 @@ public final class BrowserBookmarksScreen: ViewController {
|
||||
}, openLargeEmojiInfo: { _, _, _ in
|
||||
}, openJoinLink: { _ in
|
||||
}, openWebView: { _, _, _, _ in
|
||||
}, activateAdAction: { _, _ in
|
||||
}, activateAdAction: { _, _, _, _ in
|
||||
}, openRequestedPeerSelection: { _, _, _, _ in
|
||||
}, saveMediaToFiles: { _ in
|
||||
}, openNoAdsDemo: {
|
||||
|
@ -21,6 +21,7 @@ import UrlEscaping
|
||||
import UrlHandling
|
||||
import SaveProgressScreen
|
||||
import DeviceModel
|
||||
import LegacyMediaPickerUI
|
||||
|
||||
private final class TonSchemeHandler: NSObject, WKURLSchemeHandler {
|
||||
private final class PendingTask {
|
||||
@ -170,7 +171,7 @@ private func computedUserAgent() -> String {
|
||||
return DeviceModel.current.isIpad ? "Version/\(osVersion) Safari/605.1.15" : "Version/\(osVersion) Mobile/\(firmwareVersion) Safari/604.1"
|
||||
}
|
||||
|
||||
final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKUIDelegate, UIScrollViewDelegate {
|
||||
final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKUIDelegate, UIScrollViewDelegate, WKDownloadDelegate {
|
||||
private let context: AccountContext
|
||||
private var presentationData: PresentationData
|
||||
|
||||
@ -733,15 +734,15 @@ 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)
|
||||
// }
|
||||
// })
|
||||
// } else {
|
||||
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)
|
||||
}
|
||||
})
|
||||
} 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?") {
|
||||
decisionHandler(.cancel, preferences)
|
||||
@ -758,24 +759,25 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
||||
} else {
|
||||
decisionHandler(.allow, preferences)
|
||||
}
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
// func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
|
||||
// if navigationResponse.canShowMIMEType {
|
||||
// decisionHandler(.allow)
|
||||
// } else if #available(iOS 14.5, *) {
|
||||
// self.presentDownloadConfirmation(fileName: navigationResponse.response.suggestedFilename ?? "file", proceed: { download in
|
||||
// if download {
|
||||
// decisionHandler(.download)
|
||||
// } else {
|
||||
// decisionHandler(.cancel)
|
||||
// }
|
||||
// })
|
||||
// } else {
|
||||
// decisionHandler(.cancel)
|
||||
// }
|
||||
// }
|
||||
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
|
||||
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 {
|
||||
decisionHandler(.cancel)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
decisionHandler(.cancel)
|
||||
}
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
|
||||
if let url = navigationAction.request.url?.absoluteString {
|
||||
@ -790,6 +792,101 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
||||
decisionHandler(.allow)
|
||||
}
|
||||
}
|
||||
|
||||
private var downloadArguments: (String, String)?
|
||||
private var downloadController: (AlertController, (Int64, Int64) -> Void)?
|
||||
private var downloadProgressObserver: Any?
|
||||
|
||||
@available(iOS 14.5, *)
|
||||
func webView(_ webView: WKWebView, navigationAction: WKNavigationAction, didBecome download: WKDownload) {
|
||||
download.delegate = self
|
||||
}
|
||||
|
||||
@available(iOS 14.5, *)
|
||||
func webView(_ webView: WKWebView, navigationResponse: WKNavigationResponse, didBecome download: WKDownload) {
|
||||
download.delegate = self
|
||||
}
|
||||
|
||||
@available(iOS 14.5, *)
|
||||
func download(_ download: WKDownload, decideDestinationUsing response: URLResponse, suggestedFilename: String, completionHandler: @escaping (URL?) -> Void) {
|
||||
let path = NSTemporaryDirectory() + NSUUID().uuidString
|
||||
self.downloadArguments = (path, suggestedFilename)
|
||||
completionHandler(URL(fileURLWithPath: path))
|
||||
|
||||
let downloadController = progressAlertController(sharedContext: self.context.sharedContext, title: "", cancel: { [weak download] in
|
||||
download?.cancel()
|
||||
})
|
||||
self.downloadController = downloadController
|
||||
self.present(downloadController.0, nil)
|
||||
downloadController.1(download.progress.completedUnitCount, download.progress.totalUnitCount)
|
||||
|
||||
self.downloadProgressObserver = download.progress.observe(\.fractionCompleted) { [weak self] progress, _ in
|
||||
if let (_, update) = self?.downloadController {
|
||||
update(progress.completedUnitCount, progress.totalUnitCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.5, *)
|
||||
func downloadDidFinish(_ download: WKDownload) {
|
||||
if let (controller, _ ) = self.downloadController {
|
||||
controller.dismissAnimated()
|
||||
self.downloadController = nil
|
||||
}
|
||||
|
||||
if let (path, fileName) = self.downloadArguments {
|
||||
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)
|
||||
|
||||
self.downloadArguments = nil
|
||||
self.downloadProgressObserver = nil
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.5, *)
|
||||
func download(_ download: WKDownload, didFailWithError error: Error, resumeData: Data?) {
|
||||
self.downloadArguments = nil
|
||||
self.downloadProgressObserver = nil
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
|
||||
if let url = webView.url, !url.absoluteString.contains("beatsnvibes") {
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
authController.dismissed = { byOutsideTap in
|
||||
if byOutsideTap {
|
||||
if !completed {
|
||||
completed = true
|
||||
completionHandler(.cancelAuthenticationChallenge, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
self.present(authController, nil)
|
||||
}
|
||||
|
||||
private let isLoaded = ValuePromise<Bool>(false)
|
||||
private var instantPageDisposable = MetaDisposable()
|
||||
@ -880,7 +977,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
|
||||
if [-1003, -1100, 102].contains((error as NSError).code) {
|
||||
if [-1003, -1100].contains((error as NSError).code) {
|
||||
if let url = (error as NSError).userInfo["NSErrorFailingURLKey"] as? URL, url.absoluteString.hasPrefix("itms-appss:") {
|
||||
} else {
|
||||
self.currentError = error
|
||||
|
@ -17,6 +17,7 @@ swift_library(
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/TelegramStringFormatting",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -7,6 +7,7 @@ import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import TelegramStringFormatting
|
||||
|
||||
private final class PromptInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate {
|
||||
private var theme: PresentationTheme
|
||||
@ -39,7 +40,7 @@ private final class PromptInputFieldNode: ASDisplayNode, ASEditableTextNodeDeleg
|
||||
}
|
||||
}
|
||||
|
||||
init(theme: PresentationTheme, placeholder: String, characterLimit: Int) {
|
||||
init(theme: PresentationTheme, placeholder: String, characterLimit: Int, isPassword: Bool = false) {
|
||||
self.theme = theme
|
||||
self.characterLimit = characterLimit
|
||||
|
||||
@ -60,6 +61,7 @@ private final class PromptInputFieldNode: ASDisplayNode, ASEditableTextNodeDeleg
|
||||
self.textInputNode.returnKeyType = .done
|
||||
self.textInputNode.autocorrectionType = .default
|
||||
self.textInputNode.tintColor = theme.actionSheet.controlAccentColor
|
||||
self.textInputNode.isSecureTextEntry = isPassword
|
||||
|
||||
self.placeholderNode = ASTextNode()
|
||||
self.placeholderNode.isUserInteractionEnabled = false
|
||||
@ -448,3 +450,538 @@ public func promptController(sharedContext: SharedAccountContext, updatedPresent
|
||||
}
|
||||
return controller
|
||||
}
|
||||
|
||||
private final class AuthAlertContentNode: AlertContentNode {
|
||||
private let strings: PresentationStrings
|
||||
|
||||
private let title: String
|
||||
private let text: String
|
||||
|
||||
private let titleNode: ASTextNode
|
||||
private let textNode: ASTextNode
|
||||
let inputFieldNode: PromptInputFieldNode
|
||||
let passwordFieldNode: PromptInputFieldNode
|
||||
|
||||
private let actionNodesSeparator: ASDisplayNode
|
||||
private let actionNodes: [TextAlertContentActionNode]
|
||||
private let actionVerticalSeparators: [ASDisplayNode]
|
||||
|
||||
private let disposable = MetaDisposable()
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
var complete: (() -> Void)? {
|
||||
didSet {
|
||||
self.inputFieldNode.complete = self.complete
|
||||
}
|
||||
}
|
||||
|
||||
override var dismissOnOutsideTap: Bool {
|
||||
return self.isUserInteractionEnabled
|
||||
}
|
||||
|
||||
init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], title: String, text: String) {
|
||||
self.strings = strings
|
||||
self.title = title
|
||||
self.text = text
|
||||
|
||||
self.titleNode = ASTextNode()
|
||||
self.titleNode.maximumNumberOfLines = 2
|
||||
|
||||
self.textNode = ASTextNode()
|
||||
self.textNode.maximumNumberOfLines = 2
|
||||
|
||||
self.inputFieldNode = PromptInputFieldNode(theme: ptheme, placeholder: "User Name", characterLimit: 1024)
|
||||
self.passwordFieldNode = PromptInputFieldNode(theme: ptheme, placeholder: "Password", characterLimit: 1024, isPassword: true)
|
||||
|
||||
self.actionNodesSeparator = ASDisplayNode()
|
||||
self.actionNodesSeparator.isLayerBacked = true
|
||||
|
||||
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
|
||||
return TextAlertContentActionNode(theme: theme, action: action)
|
||||
}
|
||||
|
||||
var actionVerticalSeparators: [ASDisplayNode] = []
|
||||
if actions.count > 1 {
|
||||
for _ in 0 ..< actions.count - 1 {
|
||||
let separatorNode = ASDisplayNode()
|
||||
separatorNode.isLayerBacked = true
|
||||
actionVerticalSeparators.append(separatorNode)
|
||||
}
|
||||
}
|
||||
self.actionVerticalSeparators = actionVerticalSeparators
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.textNode)
|
||||
|
||||
self.addSubnode(self.inputFieldNode)
|
||||
self.addSubnode(self.passwordFieldNode)
|
||||
|
||||
self.addSubnode(self.actionNodesSeparator)
|
||||
|
||||
for actionNode in self.actionNodes {
|
||||
self.addSubnode(actionNode)
|
||||
}
|
||||
self.actionNodes.last?.actionEnabled = true
|
||||
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
self.addSubnode(separatorNode)
|
||||
}
|
||||
|
||||
self.inputFieldNode.updateHeight = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
if let _ = strongSelf.validLayout {
|
||||
strongSelf.requestLayout?(.animated(duration: 0.15, curve: .spring))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.updateTheme(theme)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable.dispose()
|
||||
}
|
||||
|
||||
var login: String {
|
||||
return self.inputFieldNode.text
|
||||
}
|
||||
|
||||
var password: String {
|
||||
return self.passwordFieldNode.text
|
||||
}
|
||||
|
||||
override func updateTheme(_ theme: AlertControllerTheme) {
|
||||
self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.semibold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
|
||||
self.textNode.attributedText = NSAttributedString(string: self.text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center)
|
||||
|
||||
self.actionNodesSeparator.backgroundColor = theme.separatorColor
|
||||
for actionNode in self.actionNodes {
|
||||
actionNode.updateTheme(theme)
|
||||
}
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
separatorNode.backgroundColor = theme.separatorColor
|
||||
}
|
||||
|
||||
if let size = self.validLayout {
|
||||
_ = self.updateLayout(size: size, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
var size = size
|
||||
size.width = min(size.width, 270.0)
|
||||
let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)
|
||||
|
||||
let hadValidLayout = self.validLayout != nil
|
||||
|
||||
self.validLayout = size
|
||||
|
||||
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
|
||||
let spacing: CGFloat = 5.0
|
||||
|
||||
let titleSize = self.titleNode.measure(measureSize)
|
||||
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize))
|
||||
origin.y += titleSize.height + 4.0
|
||||
|
||||
let textSize = self.textNode.measure(measureSize)
|
||||
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
|
||||
origin.y += textSize.height + 6.0 + spacing
|
||||
|
||||
let actionButtonHeight: CGFloat = 44.0
|
||||
var minActionsWidth: CGFloat = 0.0
|
||||
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
|
||||
let actionTitleInsets: CGFloat = 8.0
|
||||
|
||||
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
|
||||
for actionNode in self.actionNodes {
|
||||
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
|
||||
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
|
||||
effectiveActionLayout = .vertical
|
||||
}
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
minActionsWidth += actionTitleSize.width + actionTitleInsets
|
||||
case .vertical:
|
||||
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
|
||||
}
|
||||
}
|
||||
|
||||
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 9.0, right: 18.0)
|
||||
|
||||
var contentWidth = max(titleSize.width, minActionsWidth)
|
||||
contentWidth = max(contentWidth, 234.0)
|
||||
|
||||
var actionsHeight: CGFloat = 0.0
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionsHeight = actionButtonHeight
|
||||
case .vertical:
|
||||
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
|
||||
}
|
||||
|
||||
let resultWidth = contentWidth + insets.left + insets.right
|
||||
|
||||
let inputFieldWidth = resultWidth
|
||||
let inputFieldHeight = self.inputFieldNode.updateLayout(width: inputFieldWidth, transition: transition)
|
||||
let inputHeight = inputFieldHeight
|
||||
transition.updateFrame(node: self.inputFieldNode, frame: CGRect(x: 0.0, y: origin.y, width: resultWidth, height: inputFieldHeight))
|
||||
transition.updateAlpha(node: self.inputFieldNode, alpha: inputHeight > 0.0 ? 1.0 : 0.0)
|
||||
|
||||
let fieldSpacing: CGFloat = -11.0
|
||||
origin.y += inputFieldHeight + fieldSpacing
|
||||
|
||||
let passwordFieldHeight = self.passwordFieldNode.updateLayout(width: inputFieldWidth, transition: transition)
|
||||
transition.updateFrame(node: self.passwordFieldNode, frame: CGRect(x: 0.0, y: origin.y, width: resultWidth, height: passwordFieldHeight))
|
||||
|
||||
|
||||
let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + spacing + inputHeight + fieldSpacing + passwordFieldHeight + actionsHeight + insets.top + insets.bottom)
|
||||
|
||||
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
|
||||
var actionOffset: CGFloat = 0.0
|
||||
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
|
||||
var separatorIndex = -1
|
||||
var nodeIndex = 0
|
||||
for actionNode in self.actionNodes {
|
||||
if separatorIndex >= 0 {
|
||||
let separatorNode = self.actionVerticalSeparators[separatorIndex]
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
|
||||
case .vertical:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
}
|
||||
}
|
||||
separatorIndex += 1
|
||||
|
||||
let currentActionWidth: CGFloat
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
if nodeIndex == self.actionNodes.count - 1 {
|
||||
currentActionWidth = resultSize.width - actionOffset
|
||||
} else {
|
||||
currentActionWidth = actionWidth
|
||||
}
|
||||
case .vertical:
|
||||
currentActionWidth = resultSize.width
|
||||
}
|
||||
|
||||
let actionNodeFrame: CGRect
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += currentActionWidth
|
||||
case .vertical:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += actionButtonHeight
|
||||
}
|
||||
|
||||
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
|
||||
|
||||
nodeIndex += 1
|
||||
}
|
||||
|
||||
if !hadValidLayout {
|
||||
self.inputFieldNode.activateInput()
|
||||
}
|
||||
|
||||
return resultSize
|
||||
}
|
||||
|
||||
func animateError() {
|
||||
self.inputFieldNode.layer.addShakeAnimation()
|
||||
self.hapticFeedback.error()
|
||||
}
|
||||
}
|
||||
|
||||
public func authController(sharedContext: SharedAccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, title: String, text: String, apply: @escaping ((String, String)?) -> Void) -> AlertController {
|
||||
let presentationData = updatedPresentationData?.initial ?? sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
var dismissImpl: ((Bool) -> Void)?
|
||||
var applyImpl: (() -> Void)?
|
||||
|
||||
let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
||||
dismissImpl?(true)
|
||||
apply(nil)
|
||||
}), TextAlertAction(type: .defaultAction, title: "Sign In", action: {
|
||||
dismissImpl?(true)
|
||||
applyImpl?()
|
||||
})]
|
||||
|
||||
let contentNode = AuthAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, title: title, text: text)
|
||||
contentNode.complete = {
|
||||
applyImpl?()
|
||||
}
|
||||
applyImpl = { [weak contentNode] in
|
||||
guard let contentNode = contentNode else {
|
||||
return
|
||||
}
|
||||
apply((contentNode.login, contentNode.password))
|
||||
}
|
||||
|
||||
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
|
||||
let presentationDataDisposable = (updatedPresentationData?.signal ?? sharedContext.presentationData).start(next: { [weak controller, weak contentNode] presentationData in
|
||||
controller?.theme = AlertControllerTheme(presentationData: presentationData)
|
||||
contentNode?.inputFieldNode.updateTheme(presentationData.theme)
|
||||
})
|
||||
controller.dismissed = { _ in
|
||||
presentationDataDisposable.dispose()
|
||||
}
|
||||
dismissImpl = { [weak controller] animated in
|
||||
contentNode.inputFieldNode.deactivateInput()
|
||||
if animated {
|
||||
controller?.dismissAnimated()
|
||||
} else {
|
||||
controller?.dismiss()
|
||||
}
|
||||
}
|
||||
return controller
|
||||
}
|
||||
|
||||
|
||||
private final class ProgressAlertContentNode: AlertContentNode {
|
||||
private let theme: AlertControllerTheme
|
||||
private let strings: PresentationStrings
|
||||
|
||||
private let title: String
|
||||
var text: String {
|
||||
didSet {
|
||||
self.updateTheme(self.theme)
|
||||
}
|
||||
}
|
||||
|
||||
private let titleNode: ASTextNode
|
||||
private let textNode: ASTextNode
|
||||
|
||||
private let actionNodesSeparator: ASDisplayNode
|
||||
private let actionNodes: [TextAlertContentActionNode]
|
||||
private let actionVerticalSeparators: [ASDisplayNode]
|
||||
|
||||
private let disposable = MetaDisposable()
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
override var dismissOnOutsideTap: Bool {
|
||||
return self.isUserInteractionEnabled
|
||||
}
|
||||
|
||||
init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], title: String, text: String) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.title = title
|
||||
self.text = text
|
||||
|
||||
self.titleNode = ASTextNode()
|
||||
self.titleNode.maximumNumberOfLines = 2
|
||||
|
||||
self.textNode = ASTextNode()
|
||||
self.textNode.maximumNumberOfLines = 2
|
||||
|
||||
self.actionNodesSeparator = ASDisplayNode()
|
||||
self.actionNodesSeparator.isLayerBacked = true
|
||||
|
||||
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
|
||||
return TextAlertContentActionNode(theme: theme, action: action)
|
||||
}
|
||||
|
||||
var actionVerticalSeparators: [ASDisplayNode] = []
|
||||
if actions.count > 1 {
|
||||
for _ in 0 ..< actions.count - 1 {
|
||||
let separatorNode = ASDisplayNode()
|
||||
separatorNode.isLayerBacked = true
|
||||
actionVerticalSeparators.append(separatorNode)
|
||||
}
|
||||
}
|
||||
self.actionVerticalSeparators = actionVerticalSeparators
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.textNode)
|
||||
|
||||
self.addSubnode(self.actionNodesSeparator)
|
||||
|
||||
for actionNode in self.actionNodes {
|
||||
self.addSubnode(actionNode)
|
||||
}
|
||||
self.actionNodes.last?.actionEnabled = true
|
||||
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
self.addSubnode(separatorNode)
|
||||
}
|
||||
|
||||
|
||||
self.updateTheme(theme)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable.dispose()
|
||||
}
|
||||
|
||||
override func updateTheme(_ theme: AlertControllerTheme) {
|
||||
self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.semibold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
|
||||
self.textNode.attributedText = NSAttributedString(string: self.text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center)
|
||||
|
||||
self.actionNodesSeparator.backgroundColor = theme.separatorColor
|
||||
for actionNode in self.actionNodes {
|
||||
actionNode.updateTheme(theme)
|
||||
}
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
separatorNode.backgroundColor = theme.separatorColor
|
||||
}
|
||||
|
||||
if let size = self.validLayout {
|
||||
_ = self.updateLayout(size: size, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
var size = size
|
||||
size.width = min(size.width, 270.0)
|
||||
let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)
|
||||
|
||||
self.validLayout = size
|
||||
|
||||
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
|
||||
let spacing: CGFloat = 5.0
|
||||
|
||||
let titleSize = self.titleNode.measure(measureSize)
|
||||
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize))
|
||||
origin.y += titleSize.height + 4.0
|
||||
|
||||
let textSize = self.textNode.measure(measureSize)
|
||||
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
|
||||
origin.y += textSize.height + 6.0 + spacing
|
||||
|
||||
let actionButtonHeight: CGFloat = 44.0
|
||||
var minActionsWidth: CGFloat = 0.0
|
||||
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
|
||||
let actionTitleInsets: CGFloat = 8.0
|
||||
|
||||
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
|
||||
for actionNode in self.actionNodes {
|
||||
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
|
||||
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
|
||||
effectiveActionLayout = .vertical
|
||||
}
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
minActionsWidth += actionTitleSize.width + actionTitleInsets
|
||||
case .vertical:
|
||||
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
|
||||
}
|
||||
}
|
||||
|
||||
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 9.0, right: 18.0)
|
||||
|
||||
var contentWidth = max(titleSize.width, minActionsWidth)
|
||||
contentWidth = max(contentWidth, 234.0)
|
||||
|
||||
var actionsHeight: CGFloat = 0.0
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionsHeight = actionButtonHeight
|
||||
case .vertical:
|
||||
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
|
||||
}
|
||||
|
||||
let resultWidth = contentWidth + insets.left + insets.right
|
||||
|
||||
let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + spacing + actionsHeight + insets.top + insets.bottom)
|
||||
|
||||
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
|
||||
var actionOffset: CGFloat = 0.0
|
||||
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
|
||||
var separatorIndex = -1
|
||||
var nodeIndex = 0
|
||||
for actionNode in self.actionNodes {
|
||||
if separatorIndex >= 0 {
|
||||
let separatorNode = self.actionVerticalSeparators[separatorIndex]
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
|
||||
case .vertical:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
}
|
||||
}
|
||||
separatorIndex += 1
|
||||
|
||||
let currentActionWidth: CGFloat
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
if nodeIndex == self.actionNodes.count - 1 {
|
||||
currentActionWidth = resultSize.width - actionOffset
|
||||
} else {
|
||||
currentActionWidth = actionWidth
|
||||
}
|
||||
case .vertical:
|
||||
currentActionWidth = resultSize.width
|
||||
}
|
||||
|
||||
let actionNodeFrame: CGRect
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += currentActionWidth
|
||||
case .vertical:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += actionButtonHeight
|
||||
}
|
||||
|
||||
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
|
||||
|
||||
nodeIndex += 1
|
||||
}
|
||||
|
||||
return resultSize
|
||||
}
|
||||
}
|
||||
|
||||
public func progressAlertController(sharedContext: SharedAccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, title: String, cancel: @escaping () -> Void) -> (AlertController, (Int64, Int64) -> Void) {
|
||||
let presentationData = updatedPresentationData?.initial ?? sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
var dismissImpl: ((Bool) -> Void)?
|
||||
var applyImpl: (() -> Void)?
|
||||
|
||||
let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: "Cancel", action: {
|
||||
dismissImpl?(true)
|
||||
applyImpl?()
|
||||
})]
|
||||
|
||||
let contentNode = ProgressAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, title: "Downloading...", text: " ")
|
||||
|
||||
let updateProgress: (Int64, Int64) -> Void = { [weak contentNode] current, total in
|
||||
if let contentNode {
|
||||
contentNode.text = "\(dataSizeString(Int(current), formatting: DataSizeStringFormatting(presentationData: presentationData))) of \(dataSizeString(Int(total), formatting: DataSizeStringFormatting(presentationData: presentationData)))"
|
||||
}
|
||||
}
|
||||
applyImpl = {
|
||||
dismissImpl?(true)
|
||||
cancel()
|
||||
}
|
||||
|
||||
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
|
||||
let presentationDataDisposable = (updatedPresentationData?.signal ?? sharedContext.presentationData).start(next: { [weak controller] presentationData in
|
||||
controller?.theme = AlertControllerTheme(presentationData: presentationData)
|
||||
})
|
||||
controller.dismissed = { _ in
|
||||
presentationDataDisposable.dispose()
|
||||
}
|
||||
dismissImpl = { [weak controller] animated in
|
||||
if animated {
|
||||
controller?.dismissAnimated()
|
||||
} else {
|
||||
controller?.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
return (controller, updateProgress)
|
||||
}
|
||||
|
@ -238,6 +238,12 @@ public func combineLatest<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13
|
||||
}, initialValues: [:], queue: queue)
|
||||
}
|
||||
|
||||
public func combineLatest<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, T23, T24, T25, T26, E>(queue: Queue? = nil, _ s1: Signal<T1, E>, _ s2: Signal<T2, E>, _ s3: Signal<T3, E>, _ s4: Signal<T4, E>, _ s5: Signal<T5, E>, _ s6: Signal<T6, E>, _ s7: Signal<T7, E>, _ s8: Signal<T8, E>, _ s9: Signal<T9, E>, _ s10: Signal<T10, E>, _ s11: Signal<T11, E>, _ s12: Signal<T12, E>, _ s13: Signal<T13, E>, _ s14: Signal<T14, E>, _ s15: Signal<T15, E>, _ s16: Signal<T16, E>, _ s17: Signal<T17, E>, _ s18: Signal<T18, E>, _ s19: Signal<T19, E>, _ s20: Signal<T20, E>, _ s21: Signal<T21, E>, _ s22: Signal<T22, E>, _ s23: Signal<T23, E>, _ s24: Signal<T24, E>, _ s25: Signal<T25, E>, _ s26: Signal<T26, E>) -> Signal<(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, T23, T24, T25, T26), E> {
|
||||
return combineLatestAny([signalOfAny(s1), signalOfAny(s2), signalOfAny(s3), signalOfAny(s4), signalOfAny(s5), signalOfAny(s6), signalOfAny(s7), signalOfAny(s8), signalOfAny(s9), signalOfAny(s10), signalOfAny(s11), signalOfAny(s12), signalOfAny(s13), signalOfAny(s14), signalOfAny(s15), signalOfAny(s16), signalOfAny(s17), signalOfAny(s18), signalOfAny(s19), signalOfAny(s20), signalOfAny(s21), signalOfAny(s22), signalOfAny(s23), signalOfAny(s24), signalOfAny(s25), signalOfAny(s26)], combine: { values in
|
||||
return (values[0] as! T1, values[1] as! T2, values[2] as! T3, values[3] as! T4, values[4] as! T5, values[5] as! T6, values[6] as! T7, values[7] as! T8, values[8] as! T9, values[9] as! T10, values[10] as! T11, values[11] as! T12, values[12] as! T13, values[13] as! T14, values[14] as! T15, values[15] as! T16, values[16] as! T17, values[17] as! T18, values[18] as! T19, values[19] as! T20, values[20] as! T21, values[21] as! T22, values[22] as! T23, values[23] as! T24, values[24] as! T25, values[25] as! T26)
|
||||
}, initialValues: [:], queue: queue)
|
||||
}
|
||||
|
||||
public func combineLatest<T, E>(queue: Queue? = nil, _ signals: [Signal<T, E>]) -> Signal<[T], E> {
|
||||
if signals.count == 0 {
|
||||
return single([T](), E.self)
|
||||
|
@ -1103,7 +1103,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[-1831650802] = { return Api.UrlAuthResult.parse_urlAuthResultRequest($0) }
|
||||
dict[-2093920310] = { return Api.User.parse_user($0) }
|
||||
dict[-742634630] = { return Api.User.parse_userEmpty($0) }
|
||||
dict[-862357728] = { return Api.UserFull.parse_userFull($0) }
|
||||
dict[525919081] = { return Api.UserFull.parse_userFull($0) }
|
||||
dict[-2100168954] = { return Api.UserProfilePhoto.parse_userProfilePhoto($0) }
|
||||
dict[1326562017] = { return Api.UserProfilePhoto.parse_userProfilePhotoEmpty($0) }
|
||||
dict[-291202450] = { return Api.UserStarGift.parse_userStarGift($0) }
|
||||
|
@ -606,13 +606,13 @@ public extension Api {
|
||||
}
|
||||
public extension Api {
|
||||
enum UserFull: TypeConstructorDescription {
|
||||
case userFull(flags: Int32, flags2: Int32, id: Int64, about: String?, settings: Api.PeerSettings, personalPhoto: Api.Photo?, profilePhoto: Api.Photo?, fallbackPhoto: Api.Photo?, notifySettings: Api.PeerNotifySettings, botInfo: Api.BotInfo?, pinnedMsgId: Int32?, commonChatsCount: Int32, folderId: Int32?, ttlPeriod: Int32?, themeEmoticon: String?, privateForwardName: String?, botGroupAdminRights: Api.ChatAdminRights?, botBroadcastAdminRights: Api.ChatAdminRights?, premiumGifts: [Api.PremiumGiftOption]?, wallpaper: Api.WallPaper?, stories: Api.PeerStories?, businessWorkHours: Api.BusinessWorkHours?, businessLocation: Api.BusinessLocation?, businessGreetingMessage: Api.BusinessGreetingMessage?, businessAwayMessage: Api.BusinessAwayMessage?, businessIntro: Api.BusinessIntro?, birthday: Api.Birthday?, personalChannelId: Int64?, personalChannelMessage: Int32?)
|
||||
case userFull(flags: Int32, flags2: Int32, id: Int64, about: String?, settings: Api.PeerSettings, personalPhoto: Api.Photo?, profilePhoto: Api.Photo?, fallbackPhoto: Api.Photo?, notifySettings: Api.PeerNotifySettings, botInfo: Api.BotInfo?, pinnedMsgId: Int32?, commonChatsCount: Int32, folderId: Int32?, ttlPeriod: Int32?, themeEmoticon: String?, privateForwardName: String?, botGroupAdminRights: Api.ChatAdminRights?, botBroadcastAdminRights: Api.ChatAdminRights?, premiumGifts: [Api.PremiumGiftOption]?, wallpaper: Api.WallPaper?, stories: Api.PeerStories?, businessWorkHours: Api.BusinessWorkHours?, businessLocation: Api.BusinessLocation?, businessGreetingMessage: Api.BusinessGreetingMessage?, businessAwayMessage: Api.BusinessAwayMessage?, businessIntro: Api.BusinessIntro?, birthday: Api.Birthday?, personalChannelId: Int64?, personalChannelMessage: Int32?, stargiftsCount: Int32?)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .userFull(let flags, let flags2, let id, let about, let settings, let personalPhoto, let profilePhoto, let fallbackPhoto, let notifySettings, let botInfo, let pinnedMsgId, let commonChatsCount, let folderId, let ttlPeriod, let themeEmoticon, let privateForwardName, let botGroupAdminRights, let botBroadcastAdminRights, let premiumGifts, let wallpaper, let stories, let businessWorkHours, let businessLocation, let businessGreetingMessage, let businessAwayMessage, let businessIntro, let birthday, let personalChannelId, let personalChannelMessage):
|
||||
case .userFull(let flags, let flags2, let id, let about, let settings, let personalPhoto, let profilePhoto, let fallbackPhoto, let notifySettings, let botInfo, let pinnedMsgId, let commonChatsCount, let folderId, let ttlPeriod, let themeEmoticon, let privateForwardName, let botGroupAdminRights, let botBroadcastAdminRights, let premiumGifts, let wallpaper, let stories, let businessWorkHours, let businessLocation, let businessGreetingMessage, let businessAwayMessage, let businessIntro, let birthday, let personalChannelId, let personalChannelMessage, let stargiftsCount):
|
||||
if boxed {
|
||||
buffer.appendInt32(-862357728)
|
||||
buffer.appendInt32(525919081)
|
||||
}
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
serializeInt32(flags2, buffer: buffer, boxed: false)
|
||||
@ -647,14 +647,15 @@ public extension Api {
|
||||
if Int(flags2) & Int(1 << 5) != 0 {birthday!.serialize(buffer, true)}
|
||||
if Int(flags2) & Int(1 << 6) != 0 {serializeInt64(personalChannelId!, buffer: buffer, boxed: false)}
|
||||
if Int(flags2) & Int(1 << 6) != 0 {serializeInt32(personalChannelMessage!, buffer: buffer, boxed: false)}
|
||||
if Int(flags2) & Int(1 << 8) != 0 {serializeInt32(stargiftsCount!, buffer: buffer, boxed: false)}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .userFull(let flags, let flags2, let id, let about, let settings, let personalPhoto, let profilePhoto, let fallbackPhoto, let notifySettings, let botInfo, let pinnedMsgId, let commonChatsCount, let folderId, let ttlPeriod, let themeEmoticon, let privateForwardName, let botGroupAdminRights, let botBroadcastAdminRights, let premiumGifts, let wallpaper, let stories, let businessWorkHours, let businessLocation, let businessGreetingMessage, let businessAwayMessage, let businessIntro, let birthday, let personalChannelId, let personalChannelMessage):
|
||||
return ("userFull", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("about", about as Any), ("settings", settings as Any), ("personalPhoto", personalPhoto as Any), ("profilePhoto", profilePhoto as Any), ("fallbackPhoto", fallbackPhoto as Any), ("notifySettings", notifySettings as Any), ("botInfo", botInfo as Any), ("pinnedMsgId", pinnedMsgId as Any), ("commonChatsCount", commonChatsCount as Any), ("folderId", folderId as Any), ("ttlPeriod", ttlPeriod as Any), ("themeEmoticon", themeEmoticon as Any), ("privateForwardName", privateForwardName as Any), ("botGroupAdminRights", botGroupAdminRights as Any), ("botBroadcastAdminRights", botBroadcastAdminRights as Any), ("premiumGifts", premiumGifts as Any), ("wallpaper", wallpaper as Any), ("stories", stories as Any), ("businessWorkHours", businessWorkHours as Any), ("businessLocation", businessLocation as Any), ("businessGreetingMessage", businessGreetingMessage as Any), ("businessAwayMessage", businessAwayMessage as Any), ("businessIntro", businessIntro as Any), ("birthday", birthday as Any), ("personalChannelId", personalChannelId as Any), ("personalChannelMessage", personalChannelMessage as Any)])
|
||||
case .userFull(let flags, let flags2, let id, let about, let settings, let personalPhoto, let profilePhoto, let fallbackPhoto, let notifySettings, let botInfo, let pinnedMsgId, let commonChatsCount, let folderId, let ttlPeriod, let themeEmoticon, let privateForwardName, let botGroupAdminRights, let botBroadcastAdminRights, let premiumGifts, let wallpaper, let stories, let businessWorkHours, let businessLocation, let businessGreetingMessage, let businessAwayMessage, let businessIntro, let birthday, let personalChannelId, let personalChannelMessage, let stargiftsCount):
|
||||
return ("userFull", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("about", about as Any), ("settings", settings as Any), ("personalPhoto", personalPhoto as Any), ("profilePhoto", profilePhoto as Any), ("fallbackPhoto", fallbackPhoto as Any), ("notifySettings", notifySettings as Any), ("botInfo", botInfo as Any), ("pinnedMsgId", pinnedMsgId as Any), ("commonChatsCount", commonChatsCount as Any), ("folderId", folderId as Any), ("ttlPeriod", ttlPeriod as Any), ("themeEmoticon", themeEmoticon as Any), ("privateForwardName", privateForwardName as Any), ("botGroupAdminRights", botGroupAdminRights as Any), ("botBroadcastAdminRights", botBroadcastAdminRights as Any), ("premiumGifts", premiumGifts as Any), ("wallpaper", wallpaper as Any), ("stories", stories as Any), ("businessWorkHours", businessWorkHours as Any), ("businessLocation", businessLocation as Any), ("businessGreetingMessage", businessGreetingMessage as Any), ("businessAwayMessage", businessAwayMessage as Any), ("businessIntro", businessIntro as Any), ("birthday", birthday as Any), ("personalChannelId", personalChannelId as Any), ("personalChannelMessage", personalChannelMessage as Any), ("stargiftsCount", stargiftsCount as Any)])
|
||||
}
|
||||
}
|
||||
|
||||
@ -751,6 +752,8 @@ public extension Api {
|
||||
if Int(_2!) & Int(1 << 6) != 0 {_28 = reader.readInt64() }
|
||||
var _29: Int32?
|
||||
if Int(_2!) & Int(1 << 6) != 0 {_29 = reader.readInt32() }
|
||||
var _30: Int32?
|
||||
if Int(_2!) & Int(1 << 8) != 0 {_30 = reader.readInt32() }
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
@ -780,8 +783,9 @@ public extension Api {
|
||||
let _c27 = (Int(_2!) & Int(1 << 5) == 0) || _27 != nil
|
||||
let _c28 = (Int(_2!) & Int(1 << 6) == 0) || _28 != nil
|
||||
let _c29 = (Int(_2!) & Int(1 << 6) == 0) || _29 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 && _c22 && _c23 && _c24 && _c25 && _c26 && _c27 && _c28 && _c29 {
|
||||
return Api.UserFull.userFull(flags: _1!, flags2: _2!, id: _3!, about: _4, settings: _5!, personalPhoto: _6, profilePhoto: _7, fallbackPhoto: _8, notifySettings: _9!, botInfo: _10, pinnedMsgId: _11, commonChatsCount: _12!, folderId: _13, ttlPeriod: _14, themeEmoticon: _15, privateForwardName: _16, botGroupAdminRights: _17, botBroadcastAdminRights: _18, premiumGifts: _19, wallpaper: _20, stories: _21, businessWorkHours: _22, businessLocation: _23, businessGreetingMessage: _24, businessAwayMessage: _25, businessIntro: _26, birthday: _27, personalChannelId: _28, personalChannelMessage: _29)
|
||||
let _c30 = (Int(_2!) & Int(1 << 8) == 0) || _30 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 && _c22 && _c23 && _c24 && _c25 && _c26 && _c27 && _c28 && _c29 && _c30 {
|
||||
return Api.UserFull.userFull(flags: _1!, flags2: _2!, id: _3!, about: _4, settings: _5!, personalPhoto: _6, profilePhoto: _7, fallbackPhoto: _8, notifySettings: _9!, botInfo: _10, pinnedMsgId: _11, commonChatsCount: _12!, folderId: _13, ttlPeriod: _14, themeEmoticon: _15, privateForwardName: _16, botGroupAdminRights: _17, botBroadcastAdminRights: _18, premiumGifts: _19, wallpaper: _20, stories: _21, businessWorkHours: _22, businessLocation: _23, businessGreetingMessage: _24, businessAwayMessage: _25, businessIntro: _26, birthday: _27, personalChannelId: _28, personalChannelMessage: _29, stargiftsCount: _30)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
|
@ -9211,12 +9211,13 @@ public extension Api.functions.payments {
|
||||
}
|
||||
}
|
||||
public extension Api.functions.payments {
|
||||
static func saveStarGift(userId: Api.InputUser, msgId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
||||
static func saveStarGift(flags: Int32, userId: Api.InputUser, msgId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(-1394197223)
|
||||
buffer.appendInt32(-2018709362)
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
userId.serialize(buffer, true)
|
||||
serializeInt32(msgId, buffer: buffer, boxed: false)
|
||||
return (FunctionDescription(name: "payments.saveStarGift", parameters: [("userId", String(describing: userId)), ("msgId", String(describing: msgId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
|
||||
return (FunctionDescription(name: "payments.saveStarGift", parameters: [("flags", String(describing: flags)), ("userId", String(describing: userId)), ("msgId", String(describing: msgId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.Bool?
|
||||
if let signature = reader.readInt32() {
|
||||
|
@ -204,7 +204,7 @@ func _internal_convertStarGift(account: Account, messageId: EngineMessage.Id) ->
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_saveStarGiftToProfile(account: Account, messageId: EngineMessage.Id) -> Signal<Never, NoError> {
|
||||
func _internal_updateStarGiftAddedToProfile(account: Account, messageId: EngineMessage.Id, added: Bool) -> Signal<Never, NoError> {
|
||||
return account.postbox.transaction { transaction -> Api.InputUser? in
|
||||
return transaction.getPeer(messageId.peerId).flatMap(apiInputUser)
|
||||
}
|
||||
@ -212,7 +212,11 @@ func _internal_saveStarGiftToProfile(account: Account, messageId: EngineMessage.
|
||||
guard let inputUser else {
|
||||
return .complete()
|
||||
}
|
||||
return account.network.request(Api.functions.payments.saveStarGift(userId: inputUser, msgId: messageId.id))
|
||||
var flags: Int32 = 0
|
||||
if !added {
|
||||
flags |= (1 << 0)
|
||||
}
|
||||
return account.network.request(Api.functions.payments.saveStarGift(flags: flags, userId: inputUser, msgId: messageId.id))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Api.Bool?, NoError> in
|
||||
return .single(nil)
|
||||
|
@ -113,14 +113,12 @@ public extension TelegramEngine {
|
||||
return _internal_keepCachedStarGiftsUpdated(postbox: self.account.postbox, network: self.account.network)
|
||||
}
|
||||
|
||||
|
||||
public func convertStarGift(messageId: EngineMessage.Id) -> Signal<Never, NoError> {
|
||||
return _internal_convertStarGift(account: self.account, messageId: messageId)
|
||||
}
|
||||
|
||||
public func saveStarGiftToProfile(messageId: EngineMessage.Id) -> Signal<Never, NoError> {
|
||||
return _internal_saveStarGiftToProfile(account: self.account, messageId: messageId)
|
||||
public func updateStarGiftAddedToProfile(messageId: EngineMessage.Id, added: Bool) -> Signal<Never, NoError> {
|
||||
return _internal_updateStarGiftAddedToProfile(account: self.account, messageId: messageId, added: added)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -286,7 +286,6 @@ public func customizeDefaultDayTheme(theme: PresentationTheme, editing: Bool, ti
|
||||
reactionActiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2)
|
||||
)
|
||||
),
|
||||
linkHighlightColor: accentColor?.withAlphaComponent(0.3),
|
||||
accentTextColor: accentColor,
|
||||
accentControlColor: accentColor,
|
||||
accentControlDisabledColor: accentColor?.withAlphaComponent(0.7),
|
||||
@ -331,7 +330,7 @@ public func customizeDefaultDayTheme(theme: PresentationTheme, editing: Bool, ti
|
||||
primaryTextColor: outgoingPrimaryTextColor,
|
||||
secondaryTextColor: outgoingSecondaryTextColor,
|
||||
linkTextColor: outgoingLinkTextColor,
|
||||
linkHighlightColor: day ? nil : accentColor?.withAlphaComponent(0.3),
|
||||
linkHighlightColor: day ? nil : outgoingLinkTextColor?.withAlphaComponent(0.3),
|
||||
scamColor: outgoingScamColor,
|
||||
accentTextColor: outgoingAccentTextColor,
|
||||
accentControlColor: outgoingControlColor,
|
||||
|
@ -1051,14 +1051,27 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
|
||||
attributedString = mutableString
|
||||
case .prizeStars:
|
||||
attributedString = NSAttributedString(string: strings.Notification_StarsPrize, font: titleFont, textColor: primaryTextColor)
|
||||
case let .starGift(amount, _, nameHidden, limitNumber, limitTotal, text, entities):
|
||||
let _ = amount
|
||||
case let .starGift(gift, _, nameHidden, limitNumber, limitTotal, text, entities):
|
||||
let _ = nameHidden
|
||||
let _ = limitNumber
|
||||
let _ = limitTotal
|
||||
let _ = text
|
||||
let _ = entities
|
||||
attributedString = nil
|
||||
|
||||
let starsPrice = "\(gift.price) Stars"
|
||||
var authorName = compactAuthorName
|
||||
var peerIds: [(Int, EnginePeer.Id?)] = [(0, message.author?.id)]
|
||||
if message.id.peerId.namespace == Namespaces.Peer.CloudUser && message.id.peerId.id._internalGetInt64Value() == 777000 {
|
||||
authorName = strings.Notification_StarsGift_UnknownUser
|
||||
peerIds = []
|
||||
}
|
||||
if message.author?.id == accountPeerId {
|
||||
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_SentYou(starsPrice)._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes])
|
||||
} else {
|
||||
var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds)
|
||||
attributes[1] = boldAttributes
|
||||
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Sent(authorName, starsPrice)._tuple, body: bodyAttributes, argumentAttributes: attributes)
|
||||
}
|
||||
case .unknown:
|
||||
attributedString = nil
|
||||
}
|
||||
|
@ -459,6 +459,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/MinimizedContainer",
|
||||
"//submodules/TelegramUI/Components/SpaceWarpView",
|
||||
"//submodules/TelegramUI/Components/MiniAppListScreen",
|
||||
"//submodules/TelegramUI/Components/Gifts/GiftOptionsScreen",
|
||||
] + select({
|
||||
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
||||
"//build-system:ios_sim_arm64": [],
|
||||
|
@ -205,6 +205,8 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
|
||||
result.append((message, ChatMessageGiftBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
|
||||
} else if case .giftStars = action.action {
|
||||
result.append((message, ChatMessageGiftBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
|
||||
} else if case .starGift = action.action {
|
||||
result.append((message, ChatMessageGiftBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
|
||||
skipText = true
|
||||
} else if case .suggestedProfilePhoto = action.action {
|
||||
result.append((message, ChatMessageProfilePhotoSuggestionContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
|
||||
|
@ -243,6 +243,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
|
||||
var months: Int32 = 3
|
||||
var animationName: String = ""
|
||||
var animationFile: TelegramMediaFile?
|
||||
var title = item.presentationData.strings.Notification_PremiumGift_Title
|
||||
var text = ""
|
||||
var buttonTitle = item.presentationData.strings.Notification_PremiumGift_View
|
||||
@ -314,6 +315,35 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
buttonTitle = item.presentationData.strings.Notification_PremiumPrize_View
|
||||
hasServiceMessage = false
|
||||
}
|
||||
case let .starGift(gift, convertStars, giftText, entities, nameHidden, savedToProfile, converted)://(amount, giftId, nameHidden, limitNumber, limitTotal, giftText, _):
|
||||
let _ = nameHidden
|
||||
let authorName = item.message.author.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? ""
|
||||
title = nameHidden ? "Anonymous Gift" : "Gift from \(authorName)"
|
||||
if let giftText, !giftText.isEmpty {
|
||||
text = giftText
|
||||
let _ = entities
|
||||
} else {
|
||||
if incoming {
|
||||
if converted {
|
||||
text = "You converted this gift to \(convertStars) Stars."
|
||||
} else if savedToProfile {
|
||||
text = "You are displaying this gift on your page. You can also convert it to \(convertStars) Stars."
|
||||
} else {
|
||||
text = "Display this gift on your page or convert it to \(convertStars) Stars."
|
||||
}
|
||||
} else {
|
||||
var peerName = ""
|
||||
if let peer = item.message.peers[item.message.id.peerId] {
|
||||
peerName = EnginePeer(peer).compactDisplayTitle
|
||||
}
|
||||
if peerName.isEmpty {
|
||||
text = "Display this gift on your page or convert it to \(convertStars) Stars."
|
||||
} else {
|
||||
text = "\(peerName) can keep this gift on their page or convert it to \(convertStars) Stars."
|
||||
}
|
||||
}
|
||||
}
|
||||
animationFile = gift.file
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -396,7 +426,12 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
if let strongSelf = self {
|
||||
if strongSelf.item == nil {
|
||||
strongSelf.animationNode.autoplay = true
|
||||
strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: animationName), width: 384, height: 384, playbackMode: .still(.end), mode: .direct(cachePathPrefix: nil))
|
||||
|
||||
if let file = animationFile {
|
||||
strongSelf.animationNode.setup(source: AnimatedStickerResourceSource(account: item.context.account, resource: file.resource), width: 384, height: 384, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
|
||||
} else if animationName.hasPrefix("Gift") {
|
||||
strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: animationName), width: 384, height: 384, playbackMode: .still(.end), mode: .direct(cachePathPrefix: nil))
|
||||
}
|
||||
}
|
||||
strongSelf.item = item
|
||||
|
||||
@ -412,8 +447,13 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
strongSelf.mediaBackgroundNode.update(size: mediaBackgroundFrame.size, transition: .immediate)
|
||||
strongSelf.buttonNode.backgroundColor = item.presentationData.theme.theme.overallDarkAppearance ? UIColor(rgb: 0xffffff, alpha: 0.12) : UIColor(rgb: 0x000000, alpha: 0.12)
|
||||
|
||||
let iconSize = CGSize(width: 160.0, height: 160.0)
|
||||
strongSelf.animationNode.frame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - iconSize.width) / 2.0), y: mediaBackgroundFrame.minY - 16.0), size: iconSize)
|
||||
var iconSize = CGSize(width: 160.0, height: 160.0)
|
||||
var iconOffset: CGFloat = 0.0
|
||||
if let _ = animationFile {
|
||||
iconSize = CGSize(width: 120.0, height: 120.0)
|
||||
iconOffset = 32.0
|
||||
}
|
||||
strongSelf.animationNode.frame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - iconSize.width) / 2.0), y: mediaBackgroundFrame.minY - 16.0 + iconOffset), size: iconSize)
|
||||
strongSelf.animationNode.updateLayout(size: iconSize)
|
||||
|
||||
let _ = labelApply()
|
||||
|
@ -755,7 +755,7 @@ public final class ChatMessageAvatarHeaderNodeImpl: ListViewItemHeaderNode, Chat
|
||||
self.controllerInteraction?.displayMessageTooltip(id, self.presentationData.strings.Conversation_ForwardAuthorHiddenTooltip, false, self, self.avatarNode.frame)
|
||||
} else if let peer = self.peer {
|
||||
if let adMessageId = self.adMessageId {
|
||||
self.controllerInteraction?.activateAdAction(adMessageId, nil)
|
||||
self.controllerInteraction?.activateAdAction(adMessageId, nil, false, false)
|
||||
} else {
|
||||
if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
|
||||
self.controllerInteraction?.openPeer(EnginePeer(peer), .chat(textInputState: nil, subject: nil, peekData: nil), self.messageReference, .default)
|
||||
|
@ -118,7 +118,7 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent
|
||||
self.contentNode.activateAction = { [weak self] in
|
||||
if let strongSelf = self, let item = strongSelf.item {
|
||||
if let _ = item.message.adAttribute {
|
||||
item.controllerInteraction.activateAdAction(item.message.id, strongSelf.contentNode.makeProgress())
|
||||
item.controllerInteraction.activateAdAction(item.message.id, strongSelf.contentNode.makeProgress(), false, false)
|
||||
} else {
|
||||
var webPageContent: TelegramMediaWebpageLoadedContent?
|
||||
for media in item.message.media {
|
||||
|
@ -616,7 +616,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
}, openLargeEmojiInfo: { _, _, _ in
|
||||
}, openJoinLink: { _ in
|
||||
}, openWebView: { _, _, _, _ in
|
||||
}, activateAdAction: { _, _ in
|
||||
}, activateAdAction: { _, _, _, _ in
|
||||
}, openRequestedPeerSelection: { _, _, _, _ in
|
||||
}, saveMediaToFiles: { _ in
|
||||
}, openNoAdsDemo: {
|
||||
@ -1208,8 +1208,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
||||
if let invoice {
|
||||
let inputData = Promise<BotCheckoutController.InputData?>()
|
||||
inputData.set(BotCheckoutController.InputData.fetch(context: strongSelf.context, source: .slug(slug))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<BotCheckoutController.InputData?, NoError> in
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<BotCheckoutController.InputData?, NoError> in
|
||||
return .single(nil)
|
||||
})
|
||||
strongSelf.controllerInteraction.presentController(BotCheckoutController(context: strongSelf.context, invoice: invoice, source: .slug(slug), inputData: inputData, completed: { currencyValue, receiptMessageId in
|
||||
|
@ -473,7 +473,7 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess
|
||||
}, openLargeEmojiInfo: { _, _, _ in
|
||||
}, openJoinLink: { _ in
|
||||
}, openWebView: { _, _, _, _ in
|
||||
}, activateAdAction: { _, _ in
|
||||
}, activateAdAction: { _, _, _, _ in
|
||||
}, openRequestedPeerSelection: { _, _, _, _ in
|
||||
}, saveMediaToFiles: { _ in
|
||||
}, openNoAdsDemo: {
|
||||
|
@ -252,7 +252,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
|
||||
public let openLargeEmojiInfo: (String, String?, TelegramMediaFile) -> Void
|
||||
public let openJoinLink: (String) -> Void
|
||||
public let openWebView: (String, String, Bool, ChatOpenWebViewSource) -> Void
|
||||
public let activateAdAction: (EngineMessage.Id, Promise<Bool>?) -> Void
|
||||
public let activateAdAction: (EngineMessage.Id, Promise<Bool>?, Bool, Bool) -> Void
|
||||
public let openRequestedPeerSelection: (EngineMessage.Id, ReplyMarkupButtonRequestPeerType, Int32, Int32) -> Void
|
||||
public let saveMediaToFiles: (EngineMessage.Id) -> Void
|
||||
public let openNoAdsDemo: () -> Void
|
||||
@ -382,7 +382,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
|
||||
openLargeEmojiInfo: @escaping (String, String?, TelegramMediaFile) -> Void,
|
||||
openJoinLink: @escaping (String) -> Void,
|
||||
openWebView: @escaping (String, String, Bool, ChatOpenWebViewSource) -> Void,
|
||||
activateAdAction: @escaping (EngineMessage.Id, Promise<Bool>?) -> Void,
|
||||
activateAdAction: @escaping (EngineMessage.Id, Promise<Bool>?, Bool, Bool) -> Void,
|
||||
openRequestedPeerSelection: @escaping (EngineMessage.Id, ReplyMarkupButtonRequestPeerType, Int32, Int32) -> Void,
|
||||
saveMediaToFiles: @escaping (EngineMessage.Id) -> Void,
|
||||
openNoAdsDemo: @escaping () -> Void,
|
||||
|
@ -144,6 +144,34 @@ public func animationCacheFetchFile(postbox: Postbox, userLocation: MediaResourc
|
||||
}
|
||||
}
|
||||
|
||||
public func animationCacheLoadLocalFile(name: String, type: AnimationCacheAnimationType, keyframeOnly: Bool, customColor: UIColor?) -> (AnimationCacheFetchOptions) -> Disposable {
|
||||
return { options in
|
||||
let source = AnimatedStickerNodeLocalFileSource(name: name)
|
||||
let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in
|
||||
guard let result = result else {
|
||||
return
|
||||
}
|
||||
|
||||
switch type {
|
||||
case .video:
|
||||
cacheVideoAnimation(path: result, width: Int(options.size.width), height: Int(options.size.height), writer: options.writer, firstFrameOnly: options.firstFrameOnly, customColor: customColor)
|
||||
case .lottie:
|
||||
guard let data = try? Data(contentsOf: URL(fileURLWithPath: result)) else {
|
||||
options.writer.finish()
|
||||
return
|
||||
}
|
||||
cacheLottieAnimation(data: data, width: Int(options.size.width), height: Int(options.size.height), keyframeOnly: keyframeOnly, writer: options.writer, firstFrameOnly: options.firstFrameOnly, customColor: customColor)
|
||||
case .still:
|
||||
cacheStillSticker(path: result, width: Int(options.size.width), height: Int(options.size.height), writer: options.writer, customColor: customColor)
|
||||
}
|
||||
})
|
||||
|
||||
return ActionDisposable {
|
||||
dataDisposable.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func generatePeerNameColorImage(nameColor: PeerNameColors.Colors, isDark: Bool, bounds: CGSize = CGSize(width: 40.0, height: 40.0), size: CGSize = CGSize(width: 40.0, height: 40.0)) -> UIImage? {
|
||||
return generateImage(bounds, rotatedContext: { contextSize, context in
|
||||
let bounds = CGRect(origin: CGPoint(), size: contextSize)
|
||||
@ -310,6 +338,8 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
|
||||
private var didProcessTintColor: Bool = false
|
||||
|
||||
public private(set) var file: TelegramMediaFile?
|
||||
private var localAnimationName: String?
|
||||
|
||||
private var infoDisposable: Disposable?
|
||||
private var disposable: Disposable?
|
||||
private var fetchDisposable: Disposable?
|
||||
@ -440,6 +470,8 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
|
||||
}
|
||||
case .ton:
|
||||
self.updateTon()
|
||||
case let .animation(name):
|
||||
self.updateLocalAnimation(name: name, attemptSynchronousLoad: attemptSynchronousLoad)
|
||||
}
|
||||
} else if let file = file {
|
||||
self.updateFile(file: file, attemptSynchronousLoad: attemptSynchronousLoad)
|
||||
@ -629,6 +661,42 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
|
||||
self.contents = tonImage?.cgImage
|
||||
}
|
||||
|
||||
private func updateLocalAnimation(name: String, attemptSynchronousLoad: Bool) {
|
||||
guard let arguments = self.arguments else {
|
||||
return
|
||||
}
|
||||
|
||||
self.localAnimationName = name
|
||||
|
||||
if attemptSynchronousLoad {
|
||||
if !arguments.renderer.loadFirstFrameSynchronously(target: self, cache: arguments.cache, itemId: name, size: arguments.pixelSize) {
|
||||
|
||||
}
|
||||
|
||||
self.loadAnimation()
|
||||
} else {
|
||||
self.loadDisposable = arguments.renderer.loadFirstFrame(target: self, cache: arguments.cache, itemId: name, size: arguments.pixelSize, fetch: animationCacheLoadLocalFile(name: name, type: .lottie, keyframeOnly: true, customColor: nil), completion: { [weak self] result, isFinal in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.loadAnimation()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func loadLocalAnimation() {
|
||||
guard let arguments = self.arguments else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let name = self.localAnimationName else {
|
||||
return
|
||||
}
|
||||
|
||||
let keyframeOnly = arguments.pixelSize.width >= 120.0
|
||||
self.disposable = arguments.renderer.add(target: self, cache: arguments.cache, itemId: name, unique: arguments.unique, size: arguments.pixelSize, fetch: animationCacheLoadLocalFile(name: name, type: .lottie, keyframeOnly: keyframeOnly, customColor: nil))
|
||||
}
|
||||
|
||||
private func updateFile(file: TelegramMediaFile, attemptSynchronousLoad: Bool) {
|
||||
guard let arguments = self.arguments else {
|
||||
return
|
||||
|
@ -0,0 +1,36 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "GiftItemComponent",
|
||||
module_name = "GiftItemComponent",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/Postbox",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/Components/ViewControllerComponent",
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/Components/MultilineTextWithEntitiesComponent",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/TelegramStringFormatting",
|
||||
"//submodules/PresentationDataUtils",
|
||||
"//submodules/TextFormat",
|
||||
"//submodules/AvatarNode",
|
||||
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
|
||||
"//submodules/TelegramUI/Components/Stars/ItemShimmeringLoadingComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -0,0 +1,524 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import AppBundle
|
||||
import AccountContext
|
||||
import MultilineTextComponent
|
||||
import MultilineTextWithEntitiesComponent
|
||||
import EmojiTextAttachmentView
|
||||
import TextFormat
|
||||
import ItemShimmeringLoadingComponent
|
||||
import AvatarNode
|
||||
|
||||
public final class GiftItemComponent: Component {
|
||||
public enum Subject: Equatable {
|
||||
case premium(Int32)
|
||||
case starGift(Int64, TelegramMediaFile)
|
||||
}
|
||||
|
||||
public struct Ribbon: Equatable {
|
||||
public let text: String
|
||||
public let color: UIColor
|
||||
|
||||
public init(text: String, color: UIColor) {
|
||||
self.text = text
|
||||
self.color = color
|
||||
}
|
||||
}
|
||||
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let peer: EnginePeer?
|
||||
let subject: Subject
|
||||
let title: String?
|
||||
let subtitle: String?
|
||||
let price: String
|
||||
let ribbon: Ribbon?
|
||||
let isLoading: Bool
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
peer: EnginePeer?,
|
||||
subject: Subject,
|
||||
title: String? = nil,
|
||||
subtitle: String? = nil,
|
||||
price: String,
|
||||
ribbon: Ribbon? = nil,
|
||||
isLoading: Bool = false
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.peer = peer
|
||||
self.subject = subject
|
||||
self.title = title
|
||||
self.subtitle = subtitle
|
||||
self.price = price
|
||||
self.ribbon = ribbon
|
||||
self.isLoading = isLoading
|
||||
}
|
||||
|
||||
public static func ==(lhs: GiftItemComponent, rhs: GiftItemComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.peer != rhs.peer {
|
||||
return false
|
||||
}
|
||||
if lhs.subject != rhs.subject {
|
||||
return false
|
||||
}
|
||||
if lhs.title != rhs.title {
|
||||
return false
|
||||
}
|
||||
if lhs.subtitle != rhs.subtitle {
|
||||
return false
|
||||
}
|
||||
if lhs.price != rhs.price {
|
||||
return false
|
||||
}
|
||||
if lhs.ribbon != rhs.ribbon {
|
||||
return false
|
||||
}
|
||||
if lhs.isLoading != rhs.isLoading {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private var component: GiftItemComponent?
|
||||
private weak var componentState: EmptyComponentState?
|
||||
|
||||
private let backgroundLayer = SimpleLayer()
|
||||
private var loadingBackground: ComponentView<Empty>?
|
||||
|
||||
private var avatarNode: AvatarNode?
|
||||
private let title = ComponentView<Empty>()
|
||||
private let subtitle = ComponentView<Empty>()
|
||||
private let button = ComponentView<Empty>()
|
||||
private let ribbon = UIImageView()
|
||||
private let ribbonText = ComponentView<Empty>()
|
||||
|
||||
private var animationLayer: InlineStickerItemLayer?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.layer.addSublayer(self.backgroundLayer)
|
||||
|
||||
self.backgroundLayer.cornerRadius = 10.0
|
||||
if #available(iOS 13.0, *) {
|
||||
self.backgroundLayer.cornerCurve = .circular
|
||||
}
|
||||
self.backgroundLayer.masksToBounds = true
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: GiftItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
self.componentState = state
|
||||
|
||||
let size = CGSize(width: availableSize.width, height: component.title != nil ? 178.0 : 154.0)
|
||||
|
||||
if component.isLoading {
|
||||
let loadingBackground: ComponentView<Empty>
|
||||
if let current = self.loadingBackground {
|
||||
loadingBackground = current
|
||||
} else {
|
||||
loadingBackground = ComponentView<Empty>()
|
||||
self.loadingBackground = loadingBackground
|
||||
}
|
||||
|
||||
let _ = loadingBackground.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
ItemShimmeringLoadingComponent(color: component.theme.list.itemAccentColor, cornerRadius: 10.0)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: size
|
||||
)
|
||||
if let loadingBackgroundView = loadingBackground.view {
|
||||
if loadingBackgroundView.layer.superlayer == nil {
|
||||
self.layer.insertSublayer(loadingBackgroundView.layer, above: self.backgroundLayer)
|
||||
}
|
||||
loadingBackgroundView.frame = CGRect(origin: .zero, size: size)
|
||||
}
|
||||
} else if let loadingBackground = self.loadingBackground {
|
||||
loadingBackground.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
||||
loadingBackground.view?.layer.removeFromSuperlayer()
|
||||
})
|
||||
self.loadingBackground = nil
|
||||
}
|
||||
|
||||
let emoji: ChatTextInputTextCustomEmojiAttribute?
|
||||
var file: TelegramMediaFile?
|
||||
var animationOffset: CGFloat = 0.0
|
||||
switch component.subject {
|
||||
case let .premium(months):
|
||||
emoji = ChatTextInputTextCustomEmojiAttribute(
|
||||
interactivelySelectedFromPackId: nil,
|
||||
fileId: 0,
|
||||
file: nil,
|
||||
custom: .animation(name: "Gift\(months)")
|
||||
)
|
||||
case let .starGift(_, fileValue):
|
||||
file = fileValue
|
||||
emoji = ChatTextInputTextCustomEmojiAttribute(
|
||||
interactivelySelectedFromPackId: nil,
|
||||
fileId: fileValue.fileId.id,
|
||||
file: fileValue
|
||||
)
|
||||
animationOffset = 16.0
|
||||
}
|
||||
|
||||
let iconSize = CGSize(width: 88.0, height: 88.0)
|
||||
if self.animationLayer == nil, let emoji {
|
||||
let animationLayer = InlineStickerItemLayer(
|
||||
context: .account(component.context),
|
||||
userLocation: .other,
|
||||
attemptSynchronousLoad: false,
|
||||
emoji: emoji,
|
||||
file: file,
|
||||
cache: component.context.animationCache,
|
||||
renderer: component.context.animationRenderer,
|
||||
unique: false,
|
||||
placeholderColor: component.theme.list.mediaPlaceholderColor,
|
||||
pointSize: CGSize(width: iconSize.width * 2.0, height: iconSize.height * 2.0),
|
||||
loopCount: 1
|
||||
)
|
||||
animationLayer.isVisibleForAnimations = true
|
||||
self.animationLayer = animationLayer
|
||||
self.layer.addSublayer(animationLayer)
|
||||
}
|
||||
|
||||
if let animationLayer = self.animationLayer {
|
||||
transition.setFrame(layer: animationLayer, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - iconSize.width) / 2.0), y: animationOffset), size: iconSize))
|
||||
}
|
||||
|
||||
if let title = component.title {
|
||||
let titleSize = self.title.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: title, font: Font.semibold(15.0), textColor: component.theme.list.itemPrimaryTextColor)),
|
||||
horizontalAlignment: .center
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - titleSize.width) / 2.0), y: 94.0), size: titleSize)
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
self.addSubview(titleView)
|
||||
}
|
||||
transition.setFrame(view: titleView, frame: titleFrame)
|
||||
}
|
||||
}
|
||||
|
||||
if let subtitle = component.subtitle {
|
||||
let subtitleSize = self.subtitle.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: subtitle, font: Font.regular(13.0), textColor: component.theme.list.itemPrimaryTextColor)),
|
||||
horizontalAlignment: .center
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
let subtitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - subtitleSize.width) / 2.0), y: 112.0), size: subtitleSize)
|
||||
if let subtitleView = self.subtitle.view {
|
||||
if subtitleView.superview == nil {
|
||||
self.addSubview(subtitleView)
|
||||
}
|
||||
transition.setFrame(view: subtitleView, frame: subtitleFrame)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
let buttonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - buttonSize.width) / 2.0), y: size.height - buttonSize.height - 10.0), size: buttonSize)
|
||||
if let buttonView = self.button.view {
|
||||
if buttonView.superview == nil {
|
||||
self.addSubview(buttonView)
|
||||
}
|
||||
transition.setFrame(view: buttonView, frame: buttonFrame)
|
||||
}
|
||||
|
||||
if let ribbon = component.ribbon {
|
||||
let ribbonTextSize = self.ribbonText.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: ribbon.text, font: Font.semibold(11.0), textColor: .white)),
|
||||
horizontalAlignment: .center
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
if let ribbonTextView = self.ribbonText.view {
|
||||
if ribbonTextView.superview == nil {
|
||||
self.addSubview(self.ribbon)
|
||||
self.addSubview(ribbonTextView)
|
||||
}
|
||||
ribbonTextView.bounds = CGRect(origin: .zero, size: ribbonTextSize)
|
||||
|
||||
if self.ribbon.image == nil {
|
||||
self.ribbon.image = generateGradientTintedImage(image: UIImage(bundleImageName: "Premium/GiftRibbon"), colors: [ribbon.color.withMultipliedBrightnessBy(1.1), ribbon.color.withMultipliedBrightnessBy(0.9)], direction: .diagonal)
|
||||
}
|
||||
if let ribbonImage = self.ribbon.image {
|
||||
self.ribbon.frame = CGRect(origin: CGPoint(x: size.width - ribbonImage.size.width + 2.0, y: -2.0), size: ribbonImage.size)
|
||||
}
|
||||
ribbonTextView.transform = CGAffineTransform(rotationAngle: .pi / 4.0)
|
||||
ribbonTextView.center = CGPoint(x: size.width - 20.0, y: 20.0)
|
||||
}
|
||||
} else {
|
||||
if self.ribbonText.view?.superview != nil {
|
||||
self.ribbon.removeFromSuperview()
|
||||
self.ribbonText.view?.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
if let peer = component.peer {
|
||||
let avatarNode: AvatarNode
|
||||
if let current = self.avatarNode {
|
||||
avatarNode = current
|
||||
} else {
|
||||
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 8.0))
|
||||
self.addSubview(avatarNode.view)
|
||||
self.avatarNode = avatarNode
|
||||
}
|
||||
|
||||
avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, displayDimensions: CGSize(width: 20.0, height: 20.0))
|
||||
avatarNode.frame = CGRect(origin: CGPoint(x: 2.0, y: 2.0), size: CGSize(width: 20.0, height: 20.0))
|
||||
}
|
||||
|
||||
self.backgroundLayer.backgroundColor = component.theme.list.itemBlocksBackgroundColor.cgColor
|
||||
transition.setFrame(layer: self.backgroundLayer, frame: CGRect(origin: .zero, size: size))
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
private final class ButtonContentComponent: Component {
|
||||
let context: AccountContext
|
||||
let text: String
|
||||
let color: UIColor
|
||||
let isStars: Bool
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
text: String,
|
||||
color: UIColor,
|
||||
isStars: Bool = false
|
||||
) {
|
||||
self.context = context
|
||||
self.text = text
|
||||
self.color = color
|
||||
self.isStars = isStars
|
||||
}
|
||||
|
||||
public static func ==(lhs: ButtonContentComponent, rhs: ButtonContentComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.text != rhs.text {
|
||||
return false
|
||||
}
|
||||
if lhs.color != rhs.color {
|
||||
return false
|
||||
}
|
||||
if lhs.isStars != rhs.isStars {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private var component: ButtonContentComponent?
|
||||
private weak var componentState: EmptyComponentState?
|
||||
|
||||
private let backgroundLayer = SimpleLayer()
|
||||
private let title = ComponentView<Empty>()
|
||||
|
||||
private var starsLayer: StarsButtonEffectLayer?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.layer.addSublayer(self.backgroundLayer)
|
||||
self.backgroundLayer.masksToBounds = true
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: ButtonContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
self.componentState = state
|
||||
|
||||
let attributedText = NSMutableAttributedString(string: component.text, font: Font.semibold(11.0), textColor: component.color)
|
||||
let range = (attributedText.string as NSString).range(of: "⭐️")
|
||||
if range.location != NSNotFound {
|
||||
attributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range)
|
||||
attributedText.addAttribute(.font, value: Font.semibold(15.0), range: range)
|
||||
attributedText.addAttribute(.baselineOffset, value: 2.0, range: NSRange(location: range.upperBound, length: attributedText.length - range.upperBound))
|
||||
}
|
||||
|
||||
let titleSize = self.title.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
MultilineTextWithEntitiesComponent(
|
||||
context: component.context,
|
||||
animationCache: component.context.animationCache,
|
||||
animationRenderer: component.context.animationRenderer,
|
||||
placeholderColor: .white,
|
||||
text: .plain(attributedText)
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
|
||||
let padding: CGFloat = 9.0
|
||||
let size = CGSize(width: titleSize.width + padding * 2.0, height: 30.0)
|
||||
|
||||
if component.isStars {
|
||||
let starsLayer: StarsButtonEffectLayer
|
||||
if let current = self.starsLayer {
|
||||
starsLayer = current
|
||||
} else {
|
||||
starsLayer = StarsButtonEffectLayer()
|
||||
self.layer.addSublayer(starsLayer)
|
||||
self.starsLayer = starsLayer
|
||||
}
|
||||
starsLayer.frame = CGRect(origin: .zero, size: size)
|
||||
starsLayer.update(size: size)
|
||||
} else {
|
||||
self.starsLayer?.removeFromSuperlayer()
|
||||
self.starsLayer = nil
|
||||
}
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize)
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
self.addSubview(titleView)
|
||||
}
|
||||
transition.setFrame(view: titleView, frame: titleFrame)
|
||||
}
|
||||
|
||||
let backgroundColor: UIColor
|
||||
if component.color.rgb == 0xd3720a {
|
||||
backgroundColor = UIColor(rgb: 0xffc83d, alpha: 0.2)
|
||||
} else {
|
||||
backgroundColor = component.color.withAlphaComponent(0.1)
|
||||
}
|
||||
|
||||
self.backgroundLayer.backgroundColor = backgroundColor.cgColor
|
||||
transition.setFrame(layer: self.backgroundLayer, frame: CGRect(origin: .zero, size: size))
|
||||
self.backgroundLayer.cornerRadius = size.height / 2.0
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
private final class StarsButtonEffectLayer: SimpleLayer {
|
||||
let emitterLayer = CAEmitterLayer()
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
self.addSublayer(self.emitterLayer)
|
||||
}
|
||||
|
||||
override init(layer: Any) {
|
||||
super.init(layer: layer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func setup() {
|
||||
let color = UIColor(rgb: 0xffbe27)
|
||||
|
||||
let emitter = CAEmitterCell()
|
||||
emitter.name = "emitter"
|
||||
emitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage
|
||||
emitter.birthRate = 25.0
|
||||
emitter.lifetime = 2.0
|
||||
emitter.velocity = 12.0
|
||||
emitter.velocityRange = 3
|
||||
emitter.scale = 0.1
|
||||
emitter.scaleRange = 0.08
|
||||
emitter.alphaRange = 0.1
|
||||
emitter.emissionRange = .pi * 2.0
|
||||
emitter.setValue(3.0, forKey: "mass")
|
||||
emitter.setValue(2.0, forKey: "massRange")
|
||||
|
||||
let staticColors: [Any] = [
|
||||
color.withAlphaComponent(0.0).cgColor,
|
||||
color.cgColor,
|
||||
color.cgColor,
|
||||
color.withAlphaComponent(0.0).cgColor
|
||||
]
|
||||
let staticColorBehavior = CAEmitterCell.createEmitterBehavior(type: "colorOverLife")
|
||||
staticColorBehavior.setValue(staticColors, forKey: "colors")
|
||||
emitter.setValue([staticColorBehavior], forKey: "emitterBehaviors")
|
||||
|
||||
self.emitterLayer.emitterCells = [emitter]
|
||||
}
|
||||
|
||||
func update(size: CGSize) {
|
||||
if self.emitterLayer.emitterCells == nil {
|
||||
self.setup()
|
||||
}
|
||||
self.emitterLayer.emitterShape = .circle
|
||||
self.emitterLayer.emitterSize = CGSize(width: size.width * 0.7, height: size.height * 0.7)
|
||||
self.emitterLayer.emitterMode = .surface
|
||||
self.emitterLayer.frame = CGRect(origin: .zero, size: size)
|
||||
self.emitterLayer.emitterPosition = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "GiftOptionsScreen",
|
||||
module_name = "GiftOptionsScreen",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/Postbox",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/Components/ViewControllerComponent",
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/Components/BalancedTextComponent",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/ItemListUI",
|
||||
"//submodules/TelegramStringFormatting",
|
||||
"//submodules/PresentationDataUtils",
|
||||
"//submodules/Components/SheetComponent",
|
||||
"//submodules/UndoUI",
|
||||
"//submodules/TextFormat",
|
||||
"//submodules/TelegramUI/Components/ListSectionComponent",
|
||||
"//submodules/TelegramUI/Components/ListActionItemComponent",
|
||||
"//submodules/TelegramUI/Components/ScrollComponent",
|
||||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||
"//submodules/TelegramUI/Components/Premium/PremiumStarComponent",
|
||||
"//submodules/Components/BlurredBackgroundComponent",
|
||||
"//submodules/TelegramUI/Components/ButtonComponent",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/TelegramUI/Components/Gifts/GiftItemComponent",
|
||||
"//submodules/ConfettiEffect",
|
||||
"//submodules/InAppPurchaseManager",
|
||||
"//submodules/TelegramUI/Components/TabSelectorComponent",
|
||||
"//submodules/TelegramUI/Components/Gifts/GiftSetupScreen",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -0,0 +1,909 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import PresentationDataUtils
|
||||
import AccountContext
|
||||
import ComponentFlow
|
||||
import ViewControllerComponent
|
||||
import MultilineTextComponent
|
||||
import BalancedTextComponent
|
||||
import BundleIconComponent
|
||||
import Markdown
|
||||
import TelegramStringFormatting
|
||||
import PlainButtonComponent
|
||||
import BlurredBackgroundComponent
|
||||
import PremiumStarComponent
|
||||
import ConfettiEffect
|
||||
import TextFormat
|
||||
import GiftItemComponent
|
||||
import InAppPurchaseManager
|
||||
import TabSelectorComponent
|
||||
import GiftSetupScreen
|
||||
|
||||
final class GiftOptionsScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let context: AccountContext
|
||||
let peerId: EnginePeer.Id
|
||||
let premiumOptions: [CachedPremiumGiftOption]
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
peerId: EnginePeer.Id,
|
||||
premiumOptions: [CachedPremiumGiftOption]
|
||||
) {
|
||||
self.context = context
|
||||
self.peerId = peerId
|
||||
self.premiumOptions = premiumOptions
|
||||
}
|
||||
|
||||
static func ==(lhs: GiftOptionsScreenComponent, rhs: GiftOptionsScreenComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.peerId != rhs.peerId {
|
||||
return false
|
||||
}
|
||||
if lhs.premiumOptions != rhs.premiumOptions {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private final class ScrollView: UIScrollView {
|
||||
override func touchesShouldCancel(in view: UIView) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public enum StarsFilter: Int {
|
||||
case all
|
||||
case limited
|
||||
case stars10
|
||||
case stars25
|
||||
case stars50
|
||||
case stars100
|
||||
}
|
||||
|
||||
final class View: UIView, UIScrollViewDelegate {
|
||||
private let topOverscrollLayer = SimpleLayer()
|
||||
private let scrollView: ScrollView
|
||||
|
||||
private let topPanel = ComponentView<Empty>()
|
||||
private let topSeparator = ComponentView<Empty>()
|
||||
private let cancelButton = ComponentView<Empty>()
|
||||
|
||||
private let header = ComponentView<Empty>()
|
||||
|
||||
private let premiumTitle = ComponentView<Empty>()
|
||||
private let premiumDescription = ComponentView<Empty>()
|
||||
private var premiumItems: [AnyHashable: ComponentView<Empty>] = [:]
|
||||
private var selectedPremiumGift: String?
|
||||
|
||||
private let starsTitle = ComponentView<Empty>()
|
||||
private let starsDescription = ComponentView<Empty>()
|
||||
private var starsItems: [AnyHashable: ComponentView<Empty>] = [:]
|
||||
private let tabSelector = ComponentView<Empty>()
|
||||
private var starsFilter: StarsFilter = .all
|
||||
|
||||
private var isUpdating: Bool = false
|
||||
|
||||
private var component: GiftOptionsScreenComponent?
|
||||
private(set) weak var state: State?
|
||||
private var environment: EnvironmentType?
|
||||
|
||||
private var starsItemsOrigin: CGFloat = 0.0
|
||||
|
||||
private var chevronImage: (UIImage, PresentationTheme)?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.scrollView = ScrollView()
|
||||
self.scrollView.showsVerticalScrollIndicator = true
|
||||
self.scrollView.showsHorizontalScrollIndicator = false
|
||||
self.scrollView.scrollsToTop = false
|
||||
self.scrollView.delaysContentTouches = false
|
||||
self.scrollView.canCancelContentTouches = true
|
||||
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
if #available(iOS 13.0, *) {
|
||||
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
|
||||
}
|
||||
self.scrollView.alwaysBounceVertical = true
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.scrollView.delegate = self
|
||||
self.addSubview(self.scrollView)
|
||||
|
||||
self.scrollView.layer.addSublayer(self.topOverscrollLayer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
}
|
||||
|
||||
func scrollToTop() {
|
||||
self.scrollView.setContentOffset(CGPoint(), animated: true)
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
self.updateScrolling(transition: .immediate)
|
||||
}
|
||||
|
||||
private func updateScrolling(transition: ComponentTransition) {
|
||||
guard let environment = self.environment, let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
let availableWidth = self.scrollView.bounds.width
|
||||
let contentOffset = self.scrollView.contentOffset.y
|
||||
|
||||
let topPanelAlpha = min(20.0, max(0.0, contentOffset - 95.0)) / 20.0
|
||||
if let topPanelView = self.topPanel.view, let topSeparator = self.topSeparator.view {
|
||||
transition.setAlpha(view: topPanelView, alpha: topPanelAlpha)
|
||||
transition.setAlpha(view: topSeparator, alpha: topPanelAlpha)
|
||||
}
|
||||
|
||||
let topInset: CGFloat = environment.navigationHeight - 56.0
|
||||
|
||||
let premiumTitleInitialPosition = (topInset + 160.0)
|
||||
let premiumTitleOffsetDelta = premiumTitleInitialPosition - (environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0)
|
||||
let premiumTitleOffset = contentOffset + max(0.0, min(1.0, contentOffset / premiumTitleOffsetDelta)) * 10.0
|
||||
let premiumTitleFraction = max(0.0, min(1.0, premiumTitleOffset / premiumTitleOffsetDelta))
|
||||
let premiumTitleScale = 1.0 - premiumTitleFraction * 0.36
|
||||
var premiumTitleAdditionalOffset: CGFloat = 0.0
|
||||
|
||||
let starsTitleOffsetDelta = (topInset + 100.0) - (environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0)
|
||||
|
||||
let starsTitleOffset: CGFloat
|
||||
let starsTitleFraction: CGFloat
|
||||
if contentOffset > 350 {
|
||||
starsTitleOffset = contentOffset + max(0.0, min(1.0, (contentOffset - 350.0) / starsTitleOffsetDelta)) * 10.0
|
||||
starsTitleFraction = max(0.0, min(1.0, (starsTitleOffset - 350.0) / starsTitleOffsetDelta))
|
||||
if contentOffset > 380.0 {
|
||||
premiumTitleAdditionalOffset = contentOffset - 380.0
|
||||
}
|
||||
} else {
|
||||
starsTitleOffset = contentOffset
|
||||
starsTitleFraction = 0.0
|
||||
}
|
||||
let starsTitleScale = 1.0 - starsTitleFraction * 0.36
|
||||
if let starsTitleView = self.starsTitle.view {
|
||||
transition.setPosition(view: starsTitleView, position: CGPoint(x: availableWidth / 2.0, y: max(topInset + 455.0 - starsTitleOffset, environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0)))
|
||||
transition.setScale(view: starsTitleView, scale: starsTitleScale)
|
||||
}
|
||||
|
||||
if let premiumTitleView = self.premiumTitle.view {
|
||||
transition.setPosition(view: premiumTitleView, position: CGPoint(x: availableWidth / 2.0, y: max(premiumTitleInitialPosition - premiumTitleOffset, environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0) - premiumTitleAdditionalOffset))
|
||||
transition.setScale(view: premiumTitleView, scale: premiumTitleScale)
|
||||
}
|
||||
|
||||
|
||||
if let headerView = self.header.view {
|
||||
transition.setPosition(view: headerView, position: CGPoint(x: availableWidth / 2.0, y: topInset + headerView.bounds.height / 2.0 - 30.0 - premiumTitleOffset * premiumTitleScale))
|
||||
transition.setScale(view: headerView, scale: premiumTitleScale)
|
||||
}
|
||||
|
||||
let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -10.0)
|
||||
if let starGifts = self.state?.starGifts {
|
||||
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
|
||||
|
||||
let optionSpacing: CGFloat = 10.0
|
||||
let optionWidth = (availableWidth - sideInset * 2.0 - optionSpacing * 2.0) / 3.0
|
||||
let starsOptionSize = CGSize(width: optionWidth, height: 154.0)
|
||||
|
||||
var validIds: [AnyHashable] = []
|
||||
var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: self.starsItemsOrigin), size: starsOptionSize)
|
||||
|
||||
let controller = environment.controller
|
||||
|
||||
for gift in starGifts {
|
||||
var isVisible = false
|
||||
if visibleBounds.intersects(itemFrame) {
|
||||
isVisible = true
|
||||
}
|
||||
|
||||
if isVisible {
|
||||
if self.starsFilter != .all {
|
||||
switch self.starsFilter {
|
||||
case .all:
|
||||
break
|
||||
case .limited:
|
||||
if gift.availability == nil {
|
||||
continue
|
||||
}
|
||||
case .stars10:
|
||||
if gift.price != 10 {
|
||||
continue
|
||||
}
|
||||
case .stars25:
|
||||
if gift.price != 25 {
|
||||
continue
|
||||
}
|
||||
case .stars50:
|
||||
if gift.price != 50 {
|
||||
continue
|
||||
}
|
||||
case .stars100:
|
||||
if gift.price != 100 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let itemId = AnyHashable(gift.id)
|
||||
validIds.append(itemId)
|
||||
|
||||
var itemTransition = transition
|
||||
let visibleItem: ComponentView<Empty>
|
||||
if let current = self.starsItems[itemId] {
|
||||
visibleItem = current
|
||||
} else {
|
||||
visibleItem = ComponentView()
|
||||
if !transition.animation.isImmediate {
|
||||
itemTransition = .immediate
|
||||
}
|
||||
self.starsItems[itemId] = visibleItem
|
||||
}
|
||||
|
||||
let _ = visibleItem.update(
|
||||
transition: itemTransition,
|
||||
component: AnyComponent(
|
||||
PlainButtonComponent(
|
||||
content: AnyComponent(
|
||||
GiftItemComponent(
|
||||
context: component.context,
|
||||
theme: environment.theme,
|
||||
peer: nil,
|
||||
subject: .starGift(gift.id, gift.file),
|
||||
price: "⭐️ \(gift.price)",
|
||||
ribbon: gift.availability != nil ?
|
||||
GiftItemComponent.Ribbon(
|
||||
text: "Limited",
|
||||
color: UIColor(rgb: 0x58c1fe)
|
||||
)
|
||||
: nil
|
||||
)
|
||||
),
|
||||
effectAlignment: .center,
|
||||
action: { [weak self] in
|
||||
if let self, let component = self.component {
|
||||
controller()?.push(GiftSetupScreen(context: component.context, peerId: component.peerId, gift: gift))
|
||||
}
|
||||
},
|
||||
animateAlpha: false
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: starsOptionSize
|
||||
)
|
||||
if let itemView = visibleItem.view {
|
||||
if itemView.superview == nil {
|
||||
self.scrollView.addSubview(itemView)
|
||||
if !transition.animation.isImmediate {
|
||||
transition.animateAlpha(view: itemView, from: 0.0, to: 1.0)
|
||||
transition.animateScale(view: itemView, from: 0.01, to: 1.0)
|
||||
}
|
||||
}
|
||||
itemTransition.setFrame(view: itemView, frame: itemFrame)
|
||||
}
|
||||
}
|
||||
itemFrame.origin.x += itemFrame.width + optionSpacing
|
||||
if itemFrame.maxX > availableWidth {
|
||||
itemFrame.origin.x = sideInset
|
||||
itemFrame.origin.y += starsOptionSize.height + optionSpacing
|
||||
}
|
||||
}
|
||||
|
||||
var removeIds: [AnyHashable] = []
|
||||
for (id, item) in self.starsItems {
|
||||
if !validIds.contains(id) {
|
||||
removeIds.append(id)
|
||||
if let itemView = item.view {
|
||||
if !transition.animation.isImmediate {
|
||||
itemView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false)
|
||||
itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
itemView.removeFromSuperview()
|
||||
})
|
||||
} else {
|
||||
itemView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for id in removeIds {
|
||||
self.starsItems.removeValue(forKey: id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: GiftOptionsScreenComponent, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
self.isUpdating = false
|
||||
}
|
||||
|
||||
let environment = environment[EnvironmentType.self].value
|
||||
let controller = environment.controller
|
||||
let themeUpdated = self.environment?.theme !== environment.theme
|
||||
self.environment = environment
|
||||
|
||||
if self.component == nil {
|
||||
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
if themeUpdated {
|
||||
self.backgroundColor = environment.theme.list.blocksBackgroundColor
|
||||
}
|
||||
|
||||
// let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let theme = environment.theme
|
||||
let strings = environment.strings
|
||||
|
||||
let textColor = theme.list.itemPrimaryTextColor
|
||||
let accentColor = theme.list.itemAccentColor
|
||||
|
||||
let textFont = Font.regular(15.0)
|
||||
let boldTextFont = Font.semibold(15.0)
|
||||
|
||||
let bottomContentInset: CGFloat = 24.0
|
||||
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
|
||||
let sectionSpacing: CGFloat = 24.0
|
||||
|
||||
let _ = bottomContentInset
|
||||
let _ = sectionSpacing
|
||||
|
||||
var contentHeight: CGFloat = 0.0
|
||||
contentHeight += environment.navigationHeight - 56.0 + 188.0
|
||||
|
||||
let headerSize = self.header.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
GiftAvatarComponent(
|
||||
context: component.context,
|
||||
theme: theme,
|
||||
peers: state.peer.flatMap { [$0] } ?? [],
|
||||
isVisible: true,
|
||||
hasIdleAnimations: true,
|
||||
color: UIColor(rgb: 0xf9b004),
|
||||
hasLargeParticles: true
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: min(414.0, availableSize.width), height: 220.0)
|
||||
)
|
||||
if let headerView = self.header.view {
|
||||
if headerView.superview == nil {
|
||||
self.addSubview(headerView)
|
||||
}
|
||||
transition.setBounds(view: headerView, bounds: CGRect(origin: .zero, size: headerSize))
|
||||
}
|
||||
|
||||
let topPanelSize = self.topPanel.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(BlurredBackgroundComponent(
|
||||
color: theme.rootController.navigationBar.blurredBackgroundColor
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: environment.navigationHeight)
|
||||
)
|
||||
|
||||
let topSeparatorSize = self.topSeparator.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(Rectangle(
|
||||
color: theme.rootController.navigationBar.separatorColor
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: UIScreenPixel)
|
||||
)
|
||||
let topPanelFrame = CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: topPanelSize.height))
|
||||
let topSeparatorFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelSize.height), size: CGSize(width: topSeparatorSize.width, height: topSeparatorSize.height))
|
||||
if let topPanelView = self.topPanel.view, let topSeparatorView = self.topSeparator.view {
|
||||
if topPanelView.superview == nil {
|
||||
self.addSubview(topPanelView)
|
||||
self.addSubview(topSeparatorView)
|
||||
}
|
||||
transition.setFrame(view: topPanelView, frame: topPanelFrame)
|
||||
transition.setFrame(view: topSeparatorView, frame: topSeparatorFrame)
|
||||
}
|
||||
|
||||
let cancelButtonSize = self.cancelButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
PlainButtonComponent(
|
||||
content: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: strings.Common_Cancel, font: Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor)),
|
||||
horizontalAlignment: .center
|
||||
)
|
||||
),
|
||||
effectAlignment: .center,
|
||||
action: {
|
||||
controller()?.dismiss()
|
||||
},
|
||||
animateScale: false
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: 100.0)
|
||||
)
|
||||
let cancelButtonFrame = CGRect(origin: CGPoint(x: environment.safeInsets.left + 16.0, y: environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0 - cancelButtonSize.height / 2.0), size: cancelButtonSize)
|
||||
if let cancelButtonView = self.cancelButton.view {
|
||||
if cancelButtonView.superview == nil {
|
||||
self.addSubview(cancelButtonView)
|
||||
}
|
||||
transition.setFrame(view: cancelButtonView, frame: cancelButtonFrame)
|
||||
}
|
||||
|
||||
let premiumTitleSize = self.premiumTitle.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: "Gift Premium", font: Font.bold(28.0), textColor: theme.rootController.navigationBar.primaryTextColor)),
|
||||
horizontalAlignment: .center
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: 100.0)
|
||||
)
|
||||
if let premiumTitleView = self.premiumTitle.view {
|
||||
if premiumTitleView.superview == nil {
|
||||
self.addSubview(premiumTitleView)
|
||||
}
|
||||
transition.setBounds(view: premiumTitleView, bounds: CGRect(origin: .zero, size: premiumTitleSize))
|
||||
}
|
||||
|
||||
if self.chevronImage == nil || self.chevronImage?.1 !== theme {
|
||||
self.chevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: accentColor)!, theme)
|
||||
}
|
||||
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: accentColor), linkAttribute: { contents in
|
||||
return (TelegramTextAttributes.URL, contents)
|
||||
})
|
||||
let peerName = state.peer?.compactDisplayTitle ?? ""
|
||||
|
||||
let premiumDescriptionString = parseMarkdownIntoAttributedString("Give **\(peerName)** access to exclusive features with Telegram Premium. [See Features >]()", attributes: markdownAttributes).mutableCopy() as! NSMutableAttributedString
|
||||
if let range = premiumDescriptionString.string.range(of: ">"), let chevronImage = self.chevronImage?.0 {
|
||||
premiumDescriptionString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: premiumDescriptionString.string))
|
||||
}
|
||||
let premiumDescriptionSize = self.premiumDescription.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(BalancedTextComponent(
|
||||
text: .plain(premiumDescriptionString),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 0,
|
||||
lineSpacing: 0.2,
|
||||
highlightColor: accentColor.withAlphaComponent(0.2),
|
||||
highlightAction: { attributes in
|
||||
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
|
||||
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
},
|
||||
tapAction: { _, _ in
|
||||
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: 1000.0)
|
||||
)
|
||||
let premiumDescriptionFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - premiumDescriptionSize.width) / 2.0), y: contentHeight), size: premiumDescriptionSize)
|
||||
if let premiumDescriptionView = self.premiumDescription.view {
|
||||
if premiumDescriptionView.superview == nil {
|
||||
self.scrollView.addSubview(premiumDescriptionView)
|
||||
}
|
||||
transition.setFrame(view: premiumDescriptionView, frame: premiumDescriptionFrame)
|
||||
}
|
||||
contentHeight += premiumDescriptionSize.height
|
||||
contentHeight += 11.0
|
||||
|
||||
let optionSpacing: CGFloat = 10.0
|
||||
let optionWidth = (availableSize.width - sideInset * 2.0 - optionSpacing * 2.0) / 3.0
|
||||
|
||||
if let premiumProducts = state.premiumProducts {
|
||||
let premiumOptionSize = CGSize(width: optionWidth, height: 178.0)
|
||||
|
||||
var validIds: [AnyHashable] = []
|
||||
var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: premiumOptionSize)
|
||||
for product in premiumProducts {
|
||||
let itemId = AnyHashable(product.storeProduct.id)
|
||||
validIds.append(itemId)
|
||||
|
||||
var itemTransition = transition
|
||||
let visibleItem: ComponentView<Empty>
|
||||
if let current = self.premiumItems[itemId] {
|
||||
visibleItem = current
|
||||
} else {
|
||||
visibleItem = ComponentView()
|
||||
if !transition.animation.isImmediate {
|
||||
itemTransition = .immediate
|
||||
}
|
||||
self.premiumItems[itemId] = visibleItem
|
||||
}
|
||||
|
||||
let title: String
|
||||
switch product.months {
|
||||
case 6:
|
||||
title = "6 months"
|
||||
case 12:
|
||||
title = "1 year"
|
||||
default:
|
||||
title = "3 months"
|
||||
}
|
||||
|
||||
let _ = visibleItem.update(
|
||||
transition: itemTransition,
|
||||
component: AnyComponent(
|
||||
PlainButtonComponent(
|
||||
content: AnyComponent(
|
||||
GiftItemComponent(
|
||||
context: component.context,
|
||||
theme: theme,
|
||||
peer: nil,
|
||||
subject: .premium(product.months),
|
||||
title: title,
|
||||
subtitle: "Premium",
|
||||
price: product.price,
|
||||
ribbon: product.discount.flatMap {
|
||||
GiftItemComponent.Ribbon(
|
||||
text: "-\($0)%",
|
||||
color: UIColor(rgb: 0xfa4846)
|
||||
)
|
||||
},
|
||||
isLoading: self.selectedPremiumGift == product.id
|
||||
)
|
||||
),
|
||||
effectAlignment: .center,
|
||||
action: { [weak self] in
|
||||
self?.selectedPremiumGift = product.id
|
||||
self?.state?.updated()
|
||||
|
||||
Queue.mainQueue().after(4.0, {
|
||||
self?.selectedPremiumGift = nil
|
||||
self?.state?.updated()
|
||||
})
|
||||
},
|
||||
animateAlpha: false
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: premiumOptionSize
|
||||
)
|
||||
if let itemView = visibleItem.view {
|
||||
if itemView.superview == nil {
|
||||
self.scrollView.addSubview(itemView)
|
||||
if !transition.animation.isImmediate {
|
||||
transition.animateAlpha(view: itemView, from: 0.0, to: 1.0)
|
||||
}
|
||||
}
|
||||
itemTransition.setFrame(view: itemView, frame: itemFrame)
|
||||
}
|
||||
itemFrame.origin.x += itemFrame.width + optionSpacing
|
||||
if itemFrame.maxX > availableSize.width {
|
||||
itemFrame.origin.x = sideInset
|
||||
itemFrame.origin.y += premiumOptionSize.height + optionSpacing
|
||||
}
|
||||
}
|
||||
|
||||
var removeIds: [AnyHashable] = []
|
||||
for (id, item) in self.premiumItems {
|
||||
if !validIds.contains(id) {
|
||||
removeIds.append(id)
|
||||
if let itemView = item.view {
|
||||
if !transition.animation.isImmediate {
|
||||
itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
itemView.removeFromSuperview()
|
||||
})
|
||||
} else {
|
||||
itemView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for id in removeIds {
|
||||
self.premiumItems.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
contentHeight += ceil(CGFloat(premiumProducts.count) / 3.0) * premiumOptionSize.height
|
||||
contentHeight += 66.0
|
||||
}
|
||||
|
||||
|
||||
let starsTitleSize = self.starsTitle.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: "Send a Gift", font: Font.bold(28.0), textColor: theme.rootController.navigationBar.primaryTextColor)),
|
||||
horizontalAlignment: .center
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: 100.0)
|
||||
)
|
||||
if let starsTitleView = self.starsTitle.view {
|
||||
if starsTitleView.superview == nil {
|
||||
self.addSubview(starsTitleView)
|
||||
}
|
||||
transition.setBounds(view: starsTitleView, bounds: CGRect(origin: .zero, size: starsTitleSize))
|
||||
}
|
||||
|
||||
let starsDescriptionString = parseMarkdownIntoAttributedString("Give **\(peerName)** gifts that can be kept on the profile or converted to Stars. [What are Stars >]()", attributes: markdownAttributes).mutableCopy() as! NSMutableAttributedString
|
||||
if let range = starsDescriptionString.string.range(of: ">"), let chevronImage = self.chevronImage?.0 {
|
||||
starsDescriptionString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: starsDescriptionString.string))
|
||||
}
|
||||
let starsDescriptionSize = self.starsDescription.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(BalancedTextComponent(
|
||||
text: .plain(starsDescriptionString),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 0,
|
||||
lineSpacing: 0.2,
|
||||
highlightColor: accentColor.withAlphaComponent(0.2),
|
||||
highlightAction: { attributes in
|
||||
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
|
||||
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
},
|
||||
tapAction: { _, _ in
|
||||
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: 1000.0)
|
||||
)
|
||||
let starsDescriptionFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - starsDescriptionSize.width) / 2.0), y: contentHeight), size: starsDescriptionSize)
|
||||
if let starsDescriptionView = self.starsDescription.view {
|
||||
if starsDescriptionView.superview == nil {
|
||||
self.scrollView.addSubview(starsDescriptionView)
|
||||
}
|
||||
transition.setFrame(view: starsDescriptionView, frame: starsDescriptionFrame)
|
||||
}
|
||||
contentHeight += starsDescriptionSize.height
|
||||
contentHeight += 16.0
|
||||
|
||||
let tabSelectorSize = self.tabSelector.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(TabSelectorComponent(
|
||||
context: component.context,
|
||||
colors: TabSelectorComponent.Colors(
|
||||
foreground: theme.list.itemSecondaryTextColor,
|
||||
selection: theme.list.itemSecondaryTextColor.withMultipliedAlpha(0.15),
|
||||
simple: true
|
||||
),
|
||||
items: [
|
||||
TabSelectorComponent.Item(
|
||||
id: AnyHashable(StarsFilter.all.rawValue),
|
||||
title: "All Gifts"
|
||||
),
|
||||
TabSelectorComponent.Item(
|
||||
id: AnyHashable(StarsFilter.limited.rawValue),
|
||||
title: "Limited"
|
||||
),
|
||||
TabSelectorComponent.Item(
|
||||
id: AnyHashable(StarsFilter.stars10.rawValue),
|
||||
title: "⭐️10"
|
||||
),
|
||||
TabSelectorComponent.Item(
|
||||
id: AnyHashable(StarsFilter.stars25.rawValue),
|
||||
title: "⭐️25"
|
||||
),
|
||||
TabSelectorComponent.Item(
|
||||
id: AnyHashable(StarsFilter.stars50.rawValue),
|
||||
title: "⭐️50"
|
||||
),
|
||||
TabSelectorComponent.Item(
|
||||
id: AnyHashable(StarsFilter.stars100.rawValue),
|
||||
title: "⭐️100"
|
||||
)
|
||||
],
|
||||
selectedId: AnyHashable(self.starsFilter.rawValue),
|
||||
setSelectedId: { [weak self] id in
|
||||
guard let self, let idValue = id.base as? Int, let starsFilter = StarsFilter(rawValue: idValue) else {
|
||||
return
|
||||
}
|
||||
if self.starsFilter != starsFilter {
|
||||
self.starsFilter = starsFilter
|
||||
self.state?.updated(transition: .easeInOut(duration: 0.25))
|
||||
}
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - 10.0 * 2.0, height: 50.0)
|
||||
)
|
||||
if let tabSelectorView = self.tabSelector.view {
|
||||
if tabSelectorView.superview == nil {
|
||||
self.scrollView.addSubview(tabSelectorView)
|
||||
}
|
||||
transition.setFrame(view: tabSelectorView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - tabSelectorSize.width) / 2.0), y: contentHeight), size: tabSelectorSize))
|
||||
}
|
||||
contentHeight += tabSelectorSize.height
|
||||
contentHeight += 19.0
|
||||
|
||||
if let starGifts = state.starGifts {
|
||||
self.starsItemsOrigin = contentHeight
|
||||
|
||||
let starsOptionSize = CGSize(width: optionWidth, height: 154.0)
|
||||
contentHeight += ceil(CGFloat(starGifts.count) / 3.0) * starsOptionSize.height
|
||||
contentHeight += 66.0
|
||||
}
|
||||
|
||||
contentHeight += bottomContentInset
|
||||
contentHeight += environment.safeInsets.bottom
|
||||
|
||||
let previousBounds = self.scrollView.bounds
|
||||
|
||||
let contentSize = CGSize(width: availableSize.width, height: contentHeight)
|
||||
if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) {
|
||||
self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize)
|
||||
}
|
||||
if self.scrollView.contentSize != contentSize {
|
||||
self.scrollView.contentSize = contentSize
|
||||
}
|
||||
let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: 0.0, right: 0.0)
|
||||
if self.scrollView.scrollIndicatorInsets != scrollInsets {
|
||||
self.scrollView.scrollIndicatorInsets = scrollInsets
|
||||
}
|
||||
|
||||
if !previousBounds.isEmpty, !transition.animation.isImmediate {
|
||||
let bounds = self.scrollView.bounds
|
||||
if bounds.maxY != previousBounds.maxY {
|
||||
let offsetY = previousBounds.maxY - bounds.maxY
|
||||
transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true)
|
||||
}
|
||||
}
|
||||
|
||||
self.topOverscrollLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -3000.0), size: CGSize(width: availableSize.width, height: 3000.0))
|
||||
|
||||
self.updateScrolling(transition: transition)
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
final class State: ComponentState {
|
||||
private let context: AccountContext
|
||||
private var disposable: Disposable?
|
||||
private var updateDisposable: Disposable?
|
||||
|
||||
fileprivate var peer: EnginePeer?
|
||||
fileprivate var premiumProducts: [PremiumGiftProduct]?
|
||||
fileprivate var starGifts: [StarGift]?
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
peerId: EnginePeer.Id,
|
||||
premiumOptions: [CachedPremiumGiftOption]
|
||||
) {
|
||||
self.context = context
|
||||
|
||||
super.init()
|
||||
|
||||
let availableProducts: Signal<[InAppPurchaseManager.Product], NoError>
|
||||
if let inAppPurchaseManager = context.inAppPurchaseManager {
|
||||
availableProducts = inAppPurchaseManager.availableProducts
|
||||
} else {
|
||||
availableProducts = .single([])
|
||||
}
|
||||
|
||||
self.disposable = combineLatest(
|
||||
queue: Queue.mainQueue(),
|
||||
context.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer.init(id: peerId)
|
||||
),
|
||||
availableProducts,
|
||||
context.engine.payments.cachedStarGifts()
|
||||
).start(next: { [weak self] peer, availableProducts, starGifts in
|
||||
guard let self, let peer else {
|
||||
return
|
||||
}
|
||||
self.peer = peer
|
||||
|
||||
let shortestOptionPrice: (Int64, NSDecimalNumber)
|
||||
if let product = availableProducts.first(where: { $0.id.hasSuffix(".monthly") }) {
|
||||
shortestOptionPrice = (Int64(Float(product.priceCurrencyAndAmount.amount)), product.priceValue)
|
||||
} else {
|
||||
shortestOptionPrice = (1, NSDecimalNumber(decimal: 1))
|
||||
}
|
||||
|
||||
var premiumProducts: [PremiumGiftProduct] = []
|
||||
for option in premiumOptions {
|
||||
if let product = availableProducts.first(where: { $0.id == option.storeProductId }), !product.isSubscription {
|
||||
let fraction = Float(product.priceCurrencyAndAmount.amount) / Float(option.months) / Float(shortestOptionPrice.0)
|
||||
let discountValue = Int(round((1.0 - fraction) * 20.0) * 5.0)
|
||||
premiumProducts.append(PremiumGiftProduct(giftOption: option, storeProduct: product, discount: discountValue > 0 ? discountValue : nil))
|
||||
}
|
||||
}
|
||||
self.premiumProducts = premiumProducts.sorted(by: { $0.months < $1.months })
|
||||
|
||||
self.starGifts = starGifts
|
||||
|
||||
self.updated()
|
||||
})
|
||||
|
||||
self.updateDisposable = self.context.engine.payments.keepStarGiftsUpdated().start()
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable?.dispose()
|
||||
self.updateDisposable?.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
func makeState() -> State {
|
||||
return State(context: self.context, peerId: self.peerId, premiumOptions: self.premiumOptions)
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
public final class GiftOptionsScreen: ViewControllerComponentContainer, GiftOptionsScreenProtocol {
|
||||
private let context: AccountContext
|
||||
|
||||
public init(context: AccountContext, peerId: EnginePeer.Id, premiumOptions: [CachedPremiumGiftOption]) {
|
||||
self.context = context
|
||||
|
||||
super.init(context: context, component: GiftOptionsScreenComponent(
|
||||
context: context,
|
||||
peerId: peerId,
|
||||
premiumOptions: premiumOptions
|
||||
), navigationBarAppearance: .none, theme: .default, updatedPresentationData: nil)
|
||||
|
||||
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.context.sharedContext.currentPresentationData.with { $0 }.strings.Common_Back, style: .plain, target: nil, action: nil)
|
||||
|
||||
|
||||
self.scrollToTop = { [weak self] in
|
||||
guard let self, let componentView = self.node.hostView.componentView as? GiftOptionsScreenComponent.View else {
|
||||
return
|
||||
}
|
||||
componentView.scrollToTop()
|
||||
}
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
private struct PremiumGiftProduct: Equatable {
|
||||
let giftOption: CachedPremiumGiftOption
|
||||
let storeProduct: InAppPurchaseManager.Product
|
||||
let discount: Int?
|
||||
|
||||
var id: String {
|
||||
return self.storeProduct.id
|
||||
}
|
||||
|
||||
var months: Int32 {
|
||||
return self.giftOption.months
|
||||
}
|
||||
|
||||
var price: String {
|
||||
return self.storeProduct.price
|
||||
}
|
||||
|
||||
var pricePerMonth: String {
|
||||
return self.storeProduct.pricePerMonth(Int(self.months))
|
||||
}
|
||||
}
|
45
submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD
Normal file
45
submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD
Normal file
@ -0,0 +1,45 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "GiftSetupScreen",
|
||||
module_name = "GiftSetupScreen",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/Postbox",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/TelegramUIPreferences",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/PresentationDataUtils",
|
||||
"//submodules/Markdown",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
"//submodules/Components/ViewControllerComponent",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/Components/BalancedTextComponent",
|
||||
"//submodules/TelegramUI/Components/ListSectionComponent",
|
||||
"//submodules/TelegramUI/Components/ListActionItemComponent",
|
||||
"//submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent",
|
||||
"//submodules/TelegramUI/Components/LottieComponent",
|
||||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||
"//submodules/TelegramUI/Components/ButtonComponent",
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/WallpaperBackgroundNode",
|
||||
"//submodules/ChatPresentationInterfaceState",
|
||||
"//submodules/TelegramUI/Components/TextFieldComponent",
|
||||
"//submodules/TelegramUI/Components/ListItemComponentAdaptor",
|
||||
"//submodules/BotPaymentsUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -0,0 +1,366 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import ItemListUI
|
||||
import PresentationDataUtils
|
||||
import AccountContext
|
||||
import WallpaperBackgroundNode
|
||||
import ListItemComponentAdaptor
|
||||
|
||||
final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAdaptor.ItemGenerator {
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let componentTheme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let sectionId: ItemListSectionId
|
||||
let fontSize: PresentationFontSize
|
||||
let chatBubbleCorners: PresentationChatBubbleCorners
|
||||
let wallpaper: TelegramWallpaper
|
||||
let dateTimeFormat: PresentationDateTimeFormat
|
||||
let nameDisplayOrder: PresentationPersonNameOrder
|
||||
|
||||
let accountPeer: EnginePeer?
|
||||
let gift: StarGift
|
||||
let text: String
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
componentTheme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
sectionId: ItemListSectionId,
|
||||
fontSize: PresentationFontSize,
|
||||
chatBubbleCorners: PresentationChatBubbleCorners,
|
||||
wallpaper: TelegramWallpaper,
|
||||
dateTimeFormat: PresentationDateTimeFormat,
|
||||
nameDisplayOrder: PresentationPersonNameOrder,
|
||||
accountPeer: EnginePeer?,
|
||||
gift: StarGift,
|
||||
text: String
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.componentTheme = componentTheme
|
||||
self.strings = strings
|
||||
self.sectionId = sectionId
|
||||
self.fontSize = fontSize
|
||||
self.chatBubbleCorners = chatBubbleCorners
|
||||
self.wallpaper = wallpaper
|
||||
self.dateTimeFormat = dateTimeFormat
|
||||
self.nameDisplayOrder = nameDisplayOrder
|
||||
self.accountPeer = accountPeer
|
||||
self.gift = gift
|
||||
self.text = text
|
||||
}
|
||||
|
||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = ChatGiftPreviewItemNode()
|
||||
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
node.insets = layout.insets
|
||||
|
||||
Queue.mainQueue().async {
|
||||
completion(node, {
|
||||
return (nil, { _ in apply() })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
if let nodeValue = node() as? ChatGiftPreviewItemNode {
|
||||
let makeLayout = nodeValue.asyncLayout()
|
||||
|
||||
async {
|
||||
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, { _ in
|
||||
apply()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func item() -> ListViewItem {
|
||||
return self
|
||||
}
|
||||
|
||||
public static func ==(lhs: ChatGiftPreviewItem, rhs: ChatGiftPreviewItem) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.componentTheme !== rhs.componentTheme {
|
||||
return false
|
||||
}
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.fontSize != rhs.fontSize {
|
||||
return false
|
||||
}
|
||||
if lhs.chatBubbleCorners != rhs.chatBubbleCorners {
|
||||
return false
|
||||
}
|
||||
if lhs.wallpaper != rhs.wallpaper {
|
||||
return false
|
||||
}
|
||||
if lhs.dateTimeFormat != rhs.dateTimeFormat {
|
||||
return false
|
||||
}
|
||||
if lhs.nameDisplayOrder != rhs.nameDisplayOrder {
|
||||
return false
|
||||
}
|
||||
if lhs.accountPeer != rhs.accountPeer {
|
||||
return false
|
||||
}
|
||||
if lhs.text != rhs.text {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
final class ChatGiftPreviewItemNode: ListViewItemNode {
|
||||
private var backgroundNode: WallpaperBackgroundNode?
|
||||
private let topStripeNode: ASDisplayNode
|
||||
private let bottomStripeNode: ASDisplayNode
|
||||
private let maskNode: ASImageNode
|
||||
|
||||
private let containerNode: ASDisplayNode
|
||||
private var messageNodes: [ListViewItemNode]?
|
||||
private var itemHeaderNodes: [ListViewItemNode.HeaderId: ListViewItemHeaderNode] = [:]
|
||||
|
||||
private var item: ChatGiftPreviewItem?
|
||||
|
||||
private let disposable = MetaDisposable()
|
||||
|
||||
init() {
|
||||
self.topStripeNode = ASDisplayNode()
|
||||
self.topStripeNode.isLayerBacked = true
|
||||
|
||||
self.bottomStripeNode = ASDisplayNode()
|
||||
self.bottomStripeNode.isLayerBacked = true
|
||||
|
||||
self.maskNode = ASImageNode()
|
||||
|
||||
self.containerNode = ASDisplayNode()
|
||||
self.containerNode.subnodeTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.clipsToBounds = true
|
||||
self.isUserInteractionEnabled = false
|
||||
|
||||
self.addSubnode(self.containerNode)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable.dispose()
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: ChatGiftPreviewItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let currentNodes = self.messageNodes
|
||||
|
||||
var currentBackgroundNode = self.backgroundNode
|
||||
|
||||
return { item, params, neighbors in
|
||||
if currentBackgroundNode == nil {
|
||||
currentBackgroundNode = createWallpaperBackgroundNode(context: item.context, forChatDisplay: false)
|
||||
currentBackgroundNode?.update(wallpaper: item.wallpaper, animated: false)
|
||||
currentBackgroundNode?.updateBubbleTheme(bubbleTheme: item.componentTheme, bubbleCorners: item.chatBubbleCorners)
|
||||
}
|
||||
|
||||
var insets: UIEdgeInsets
|
||||
let separatorHeight = UIScreenPixel
|
||||
|
||||
let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(1))
|
||||
|
||||
var items: [ListViewItem] = []
|
||||
for _ in 0 ..< 1 {
|
||||
let authorPeerId = item.context.account.peerId
|
||||
|
||||
var peers = SimpleDictionary<PeerId, Peer>()
|
||||
let messages = SimpleDictionary<MessageId, Message>()
|
||||
|
||||
peers[authorPeerId] = item.accountPeer?._asPeer()
|
||||
|
||||
let media: [Media] = [
|
||||
TelegramMediaAction(action: .starGift(gift: item.gift, convertStars: item.gift.price, text: item.text, entities: [], nameHidden: false, savedToProfile: false, converted: false))
|
||||
//TelegramMediaAction(action: .starGift(amount: item.gift.price, giftId: item.gift.id, nameHidden: false, limitNumber: item.gift.availability != nil ? 1 : nil, limitTotal: item.gift.availability?.total, text: item.text, entities: []))
|
||||
]
|
||||
let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[authorPeerId], text: "", attributes: [], media: media, peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
|
||||
items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false))
|
||||
}
|
||||
|
||||
var nodes: [ListViewItemNode] = []
|
||||
if let messageNodes = currentNodes {
|
||||
nodes = messageNodes
|
||||
for i in 0 ..< items.count {
|
||||
let itemNode = messageNodes[i]
|
||||
items[i].updateNode(async: { $0() }, node: {
|
||||
return itemNode
|
||||
}, params: params, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], animation: .None, completion: { (layout, apply) in
|
||||
let nodeFrame = CGRect(origin: itemNode.frame.origin, size: CGSize(width: layout.size.width, height: layout.size.height))
|
||||
|
||||
itemNode.contentSize = layout.contentSize
|
||||
itemNode.insets = layout.insets
|
||||
itemNode.frame = nodeFrame
|
||||
itemNode.isUserInteractionEnabled = false
|
||||
|
||||
Queue.mainQueue().after(0.01) {
|
||||
apply(ListViewItemApply(isOnScreen: true))
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
var messageNodes: [ListViewItemNode] = []
|
||||
for i in 0 ..< items.count {
|
||||
var itemNode: ListViewItemNode?
|
||||
items[i].nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], completion: { node, apply in
|
||||
itemNode = node
|
||||
apply().1(ListViewItemApply(isOnScreen: true))
|
||||
})
|
||||
itemNode!.isUserInteractionEnabled = false
|
||||
messageNodes.append(itemNode!)
|
||||
}
|
||||
nodes = messageNodes
|
||||
}
|
||||
|
||||
var contentSize = CGSize(width: params.width, height: 4.0 + 4.0)
|
||||
// for node in nodes {
|
||||
// contentSize.height += node.frame.size.height
|
||||
// }
|
||||
contentSize.height = 346.0
|
||||
insets = itemListNeighborsGroupedInsets(neighbors, params)
|
||||
if params.width <= 320.0 {
|
||||
insets.top = 0.0
|
||||
}
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: .zero)
|
||||
let layoutSize = layout.size
|
||||
|
||||
return (layout, { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
|
||||
if let currentBackgroundNode {
|
||||
currentBackgroundNode.update(wallpaper: item.wallpaper, animated: false)
|
||||
currentBackgroundNode.updateBubbleTheme(bubbleTheme: item.theme, bubbleCorners: item.chatBubbleCorners)
|
||||
}
|
||||
|
||||
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: contentSize)
|
||||
|
||||
strongSelf.messageNodes = nodes
|
||||
//var topOffset: CGFloat = 4.0
|
||||
for node in nodes {
|
||||
if node.supernode == nil {
|
||||
strongSelf.containerNode.addSubnode(node)
|
||||
}
|
||||
node.updateFrame(CGRect(origin: CGPoint(x: 0.0, y: floor((contentSize.height - node.frame.size.height) / 2.0)), size: node.frame.size), within: layoutSize)
|
||||
//topOffset += node.frame.size.height
|
||||
}
|
||||
|
||||
if let currentBackgroundNode = currentBackgroundNode, strongSelf.backgroundNode !== currentBackgroundNode {
|
||||
strongSelf.backgroundNode = currentBackgroundNode
|
||||
strongSelf.insertSubnode(currentBackgroundNode, at: 0)
|
||||
}
|
||||
|
||||
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
|
||||
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
|
||||
|
||||
if strongSelf.topStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
|
||||
}
|
||||
if strongSelf.maskNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
|
||||
}
|
||||
|
||||
if params.isStandalone {
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
strongSelf.bottomStripeNode.isHidden = true
|
||||
strongSelf.maskNode.isHidden = true
|
||||
} else {
|
||||
let hasCorners = itemListHasRoundedBlockLayout(params)
|
||||
|
||||
var hasTopCorners = false
|
||||
var hasBottomCorners = false
|
||||
|
||||
switch neighbors.top {
|
||||
case .sameSection(false):
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
default:
|
||||
hasTopCorners = true
|
||||
strongSelf.topStripeNode.isHidden = hasCorners
|
||||
}
|
||||
let bottomStripeInset: CGFloat
|
||||
let bottomStripeOffset: CGFloat
|
||||
switch neighbors.bottom {
|
||||
case .sameSection(false):
|
||||
bottomStripeInset = 0.0
|
||||
bottomStripeOffset = -separatorHeight
|
||||
strongSelf.bottomStripeNode.isHidden = false
|
||||
default:
|
||||
bottomStripeInset = 0.0
|
||||
bottomStripeOffset = 0.0
|
||||
hasBottomCorners = true
|
||||
strongSelf.bottomStripeNode.isHidden = hasCorners
|
||||
}
|
||||
|
||||
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.componentTheme, top: hasTopCorners, bottom: hasBottomCorners) : nil
|
||||
|
||||
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
|
||||
}
|
||||
|
||||
let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||
|
||||
let displayMode: WallpaperDisplayMode
|
||||
if abs(params.availableHeight - params.width) < 100.0, params.availableHeight > 700.0 {
|
||||
displayMode = .halfAspectFill
|
||||
} else {
|
||||
if backgroundFrame.width > backgroundFrame.height * 4.0 {
|
||||
if params.availableHeight < 700.0 {
|
||||
displayMode = .halfAspectFill
|
||||
} else {
|
||||
displayMode = .aspectFill
|
||||
}
|
||||
} else {
|
||||
displayMode = .aspectFill
|
||||
}
|
||||
}
|
||||
|
||||
if let backgroundNode = strongSelf.backgroundNode {
|
||||
backgroundNode.frame = backgroundFrame
|
||||
backgroundNode.updateLayout(size: backgroundNode.bounds.size, displayMode: displayMode, transition: .immediate)
|
||||
}
|
||||
strongSelf.maskNode.frame = backgroundFrame.insetBy(dx: params.leftInset, dy: 0.0)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
}
|
@ -0,0 +1,607 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import PresentationDataUtils
|
||||
import AccountContext
|
||||
import ComponentFlow
|
||||
import ViewControllerComponent
|
||||
import MultilineTextComponent
|
||||
import BalancedTextComponent
|
||||
import ListSectionComponent
|
||||
import ListActionItemComponent
|
||||
import ListMultilineTextFieldItemComponent
|
||||
import ListItemComponentAdaptor
|
||||
import BundleIconComponent
|
||||
import LottieComponent
|
||||
import TextFieldComponent
|
||||
import ButtonComponent
|
||||
import BotPaymentsUI
|
||||
|
||||
final class GiftSetupScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let context: AccountContext
|
||||
let peerId: EnginePeer.Id
|
||||
let gift: StarGift
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
peerId: EnginePeer.Id,
|
||||
gift: StarGift
|
||||
) {
|
||||
self.context = context
|
||||
self.peerId = peerId
|
||||
self.gift = gift
|
||||
}
|
||||
|
||||
static func ==(lhs: GiftSetupScreenComponent, rhs: GiftSetupScreenComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.peerId != rhs.peerId {
|
||||
return false
|
||||
}
|
||||
if lhs.gift != rhs.gift {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private final class ScrollView: UIScrollView {
|
||||
override func touchesShouldCancel(in view: UIView) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
final class View: UIView, UIScrollViewDelegate {
|
||||
private let topOverscrollLayer = SimpleLayer()
|
||||
private let scrollView: ScrollView
|
||||
|
||||
private let navigationTitle = ComponentView<Empty>()
|
||||
private let introContent = ComponentView<Empty>()
|
||||
private let introSection = ComponentView<Empty>()
|
||||
private let hideSection = ComponentView<Empty>()
|
||||
private let button = ComponentView<Empty>()
|
||||
|
||||
private var ignoreScrolling: Bool = false
|
||||
private var isUpdating: Bool = false
|
||||
|
||||
private var component: GiftSetupScreenComponent?
|
||||
private(set) weak var state: EmptyComponentState?
|
||||
private var environment: EnvironmentType?
|
||||
|
||||
private let introPlaceholderTag = NSObject()
|
||||
private let textInputState = ListMultilineTextFieldItemComponent.ExternalState()
|
||||
private let textInputTag = NSObject()
|
||||
private var resetText: String?
|
||||
|
||||
private var hideName = false
|
||||
|
||||
private var previousHadInputHeight: Bool = false
|
||||
private var recenterOnTag: NSObject?
|
||||
|
||||
private var peerMap: [EnginePeer.Id: EnginePeer] = [:]
|
||||
|
||||
private var starImage: (UIImage, PresentationTheme)?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.scrollView = ScrollView()
|
||||
self.scrollView.showsVerticalScrollIndicator = true
|
||||
self.scrollView.showsHorizontalScrollIndicator = false
|
||||
self.scrollView.scrollsToTop = false
|
||||
self.scrollView.delaysContentTouches = false
|
||||
self.scrollView.canCancelContentTouches = true
|
||||
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
if #available(iOS 13.0, *) {
|
||||
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
|
||||
}
|
||||
self.scrollView.alwaysBounceVertical = true
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.scrollView.delegate = self
|
||||
self.addSubview(self.scrollView)
|
||||
|
||||
self.scrollView.layer.addSublayer(self.topOverscrollLayer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
}
|
||||
|
||||
func scrollToTop() {
|
||||
self.scrollView.setContentOffset(CGPoint(), animated: true)
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
if !self.ignoreScrolling {
|
||||
self.updateScrolling(transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
private var scrolledUp = true
|
||||
private func updateScrolling(transition: ComponentTransition) {
|
||||
let navigationRevealOffsetY: CGFloat = 0.0
|
||||
|
||||
let navigationAlphaDistance: CGFloat = 16.0
|
||||
let navigationAlpha: CGFloat = max(0.0, min(1.0, (self.scrollView.contentOffset.y - navigationRevealOffsetY) / navigationAlphaDistance))
|
||||
if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar {
|
||||
transition.setAlpha(layer: navigationBar.backgroundNode.layer, alpha: navigationAlpha)
|
||||
transition.setAlpha(layer: navigationBar.stripeNode.layer, alpha: navigationAlpha)
|
||||
}
|
||||
|
||||
var scrolledUp = false
|
||||
if navigationAlpha < 0.5 {
|
||||
scrolledUp = true
|
||||
} else if navigationAlpha > 0.5 {
|
||||
scrolledUp = false
|
||||
}
|
||||
|
||||
if self.scrolledUp != scrolledUp {
|
||||
self.scrolledUp = scrolledUp
|
||||
if !self.isUpdating {
|
||||
self.state?.updated()
|
||||
}
|
||||
}
|
||||
|
||||
if let navigationTitleView = self.navigationTitle.view {
|
||||
transition.setAlpha(view: navigationTitleView, alpha: 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
func proceed() {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
let source: BotPaymentInvoiceSource = .starGift(hideName: self.hideName, peerId: component.peerId, giftId: component.gift.id, text: self.textInputState.text.string, entities: [])
|
||||
let inputData = BotCheckoutController.InputData.fetch(context: component.context, source: source)
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<BotCheckoutController.InputData?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|
||||
let _ = (inputData
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] inputData in
|
||||
guard let inputData else {
|
||||
return
|
||||
}
|
||||
let _ = (component.context.engine.payments.sendStarsPaymentForm(formId: inputData.form.id, source: source)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
guard let self, let controller = self.environment?.controller(), let navigationController = controller.navigationController as? NavigationController else {
|
||||
return
|
||||
}
|
||||
|
||||
var controllers = navigationController.viewControllers
|
||||
controllers = controllers.filter { !($0 is GiftSetupScreen) && !($0 is GiftOptionsScreenProtocol) }
|
||||
var foundController = false
|
||||
for controller in controllers.reversed() {
|
||||
if let chatController = controller as? ChatController, case .peer(id: component.peerId) = chatController.chatLocation {
|
||||
chatController.hintPlayNextOutgoingGift()
|
||||
foundController = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundController {
|
||||
let chatController = component.context.sharedContext.makeChatController(context: component.context, chatLocation: .peer(id: component.peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil)
|
||||
chatController.hintPlayNextOutgoingGift()
|
||||
controllers.append(chatController)
|
||||
}
|
||||
navigationController.setViewControllers(controllers, animated: true)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func update(component: GiftSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
self.isUpdating = false
|
||||
}
|
||||
|
||||
if self.component == nil {
|
||||
let _ = (component.context.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: component.peerId),
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: component.context.account.peerId)
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer, accountPeer in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let peer {
|
||||
self.peerMap[peer.id] = peer
|
||||
}
|
||||
if let accountPeer {
|
||||
self.peerMap[accountPeer.id] = accountPeer
|
||||
}
|
||||
|
||||
self.state?.updated()
|
||||
})
|
||||
}
|
||||
|
||||
let environment = environment[EnvironmentType.self].value
|
||||
let themeUpdated = self.environment?.theme !== environment.theme
|
||||
self.environment = environment
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let alphaTransition: ComponentTransition
|
||||
if !transition.animation.isImmediate {
|
||||
alphaTransition = .easeInOut(duration: 0.25)
|
||||
} else {
|
||||
alphaTransition = .immediate
|
||||
}
|
||||
|
||||
if themeUpdated {
|
||||
self.backgroundColor = environment.theme.list.blocksBackgroundColor
|
||||
}
|
||||
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let _ = alphaTransition
|
||||
let _ = presentationData
|
||||
|
||||
let navigationTitleSize = self.navigationTitle.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: "Send a Gift", font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)),
|
||||
horizontalAlignment: .center
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: 100.0)
|
||||
)
|
||||
let navigationTitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - navigationTitleSize.width) / 2.0), y: environment.statusBarHeight + floor((environment.navigationHeight - environment.statusBarHeight - navigationTitleSize.height) / 2.0)), size: navigationTitleSize)
|
||||
if let navigationTitleView = self.navigationTitle.view {
|
||||
if navigationTitleView.superview == nil {
|
||||
if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar {
|
||||
navigationBar.view.addSubview(navigationTitleView)
|
||||
}
|
||||
}
|
||||
transition.setFrame(view: navigationTitleView, frame: navigationTitleFrame)
|
||||
}
|
||||
|
||||
let bottomContentInset: CGFloat = 24.0
|
||||
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
|
||||
let sectionSpacing: CGFloat = 24.0
|
||||
|
||||
var contentHeight: CGFloat = 0.0
|
||||
|
||||
contentHeight += environment.navigationHeight
|
||||
contentHeight += 26.0
|
||||
|
||||
self.recenterOnTag = nil
|
||||
if let hint = transition.userData(TextFieldComponent.AnimationHint.self), let targetView = hint.view {
|
||||
if let textView = self.introSection.findTaggedView(tag: self.textInputTag) {
|
||||
if targetView.isDescendant(of: textView) {
|
||||
self.recenterOnTag = self.textInputTag
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var introSectionItems: [AnyComponentWithIdentity<Empty>] = []
|
||||
introSectionItems.append(AnyComponentWithIdentity(id: introSectionItems.count, component: AnyComponent(Rectangle(color: .clear, height: 346.0, tag: self.introPlaceholderTag))))
|
||||
introSectionItems.append(AnyComponentWithIdentity(id: introSectionItems.count, component: AnyComponent(ListMultilineTextFieldItemComponent(
|
||||
externalState: self.textInputState,
|
||||
context: component.context,
|
||||
theme: environment.theme,
|
||||
strings: environment.strings,
|
||||
initialText: "",
|
||||
resetText: self.resetText.flatMap {
|
||||
return ListMultilineTextFieldItemComponent.ResetText(value: $0)
|
||||
},
|
||||
placeholder: environment.strings.Business_Intro_IntroTextPlaceholder,
|
||||
autocapitalizationType: .none,
|
||||
autocorrectionType: .no,
|
||||
returnKeyType: .done,
|
||||
characterLimit: 70,
|
||||
displayCharacterLimit: true,
|
||||
emptyLineHandling: .notAllowed,
|
||||
updated: { _ in
|
||||
},
|
||||
returnKeyAction: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let titleView = self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View {
|
||||
titleView.endEditing(true)
|
||||
}
|
||||
},
|
||||
textUpdateTransition: .spring(duration: 0.4),
|
||||
tag: self.textInputTag
|
||||
))))
|
||||
self.resetText = nil
|
||||
|
||||
let introSectionSize = self.introSection.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(ListSectionComponent(
|
||||
theme: environment.theme,
|
||||
header: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: "CUSTOMIZE YOUR GIFT",
|
||||
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
||||
textColor: environment.theme.list.freeTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 0
|
||||
)),
|
||||
footer: nil,
|
||||
items: introSectionItems
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
|
||||
)
|
||||
let introSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: introSectionSize)
|
||||
if let introSectionView = self.introSection.view {
|
||||
if introSectionView.superview == nil {
|
||||
self.scrollView.addSubview(introSectionView)
|
||||
self.introSection.parentState = state
|
||||
}
|
||||
transition.setFrame(view: introSectionView, frame: introSectionFrame)
|
||||
}
|
||||
contentHeight += introSectionSize.height
|
||||
contentHeight += sectionSpacing
|
||||
|
||||
// let titleText: String
|
||||
// if self.titleInputState.text.string.isEmpty {
|
||||
// titleText = environment.strings.Conversation_EmptyPlaceholder
|
||||
// } else {
|
||||
// let rawTitle = self.titleInputState.text.string
|
||||
// titleText = rawTitle.count <= maxTitleLength ? rawTitle : String(rawTitle[rawTitle.startIndex ..< rawTitle.index(rawTitle.startIndex, offsetBy: maxTitleLength)])
|
||||
// }
|
||||
|
||||
// let textText: String
|
||||
// if self.textInputState.text.string.isEmpty {
|
||||
// textText = environment.strings.Conversation_GreetingText
|
||||
// } else {
|
||||
// let rawText = self.textInputState.text.string
|
||||
// textText = rawText.count <= maxTextLength ? rawText : String(rawText[rawText.startIndex ..< rawText.index(rawText.startIndex, offsetBy: maxTextLength)])
|
||||
// }
|
||||
|
||||
let listItemParams = ListViewItemLayoutParams(width: availableSize.width - sideInset * 2.0, leftInset: 0.0, rightInset: 0.0, availableHeight: 10000.0, isStandalone: true)
|
||||
let introContentSize = self.introContent.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
ListItemComponentAdaptor(
|
||||
itemGenerator: ChatGiftPreviewItem(
|
||||
context: component.context,
|
||||
theme: environment.theme,
|
||||
componentTheme: environment.theme,
|
||||
strings: environment.strings,
|
||||
sectionId: 0,
|
||||
fontSize: presentationData.chatFontSize,
|
||||
chatBubbleCorners: presentationData.chatBubbleCorners,
|
||||
wallpaper: presentationData.chatWallpaper,
|
||||
dateTimeFormat: environment.dateTimeFormat,
|
||||
nameDisplayOrder: presentationData.nameDisplayOrder,
|
||||
accountPeer: self.peerMap[component.context.account.peerId],
|
||||
gift: component.gift,
|
||||
text: self.textInputState.text.string
|
||||
),
|
||||
params: listItemParams
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
|
||||
)
|
||||
if let introContentView = self.introContent.view {
|
||||
if introContentView.superview == nil {
|
||||
if let placeholderView = self.introSection.findTaggedView(tag: self.introPlaceholderTag) {
|
||||
placeholderView.addSubview(introContentView)
|
||||
}
|
||||
}
|
||||
transition.setFrame(view: introContentView, frame: CGRect(origin: CGPoint(), size: introContentSize))
|
||||
}
|
||||
|
||||
if self.recenterOnTag == nil && self.previousHadInputHeight != (environment.inputHeight > 0.0) {
|
||||
if self.textInputState.isEditing {
|
||||
self.recenterOnTag = self.textInputTag
|
||||
}
|
||||
}
|
||||
self.previousHadInputHeight = environment.inputHeight > 0.0
|
||||
|
||||
let peerName = self.peerMap[component.peerId]?.compactDisplayTitle ?? ""
|
||||
let hideSectionSize = self.hideSection.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(ListSectionComponent(
|
||||
theme: environment.theme,
|
||||
header: nil,
|
||||
footer: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: "Hide my name and message from visitors to \(peerName)'s profile. \(peerName) will still see your name and message.",
|
||||
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
||||
textColor: environment.theme.list.freeTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 0
|
||||
)),
|
||||
items: [
|
||||
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
|
||||
theme: environment.theme,
|
||||
title: AnyComponent(VStack([
|
||||
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: "Hide My Name",
|
||||
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
|
||||
textColor: environment.theme.list.itemPrimaryTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 1
|
||||
))),
|
||||
], alignment: .left, spacing: 2.0)),
|
||||
accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.hideName, action: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.hideName = !self.hideName
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
})),
|
||||
action: nil
|
||||
)))
|
||||
]
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
|
||||
)
|
||||
let hideSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: hideSectionSize)
|
||||
if let hideSectionView = self.hideSection.view {
|
||||
if hideSectionView.superview == nil {
|
||||
self.scrollView.addSubview(hideSectionView)
|
||||
}
|
||||
transition.setFrame(view: hideSectionView, frame: hideSectionFrame)
|
||||
}
|
||||
contentHeight += hideSectionSize.height
|
||||
|
||||
contentHeight += bottomContentInset
|
||||
|
||||
let inputHeight: CGFloat = environment.inputHeight
|
||||
let combinedBottomInset = max(inputHeight, environment.safeInsets.bottom)
|
||||
contentHeight += combinedBottomInset
|
||||
|
||||
|
||||
if self.starImage == nil || self.starImage?.1 !== environment.theme {
|
||||
self.starImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: environment.theme.list.itemCheckColors.foregroundColor)!, environment.theme)
|
||||
}
|
||||
let amountString = presentationStringsFormattedNumber(Int32(component.gift.price), presentationData.dateTimeFormat.groupingSeparator)
|
||||
let buttonAttributedString = NSMutableAttributedString(string: "Send a Gift for # \(amountString)", font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
|
||||
if let range = buttonAttributedString.string.range(of: "#"), let starImage = self.starImage?.0 {
|
||||
buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string))
|
||||
buttonAttributedString.addAttribute(.foregroundColor, value: environment.theme.list.itemCheckColors.foregroundColor, range: NSRange(range, in: buttonAttributedString.string))
|
||||
buttonAttributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: buttonAttributedString.string))
|
||||
}
|
||||
|
||||
let buttonSize = self.button.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(ButtonComponent(
|
||||
background: ButtonComponent.Background(
|
||||
color: environment.theme.list.itemCheckColors.fillColor,
|
||||
foreground: environment.theme.list.itemCheckColors.foregroundColor,
|
||||
pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9),
|
||||
cornerRadius: 10.0
|
||||
),
|
||||
content: AnyComponentWithIdentity(
|
||||
id: AnyHashable(0),
|
||||
component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))
|
||||
),
|
||||
isEnabled: true,
|
||||
displaysProgress: false,
|
||||
action: { [weak self] in
|
||||
self?.proceed()
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50)
|
||||
)
|
||||
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)
|
||||
}
|
||||
|
||||
let previousBounds = self.scrollView.bounds
|
||||
|
||||
self.ignoreScrolling = true
|
||||
let contentSize = CGSize(width: availableSize.width, height: contentHeight)
|
||||
if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) {
|
||||
self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize)
|
||||
}
|
||||
if self.scrollView.contentSize != contentSize {
|
||||
self.scrollView.contentSize = contentSize
|
||||
}
|
||||
let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: 0.0, right: 0.0)
|
||||
if self.scrollView.scrollIndicatorInsets != scrollInsets {
|
||||
self.scrollView.scrollIndicatorInsets = scrollInsets
|
||||
}
|
||||
|
||||
if !previousBounds.isEmpty, !transition.animation.isImmediate {
|
||||
let bounds = self.scrollView.bounds
|
||||
if bounds.maxY != previousBounds.maxY {
|
||||
let offsetY = previousBounds.maxY - bounds.maxY
|
||||
transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true)
|
||||
}
|
||||
}
|
||||
|
||||
if let recenterOnTag = self.recenterOnTag {
|
||||
self.recenterOnTag = nil
|
||||
|
||||
if let targetView = self.introSection.findTaggedView(tag: recenterOnTag) {
|
||||
let caretRect = targetView.convert(targetView.bounds, to: self.scrollView)
|
||||
var scrollViewBounds = self.scrollView.bounds
|
||||
let minButtonDistance: CGFloat = 16.0
|
||||
if -scrollViewBounds.minY + caretRect.maxY > availableSize.height - combinedBottomInset - minButtonDistance {
|
||||
scrollViewBounds.origin.y = -(availableSize.height - combinedBottomInset - minButtonDistance - caretRect.maxY)
|
||||
if scrollViewBounds.origin.y < 0.0 {
|
||||
scrollViewBounds.origin.y = 0.0
|
||||
}
|
||||
}
|
||||
if self.scrollView.bounds != scrollViewBounds {
|
||||
transition.setBounds(view: self.scrollView, bounds: scrollViewBounds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.topOverscrollLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -3000.0), size: CGSize(width: availableSize.width, height: 3000.0))
|
||||
self.ignoreScrolling = false
|
||||
|
||||
self.updateScrolling(transition: transition)
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
public final class GiftSetupScreen: ViewControllerComponentContainer {
|
||||
private let context: AccountContext
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
peerId: EnginePeer.Id,
|
||||
gift: StarGift
|
||||
) {
|
||||
self.context = context
|
||||
|
||||
super.init(context: context, component: GiftSetupScreenComponent(
|
||||
context: context,
|
||||
peerId: peerId,
|
||||
gift: gift
|
||||
), navigationBarAppearance: .default, theme: .default, updatedPresentationData: nil)
|
||||
|
||||
self.title = ""
|
||||
//self.navigationItem.backBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
|
||||
|
||||
self.scrollToTop = { [weak self] in
|
||||
guard let self, let componentView = self.node.hostView.componentView as? GiftSetupScreenComponent.View else {
|
||||
return
|
||||
}
|
||||
componentView.scrollToTop()
|
||||
}
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
}
|
||||
|
||||
@objc private func cancelPressed() {
|
||||
self.dismiss()
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
}
|
||||
}
|
44
submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD
Normal file
44
submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD
Normal file
@ -0,0 +1,44 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "GiftViewScreen",
|
||||
module_name = "GiftViewScreen",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/Postbox",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/TelegramUIPreferences",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/PresentationDataUtils",
|
||||
"//submodules/Markdown",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
"//submodules/Components/ViewControllerComponent",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/Components/BalancedTextComponent",
|
||||
"//submodules/TelegramUI/Components/ListSectionComponent",
|
||||
"//submodules/TelegramUI/Components/ListActionItemComponent",
|
||||
"//submodules/TelegramUI/Components/LottieComponent",
|
||||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||
"//submodules/TelegramUI/Components/ButtonComponent",
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/Components/SheetComponent",
|
||||
"//submodules/Components/SolidRoundedButtonComponent",
|
||||
"//submodules/TelegramUI/Components/Stars/StarsAvatarComponent",
|
||||
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
|
||||
"//submodules/UndoUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
File diff suppressed because it is too large
Load Diff
@ -20,6 +20,7 @@ public enum PeerInfoPaneKey: Int32 {
|
||||
case gifs
|
||||
case groupsInCommon
|
||||
case recommended
|
||||
case gifts
|
||||
}
|
||||
|
||||
public struct PeerInfoStatusData: Equatable {
|
||||
|
@ -383,6 +383,7 @@ final class PeerInfoScreenData {
|
||||
let starsRevenueStatsState: StarsRevenueStats?
|
||||
let starsRevenueStatsContext: StarsRevenueStatsContext?
|
||||
let revenueStatsState: RevenueStats?
|
||||
let profileGiftsContext: ProfileGiftsContext?
|
||||
|
||||
let _isContact: Bool
|
||||
var forceIsContact: Bool = false
|
||||
@ -428,7 +429,8 @@ final class PeerInfoScreenData {
|
||||
starsState: StarsContext.State?,
|
||||
starsRevenueStatsState: StarsRevenueStats?,
|
||||
starsRevenueStatsContext: StarsRevenueStatsContext?,
|
||||
revenueStatsState: RevenueStats?
|
||||
revenueStatsState: RevenueStats?,
|
||||
profileGiftsContext: ProfileGiftsContext?
|
||||
) {
|
||||
self.peer = peer
|
||||
self.chatPeer = chatPeer
|
||||
@ -463,6 +465,7 @@ final class PeerInfoScreenData {
|
||||
self.starsRevenueStatsState = starsRevenueStatsState
|
||||
self.starsRevenueStatsContext = starsRevenueStatsContext
|
||||
self.revenueStatsState = revenueStatsState
|
||||
self.profileGiftsContext = profileGiftsContext
|
||||
}
|
||||
}
|
||||
|
||||
@ -955,7 +958,8 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id,
|
||||
starsState: starsState,
|
||||
starsRevenueStatsState: nil,
|
||||
starsRevenueStatsContext: nil,
|
||||
revenueStatsState: nil
|
||||
revenueStatsState: nil,
|
||||
profileGiftsContext: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1000,7 +1004,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
starsState: nil,
|
||||
starsRevenueStatsState: nil,
|
||||
starsRevenueStatsContext: nil,
|
||||
revenueStatsState: nil
|
||||
revenueStatsState: nil,
|
||||
profileGiftsContext: nil
|
||||
))
|
||||
case let .user(userPeerId, secretChatId, kind):
|
||||
let groupsInCommon: GroupsInCommonContext?
|
||||
@ -1012,6 +1017,13 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
groupsInCommon = nil
|
||||
}
|
||||
|
||||
let profileGiftsContext: ProfileGiftsContext?
|
||||
if case .user = kind {
|
||||
profileGiftsContext = ProfileGiftsContext(account: context.account, peerId: userPeerId)
|
||||
} else {
|
||||
profileGiftsContext = nil
|
||||
}
|
||||
|
||||
enum StatusInputData: Equatable {
|
||||
case none
|
||||
case presence(TelegramUserPresence)
|
||||
@ -1247,6 +1259,16 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
}
|
||||
}
|
||||
|
||||
let profileGiftsCount: Signal<Int32, NoError>
|
||||
if let profileGiftsContext {
|
||||
profileGiftsCount = profileGiftsContext.state
|
||||
|> map { state in
|
||||
return state.count ?? 0
|
||||
}
|
||||
} else {
|
||||
profileGiftsCount = .single(0)
|
||||
}
|
||||
|
||||
return combineLatest(
|
||||
context.account.viewTracker.peerView(peerId, updateData: true),
|
||||
peerInfoAvailableMediaPanes(context: context, peerId: peerId, chatLocation: chatLocation, isMyProfile: isMyProfile, chatLocationContextHolder: chatLocationContextHolder),
|
||||
@ -1263,9 +1285,10 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
hasBotPreviewItems,
|
||||
peerInfoPersonalChannel(context: context, peerId: peerId, isSettings: false),
|
||||
privacySettings,
|
||||
starsRevenueContextAndState
|
||||
starsRevenueContextAndState,
|
||||
profileGiftsCount
|
||||
)
|
||||
|> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, hasStoryArchive, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, hasBotPreviewItems, personalChannel, privacySettings, starsRevenueContextAndState -> PeerInfoScreenData in
|
||||
|> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, hasStoryArchive, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, hasBotPreviewItems, personalChannel, privacySettings, starsRevenueContextAndState, profileGiftsCount -> PeerInfoScreenData in
|
||||
var availablePanes = availablePanes
|
||||
if isMyProfile {
|
||||
availablePanes?.insert(.stories, at: 0)
|
||||
@ -1277,6 +1300,10 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
availablePanes?.insert(.stories, at: 0)
|
||||
}
|
||||
|
||||
if profileGiftsCount > 0 {
|
||||
availablePanes?.insert(.gifts, at: hasStories ? 1 : 0)
|
||||
}
|
||||
|
||||
if availablePanes != nil, groupsInCommon != nil, let cachedData = peerView.cachedData as? CachedUserData {
|
||||
if cachedData.commonGroupCount != 0 {
|
||||
availablePanes?.append(.groupsInCommon)
|
||||
@ -1370,7 +1397,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
starsState: nil,
|
||||
starsRevenueStatsState: starsRevenueContextAndState.1,
|
||||
starsRevenueStatsContext: starsRevenueContextAndState.0,
|
||||
revenueStatsState: nil
|
||||
revenueStatsState: nil,
|
||||
profileGiftsContext: profileGiftsContext
|
||||
)
|
||||
}
|
||||
case .channel:
|
||||
@ -1579,7 +1607,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
starsState: nil,
|
||||
starsRevenueStatsState: starsRevenueContextAndState.1,
|
||||
starsRevenueStatsContext: starsRevenueContextAndState.0,
|
||||
revenueStatsState: revenueContextAndState.1
|
||||
revenueStatsState: revenueContextAndState.1,
|
||||
profileGiftsContext: nil
|
||||
)
|
||||
}
|
||||
case let .group(groupId):
|
||||
@ -1879,7 +1908,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|
||||
starsState: nil,
|
||||
starsRevenueStatsState: nil,
|
||||
starsRevenueStatsContext: nil,
|
||||
revenueStatsState: nil
|
||||
revenueStatsState: nil,
|
||||
profileGiftsContext: nil
|
||||
))
|
||||
}
|
||||
}
|
||||
|
@ -407,6 +407,8 @@ private final class PeerInfoPendingPane {
|
||||
var captureProtected = data.peer?.isCopyProtectionEnabled ?? false
|
||||
let paneNode: PeerInfoPaneNode
|
||||
switch key {
|
||||
case .gifts:
|
||||
paneNode = PeerInfoGiftsPaneNode(context: context, peerId: peerId, chatControllerInteraction: chatControllerInteraction, openPeerContextAction: openPeerContextAction, profileGifts: data.profileGiftsContext!)
|
||||
case .stories, .storyArchive, .botPreview:
|
||||
var canManage = false
|
||||
if let peer = data.peer {
|
||||
@ -1149,6 +1151,9 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat
|
||||
title = presentationData.strings.DialogList_TabTitle
|
||||
case .savedMessages:
|
||||
title = presentationData.strings.PeerInfo_SavedMessagesTabTitle
|
||||
case .gifts:
|
||||
//TODO:localize
|
||||
title = "Gifts"
|
||||
}
|
||||
return PeerInfoPaneSpecifier(key: key, title: title)
|
||||
}, selectedPane: self.currentPaneKey, disableSwitching: disableTabSwitching, transitionFraction: self.transitionFraction, transition: transition)
|
||||
|
@ -3472,7 +3472,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
}, openLargeEmojiInfo: { _, _, _ in
|
||||
}, openJoinLink: { _ in
|
||||
}, openWebView: { _, _, _, _ in
|
||||
}, activateAdAction: { _, _ in
|
||||
}, activateAdAction: { _, _, _, _ in
|
||||
}, openRequestedPeerSelection: { _, _, _, _ in
|
||||
}, saveMediaToFiles: { _ in
|
||||
}, openNoAdsDemo: {
|
||||
|
@ -48,6 +48,9 @@ swift_library(
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/TelegramUI/Components/TabSelectorComponent",
|
||||
"//submodules/TelegramUI/Components/Settings/LanguageSelectionScreen",
|
||||
"//submodules/TelegramUI/Components/Gifts/GiftItemComponent",
|
||||
"//submodules/TelegramUI/Components/Gifts/GiftViewScreen",
|
||||
"//submodules/TelegramUI/Components/ButtonComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -0,0 +1,290 @@
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import ContextUI
|
||||
import PhotoResources
|
||||
import TelegramUIPreferences
|
||||
import ItemListPeerItem
|
||||
import ItemListPeerActionItem
|
||||
import MergeLists
|
||||
import ItemListUI
|
||||
import ChatControllerInteraction
|
||||
import MultilineTextComponent
|
||||
import Markdown
|
||||
import PeerInfoPaneNode
|
||||
import GiftItemComponent
|
||||
import PlainButtonComponent
|
||||
import GiftViewScreen
|
||||
import ButtonComponent
|
||||
|
||||
public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDelegate {
|
||||
private let context: AccountContext
|
||||
private let peerId: PeerId
|
||||
private let profileGifts: ProfileGiftsContext
|
||||
|
||||
private var dataDisposable: Disposable?
|
||||
|
||||
private let chatControllerInteraction: ChatControllerInteraction
|
||||
private let openPeerContextAction: (Bool, Peer, ASDisplayNode, ContextGesture?) -> Void
|
||||
|
||||
public weak var parentController: ViewController?
|
||||
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let scrollNode: ASScrollNode
|
||||
|
||||
private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData)?
|
||||
|
||||
private var theme: PresentationTheme?
|
||||
private let presentationDataPromise = Promise<PresentationData>()
|
||||
|
||||
private let ready = Promise<Bool>()
|
||||
private var didSetReady: Bool = false
|
||||
public var isReady: Signal<Bool, NoError> {
|
||||
return self.ready.get()
|
||||
}
|
||||
|
||||
private let statusPromise = Promise<PeerInfoStatusData?>(nil)
|
||||
public var status: Signal<PeerInfoStatusData?, NoError> {
|
||||
self.statusPromise.get()
|
||||
}
|
||||
|
||||
public var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)?
|
||||
public var tabBarOffset: CGFloat {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
private var starsProducts: [ProfileGiftsContext.State.StarGift]?
|
||||
|
||||
private var starsItems: [AnyHashable: ComponentView<Empty>] = [:]
|
||||
|
||||
public init(context: AccountContext, peerId: PeerId, chatControllerInteraction: ChatControllerInteraction, openPeerContextAction: @escaping (Bool, Peer, ASDisplayNode, ContextGesture?) -> Void, profileGifts: ProfileGiftsContext) {
|
||||
self.context = context
|
||||
self.peerId = peerId
|
||||
self.chatControllerInteraction = chatControllerInteraction
|
||||
self.openPeerContextAction = openPeerContextAction
|
||||
self.profileGifts = profileGifts
|
||||
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.scrollNode = ASScrollNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.scrollNode)
|
||||
|
||||
self.dataDisposable = (profileGifts.state
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] state in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.statusPromise.set(.single(PeerInfoStatusData(text: "\(state.count ?? 0) gifts", isActivity: true, key: .gifts)))
|
||||
self.starsProducts = state.gifts
|
||||
|
||||
if !self.didSetReady {
|
||||
self.didSetReady = true
|
||||
self.ready.set(.single(true))
|
||||
}
|
||||
|
||||
self.updateScrolling()
|
||||
})
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.dataDisposable?.dispose()
|
||||
}
|
||||
|
||||
public override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.scrollNode.view.delegate = self
|
||||
}
|
||||
|
||||
public func ensureMessageIsVisible(id: MessageId) {
|
||||
}
|
||||
|
||||
public func scrollToTop() -> Bool {
|
||||
self.scrollNode.view.setContentOffset(.zero, animated: true)
|
||||
return true
|
||||
}
|
||||
|
||||
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
self.updateScrolling()
|
||||
}
|
||||
|
||||
func updateScrolling() {
|
||||
if let starsProducts = self.starsProducts, let params = self.currentParams {
|
||||
let optionSpacing: CGFloat = 10.0
|
||||
let sideInset = params.sideInset + 16.0
|
||||
|
||||
let itemsInRow = min(starsProducts.count, 3)
|
||||
let optionWidth = (params.size.width - sideInset * 2.0 - optionSpacing * CGFloat(itemsInRow - 1)) / CGFloat(itemsInRow)
|
||||
|
||||
let starsOptionSize = CGSize(width: optionWidth, height: 154.0)
|
||||
|
||||
let visibleBounds = self.scrollNode.bounds.insetBy(dx: 0.0, dy: -10.0)
|
||||
|
||||
var validIds: [AnyHashable] = []
|
||||
var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: 60.0), size: starsOptionSize)
|
||||
for product in starsProducts {
|
||||
let itemId = AnyHashable(product.date)
|
||||
validIds.append(itemId)
|
||||
|
||||
let itemTransition = ComponentTransition.immediate
|
||||
let visibleItem: ComponentView<Empty>
|
||||
if let current = self.starsItems[itemId] {
|
||||
visibleItem = current
|
||||
} else {
|
||||
visibleItem = ComponentView()
|
||||
self.starsItems[itemId] = visibleItem
|
||||
}
|
||||
|
||||
var isVisible = false
|
||||
if visibleBounds.intersects(itemFrame) {
|
||||
isVisible = true
|
||||
}
|
||||
|
||||
if isVisible {
|
||||
let _ = visibleItem.update(
|
||||
transition: itemTransition,
|
||||
component: AnyComponent(
|
||||
PlainButtonComponent(
|
||||
content: AnyComponent(
|
||||
GiftItemComponent(
|
||||
context: self.context,
|
||||
theme: params.presentationData.theme,
|
||||
peer: product.fromPeer,
|
||||
subject: .starGift(product.gift.id, product.gift.file),
|
||||
price: "⭐️ \(product.gift.price)",
|
||||
ribbon: product.gift.availability != nil ?
|
||||
GiftItemComponent.Ribbon(
|
||||
text: "1 of 1K",
|
||||
color: UIColor(rgb: 0x58c1fe)
|
||||
)
|
||||
: nil
|
||||
)
|
||||
),
|
||||
effectAlignment: .center,
|
||||
action: { [weak self] in
|
||||
if let self {
|
||||
let controller = GiftViewScreen(
|
||||
context: self.context,
|
||||
subject: .profileGift(self.peerId, product)
|
||||
)
|
||||
self.parentController?.push(controller)
|
||||
}
|
||||
},
|
||||
animateAlpha: false
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: starsOptionSize
|
||||
)
|
||||
if let itemView = visibleItem.view {
|
||||
if itemView.superview == nil {
|
||||
self.scrollNode.view.addSubview(itemView)
|
||||
}
|
||||
itemTransition.setFrame(view: itemView, frame: itemFrame)
|
||||
}
|
||||
}
|
||||
itemFrame.origin.x += itemFrame.width + optionSpacing
|
||||
if itemFrame.maxX > params.size.width {
|
||||
itemFrame.origin.x = sideInset
|
||||
itemFrame.origin.y += starsOptionSize.height + optionSpacing
|
||||
}
|
||||
}
|
||||
|
||||
let contentHeight = ceil(CGFloat(starsProducts.count) / 3.0) * starsOptionSize.height + 60.0 + params.bottomInset + 16.0
|
||||
|
||||
// //TODO:localize
|
||||
// let buttonSize = self.button.update(
|
||||
// transition: .immediate,
|
||||
// component: AnyComponent(ButtonComponent(
|
||||
// background: ButtonComponent.Background(
|
||||
// color: params.presentationData.theme.list.itemCheckColors.fillColor,
|
||||
// foreground: params.presentationData.theme.list.itemCheckColors.foregroundColor,
|
||||
// pressedColor: params.presentationData.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9),
|
||||
// cornerRadius: 10.0
|
||||
// ),
|
||||
// content: AnyComponentWithIdentity(
|
||||
// id: AnyHashable(0),
|
||||
// component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "Send Gifts to Friends", font: Font.semibold(17.0), textColor: )params.presentationData.theme.list.itemCheckColors.foregroundColor)))
|
||||
// ),
|
||||
// isEnabled: true,
|
||||
// displaysProgress: false,
|
||||
// action: {
|
||||
//
|
||||
// }
|
||||
// )),
|
||||
// environment: {},
|
||||
// containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50)
|
||||
// )
|
||||
// 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)
|
||||
// }
|
||||
|
||||
// contentHeight += 100.0
|
||||
|
||||
|
||||
let contentSize = CGSize(width: params.size.width, height: contentHeight)
|
||||
if self.scrollNode.view.contentSize != contentSize {
|
||||
self.scrollNode.view.contentSize = contentSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
|
||||
self.currentParams = (size, sideInset, bottomInset, isScrollingLockedAtTop, presentationData)
|
||||
self.presentationDataPromise.set(.single(presentationData))
|
||||
|
||||
self.backgroundNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor
|
||||
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 48.0), size: size))
|
||||
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
if isScrollingLockedAtTop {
|
||||
self.scrollNode.view.contentOffset = .zero
|
||||
}
|
||||
self.scrollNode.view.isScrollEnabled = !isScrollingLockedAtTop
|
||||
|
||||
self.updateScrolling()
|
||||
}
|
||||
|
||||
public func findLoadedMessage(id: MessageId) -> Message? {
|
||||
return nil
|
||||
}
|
||||
|
||||
public func updateHiddenMedia() {
|
||||
}
|
||||
|
||||
public func transferVelocity(_ velocity: CGFloat) {
|
||||
if velocity > 0.0 {
|
||||
// self.scrollNode.transferVelocity(velocity)
|
||||
}
|
||||
}
|
||||
|
||||
public func cancelPreviewGestures() {
|
||||
}
|
||||
|
||||
public func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
|
||||
return nil
|
||||
}
|
||||
|
||||
public func addToTransitionSurface(view: UIView) {
|
||||
}
|
||||
|
||||
public func updateSelectedMessages(animated: Bool) {
|
||||
}
|
||||
}
|
||||
|
||||
private struct StarsGiftProduct: Equatable {
|
||||
let emoji: String
|
||||
let price: Int64
|
||||
let isLimited: Bool
|
||||
}
|
@ -870,7 +870,10 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding, ListShimme
|
||||
}
|
||||
|
||||
let message = item.message
|
||||
let hasSpoiler = message.attributes.contains(where: { $0 is MediaSpoilerMessageAttribute }) && !self.revealedSpoilerMessageIds.contains(message.id)
|
||||
var hasSpoiler = message.attributes.contains(where: { $0 is MediaSpoilerMessageAttribute }) && !self.revealedSpoilerMessageIds.contains(message.id)
|
||||
if message.isSensitiveContent(platform: "ios") {
|
||||
hasSpoiler = true
|
||||
}
|
||||
layer.updateHasSpoiler(hasSpoiler: hasSpoiler)
|
||||
|
||||
var selectedMedia: Media?
|
||||
@ -1272,7 +1275,11 @@ public final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode,
|
||||
}
|
||||
strongSelf.chatControllerInteraction.toggleMessagesSelection([item.message.id], toggledValue)
|
||||
} else {
|
||||
let _ = strongSelf.chatControllerInteraction.openMessage(item.message, OpenMessageParams(mode: .default))
|
||||
if item.message.isSensitiveContent(platform: "ios") {
|
||||
// strongSelf.context.currentContentSettings.with { $0 }.ignoreContentRestrictionReasons
|
||||
} else {
|
||||
let _ = strongSelf.chatControllerInteraction.openMessage(item.message, OpenMessageParams(mode: .default))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,24 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "ItemShimmeringLoadingComponent",
|
||||
module_name = "ItemShimmeringLoadingComponent",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/Postbox",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/Components/HierarchyTrackingLayer",
|
||||
"//submodules/TelegramUI/Components/TextLoadingEffect",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -6,17 +6,25 @@ import HierarchyTrackingLayer
|
||||
import ComponentFlow
|
||||
import TextLoadingEffect
|
||||
|
||||
final class ItemLoadingComponent: Component {
|
||||
public final class ItemShimmeringLoadingComponent: Component {
|
||||
private let color: UIColor
|
||||
private let cornerRadius: CGFloat
|
||||
|
||||
public init(color: UIColor) {
|
||||
public init(
|
||||
color: UIColor,
|
||||
cornerRadius: CGFloat = 10.0
|
||||
) {
|
||||
self.color = color
|
||||
self.cornerRadius = cornerRadius
|
||||
}
|
||||
|
||||
public static func ==(lhs: ItemLoadingComponent, rhs: ItemLoadingComponent) -> Bool {
|
||||
public static func ==(lhs: ItemShimmeringLoadingComponent, rhs: ItemShimmeringLoadingComponent) -> Bool {
|
||||
if !lhs.color.isEqual(rhs.color) {
|
||||
return false
|
||||
}
|
||||
if lhs.cornerRadius != rhs.cornerRadius {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -28,16 +36,14 @@ final class ItemLoadingComponent: Component {
|
||||
private let borderMaskGradientView = UIImageView()
|
||||
private let borderMaskFillView = UIImageView()
|
||||
|
||||
private var component: ItemLoadingComponent?
|
||||
private var component: ItemShimmeringLoadingComponent?
|
||||
|
||||
override public init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.loadingView)
|
||||
self.addSubview(self.borderView)
|
||||
|
||||
self.borderView.image = generateFilledRoundedRectImage(size: CGSize(width: 24.0, height: 24.0), cornerRadius: 10.0, color: nil, strokeColor: .white, strokeWidth: 1.0 + UIScreenPixel, backgroundColor: nil)?.stretchableImage(withLeftCapWidth: 10, topCapHeight: 10).withRenderingMode(.alwaysTemplate)
|
||||
|
||||
|
||||
self.borderMaskView.backgroundColor = .clear
|
||||
self.borderMaskFillView.backgroundColor = .white
|
||||
|
||||
@ -63,14 +69,23 @@ final class ItemLoadingComponent: Component {
|
||||
})
|
||||
}
|
||||
|
||||
func update(component: ItemLoadingComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
func update(component: ItemShimmeringLoadingComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
let isFirstTime = self.component == nil
|
||||
let previousCornerRadius = self.component?.cornerRadius
|
||||
|
||||
self.component = component
|
||||
|
||||
if previousCornerRadius != component.cornerRadius {
|
||||
self.borderView.image = generateFilledRoundedRectImage(size: CGSize(width: 24.0, height: 24.0), cornerRadius: component.cornerRadius, color: nil, strokeColor: .white, strokeWidth: 1.0 + UIScreenPixel, backgroundColor: nil)?.stretchableImage(withLeftCapWidth: Int(component.cornerRadius), topCapHeight: Int(component.cornerRadius)).withRenderingMode(.alwaysTemplate)
|
||||
}
|
||||
|
||||
self.borderView.tintColor = component.color
|
||||
|
||||
self.loadingView.update(color: component.color, rect: CGRect(origin: .zero, size: availableSize))
|
||||
self.loadingView.frame = CGRect(origin: .zero, size: availableSize)
|
||||
self.loadingView.layer.cornerRadius = component.cornerRadius
|
||||
self.loadingView.clipsToBounds = true
|
||||
|
||||
transition.setFrame(view: self.borderView, frame: CGRect(origin: .zero, size: availableSize))
|
||||
self.borderMaskView.frame = self.borderView.bounds
|
||||
|
@ -37,6 +37,9 @@ swift_library(
|
||||
"//submodules/Components/BlurredBackgroundComponent",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/ConfettiEffect",
|
||||
"//submodules/AnimatedStickerNode",
|
||||
"//submodules/TelegramAnimatedStickerNode",
|
||||
"//submodules/TelegramUI/Components/Stars/ItemShimmeringLoadingComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -25,6 +25,7 @@ import TextFormat
|
||||
import PremiumStarComponent
|
||||
import BundleIconComponent
|
||||
import ConfettiEffect
|
||||
import ItemShimmeringLoadingComponent
|
||||
|
||||
private struct StarsProduct: Equatable {
|
||||
enum Option: Equatable {
|
||||
@ -328,7 +329,7 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent {
|
||||
let backgroundComponent: AnyComponent<Empty>?
|
||||
if product.storeProduct.id == context.component.selectedProductId {
|
||||
backgroundComponent = AnyComponent(
|
||||
ItemLoadingComponent(color: environment.theme.list.itemAccentColor)
|
||||
ItemShimmeringLoadingComponent(color: environment.theme.list.itemAccentColor)
|
||||
)
|
||||
} else {
|
||||
backgroundComponent = nil
|
||||
|
@ -239,7 +239,6 @@ final class StarsTransactionsScreenComponent: Component {
|
||||
|
||||
if let starView = self.starView.view {
|
||||
let starPosition = CGPoint(x: self.scrollView.frame.width / 2.0, y: topInset + starView.bounds.height / 2.0 - 30.0 - titleOffset * titleScale)
|
||||
|
||||
headerTransition.setPosition(view: starView, position: starPosition)
|
||||
headerTransition.setScale(view: starView, scale: titleScale)
|
||||
}
|
||||
|
@ -918,7 +918,7 @@ final class StoryItemContentComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
let shimmeringMediaAreas: [MediaArea] = component.item.mediaAreas.filter { mediaArea in
|
||||
var shimmeringMediaAreas: [MediaArea] = component.item.mediaAreas.filter { mediaArea in
|
||||
if case .link = mediaArea {
|
||||
return true
|
||||
} else if case .venue = mediaArea {
|
||||
@ -928,6 +928,10 @@ final class StoryItemContentComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
if component.peer.id.isTelegramNotifications {
|
||||
shimmeringMediaAreas = []
|
||||
}
|
||||
|
||||
if !shimmeringMediaAreas.isEmpty {
|
||||
let mediaAreasEffectView: StoryItemLoadingEffectView
|
||||
if let current = self.mediaAreasEffectView {
|
||||
|
@ -13,6 +13,9 @@ swift_library(
|
||||
"//submodules/Display",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||
"//submodules/Components/MultilineTextWithEntitiesComponent",
|
||||
"//submodules/TextFormat",
|
||||
"//submodules/AccountContext"
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -3,18 +3,24 @@ import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import PlainButtonComponent
|
||||
import MultilineTextWithEntitiesComponent
|
||||
import TextFormat
|
||||
import AccountContext
|
||||
|
||||
public final class TabSelectorComponent: Component {
|
||||
public struct Colors: Equatable {
|
||||
public var foreground: UIColor
|
||||
public var selection: UIColor
|
||||
public var simple: Bool
|
||||
|
||||
public init(
|
||||
foreground: UIColor,
|
||||
selection: UIColor
|
||||
selection: UIColor,
|
||||
simple: Bool = false
|
||||
) {
|
||||
self.foreground = foreground
|
||||
self.selection = selection
|
||||
self.simple = simple
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,6 +51,7 @@ public final class TabSelectorComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
public let context: AccountContext?
|
||||
public let colors: Colors
|
||||
public let customLayout: CustomLayout?
|
||||
public let items: [Item]
|
||||
@ -53,6 +60,7 @@ public final class TabSelectorComponent: Component {
|
||||
public let transitionFraction: CGFloat?
|
||||
|
||||
public init(
|
||||
context: AccountContext? = nil,
|
||||
colors: Colors,
|
||||
customLayout: CustomLayout? = nil,
|
||||
items: [Item],
|
||||
@ -60,6 +68,7 @@ public final class TabSelectorComponent: Component {
|
||||
setSelectedId: @escaping (AnyHashable) -> Void,
|
||||
transitionFraction: CGFloat? = nil
|
||||
) {
|
||||
self.context = context
|
||||
self.colors = colors
|
||||
self.customLayout = customLayout
|
||||
self.items = items
|
||||
@ -69,6 +78,9 @@ public final class TabSelectorComponent: Component {
|
||||
}
|
||||
|
||||
public static func ==(lhs: TabSelectorComponent, rhs: TabSelectorComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.colors != rhs.colors {
|
||||
return false
|
||||
}
|
||||
@ -211,6 +223,7 @@ public final class TabSelectorComponent: Component {
|
||||
transition: .immediate,
|
||||
component: AnyComponent(PlainButtonComponent(
|
||||
content: AnyComponent(ItemComponent(
|
||||
context: component.context,
|
||||
text: item.title,
|
||||
font: itemFont,
|
||||
color: component.colors.foreground,
|
||||
@ -254,7 +267,7 @@ public final class TabSelectorComponent: Component {
|
||||
}
|
||||
itemTransition.setPosition(view: itemTitleView, position: itemTitleFrame.origin)
|
||||
itemTransition.setBounds(view: itemTitleView, bounds: CGRect(origin: CGPoint(), size: itemTitleFrame.size))
|
||||
itemTransition.setAlpha(view: itemTitleView, alpha: item.id == component.selectedId || isLineSelection ? 1.0 : 0.4)
|
||||
itemTransition.setAlpha(view: itemTitleView, alpha: item.id == component.selectedId || isLineSelection || component.colors.simple ? 1.0 : 0.4)
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
@ -302,7 +315,7 @@ public final class TabSelectorComponent: Component {
|
||||
self.contentSize = CGSize(width: contentWidth, height: baseHeight + verticalInset * 2.0)
|
||||
self.disablesInteractiveTransitionGestureRecognizer = contentWidth > availableSize.width
|
||||
|
||||
if let selectedBackgroundRect {
|
||||
if let selectedBackgroundRect, self.bounds.width > 0.0 {
|
||||
self.scrollRectToVisible(selectedBackgroundRect.insetBy(dx: -spacing, dy: 0.0), animated: false)
|
||||
}
|
||||
|
||||
@ -331,6 +344,7 @@ extension CGRect {
|
||||
}
|
||||
|
||||
private final class ItemComponent: CombinedComponent {
|
||||
let context: AccountContext?
|
||||
let text: String
|
||||
let font: UIFont
|
||||
let color: UIColor
|
||||
@ -338,12 +352,14 @@ private final class ItemComponent: CombinedComponent {
|
||||
let selectionFraction: CGFloat
|
||||
|
||||
init(
|
||||
context: AccountContext?,
|
||||
text: String,
|
||||
font: UIFont,
|
||||
color: UIColor,
|
||||
selectedColor: UIColor,
|
||||
selectionFraction: CGFloat
|
||||
) {
|
||||
self.context = context
|
||||
self.text = text
|
||||
self.font = font
|
||||
self.color = color
|
||||
@ -352,6 +368,9 @@ private final class ItemComponent: CombinedComponent {
|
||||
}
|
||||
|
||||
static func ==(lhs: ItemComponent, rhs: ItemComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.text != rhs.text {
|
||||
return false
|
||||
}
|
||||
@ -371,17 +390,25 @@ private final class ItemComponent: CombinedComponent {
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let title = Child(Text.self)
|
||||
let selectedTitle = Child(Text.self)
|
||||
let title = Child(MultilineTextWithEntitiesComponent.self)
|
||||
let selectedTitle = Child(MultilineTextWithEntitiesComponent.self)
|
||||
|
||||
return { context in
|
||||
let component = context.component
|
||||
|
||||
|
||||
let attributedTitle = NSMutableAttributedString(string: component.text, font: component.font, textColor: component.color)
|
||||
var range = (attributedTitle.string as NSString).range(of: "⭐️")
|
||||
if range.location != NSNotFound {
|
||||
attributedTitle.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range)
|
||||
}
|
||||
|
||||
let title = title.update(
|
||||
component: Text(
|
||||
text: component.text,
|
||||
font: component.font,
|
||||
color: component.color
|
||||
component: MultilineTextWithEntitiesComponent(
|
||||
context: component.context,
|
||||
animationCache: component.context?.animationCache,
|
||||
animationRenderer: component.context?.animationRenderer,
|
||||
placeholderColor: .white,
|
||||
text: .plain(attributedTitle)
|
||||
),
|
||||
availableSize: context.availableSize,
|
||||
transition: .immediate
|
||||
@ -391,11 +418,19 @@ private final class ItemComponent: CombinedComponent {
|
||||
.opacity(1.0 - component.selectionFraction)
|
||||
)
|
||||
|
||||
let selectedAttributedTitle = NSMutableAttributedString(string: component.text, font: component.font, textColor: component.selectedColor)
|
||||
range = (selectedAttributedTitle.string as NSString).range(of: "⭐️")
|
||||
if range.location != NSNotFound {
|
||||
selectedAttributedTitle.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range)
|
||||
}
|
||||
|
||||
let selectedTitle = selectedTitle.update(
|
||||
component: Text(
|
||||
text: component.text,
|
||||
font: component.font,
|
||||
color: component.selectedColor
|
||||
component: MultilineTextWithEntitiesComponent(
|
||||
context: nil,
|
||||
animationCache: nil,
|
||||
animationRenderer: nil,
|
||||
placeholderColor: .white,
|
||||
text: .plain(selectedAttributedTitle)
|
||||
),
|
||||
availableSize: context.availableSize,
|
||||
transition: .immediate
|
||||
|
12
submodules/TelegramUI/Images.xcassets/Chat/Message/BotCopy.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Message/BotCopy.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "copy_10.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
65
submodules/TelegramUI/Images.xcassets/Chat/Message/BotCopy.imageset/copy_10.pdf
vendored
Normal file
65
submodules/TelegramUI/Images.xcassets/Chat/Message/BotCopy.imageset/copy_10.pdf
vendored
Normal file
@ -0,0 +1,65 @@
|
||||
%PDF-1.7
|
||||
|
||||
1 0 obj
|
||||
<< >>
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
<< /Filter /FlateDecode
|
||||
/Length 3 0 R
|
||||
>>
|
||||
stream
|
||||
x<01>–ÍŽ$5„ïõ{F¢Öÿi_‰3ð]<5D>4³Z‰ççÛåòL<C3B2>uiw”3<E2809D>?‘áúüË—þz|ùýן>ýüÇñùþ÷ø~ü}øÓõç“»?®Õz·ëÕãõ¶|^||;™XògÙ¼é,,X.ìåÈgb£µËg-Îûz#åôÕµZ@|‰©qþQÎTb²‹ÎbßUZ0î̱˜y<CB9C>KöBªoÎ'ììrêO[áÓ†äÜZð7²’ØìâY,eoľ°¤¸J«·e²†Z‰¡¥dIH‰5Å@\-[*`5‚eWõ©ÅJò.Z Ä\Uå¾dSåÌÕçf`É×ä"»8¦¤’›<E28099>Yv®K¤E¹b—Ʋ)<½ôÄ>±ra¹cÝIënà<È®£ÓŽ]ÊÿÚ®4â•ÈH•²<E280A2>ôí*Èãˆ:FEªWÙâUÈ…„«ÜyÜÜZØtœÍ[ØjðB
ˆjãq,²ØMŸI(ªpQŒ¨D;»ˆ(;<òPðgº>Sz³[I¼ÜØJbCf+†•„|ÍHWÛb½šÛ—ƒµZN9ƒ‡cÞ¼Â[Èãøóøz|;~û¿²¯E¸ÿ-L Kþ,w¯w»¶µ{aÏI}”úGZ‹õjn_d‡GžŽye¼Ù-
|
||||
líÎ'Ôv¾lÅCü̲ÏÃYeT™ãÖ|ÒXɤ`GrjD³!ýìx#²Óy¦’¤¹é_˜Ç²†öé3[„0³Q¾ü˜ì¦ÉfÄ鮦_QiúY„¡ŠA<1A>Xô™•’È—”¤»ºÚt5Cmº'iRQ6Ò¤è±C[X"O±¿¤Ò)°ŒÝ/Q®ê)0ÇÄAtt<74>
|
||||
ñ“/ˆôÀLB¦Ê $‘G+-$]$ÕA”|ŽØŽazø?ÊX<C38A>°…P4Âu¶ÛÁ1
|
||||
JEuß<EFBFBD>L³tYŠQ½½=Z^&£D‚;Ä"3»tÎλçáÑÌÒ¼ÖÐ6¡Í{jóÊMÂ[G8ÓZ© ÑpH"q™+µß$z´'–Ø‚*1±-Ë—‰qkøÓ²QáKqÔ'û:bp‰¶Z&"4oÞ<07>jÌ™¢…B)–è<>·Jîì2Šâ~
|
||||
µ€@ÙN<-6×o¸|ÂåR%p)éžÊ§÷ ¡Uî’e‘kNPCU5n›ìÁJJº?_¸•
|
||||
wC’˜á`NÇx]tÏÊ€(„É
þÇÔP˜Ã–OŽ/F<>T¨ké!š
|
||||
°Ñ®sáUŸ^RC®ý"N)Ñ3³Ü/œL˜<ÊD½ø¤ðæš<18>öPLŽá§¼8Ð\'î#‰…ëm<C3AB>Úqľ)¾ôˆ™K9Þ`j=<™˜òžØÚ70‘Æ”7H£þƒXŒÊ8µÓ[ô£G}Oá~TÞ;cÉ®’vi »<C2A0>)0ˆ CŽAîÛwŠövGOõÞîñ©ÞÛmOu†z¯o*8Ô{!ô‚QT¯ý+D½&.ÈpcPy@6D"òþ«GRƒÜ—ÄG„”õ1>ú~‚´ˆÂùæ+i{÷-&ù£éTA‚(OI†Kõì²)»Ž<C2BB>ƒ©_L£–RÚþRªÊg‹œˆãÝš>B¡ÇÑÐs„óæ«t<06><><EFBFBD>4@®Äfª´|¤Ïœ<C38F>‚l_Ô$6ʶ!³´¡<>dC6»Õ¨íK|µsCfˉjÐ`C±/²¦èC.BMŠ1ʃv‹ˆ²ëäì4‡®Uv8!t Pu¡´Ð|×]©ñ×ú'Ô¿“ŸT
|
||||
endstream
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
1308
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 10.000000 10.000000 ]
|
||||
/Resources 1 0 R
|
||||
/Contents 2 0 R
|
||||
/Parent 5 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
<< /Kids [ 4 0 R ]
|
||||
/Count 1
|
||||
/Type /Pages
|
||||
>>
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
<< /Pages 5 0 R
|
||||
/Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000010 00000 n
|
||||
0000000034 00000 n
|
||||
0000001426 00000 n
|
||||
0000001449 00000 n
|
||||
0000001622 00000 n
|
||||
0000001696 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 6 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
1755
|
||||
%%EOF
|
12
submodules/TelegramUI/Images.xcassets/Chat/Message/GiftRibbon.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Message/GiftRibbon.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "giftline_chat.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
83
submodules/TelegramUI/Images.xcassets/Chat/Message/GiftRibbon.imageset/giftline_chat.pdf
vendored
Normal file
83
submodules/TelegramUI/Images.xcassets/Chat/Message/GiftRibbon.imageset/giftline_chat.pdf
vendored
Normal file
@ -0,0 +1,83 @@
|
||||
%PDF-1.7
|
||||
|
||||
1 0 obj
|
||||
<< >>
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
<< /Length 3 0 R >>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 0.000000 2.784424 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
62.376457 30.839119 m
|
||||
33.623550 59.592026 l
|
||||
31.548130 61.667446 30.510420 62.705158 29.299419 63.447258 c
|
||||
28.225752 64.105202 27.055216 64.590057 25.830782 64.884018 c
|
||||
24.449732 65.215576 22.982187 65.215576 20.047100 65.215576 c
|
||||
7.725484 65.215576 l
|
||||
5.302220 65.215576 4.090588 65.215576 3.529531 64.736389 c
|
||||
3.042711 64.320602 2.784362 63.696896 2.834592 63.058659 c
|
||||
2.892483 62.323093 3.749237 61.466339 5.462745 59.752831 c
|
||||
62.537258 2.678318 l
|
||||
64.250763 0.964813 65.107521 0.108055 65.843079 0.050171 c
|
||||
66.481316 -0.000061 67.105026 0.258286 67.520813 0.745110 c
|
||||
68.000000 1.306164 68.000000 2.517796 68.000000 4.941059 c
|
||||
68.000000 17.262676 l
|
||||
68.000000 20.197762 68.000000 21.665306 67.668442 23.046356 c
|
||||
67.374481 24.270794 66.889626 25.441330 66.231682 26.514996 c
|
||||
65.489578 27.725994 64.451874 28.763702 62.376457 30.839119 c
|
||||
h
|
||||
f
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
962
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 68.000000 68.000000 ]
|
||||
/Resources 1 0 R
|
||||
/Contents 2 0 R
|
||||
/Parent 5 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
<< /Kids [ 4 0 R ]
|
||||
/Count 1
|
||||
/Type /Pages
|
||||
>>
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
<< /Pages 5 0 R
|
||||
/Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000010 00000 n
|
||||
0000000034 00000 n
|
||||
0000001052 00000 n
|
||||
0000001074 00000 n
|
||||
0000001247 00000 n
|
||||
0000001321 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 6 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
1380
|
||||
%%EOF
|
12
submodules/TelegramUI/Images.xcassets/Premium/GiftRibbon.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Premium/GiftRibbon.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "giftline_cell.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
84
submodules/TelegramUI/Images.xcassets/Premium/GiftRibbon.imageset/giftline_cell.pdf
vendored
Normal file
84
submodules/TelegramUI/Images.xcassets/Premium/GiftRibbon.imageset/giftline_cell.pdf
vendored
Normal file
@ -0,0 +1,84 @@
|
||||
%PDF-1.7
|
||||
|
||||
1 0 obj
|
||||
<< >>
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
<< /Length 3 0 R >>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 0.000000 2.784363 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
52.376450 30.839190 m
|
||||
33.623550 49.592087 l
|
||||
31.548130 51.667507 30.510420 52.705219 29.299419 53.447319 c
|
||||
28.225752 54.105263 27.055216 54.590115 25.830782 54.884075 c
|
||||
24.449732 55.215637 22.982187 55.215637 20.047100 55.215637 c
|
||||
7.725484 55.215637 l
|
||||
5.302220 55.215637 4.090588 55.215637 3.529531 54.736450 c
|
||||
3.042711 54.320667 2.784362 53.696957 2.834592 53.058720 c
|
||||
2.892483 52.323154 3.749236 51.466400 5.462744 49.752892 c
|
||||
52.537258 2.678379 l
|
||||
54.250763 0.964874 55.107517 0.108120 55.843082 0.050228 c
|
||||
56.481319 0.000000 57.105030 0.258350 57.520813 0.745167 c
|
||||
58.000000 1.306225 58.000000 2.517857 58.000000 4.941120 c
|
||||
58.000000 17.262737 l
|
||||
58.000000 20.197823 58.000000 21.665367 57.668438 23.046417 c
|
||||
57.374477 24.270853 56.889626 25.441389 56.231682 26.515057 c
|
||||
55.489586 27.726049 54.451885 28.763756 52.376484 30.839149 c
|
||||
52.376450 30.839190 l
|
||||
h
|
||||
f
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
983
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 58.000000 58.000000 ]
|
||||
/Resources 1 0 R
|
||||
/Contents 2 0 R
|
||||
/Parent 5 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
<< /Kids [ 4 0 R ]
|
||||
/Count 1
|
||||
/Type /Pages
|
||||
>>
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
<< /Pages 5 0 R
|
||||
/Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000010 00000 n
|
||||
0000000034 00000 n
|
||||
0000001073 00000 n
|
||||
0000001095 00000 n
|
||||
0000001268 00000 n
|
||||
0000001342 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 6 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
1401
|
||||
%%EOF
|
@ -2365,7 +2365,7 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
|
||||
if let primary = primary {
|
||||
for context in contexts {
|
||||
if let context = context, context.account.id == primary {
|
||||
self.openChatWhenReady(accountId: nil, peerId: peerId, threadId: nil, storyId: nil)
|
||||
self.openChatWhenReady(accountId: nil, peerId: peerId, threadId: nil, storyId: nil, openAppIfAny: true)
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -2373,7 +2373,7 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
|
||||
|
||||
for context in contexts {
|
||||
if let context = context {
|
||||
self.openChatWhenReady(accountId: context.account.id, peerId: peerId, threadId: nil, storyId: nil)
|
||||
self.openChatWhenReady(accountId: context.account.id, peerId: peerId, threadId: nil, storyId: nil, openAppIfAny: true)
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -2459,7 +2459,7 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
|
||||
}))
|
||||
}
|
||||
|
||||
private func openChatWhenReady(accountId: AccountRecordId?, peerId: PeerId, threadId: Int64?, messageId: MessageId? = nil, activateInput: Bool = false, storyId: StoryId?) {
|
||||
private func openChatWhenReady(accountId: AccountRecordId?, peerId: PeerId, threadId: Int64?, messageId: MessageId? = nil, activateInput: Bool = false, storyId: StoryId?, openAppIfAny: Bool = false) {
|
||||
let signal = self.sharedContextPromise.get()
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue
|
||||
@ -2478,7 +2478,7 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
|
||||
}
|
||||
self.openChatWhenReadyDisposable.set((signal
|
||||
|> deliverOnMainQueue).start(next: { context in
|
||||
context.openChatWithPeerId(peerId: peerId, threadId: threadId, messageId: messageId, activateInput: activateInput, storyId: storyId)
|
||||
context.openChatWithPeerId(peerId: peerId, threadId: threadId, messageId: messageId, activateInput: activateInput, storyId: storyId, openAppIfAny: openAppIfAny)
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -894,7 +894,7 @@ final class AuthorizedApplicationContext {
|
||||
}))
|
||||
}
|
||||
|
||||
func openChatWithPeerId(peerId: PeerId, threadId: Int64?, messageId: MessageId? = nil, activateInput: Bool = false, storyId: StoryId?) {
|
||||
func openChatWithPeerId(peerId: PeerId, threadId: Int64?, messageId: MessageId? = nil, activateInput: Bool = false, storyId: StoryId?, openAppIfAny: Bool = false) {
|
||||
if let storyId {
|
||||
var controllers = self.rootController.viewControllers
|
||||
controllers = controllers.filter { c in
|
||||
@ -945,7 +945,11 @@ final class AuthorizedApplicationContext {
|
||||
chatLocation = .peer(peer)
|
||||
}
|
||||
|
||||
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: self.rootController, context: self.context, chatLocation: chatLocation, subject: isOutgoingMessage ? messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false) } : nil, activateInput: activateInput ? .text : nil))
|
||||
if openAppIfAny, let parentController = self.rootController.viewControllers.last as? ViewController {
|
||||
self.context.sharedContext.openWebApp(context: self.context, parentController: parentController, updatedPresentationData: nil, peer: peer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: true)
|
||||
} else {
|
||||
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: self.rootController, context: self.context, chatLocation: chatLocation, subject: isOutgoingMessage ? messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false) } : nil, activateInput: activateInput ? .text : nil))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -242,7 +242,7 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u
|
||||
|> afterDisposed {
|
||||
updateProgress()
|
||||
})
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak parentController] result in
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak parentController] result in
|
||||
guard let parentController else {
|
||||
return
|
||||
}
|
||||
|
@ -1113,12 +1113,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
} else {
|
||||
strongSelf.present(BotReceiptController(context: strongSelf.context, messageId: message.id), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
}
|
||||
/*for attribute in message.attributes {
|
||||
if let attribute = attribute as? ReplyMessageAttribute {
|
||||
//strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId))
|
||||
break
|
||||
}
|
||||
}*/
|
||||
return true
|
||||
case .setChatTheme:
|
||||
strongSelf.presentThemeSelection()
|
||||
@ -1185,6 +1179,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
let controller = PremiumIntroScreen(context: strongSelf.context, source: .gift(from: fromPeerId, to: toPeerId, duration: duration, giftCode: nil))
|
||||
strongSelf.push(controller)
|
||||
return true
|
||||
case .starGift:
|
||||
let controller = strongSelf.context.sharedContext.makeGiftViewScreen(context: strongSelf.context, message: EngineMessage(message))
|
||||
strongSelf.push(controller)
|
||||
return true
|
||||
case .giftStars:
|
||||
let controller = strongSelf.context.sharedContext.makeStarsGiftScreen(context: strongSelf.context, message: EngineMessage(message))
|
||||
strongSelf.push(controller)
|
||||
@ -1280,6 +1278,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
standalone = true
|
||||
}
|
||||
|
||||
if let adAttribute = message.attributes.first(where: { $0 is AdMessageAttribute }) as? AdMessageAttribute {
|
||||
if let file = message.media.first(where: { $0 is TelegramMediaFile}) as? TelegramMediaFile, file.isVideo && !file.isAnimated {
|
||||
strongSelf.chatDisplayNode.historyNode.adMessagesContext?.markAction(opaqueId: adAttribute.opaqueId, media: true, fullscreen: false)
|
||||
} else {
|
||||
strongSelf.controllerInteraction?.activateAdAction(message.id, nil, true, false)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, updatedPresentationData: strongSelf.updatedPresentationData, chatLocation: openChatLocation, chatFilterTag: chatFilterTag, chatLocationContextHolder: strongSelf.chatLocationContextHolder, message: message, mediaIndex: params.mediaIndex, standalone: standalone, reverseMessageGalleryOrder: false, mode: mode, navigationController: strongSelf.effectiveNavigationController, dismissInput: {
|
||||
self?.chatDisplayNode.dismissInput()
|
||||
}, present: { c, a, i in
|
||||
@ -1391,7 +1398,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
}, openAd: { [weak self] messageId in
|
||||
if let strongSelf = self {
|
||||
strongSelf.controllerInteraction?.activateAdAction(messageId, nil)
|
||||
strongSelf.controllerInteraction?.activateAdAction(messageId, nil, true, true)
|
||||
}
|
||||
}, addContact: { [weak self] phoneNumber in
|
||||
if let strongSelf = self {
|
||||
@ -3903,7 +3910,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
return
|
||||
}
|
||||
self.openWebApp(buttonText: buttonText, url: url, simple: simple, source: source)
|
||||
}, activateAdAction: { [weak self] messageId, progress in
|
||||
}, activateAdAction: { [weak self] messageId, progress, media, fullscreen in
|
||||
guard let self, let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId), let adAttribute = message.adAttribute else {
|
||||
return
|
||||
}
|
||||
@ -3917,7 +3924,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
}
|
||||
|
||||
self.chatDisplayNode.historyNode.adMessagesContext?.markAction(opaqueId: adAttribute.opaqueId, media: false, fullscreen: false)
|
||||
self.chatDisplayNode.historyNode.adMessagesContext?.markAction(opaqueId: adAttribute.opaqueId, media: media, fullscreen: fullscreen)
|
||||
self.controllerInteraction?.openUrl(ChatControllerInteraction.OpenUrl(url: adAttribute.url, concealed: false, external: true, progress: progress))
|
||||
}, openRequestedPeerSelection: { [weak self] messageId, peerType, buttonId, maxQuantity in
|
||||
guard let self else {
|
||||
|
@ -351,7 +351,8 @@ private func extractAssociatedData(
|
||||
chatThemes: [TelegramTheme],
|
||||
deviceContactsNumbers: Set<String>,
|
||||
isInline: Bool,
|
||||
showSensitiveContent: Bool
|
||||
showSensitiveContent: Bool,
|
||||
starGifts: [Int64 : TelegramMediaFile]
|
||||
) -> ChatMessageItemAssociatedData {
|
||||
var automaticDownloadPeerId: EnginePeer.Id?
|
||||
var automaticMediaDownloadPeerType: MediaAutoDownloadPeerType = .channel
|
||||
@ -406,7 +407,7 @@ private func extractAssociatedData(
|
||||
automaticDownloadPeerId = message.peerId
|
||||
}
|
||||
|
||||
return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadPeerId: automaticDownloadPeerId, automaticDownloadNetworkType: automaticDownloadNetworkType, preferredStoryHighQuality: preferredStoryHighQuality, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, accountPeer: accountPeer, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, topicAuthorId: topicAuthorId, hasBots: hasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: isInline, showSensitiveContent: showSensitiveContent)
|
||||
return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadPeerId: automaticDownloadPeerId, automaticDownloadNetworkType: automaticDownloadNetworkType, preferredStoryHighQuality: preferredStoryHighQuality, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, accountPeer: accountPeer, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, topicAuthorId: topicAuthorId, hasBots: hasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: isInline, showSensitiveContent: showSensitiveContent, starGifts: starGifts)
|
||||
}
|
||||
|
||||
private extension ChatHistoryLocationInput {
|
||||
@ -1610,6 +1611,17 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|
||||
let starGifts: Signal<[Int64 : TelegramMediaFile], NoError> = context.engine.payments.cachedStarGifts()
|
||||
|> map { gifts in
|
||||
var files: [Int64 : TelegramMediaFile] = [:]
|
||||
if let gifts {
|
||||
for gift in gifts {
|
||||
files[gift.id] = gift.file
|
||||
}
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
let messageViewQueue = Queue.mainQueue()
|
||||
let historyViewTransitionDisposable = combineLatest(queue: messageViewQueue,
|
||||
historyViewUpdate,
|
||||
@ -1636,8 +1648,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
||||
audioTranscriptionTrial,
|
||||
chatThemes,
|
||||
deviceContactsNumbers,
|
||||
contentSettings
|
||||
).startStrict(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, preferredStoryHighQuality, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, availableReactions, availableMessageEffects, savedMessageTags, defaultReaction, accountPeer, suggestAudioTranscription, promises, topicAuthorId, translationState, maxReadStoryId, recommendedChannels, audioTranscriptionTrial, chatThemes, deviceContactsNumbers, contentSettings in
|
||||
contentSettings,
|
||||
starGifts
|
||||
).startStrict(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, preferredStoryHighQuality, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, availableReactions, availableMessageEffects, savedMessageTags, defaultReaction, accountPeer, suggestAudioTranscription, promises, topicAuthorId, translationState, maxReadStoryId, recommendedChannels, audioTranscriptionTrial, chatThemes, deviceContactsNumbers, contentSettings, starGifts in
|
||||
let (historyAppearsCleared, pendingUnpinnedAllMessages, pendingRemovedMessages, currentlyPlayingMessageIdAndType, scrollToMessageId, chatHasBots, allAdMessages) = promises
|
||||
|
||||
func applyHole() {
|
||||
@ -1856,7 +1869,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
||||
translateToLanguage = languageCode
|
||||
}
|
||||
|
||||
let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, preferredStoryHighQuality: preferredStoryHighQuality, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageIdAndType?.0, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, accountPeer: accountPeer, topicAuthorId: topicAuthorId, hasBots: chatHasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: !rotated, showSensitiveContent: contentSettings.ignoreContentRestrictionReasons.contains("sensitive"))
|
||||
let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, preferredStoryHighQuality: preferredStoryHighQuality, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageIdAndType?.0, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, accountPeer: accountPeer, topicAuthorId: topicAuthorId, hasBots: chatHasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: !rotated, showSensitiveContent: contentSettings.ignoreContentRestrictionReasons.contains("sensitive"), starGifts: starGifts)
|
||||
|
||||
var includeEmbeddedSavedChatInfo = false
|
||||
if case let .replyThread(message) = chatLocation, message.peerId == context.account.peerId, !rotated {
|
||||
|
@ -165,7 +165,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
|
||||
}, openLargeEmojiInfo: { _, _, _ in
|
||||
}, openJoinLink: { _ in
|
||||
}, openWebView: { _, _, _, _ in
|
||||
}, activateAdAction: { _, _ in
|
||||
}, activateAdAction: { _, _, _, _ in
|
||||
}, openRequestedPeerSelection: { _, _, _, _ in
|
||||
}, saveMediaToFiles: { _ in
|
||||
}, openNoAdsDemo: {
|
||||
|
@ -70,6 +70,8 @@ import StarsTransferScreen
|
||||
import StarsTransactionScreen
|
||||
import StarsWithdrawalScreen
|
||||
import MiniAppListScreen
|
||||
import GiftOptionsScreen
|
||||
import GiftViewScreen
|
||||
|
||||
private final class AccountUserInterfaceInUseContext {
|
||||
let subscribers = Bag<(Bool) -> Void>()
|
||||
@ -1776,7 +1778,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
}, openLargeEmojiInfo: { _, _, _ in
|
||||
}, openJoinLink: { _ in
|
||||
}, openWebView: { _, _, _, _ in
|
||||
}, activateAdAction: { _, _ in
|
||||
}, activateAdAction: { _, _, _, _ in
|
||||
}, openRequestedPeerSelection: { _, _, _, _ in
|
||||
}, saveMediaToFiles: { _ in
|
||||
}, openNoAdsDemo: {
|
||||
@ -2202,26 +2204,22 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
public func makePremiumGiftController(context: AccountContext, source: PremiumGiftSource, completion: (([EnginePeer.Id]) -> Void)?) -> ViewController {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let limit: Int32 = 10
|
||||
var reachedLimitImpl: ((Int32) -> Void)?
|
||||
// let limit: Int32 = 10
|
||||
// var reachedLimitImpl: ((Int32) -> Void)?
|
||||
var presentBirthdayPickerImpl: (() -> Void)?
|
||||
let mode: ContactMultiselectionControllerMode
|
||||
var starsMode: ContactSelectionControllerMode = .generic
|
||||
var currentBirthdays: [EnginePeer.Id: TelegramBirthday]?
|
||||
|
||||
if case let .chatList(birthdays) = source, let birthdays, !birthdays.isEmpty {
|
||||
mode = .premiumGifting(birthdays: birthdays, selectToday: true, hasActions: true)
|
||||
starsMode = .starsGifting(birthdays: birthdays, hasActions: true)
|
||||
currentBirthdays = birthdays
|
||||
} else if case let .settings(birthdays) = source, let birthdays, !birthdays.isEmpty {
|
||||
mode = .premiumGifting(birthdays: birthdays, selectToday: false, hasActions: true)
|
||||
currentBirthdays = birthdays
|
||||
} else if case let .stars(birthdays) = source {
|
||||
mode = .premiumGifting(birthdays: birthdays, selectToday: false, hasActions: false)
|
||||
starsMode = .starsGifting(birthdays: birthdays, hasActions: false)
|
||||
starsMode = .starsGifting(birthdays: birthdays, hasActions: true)
|
||||
currentBirthdays = birthdays
|
||||
} else {
|
||||
mode = .premiumGifting(birthdays: nil, selectToday: false, hasActions: true)
|
||||
starsMode = .starsGifting(birthdays: nil, hasActions: true)
|
||||
}
|
||||
|
||||
|
||||
let contactOptions: Signal<[ContactListAdditionalOption], NoError>
|
||||
if currentBirthdays != nil || "".isEmpty {
|
||||
contactOptions = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Birthday(id: context.account.peerId))
|
||||
@ -2247,104 +2245,122 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
var openProfileImpl: ((EnginePeer) -> Void)?
|
||||
var sendMessageImpl: ((EnginePeer) -> Void)?
|
||||
|
||||
//TODO:localize
|
||||
let controller: ViewController
|
||||
if case .stars = source {
|
||||
let options = Promise<[StarsGiftOption]>()
|
||||
options.set(context.engine.payments.starsGiftOptions(peerId: nil))
|
||||
// if case .stars = source {
|
||||
// let options = Promise<[StarsGiftOption]>()
|
||||
// options.set(context.engine.payments.starsGiftOptions(peerId: nil))
|
||||
let options = Promise<[PremiumGiftCodeOption]>()
|
||||
options.set(context.engine.payments.premiumGiftCodeOptions(peerId: nil))
|
||||
let contactsController = context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams(
|
||||
context: context,
|
||||
mode: starsMode,
|
||||
autoDismiss: false,
|
||||
title: { strings in return strings.Stars_Purchase_GiftStars },
|
||||
options: contactOptions
|
||||
))
|
||||
let _ = (contactsController.result
|
||||
|> deliverOnMainQueue).start(next: { result in
|
||||
if let (peers, _, _, _, _, _) = result, let contactPeer = peers.first, case let .peer(peer, _, _) = contactPeer {
|
||||
completion?([peer.id])
|
||||
title: { strings in return "Gift Premium or Stars" },
|
||||
options: contactOptions,
|
||||
openProfile: { peer in
|
||||
openProfileImpl?(peer)
|
||||
},
|
||||
sendMessage: { peer in
|
||||
sendMessageImpl?(peer)
|
||||
}
|
||||
})
|
||||
controller = contactsController
|
||||
} else {
|
||||
let options = Promise<[PremiumGiftCodeOption]>()
|
||||
options.set(context.engine.payments.premiumGiftCodeOptions(peerId: nil))
|
||||
let contactsController = context.sharedContext.makeContactMultiselectionController(
|
||||
ContactMultiselectionControllerParams(
|
||||
context: context,
|
||||
mode: mode,
|
||||
options: contactOptions,
|
||||
isPeerEnabled: { peer in
|
||||
if case let .user(user) = peer, user.botInfo == nil && !peer.isService && !user.flags.contains(.isSupport) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
},
|
||||
limit: limit,
|
||||
reachedLimit: { limit in
|
||||
reachedLimitImpl?(limit)
|
||||
},
|
||||
openProfile: { peer in
|
||||
openProfileImpl?(peer)
|
||||
},
|
||||
sendMessage: { peer in
|
||||
sendMessageImpl?(peer)
|
||||
}
|
||||
)
|
||||
)
|
||||
))
|
||||
let _ = combineLatest(queue: Queue.mainQueue(), contactsController.result, options.get())
|
||||
.startStandalone(next: { [weak contactsController] result, options in
|
||||
guard let controller = contactsController else {
|
||||
return
|
||||
}
|
||||
var peerIds: [PeerId] = []
|
||||
if case let .result(peerIdsValue, _) = result {
|
||||
peerIds = peerIdsValue.compactMap({ peerId in
|
||||
if case let .peer(peerId) = peerId {
|
||||
return peerId
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
})
|
||||
}
|
||||
guard !peerIds.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
let mappedOptions = options.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) }
|
||||
var pushImpl: ((ViewController) -> Void)?
|
||||
var filterImpl: (() -> Void)?
|
||||
let giftController = PremiumGiftScreen(context: context, peerIds: peerIds, options: mappedOptions, source: source, pushController: { c in
|
||||
pushImpl?(c)
|
||||
}, completion: {
|
||||
filterImpl?()
|
||||
if let (peers, _, _, _, _, _) = result, let contactPeer = peers.first, case let .peer(peer, _, _) = contactPeer {
|
||||
let premiumOptions = options.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) }
|
||||
let giftController = GiftOptionsScreen(context: context, peerId: peer.id, premiumOptions: premiumOptions)
|
||||
giftController.navigationPresentation = .modal
|
||||
contactsController?.push(giftController)
|
||||
|
||||
// completion?([peer.id])
|
||||
|
||||
if case .chatList = source, let _ = currentBirthdays {
|
||||
let _ = context.engine.notices.dismissServerProvidedSuggestion(suggestion: .todayBirthdays).startStandalone()
|
||||
}
|
||||
})
|
||||
pushImpl = { [weak giftController] c in
|
||||
giftController?.push(c)
|
||||
}
|
||||
filterImpl = { [weak giftController] in
|
||||
if let navigationController = giftController?.navigationController as? NavigationController {
|
||||
var controllers = navigationController.viewControllers
|
||||
controllers = controllers.filter { !($0 is ContactMultiselectionController) && !($0 is PremiumGiftScreen) }
|
||||
navigationController.setViewControllers(controllers, animated: true)
|
||||
}
|
||||
}
|
||||
controller.push(giftController)
|
||||
})
|
||||
controller = contactsController
|
||||
}
|
||||
// } else {
|
||||
// let options = Promise<[PremiumGiftCodeOption]>()
|
||||
// options.set(context.engine.payments.premiumGiftCodeOptions(peerId: nil))
|
||||
// let contactsController = context.sharedContext.makeContactMultiselectionController(
|
||||
// ContactMultiselectionControllerParams(
|
||||
// context: context,
|
||||
// mode: mode,
|
||||
// options: contactOptions,
|
||||
// isPeerEnabled: { peer in
|
||||
// if case let .user(user) = peer, user.botInfo == nil && !peer.isService && !user.flags.contains(.isSupport) {
|
||||
// return true
|
||||
// } else {
|
||||
// return false
|
||||
// }
|
||||
// },
|
||||
// limit: limit,
|
||||
// reachedLimit: { limit in
|
||||
// reachedLimitImpl?(limit)
|
||||
// },
|
||||
// openProfile: { peer in
|
||||
// openProfileImpl?(peer)
|
||||
// },
|
||||
// sendMessage: { peer in
|
||||
// sendMessageImpl?(peer)
|
||||
// }
|
||||
// )
|
||||
// )
|
||||
// let _ = combineLatest(queue: Queue.mainQueue(), contactsController.result, options.get())
|
||||
// .startStandalone(next: { [weak contactsController] result, options in
|
||||
// guard let controller = contactsController else {
|
||||
// return
|
||||
// }
|
||||
// var peerIds: [PeerId] = []
|
||||
// if case let .result(peerIdsValue, _) = result {
|
||||
// peerIds = peerIdsValue.compactMap({ peerId in
|
||||
// if case let .peer(peerId) = peerId {
|
||||
// return peerId
|
||||
// } else {
|
||||
// return nil
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// guard !peerIds.isEmpty else {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// let mappedOptions = options.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) }
|
||||
// var pushImpl: ((ViewController) -> Void)?
|
||||
// var filterImpl: (() -> Void)?
|
||||
// let giftController = PremiumGiftScreen(context: context, peerIds: peerIds, options: mappedOptions, source: source, pushController: { c in
|
||||
// pushImpl?(c)
|
||||
// }, completion: {
|
||||
// filterImpl?()
|
||||
//
|
||||
// if case .chatList = source, let _ = currentBirthdays {
|
||||
// let _ = context.engine.notices.dismissServerProvidedSuggestion(suggestion: .todayBirthdays).startStandalone()
|
||||
// }
|
||||
// })
|
||||
// pushImpl = { [weak giftController] c in
|
||||
// giftController?.push(c)
|
||||
// }
|
||||
// filterImpl = { [weak giftController] in
|
||||
// if let navigationController = giftController?.navigationController as? NavigationController {
|
||||
// var controllers = navigationController.viewControllers
|
||||
// controllers = controllers.filter { !($0 is ContactMultiselectionController) && !($0 is PremiumGiftScreen) }
|
||||
// navigationController.setViewControllers(controllers, animated: true)
|
||||
// }
|
||||
// }
|
||||
// controller.push(giftController)
|
||||
// })
|
||||
// controller = contactsController
|
||||
// }
|
||||
|
||||
reachedLimitImpl = { [weak controller] limit in
|
||||
guard let controller else {
|
||||
return
|
||||
}
|
||||
HapticFeedback().error()
|
||||
controller.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Premium_Gift_ContactSelection_MaximumReached("\(limit)").string, timeout: nil, customUndoText: nil), elevatedLayout: true, position: .bottom, animateInAsReplacement: false, action: { _ in return false }), in: .current)
|
||||
}
|
||||
// reachedLimitImpl = { [weak controller] limit in
|
||||
// guard let controller else {
|
||||
// return
|
||||
// }
|
||||
// HapticFeedback().error()
|
||||
// controller.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Premium_Gift_ContactSelection_MaximumReached("\(limit)").string, timeout: nil, customUndoText: nil), elevatedLayout: true, position: .bottom, animateInAsReplacement: false, action: { _ in return false }), in: .current)
|
||||
// }
|
||||
|
||||
sendMessageImpl = { [weak self, weak controller] peer in
|
||||
guard let self, let controller, let navigationController = controller.navigationController as? NavigationController else {
|
||||
@ -2795,6 +2811,10 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
return StarsTransactionScreen(context: context, subject: .boost(peerId, boost))
|
||||
}
|
||||
|
||||
public func makeGiftViewScreen(context: AccountContext, message: EngineMessage) -> ViewController {
|
||||
return GiftViewScreen(context: context, subject: .message(message))
|
||||
}
|
||||
|
||||
public func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal<MiniAppListScreenInitialData, NoError> {
|
||||
return MiniAppListScreen.initialData(context: context)
|
||||
}
|
||||
|
@ -186,12 +186,38 @@ private func manageableSpotlightContacts(appBasePath: String, accounts: Signal<[
|
||||
return accounts
|
||||
|> mapToSignal { accounts -> Signal<[[EnginePeer.Id: SpotlightIndexStorageItem]], NoError> in
|
||||
return combineLatest(queue: queue, accounts.map { account -> Signal<[EnginePeer.Id: SpotlightIndexStorageItem], NoError> in
|
||||
return TelegramEngine(account: account).data.subscribe(
|
||||
TelegramEngine.EngineData.Item.Contacts.List(includePresences: false)
|
||||
let engine = TelegramEngine(account: account)
|
||||
let recentApps = engine.peers.recentApps()
|
||||
|> mapToSignal { peerIds -> Signal<[EnginePeer], NoError> in
|
||||
return engine.data.get(
|
||||
EngineDataMap(
|
||||
peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.Peer in
|
||||
return TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
|
||||
}
|
||||
)
|
||||
)
|
||||
|> map { result -> [EnginePeer] in
|
||||
var peers: [EnginePeer] = []
|
||||
for (_, maybePeer) in result {
|
||||
if let peer = maybePeer {
|
||||
peers.append(peer)
|
||||
}
|
||||
}
|
||||
return peers
|
||||
}
|
||||
}
|
||||
return combineLatest(
|
||||
engine.data.subscribe(
|
||||
TelegramEngine.EngineData.Item.Contacts.List(includePresences: false)
|
||||
),
|
||||
recentApps
|
||||
)
|
||||
|> map { view -> [EnginePeer.Id: SpotlightIndexStorageItem] in
|
||||
|> map { view, recentApps -> [EnginePeer.Id: SpotlightIndexStorageItem] in
|
||||
var result: [EnginePeer.Id: SpotlightIndexStorageItem] = [:]
|
||||
for peer in view.peers {
|
||||
var peers: [EnginePeer] = []
|
||||
peers.append(contentsOf: view.peers)
|
||||
peers.append(contentsOf: recentApps)
|
||||
for peer in peers {
|
||||
if case let .user(user) = peer {
|
||||
let avatarSourcePath = smallestImageRepresentation(user.photo).flatMap { representation -> String? in
|
||||
let resourcePath = account.postbox.mediaBox.resourcePath(representation.resource)
|
||||
|
@ -409,6 +409,7 @@ public final class ChatTextInputTextCustomEmojiAttribute: NSObject, Codable {
|
||||
case nameColors([UInt32])
|
||||
case stars(tinted: Bool)
|
||||
case ton
|
||||
case animation(name: String)
|
||||
}
|
||||
|
||||
public let interactivelySelectedFromPackId: ItemCollectionId?
|
||||
|
Loading…
x
Reference in New Issue
Block a user