diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 8a704ace67..fef2ed9501 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -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 func makeMiniAppListScreen(context: AccountContext, initialData: MiniAppListScreenInitialData) -> ViewController diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index 01a4f4a292..b2dd59e404 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -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 = 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 } } diff --git a/submodules/AccountContext/Sources/Premium.swift b/submodules/AccountContext/Sources/Premium.swift index 5cd4fd732e..7f8d9fdaaa 100644 --- a/submodules/AccountContext/Sources/Premium.swift +++ b/submodules/AccountContext/Sources/Premium.swift @@ -276,3 +276,7 @@ public struct PremiumConfiguration { } } } + +public protocol GiftOptionsScreenProtocol { + +} diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index c472ceb465..5344b52b73 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -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 } diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift index dba9e62241..c224ed88b9 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift @@ -29,22 +29,27 @@ public final class BotCheckoutController: ViewController { public static func fetch(context: AccountContext, source: BotPaymentInvoiceSource) -> Signal { 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 diff --git a/submodules/BrowserUI/BUILD b/submodules/BrowserUI/BUILD index 9f7432956e..e031d7675d 100644 --- a/submodules/BrowserUI/BUILD +++ b/submodules/BrowserUI/BUILD @@ -49,6 +49,7 @@ swift_library( "//submodules/TelegramUI/Components/SaveProgressScreen", "//submodules/TelegramUI/Components/ListActionItemComponent", "//submodules/Utils/DeviceModel", + "//submodules/LegacyMediaPickerUI", ], visibility = [ "//visibility:public", diff --git a/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift index cef645b788..9fe3b22f61 100644 --- a/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift @@ -147,7 +147,7 @@ public final class BrowserBookmarksScreen: ViewController { }, openLargeEmojiInfo: { _, _, _ in }, openJoinLink: { _ in }, openWebView: { _, _, _, _ in - }, activateAdAction: { _, _ in + }, activateAdAction: { _, _, _, _ in }, openRequestedPeerSelection: { _, _, _, _ in }, saveMediaToFiles: { _ in }, openNoAdsDemo: { diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index 8ea2acde0d..3229d42863 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -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(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 diff --git a/submodules/PromptUI/BUILD b/submodules/PromptUI/BUILD index aec3f5594f..ae10576e23 100644 --- a/submodules/PromptUI/BUILD +++ b/submodules/PromptUI/BUILD @@ -17,6 +17,7 @@ swift_library( "//submodules/TelegramCore:TelegramCore", "//submodules/AccountContext:AccountContext", "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/TelegramStringFormatting", ], visibility = [ "//visibility:public", diff --git a/submodules/PromptUI/Sources/PromptController.swift b/submodules/PromptUI/Sources/PromptController.swift index e6e9e19889..46e58a30ab 100644 --- a/submodules/PromptUI/Sources/PromptController.swift +++ b/submodules/PromptUI/Sources/PromptController.swift @@ -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)? = 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)? = 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) +} diff --git a/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift b/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift index a32b6c972e..675ccef464 100644 --- a/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift +++ b/submodules/SSignalKit/SwiftSignalKit/Source/Signal_Combine.swift @@ -238,6 +238,12 @@ public func combineLatest(queue: Queue? = nil, _ s1: Signal, _ s2: Signal, _ s3: Signal, _ s4: Signal, _ s5: Signal, _ s6: Signal, _ s7: Signal, _ s8: Signal, _ s9: Signal, _ s10: Signal, _ s11: Signal, _ s12: Signal, _ s13: Signal, _ s14: Signal, _ s15: Signal, _ s16: Signal, _ s17: Signal, _ s18: Signal, _ s19: Signal, _ s20: Signal, _ s21: Signal, _ s22: Signal, _ s23: Signal, _ s24: Signal, _ s25: Signal, _ s26: Signal) -> 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(queue: Queue? = nil, _ signals: [Signal]) -> Signal<[T], E> { if signals.count == 0 { return single([T](), E.self) diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index bb11da56d3..b3c887f01a 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -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) } diff --git a/submodules/TelegramApi/Sources/Api26.swift b/submodules/TelegramApi/Sources/Api26.swift index 345153edc1..820f50b8f8 100644 --- a/submodules/TelegramApi/Sources/Api26.swift +++ b/submodules/TelegramApi/Sources/Api26.swift @@ -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 diff --git a/submodules/TelegramApi/Sources/Api36.swift b/submodules/TelegramApi/Sources/Api36.swift index 70376e3095..740c313af2 100644 --- a/submodules/TelegramApi/Sources/Api36.swift +++ b/submodules/TelegramApi/Sources/Api36.swift @@ -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) { + static func saveStarGift(flags: Int32, userId: Api.InputUser, msgId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { 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() { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index 0d9cb8bb26..e7f5a7bc62 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -204,7 +204,7 @@ func _internal_convertStarGift(account: Account, messageId: EngineMessage.Id) -> } } -func _internal_saveStarGiftToProfile(account: Account, messageId: EngineMessage.Id) -> Signal { +func _internal_updateStarGiftAddedToProfile(account: Account, messageId: EngineMessage.Id, added: Bool) -> Signal { 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 in return .single(nil) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift index a52042b0a9..b383dd994a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift @@ -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 { return _internal_convertStarGift(account: self.account, messageId: messageId) } - public func saveStarGiftToProfile(messageId: EngineMessage.Id) -> Signal { - return _internal_saveStarGiftToProfile(account: self.account, messageId: messageId) + public func updateStarGiftAddedToProfile(messageId: EngineMessage.Id, added: Bool) -> Signal { + return _internal_updateStarGiftAddedToProfile(account: self.account, messageId: messageId, added: added) } - } } diff --git a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift index 69b9a6813a..0a7d969a05 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDayPresentationTheme.swift @@ -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, diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index d2d9eea5c7..729ef31388 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -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 } diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 675a306fea..328418a38e 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -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": [], diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index eca22e7b7f..8d74ca2fe0 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -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))) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index 69497821c0..d599e4f5a9 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -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() diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift index 5ec7990494..644158a83a 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift @@ -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) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift index 49875649c4..ba57a1641a 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift @@ -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 { diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index 6c39fe96db..80126b29ca 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -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() inputData.set(BotCheckoutController.InputData.fetch(context: strongSelf.context, source: .slug(slug)) - |> map(Optional.init) - |> `catch` { _ -> Signal in + |> map(Optional.init) + |> `catch` { _ -> Signal in return .single(nil) }) strongSelf.controllerInteraction.presentController(BotCheckoutController(context: strongSelf.context, invoice: invoice, source: .slug(slug), inputData: inputData, completed: { currencyValue, receiptMessageId in diff --git a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift index 714ce82b87..1c43afa28c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift @@ -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: { diff --git a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift index 1d555712bf..d78c1a3c8d 100644 --- a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift @@ -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?) -> Void + public let activateAdAction: (EngineMessage.Id, Promise?, 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?) -> Void, + activateAdAction: @escaping (EngineMessage.Id, Promise?, Bool, Bool) -> Void, openRequestedPeerSelection: @escaping (EngineMessage.Id, ReplyMarkupButtonRequestPeerType, Int32, Int32) -> Void, saveMediaToFiles: @escaping (EngineMessage.Id) -> Void, openNoAdsDemo: @escaping () -> Void, diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift index 4d91e53e12..84a6c22509 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift @@ -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 diff --git a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/BUILD b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/BUILD new file mode 100644 index 0000000000..2a22ef269a --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/BUILD @@ -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", + ], +) diff --git a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift new file mode 100644 index 0000000000..340460ce72 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift @@ -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? + + private var avatarNode: AvatarNode? + private let title = ComponentView() + private let subtitle = ComponentView() + private let button = ComponentView() + private let ribbon = UIImageView() + private let ribbonText = ComponentView() + + 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, 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 + if let current = self.loadingBackground { + loadingBackground = current + } else { + loadingBackground = ComponentView() + 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, 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() + + 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, 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, 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) + } +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/BUILD new file mode 100644 index 0000000000..50f4c91908 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/BUILD @@ -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", + ], +) diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift new file mode 100644 index 0000000000..f551c43f45 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift @@ -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() + private let topSeparator = ComponentView() + private let cancelButton = ComponentView() + + private let header = ComponentView() + + private let premiumTitle = ComponentView() + private let premiumDescription = ComponentView() + private var premiumItems: [AnyHashable: ComponentView] = [:] + private var selectedPremiumGift: String? + + private let starsTitle = ComponentView() + private let starsDescription = ComponentView() + private var starsItems: [AnyHashable: ComponentView] = [:] + private let tabSelector = ComponentView() + 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 + 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, 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 + 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, 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)) + } +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD new file mode 100644 index 0000000000..1c0a9254dc --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD @@ -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", + ], +) diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift new file mode 100644 index 0000000000..4273baca41 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift @@ -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?, (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() + let messages = SimpleDictionary() + + 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) + } +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift new file mode 100644 index 0000000000..f26cb3deb2 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -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() + private let introContent = ComponentView() + private let introSection = ComponentView() + private let hideSection = ComponentView() + private let button = ComponentView() + + 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 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, 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] = [] + 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, 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) + } +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD new file mode 100644 index 0000000000..342a21824d --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD @@ -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", + ], +) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift new file mode 100644 index 0000000000..28b3467edd --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -0,0 +1,1325 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit +import AccountContext +import TelegramPresentationData +import PresentationDataUtils +import ComponentFlow +import ViewControllerComponent +import SheetComponent +import MultilineTextComponent +import BundleIconComponent +import SolidRoundedButtonComponent +import Markdown +import BalancedTextComponent +import AvatarNode +import TextFormat +import TelegramStringFormatting +import StarsAvatarComponent +import EmojiTextAttachmentView +import UndoUI + +private final class GiftViewSheetContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let subject: GiftViewScreen.Subject + let cancel: (Bool) -> Void + let openPeer: (EnginePeer) -> Void + let updateSavedToProfile: (Bool) -> Void + let convertToStars: () -> Void + + init( + context: AccountContext, + subject: GiftViewScreen.Subject, + cancel: @escaping (Bool) -> Void, + openPeer: @escaping (EnginePeer) -> Void, + updateSavedToProfile: @escaping (Bool) -> Void, + convertToStars: @escaping () -> Void + ) { + self.context = context + self.subject = subject + self.cancel = cancel + self.openPeer = openPeer + self.updateSavedToProfile = updateSavedToProfile + self.convertToStars = convertToStars + } + + static func ==(lhs: GiftViewSheetContent, rhs: GiftViewSheetContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.subject != rhs.subject { + return false + } + return true + } + + final class State: ComponentState { + private let context: AccountContext + private var disposable: Disposable? + var initialized = false + + var peerMap: [EnginePeer.Id: EnginePeer] = [:] + + var cachedCloseImage: (UIImage, PresentationTheme)? + var cachedChevronImage: (UIImage, PresentationTheme)? + + var inProgress = false + + init(context: AccountContext, subject: GiftViewScreen.Subject) { + self.context = context + + super.init() + + if let arguments = subject.arguments { + let peerIds: [EnginePeer.Id] = [arguments.peerId, context.account.peerId] + self.disposable = (context.engine.data.get( + EngineDataMap( + peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.Peer in + return TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) + } + ) + ) + |> deliverOnMainQueue).startStrict(next: { [weak self] peers in + if let strongSelf = self { + var peersMap: [EnginePeer.Id: EnginePeer] = [:] + for peerId in peerIds { + if let maybePeer = peers[peerId], let peer = maybePeer { + peersMap[peerId] = peer + } + } + strongSelf.peerMap = peersMap + + strongSelf.initialized = true + + strongSelf.updated(transition: .immediate) + } + }) + } + } + + deinit { + self.disposable?.dispose() + } + } + + func makeState() -> State { + return State(context: self.context, subject: self.subject) + } + + static var body: Body { + let closeButton = Child(Button.self) + let animation = Child(GiftAnimationComponent.self) + let title = Child(MultilineTextComponent.self) + let amount = Child(BalancedTextComponent.self) + let amountStar = Child(BundleIconComponent.self) + let description = Child(MultilineTextComponent.self) + let table = Child(TableComponent.self) + let button = Child(SolidRoundedButtonComponent.self) + let secondaryButton = Child(SolidRoundedButtonComponent.self) + + let spaceRegex = try? NSRegularExpression(pattern: "\\[(.*?)\\]", options: []) + + return { context in + let environment = context.environment[ViewControllerComponentContainer.Environment.self].value + let controller = environment.controller + + let component = context.component + let theme = environment.theme + let strings = environment.strings + let dateTimeFormat = environment.dateTimeFormat + + let state = context.state + + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + let textSideInset: CGFloat = 32.0 + environment.safeInsets.left + + let closeImage: UIImage + if let (image, theme) = state.cachedCloseImage, theme === environment.theme { + closeImage = image + } else { + closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: theme.actionSheet.inputClearButtonColor)! + state.cachedCloseImage = (closeImage, theme) + } + + let closeButton = closeButton.update( + component: Button( + content: AnyComponent(Image(image: closeImage)), + action: { [weak component] in + component?.cancel(true) + } + ), + availableSize: CGSize(width: 30.0, height: 30.0), + transition: .immediate + ) + + let animationFile: TelegramMediaFile? + let stars: Int64 + let convertStars: Int64 + let text: String? + let entities: [MessageTextEntity]? + let limitNumber: Int32? + let limitTotal: Int32? + var incoming = false + var savedToProfile = false + var converted = false + if let arguments = component.subject.arguments { + animationFile = arguments.gift.file + stars = arguments.gift.price + text = arguments.text + entities = arguments.entities + limitNumber = arguments.gift.availability?.remains + limitTotal = arguments.gift.availability?.total + convertStars = arguments.convertStars + incoming = arguments.incoming + savedToProfile = arguments.savedToProfile + converted = arguments.converted + } else { + animationFile = nil + stars = 0 + text = nil + entities = nil + limitNumber = nil + limitTotal = nil + convertStars = 0 + } + let _ = entities + let _ = limitNumber + + var descriptionText: String + if incoming { + if !converted { + descriptionText = "You can keep this gift in your Profile or convert it to \(convertStars) Stars. [More About Stars >]()" + } else { + descriptionText = "You converted this gift to \(convertStars) Stars. [More About Stars >]()" + } + } else if let peerId = component.subject.arguments?.peerId, let peer = state.peerMap[peerId] { + descriptionText = "\(peer.compactDisplayTitle) can keep this gift in their Profile or convert it to \(convertStars) Stars. [More About Stars >]()" + } else { + descriptionText = "" + } + if let spaceRegex { + let nsRange = NSRange(descriptionText.startIndex..., in: descriptionText) + let matches = spaceRegex.matches(in: descriptionText, options: [], range: nsRange) + var modifiedString = descriptionText + + for match in matches.reversed() { + let matchRange = Range(match.range, in: descriptionText)! + let matchedSubstring = String(descriptionText[matchRange]) + let replacedSubstring = matchedSubstring.replacingOccurrences(of: " ", with: "\u{00A0}") + modifiedString.replaceSubrange(matchRange, with: replacedSubstring) + } + descriptionText = modifiedString + } + + let formattedAmount = presentationStringsFormattedNumber(abs(Int32(stars)), dateTimeFormat.groupingSeparator) + let countFont: UIFont = Font.semibold(17.0) + let amountText = formattedAmount + let countColor = theme.list.itemDisclosureActions.constructive.fillColor + + let title = title.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: incoming ? "Received Gift" : "Gift", + font: Font.bold(25.0), + textColor: theme.actionSheet.primaryTextColor, + paragraphAlignment: .center + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + + let amountAttributedText = NSMutableAttributedString(string: amountText, font: countFont, textColor: countColor) + let amount = amount.update( + component: BalancedTextComponent( + text: .plain(amountAttributedText), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + ), + availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + + let amountStar = amountStar.update( + component: BundleIconComponent( + name: "Premium/Stars/StarMedium", + tintColor: nil + ), + availableSize: context.availableSize, + transition: .immediate + ) + + let tableFont = Font.regular(15.0) + let tableTextColor = theme.list.itemPrimaryTextColor + var tableItems: [TableComponent.Item] = [] + + if let peerId = component.subject.arguments?.peerId, let peer = state.peerMap[peerId] { + tableItems.append(.init( + id: "to", + title: incoming ? strings.Stars_Transaction_From : strings.Stars_Transaction_To, + component: AnyComponent( + Button( + content: AnyComponent( + PeerCellComponent( + context: component.context, + theme: theme, + peer: peer + ) + ), + action: { + if "".isEmpty { + component.openPeer(peer) + Queue.mainQueue().after(1.0, { + component.cancel(false) + }) + } else { + if let controller = controller() as? GiftViewScreen, let navigationController = controller.navigationController, let chatController = navigationController.viewControllers.first(where: { $0 is ChatController }) as? ChatController { + chatController.playShakeAnimation() + } + component.cancel(true) + } + } + ) + ) + )) + } + + tableItems.append(.init( + id: "date", + title: strings.Stars_Transaction_Date, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: Int32(Date().timeIntervalSince1970), strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) + ) + )) + + if let limitTotal { + tableItems.append(.init( + id: "availability", + title: "Availability", + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: "1 of \(limitTotal)", font: tableFont, textColor: tableTextColor))) + ) + )) + } + + if let text { + tableItems.append(.init( + id: "text", + title: nil, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: text, font: tableFont, textColor: tableTextColor))) + ) + )) + } + + let table = table.update( + component: TableComponent( + theme: environment.theme, + items: tableItems + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude), + transition: .immediate + ) + + let textFont = Font.regular(15.0) +// let boldTextFont = Font.semibold(15.0) +// let textColor = theme.actionSheet.secondaryTextColor + let linkColor = theme.actionSheet.controlAccentColor +// let destructiveColor = theme.actionSheet.destructiveActionTextColor +// let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in +// return (TelegramTextAttributes.URL, contents) +// }) +// let additional = additional.update( +// component: BalancedTextComponent( +// text: .markdown(text: additionalText, attributes: markdownAttributes), +// horizontalAlignment: .center, +// maximumNumberOfLines: 0, +// lineSpacing: 0.2, +// highlightColor: linkColor.withAlphaComponent(0.2), +// highlightAction: { attributes in +// if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { +// return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) +// } else { +// return nil +// } +// }, +// tapAction: { attributes, _ in +// if let controller = controller() as? GiftViewScreen, let navigationController = controller.navigationController as? NavigationController { +// let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } +// component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Stars_Transaction_Terms_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) +// component.cancel(true) +// } +// } +// ), +// availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), +// transition: .immediate +// ) + + + + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: 177.0)) + ) + + var originY: CGFloat = 0.0 + if let animationFile { + let animation = animation.update( + component: GiftAnimationComponent( + context: component.context, + theme: environment.theme, + file: animationFile + ), + availableSize: CGSize(width: 128.0, height: 128.0), + transition: .immediate + ) + context.add(animation + .position(CGPoint(x: context.availableSize.width / 2.0, y: animation.size.height / 2.0 + 25.0)) + ) + originY += animation.size.height + } + originY += 69.0 + + var descriptionSize: CGSize = .zero + if !descriptionText.isEmpty { + if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme { + state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme) + } + + let textColor = theme.list.itemPrimaryTextColor + let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: textFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + }) + let attributedString = parseMarkdownIntoAttributedString(descriptionText, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString + if let range = attributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 { + attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string)) + } + let description = description.update( + component: MultilineTextComponent( + text: .plain(attributedString), + horizontalAlignment: .center, + maximumNumberOfLines: 5, + lineSpacing: 0.2, + highlightColor: linkColor.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 + + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + descriptionSize = description.size + var descriptionOrigin = originY + if "".isEmpty { + descriptionOrigin += amount.size.height + 13.0 + } + context.add(description + .position(CGPoint(x: context.availableSize.width / 2.0, y: descriptionOrigin + description.size.height / 2.0)) + ) + originY += description.size.height + 10.0 + } + + let amountSpacing: CGFloat = 1.0 + let totalAmountWidth: CGFloat = amount.size.width + amountSpacing + amountStar.size.width + let amountOriginX: CGFloat = floor(context.availableSize.width - totalAmountWidth) / 2.0 + + var amountOrigin = originY + if "".isEmpty { + amountOrigin -= descriptionSize.height + 10.0 + originY += amount.size.height + 26.0 + } else { + originY += amount.size.height + 20.0 + } + + let amountLabelOriginX: CGFloat + let amountStarOriginX: CGFloat + if !"".isEmpty { + amountStarOriginX = amountOriginX + amountStar.size.width / 2.0 + amountLabelOriginX = amountOriginX + amountStar.size.width + amountSpacing + amount.size.width / 2.0 + } else { + amountLabelOriginX = amountOriginX + amount.size.width / 2.0 + amountStarOriginX = amountOriginX + amount.size.width + amountSpacing + amountStar.size.width / 2.0 + } + + context.add(amount + .position(CGPoint(x: amountLabelOriginX, y: amountOrigin + amount.size.height / 2.0)) + ) + context.add(amountStar + .position(CGPoint(x: amountStarOriginX, y: amountOrigin + amountStar.size.height / 2.0 - UIScreenPixel)) + ) + + context.add(table + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + table.size.height / 2.0)) + ) + originY += table.size.height + 23.0 + +// context.add(additional +// .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + additional.size.height / 2.0)) +// ) +// originY += additional.size.height + 23.0 + +// if let statusText { +// originY += 7.0 +// let status = status.update( +// component: BalancedTextComponent( +// text: .plain(NSAttributedString(string: statusText, font: textFont, textColor: statusIsDestructive ? destructiveColor : textColor)), +// horizontalAlignment: .center, +// maximumNumberOfLines: 0, +// lineSpacing: 0.1 +// ), +// availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), +// transition: .immediate +// ) +// context.add(status +// .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + status.size.height / 2.0)) +// ) +// originY += status.size.height + (statusIsDestructive ? 23.0 : 13.0) +// } + + + if incoming && !converted { + let button = button.update( + component: SolidRoundedButtonComponent( + title: savedToProfile ? "Hide from My Page" : "Display on My Page", + theme: SolidRoundedButtonComponent.Theme(theme: theme), + font: .bold, + fontSize: 17.0, + height: 50.0, + cornerRadius: 10.0, + gloss: false, + iconName: nil, + animationName: nil, + iconPosition: .left, + isLoading: state.inProgress, + action: { + component.updateSavedToProfile(!savedToProfile) + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + transition: context.transition + ) + let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: originY), size: button.size) + context.add(button + .position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY)) + ) + originY += button.size.height + originY += 7.0 + + let secondaryButton = secondaryButton.update( + component: SolidRoundedButtonComponent( + title: "Convert to \(convertStars) Stars", + theme: SolidRoundedButtonComponent.Theme(backgroundColor: .clear, foregroundColor: linkColor), + font: .regular, + fontSize: 17.0, + height: 50.0, + cornerRadius: 10.0, + gloss: false, + iconName: nil, + animationName: nil, + iconPosition: .left, + isLoading: false, + action: { + component.convertToStars() + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + transition: context.transition + ) + let secondaryButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: originY), size: secondaryButton.size) + context.add(secondaryButton + .position(CGPoint(x: secondaryButtonFrame.midX, y: secondaryButtonFrame.midY)) + ) + originY += secondaryButton.size.height + } + + context.add(closeButton + .position(CGPoint(x: context.availableSize.width - environment.safeInsets.left - closeButton.size.width, y: 28.0)) + ) + + let contentSize = CGSize(width: context.availableSize.width, height: originY + 5.0 + environment.safeInsets.bottom) + + return contentSize + } + } +} + +private final class GiftViewSheetComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let subject: GiftViewScreen.Subject + let openPeer: (EnginePeer) -> Void + let updateSavedToProfile: (Bool) -> Void + let convertToStars: () -> Void + + init( + context: AccountContext, + subject: GiftViewScreen.Subject, + openPeer: @escaping (EnginePeer) -> Void, + updateSavedToProfile: @escaping (Bool) -> Void, + convertToStars: @escaping () -> Void + ) { + self.context = context + self.subject = subject + self.openPeer = openPeer + self.updateSavedToProfile = updateSavedToProfile + self.convertToStars = convertToStars + } + + static func ==(lhs: GiftViewSheetComponent, rhs: GiftViewSheetComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.subject != rhs.subject { + return false + } + return true + } + + static var body: Body { + let sheet = Child(SheetComponent.self) + let animateOut = StoredActionSlot(Action.self) + + let sheetExternalState = SheetComponent.ExternalState() + + return { context in + let environment = context.environment[EnvironmentType.self] + let controller = environment.controller + + let sheet = sheet.update( + component: SheetComponent( + content: AnyComponent(GiftViewSheetContent( + context: context.component.context, + subject: context.component.subject, + cancel: { animate in + if animate { + if let controller = controller() as? GiftViewScreen { + controller.dismissAllTooltips() + animateOut.invoke(Action { [weak controller] _ in + controller?.dismiss(completion: nil) + }) + } + } else if let controller = controller() { + controller.dismiss(animated: false, completion: nil) + } + }, + openPeer: context.component.openPeer, + updateSavedToProfile: context.component.updateSavedToProfile, + convertToStars: context.component.convertToStars + )), + backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), + followContentSizeChanges: true, + clipsContent: true, + externalState: sheetExternalState, + animateOut: animateOut, + onPan: { + if let controller = controller() as? GiftViewScreen { + controller.dismissAllTooltips() + } + } + ), + environment: { + environment + SheetComponentEnvironment( + isDisplaying: environment.value.isVisible, + isCentered: environment.metrics.widthClass == .regular, + hasInputHeight: !environment.inputHeight.isZero, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { animated in + if animated { + if let controller = controller() as? GiftViewScreen { + controller.dismissAllTooltips() + animateOut.invoke(Action { _ in + controller.dismiss(completion: nil) + }) + } + } else { + if let controller = controller() as? GiftViewScreen { + controller.dismissAllTooltips() + controller.dismiss(completion: nil) + } + } + } + ) + }, + availableSize: context.availableSize, + transition: context.transition + ) + + context.add(sheet + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + if let controller = controller(), !controller.automaticallyControlPresentationContextLayout { + let layout = ContainerViewLayout( + size: context.availableSize, + metrics: environment.metrics, + deviceMetrics: environment.deviceMetrics, + intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: max(environment.safeInsets.bottom, sheetExternalState.contentHeight), right: 0.0), + safeInsets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: 0.0, right: environment.safeInsets.right), + additionalInsets: .zero, + statusBarHeight: environment.statusBarHeight, + inputHeight: nil, + inputHeightIsInteractivellyChanging: false, + inVoiceOver: false + ) + controller.presentationContext.containerLayoutUpdated(layout, transition: context.transition.containedViewLayoutTransition) + } + + return context.availableSize + } + } +} + +public class GiftViewScreen: ViewControllerComponentContainer { + public enum Subject: Equatable { + case message(EngineMessage) + case profileGift(EnginePeer.Id, ProfileGiftsContext.State.StarGift) + + var arguments: (peerId: EnginePeer.Id, messageId: EngineMessage.Id?, incoming: Bool, gift: StarGift, convertStars: Int64, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool)? { + switch self { + case let .message(message): + if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted) = action.action { + return (message.id.peerId, message.id, message.flags.contains(.Incoming), gift, convertStars, text, entities, nameHidden, savedToProfile, converted) + } + case let .profileGift(peerId, gift): + return (peerId, gift.messageId, false, gift.gift, gift.convertStars ?? 0, gift.text, gift.entities, gift.nameHidden, true, false) + } + return nil + } + } + + private let context: AccountContext + public var disposed: () -> Void = {} + + private let hapticFeedback = HapticFeedback() + + public init( + context: AccountContext, + subject: GiftViewScreen.Subject, + forceDark: Bool = false + ) { + self.context = context + + var openPeerImpl: ((EnginePeer) -> Void)? + var updateSavedToProfileImpl: ((Bool) -> Void)? + var convertToStarsImpl: (() -> Void)? + super.init( + context: context, + component: GiftViewSheetComponent( + context: context, + subject: subject, + openPeer: { peerId in + openPeerImpl?(peerId) + }, + updateSavedToProfile: { added in + updateSavedToProfileImpl?(added) + }, + convertToStars: { + convertToStarsImpl?() + } + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: forceDark ? .dark : .default + ) + + self.navigationPresentation = .flatModal + self.automaticallyControlPresentationContextLayout = false + + openPeerImpl = { [weak self] peer in + guard let self, let navigationController = self.navigationController as? NavigationController else { + return + } + self.dismissAllTooltips() + + let _ = (context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id) + ) + |> deliverOnMainQueue).start(next: { peer in + guard let peer else { + return + } + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, chatController: nil, context: context, chatLocation: .peer(peer), subject: nil, botStart: nil, updateTextInputState: nil, keepStack: .always, useExisting: true, purposefulAction: nil, scrollToEndIfExists: false, activateMessageSearch: nil, animated: true)) + }) + } + + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + updateSavedToProfileImpl = { [weak self] added in + guard let self, let arguments = subject.arguments, let messageId = arguments.messageId else { + return + } + let _ = (context.engine.payments.updateStarGiftAddedToProfile(messageId: messageId, added: added) + |> deliverOnMainQueue).startStandalone() + + self.dismissAnimated() + + if let navigationController { + Queue.mainQueue().after(0.5) { + if let lastController = navigationController.viewControllers.last as? ViewController { + let resultController = UndoOverlayController( + presentationData: presentationData, + content: .sticker(context: context, file: arguments.gift.file, loop: false, title: "Gift Saved to Profile", text: "The gift is now displayed in your profile.", undoText: nil, customAction: nil), + elevatedLayout: lastController is ChatController, + action: { _ in return true} + ) + lastController.present(resultController, in: .window(.root)) + } + } + } + } + + convertToStarsImpl = { [weak self] in + guard let self, case let .message(message) = subject, let arguments = subject.arguments, let messageId = arguments.messageId, let navigationController = self.navigationController as? NavigationController else { + return + } + let controller = textAlertController( + context: self.context, + title: "Convert Gift to Stars", + text: "Do you want to convert this gift from **\(message.author?.compactDisplayTitle ?? "")** to **\(arguments.convertStars) Stars**?\n\nThis action cannot be undone.", + actions: [ + TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), + TextAlertAction(type: .defaultAction, title: "Convert", action: { [weak self, weak navigationController] in + let _ = (context.engine.payments.convertStarGift(messageId: messageId) + |> deliverOnMainQueue).startStandalone() + + self?.dismissAnimated() + + if let navigationController { + Queue.mainQueue().after(0.5) { + if let lastController = navigationController.viewControllers.last as? ViewController { + let resultController = UndoOverlayController( + presentationData: presentationData, + content: .universal( + animation: "StarsBuy", + scale: 0.066, + colors: [:], + title: "Gift Converted", + text: "You received **\(arguments.convertStars) Stars** instead.", + customUndoText: nil, + timeout: nil + ), + elevatedLayout: lastController is ChatController, + action: { _ in return true} + ) + lastController.present(resultController, in: .window(.root)) + } + } + } + }) + ], + parseMarkdown: true + ) + self.present(controller, in: .window(.root)) + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.disposed() + } + + public override func viewDidLoad() { + super.viewDidLoad() + + self.view.disablesInteractiveModalDismiss = true + } + + public override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + self.dismissAllTooltips() + } + + public func dismissAnimated() { + self.dismissAllTooltips() + + if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { + view.dismissAnimated() + } + } + + fileprivate func dismissAllTooltips() { +// self.window?.forEachController({ controller in +// if let controller = controller as? UndoOverlayController { +// controller.dismiss() +// } +// }) +// self.forEachController({ controller in +// if let controller = controller as? UndoOverlayController { +// controller.dismiss() +// } +// return true +// }) + } +} + +private final class TableComponent: CombinedComponent { + class Item: Equatable { + public let id: AnyHashable + public let title: String? + public let component: AnyComponent + public let insets: UIEdgeInsets? + + public init(id: IdType, title: String?, component: AnyComponent, insets: UIEdgeInsets? = nil) { + self.id = AnyHashable(id) + self.title = title + self.component = component + self.insets = insets + } + + public static func == (lhs: Item, rhs: Item) -> Bool { + if lhs.id != rhs.id { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.component != rhs.component { + return false + } + if lhs.insets != rhs.insets { + return false + } + return true + } + } + + private let theme: PresentationTheme + private let items: [Item] + + public init(theme: PresentationTheme, items: [Item]) { + self.theme = theme + self.items = items + } + + public static func ==(lhs: TableComponent, rhs: TableComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.items != rhs.items { + return false + } + return true + } + + final class State: ComponentState { + var cachedBorderImage: (UIImage, PresentationTheme)? + } + + func makeState() -> State { + return State() + } + + public static var body: Body { + let leftColumnBackground = Child(Rectangle.self) + let verticalBorder = Child(Rectangle.self) + let titleChildren = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) + let valueChildren = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) + let borderChildren = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) + let outerBorder = Child(Image.self) + + return { context in + let verticalPadding: CGFloat = 11.0 + let horizontalPadding: CGFloat = 12.0 + let borderWidth: CGFloat = 1.0 + + let backgroundColor = context.component.theme.actionSheet.opaqueItemBackgroundColor + let borderColor = backgroundColor.mixedWith(context.component.theme.list.itemBlocksSeparatorColor, alpha: 0.6) + + var leftColumnWidth: CGFloat = 0.0 + + var updatedTitleChildren: [Int: _UpdatedChildComponent] = [:] + var updatedValueChildren: [(_UpdatedChildComponent, UIEdgeInsets)] = [] + var updatedBorderChildren: [_UpdatedChildComponent] = [] + + var i = 0 + for item in context.component.items { + guard let title = item.title else { + i += 1 + continue + } + let titleChild = titleChildren[item.id].update( + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: title, font: Font.regular(15.0), textColor: context.component.theme.list.itemPrimaryTextColor)) + )), + availableSize: context.availableSize, + transition: context.transition + ) + updatedTitleChildren[i] = titleChild + + if titleChild.size.width > leftColumnWidth { + leftColumnWidth = titleChild.size.width + } + i += 1 + } + + leftColumnWidth = max(100.0, leftColumnWidth + horizontalPadding * 2.0) + let rightColumnWidth = context.availableSize.width - leftColumnWidth + + i = 0 + var rowHeights: [Int: CGFloat] = [:] + var totalHeight: CGFloat = 0.0 + var innerTotalHeight: CGFloat = 0.0 + + for item in context.component.items { + let insets: UIEdgeInsets + if let customInsets = item.insets { + insets = customInsets + } else { + insets = UIEdgeInsets(top: 0.0, left: horizontalPadding, bottom: 0.0, right: horizontalPadding) + } + let valueChild = valueChildren[item.id].update( + component: item.component, + availableSize: CGSize(width: rightColumnWidth - insets.left - insets.right, height: context.availableSize.height), + transition: context.transition + ) + updatedValueChildren.append((valueChild, insets)) + + var titleHeight: CGFloat = 0.0 + if let titleChild = updatedTitleChildren[i] { + titleHeight = titleChild.size.height + } + let rowHeight = max(40.0, max(titleHeight, valueChild.size.height) + verticalPadding * 2.0) + rowHeights[i] = rowHeight + totalHeight += rowHeight + if titleHeight > 0.0 { + innerTotalHeight += rowHeight + } + + if i < context.component.items.count - 1 { + let borderChild = borderChildren[item.id].update( + component: AnyComponent(Rectangle(color: borderColor)), + availableSize: CGSize(width: context.availableSize.width, height: borderWidth), + transition: context.transition + ) + updatedBorderChildren.append(borderChild) + } + + i += 1 + } + + let leftColumnBackground = leftColumnBackground.update( + component: Rectangle(color: context.component.theme.list.itemInputField.backgroundColor), + availableSize: CGSize(width: leftColumnWidth, height: innerTotalHeight), + transition: context.transition + ) + context.add( + leftColumnBackground + .position(CGPoint(x: leftColumnWidth / 2.0, y: innerTotalHeight / 2.0)) + ) + + let borderImage: UIImage + if let (currentImage, theme) = context.state.cachedBorderImage, theme === context.component.theme { + borderImage = currentImage + } else { + let borderRadius: CGFloat = 5.0 + borderImage = generateImage(CGSize(width: 16.0, height: 16.0), rotatedContext: { size, context in + let bounds = CGRect(origin: .zero, size: size) + context.setFillColor(backgroundColor.cgColor) + context.fill(bounds) + + let path = CGPath(roundedRect: bounds.insetBy(dx: borderWidth / 2.0, dy: borderWidth / 2.0), cornerWidth: borderRadius, cornerHeight: borderRadius, transform: nil) + context.setBlendMode(.clear) + context.addPath(path) + context.fillPath() + + context.setBlendMode(.normal) + context.setStrokeColor(borderColor.cgColor) + context.setLineWidth(borderWidth) + context.addPath(path) + context.strokePath() + })!.stretchableImage(withLeftCapWidth: 5, topCapHeight: 5) + context.state.cachedBorderImage = (borderImage, context.component.theme) + } + + let outerBorder = outerBorder.update( + component: Image(image: borderImage), + availableSize: CGSize(width: context.availableSize.width, height: totalHeight), + transition: context.transition + ) + context.add(outerBorder + .position(CGPoint(x: context.availableSize.width / 2.0, y: totalHeight / 2.0)) + ) + + let verticalBorder = verticalBorder.update( + component: Rectangle(color: borderColor), + availableSize: CGSize(width: borderWidth, height: innerTotalHeight), + transition: context.transition + ) + context.add( + verticalBorder + .position(CGPoint(x: leftColumnWidth - borderWidth / 2.0, y: innerTotalHeight / 2.0)) + ) + + i = 0 + var originY: CGFloat = 0.0 + for (valueChild, valueInsets) in updatedValueChildren { + let rowHeight = rowHeights[i] ?? 0.0 + + let valueFrame: CGRect + if let titleChild = updatedTitleChildren[i] { + let titleFrame = CGRect(origin: CGPoint(x: horizontalPadding, y: originY + verticalPadding), size: titleChild.size) + context.add(titleChild + .position(titleFrame.center) + ) + valueFrame = CGRect(origin: CGPoint(x: leftColumnWidth + valueInsets.left, y: originY + verticalPadding), size: valueChild.size) + } else { + valueFrame = CGRect(origin: CGPoint(x: horizontalPadding, y: originY + verticalPadding), size: valueChild.size) + } + + context.add(valueChild + .position(valueFrame.center) + ) + + if i < updatedBorderChildren.count { + let borderChild = updatedBorderChildren[i] + context.add(borderChild + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + rowHeight - borderWidth / 2.0)) + ) + } + + originY += rowHeight + i += 1 + } + + return CGSize(width: context.availableSize.width, height: totalHeight) + } + } +} + +private final class PeerCellComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let peer: EnginePeer? + + init(context: AccountContext, theme: PresentationTheme, peer: EnginePeer?) { + self.context = context + self.theme = theme + self.peer = peer + } + + static func ==(lhs: PeerCellComponent, rhs: PeerCellComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.peer != rhs.peer { + return false + } + return true + } + + final class View: UIView { + private let avatar = ComponentView() + private let text = ComponentView() + + private var component: PeerCellComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: PeerCellComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let avatarSize = CGSize(width: 22.0, height: 22.0) + let spacing: CGFloat = 6.0 + + let peerName: String + let peer: StarsContext.State.Transaction.Peer + if let peerValue = component.peer { + peerName = peerValue.compactDisplayTitle + peer = .peer(peerValue) + } else { + peerName = "Hidden Name" + peer = .fragment + } + + let avatarNaturalSize = self.avatar.update( + transition: .immediate, + component: AnyComponent( + StarsAvatarComponent(context: component.context, theme: component.theme, peer: peer, photo: nil, media: [], backgroundColor: .clear) + ), + environment: {}, + containerSize: CGSize(width: 40.0, height: 40.0) + ) + + let textSize = self.text.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: peerName, font: Font.regular(15.0), textColor: component.theme.list.itemAccentColor, paragraphAlignment: .left)) + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - avatarSize.width - spacing, height: availableSize.height) + ) + + let size = CGSize(width: avatarSize.width + textSize.width + spacing, height: textSize.height) + + let avatarFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - avatarSize.height) / 2.0)), size: avatarSize) + + if let view = self.avatar.view { + if view.superview == nil { + self.addSubview(view) + } + let scale = avatarSize.width / avatarNaturalSize.width + view.transform = CGAffineTransform(scaleX: scale, y: scale) + view.frame = avatarFrame + } + + if let view = self.text.view { + if view.superview == nil { + self.addSubview(view) + } + let textFrame = CGRect(origin: CGPoint(x: avatarSize.width + spacing, y: floorToScreenPixels((size.height - textSize.height) / 2.0)), size: textSize) + transition.setFrame(view: view, frame: textFrame) + } + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { + return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(backgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + context.setLineWidth(2.0) + context.setLineCap(.round) + context.setStrokeColor(foregroundColor.cgColor) + + context.move(to: CGPoint(x: 10.0, y: 10.0)) + context.addLine(to: CGPoint(x: 20.0, y: 20.0)) + context.strokePath() + + context.move(to: CGPoint(x: 20.0, y: 10.0)) + context.addLine(to: CGPoint(x: 10.0, y: 20.0)) + context.strokePath() + }) +} + +private final class GiftAnimationComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let file: TelegramMediaFile? + + public init( + context: AccountContext, + theme: PresentationTheme, + file: TelegramMediaFile? + ) { + self.context = context + self.theme = theme + self.file = file + } + + public static func ==(lhs: GiftAnimationComponent, rhs: GiftAnimationComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.file != rhs.file { + return false + } + return true + } + + public final class View: UIView { + private var component: GiftAnimationComponent? + private weak var componentState: EmptyComponentState? + + private var animationLayer: InlineStickerItemLayer? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: GiftAnimationComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.componentState = state + + let emoji = ChatTextInputTextCustomEmojiAttribute( + interactivelySelectedFromPackId: nil, + fileId: component.file?.fileId.id ?? 0, + file: component.file + ) + + let iconSize = availableSize + if self.animationLayer == nil { + let animationLayer = InlineStickerItemLayer( + context: .account(component.context), + userLocation: .other, + attemptSynchronousLoad: false, + emoji: emoji, + file: component.file, + cache: component.context.animationCache, + renderer: component.context.animationRenderer, + unique: true, + placeholderColor: component.theme.list.mediaPlaceholderColor, + pointSize: CGSize(width: iconSize.width * 1.2, height: iconSize.height * 1.2), + loopCount: 1 + ) + animationLayer.isVisibleForAnimations = true + self.animationLayer = animationLayer + self.layer.addSublayer(animationLayer) + } + if let animationLayer = self.animationLayer { + transition.setFrame(layer: animationLayer, frame: CGRect(origin: .zero, size: iconSize)) + } + + return iconSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift index 0306135d38..28ee1394a3 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoPaneNode/Sources/PeerInfoPaneNode.swift @@ -20,6 +20,7 @@ public enum PeerInfoPaneKey: Int32 { case gifs case groupsInCommon case recommended + case gifts } public struct PeerInfoStatusData: Equatable { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift index 8e0477d4f1..a98f6b9ebb 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift @@ -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 + 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 )) } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift index dd48610be3..4d83efa582 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift @@ -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) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 37a38e0da3..82226426c4 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -3472,7 +3472,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, openLargeEmojiInfo: { _, _, _ in }, openJoinLink: { _ in }, openWebView: { _, _, _, _ in - }, activateAdAction: { _, _ in + }, activateAdAction: { _, _, _, _ in }, openRequestedPeerSelection: { _, _, _, _ in }, saveMediaToFiles: { _ in }, openNoAdsDemo: { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD index 1bef45d94e..6517691155 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD @@ -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", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift new file mode 100644 index 0000000000..9a4c7f8122 --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -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() + + private let ready = Promise() + private var didSetReady: Bool = false + public var isReady: Signal { + return self.ready.get() + } + + private let statusPromise = Promise(nil) + public var status: Signal { + 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] = [:] + + 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 + 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 +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift index f43cb26bca..229ca4404e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift @@ -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)) + } } } diff --git a/submodules/TelegramUI/Components/Stars/ItemShimmeringLoadingComponent/BUILD b/submodules/TelegramUI/Components/Stars/ItemShimmeringLoadingComponent/BUILD new file mode 100644 index 0000000000..e75b7cea6e --- /dev/null +++ b/submodules/TelegramUI/Components/Stars/ItemShimmeringLoadingComponent/BUILD @@ -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", + ], +) diff --git a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/ItemLoadingComponent.swift b/submodules/TelegramUI/Components/Stars/ItemShimmeringLoadingComponent/Sources/ItemShimmeringLoadingComponent.swift similarity index 69% rename from submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/ItemLoadingComponent.swift rename to submodules/TelegramUI/Components/Stars/ItemShimmeringLoadingComponent/Sources/ItemShimmeringLoadingComponent.swift index 96600232dd..83377ab8e1 100644 --- a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/ItemLoadingComponent.swift +++ b/submodules/TelegramUI/Components/Stars/ItemShimmeringLoadingComponent/Sources/ItemShimmeringLoadingComponent.swift @@ -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, transition: ComponentTransition) -> CGSize { + func update(component: ItemShimmeringLoadingComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, 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 diff --git a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/BUILD b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/BUILD index dd5b8cbbb6..8eea622fc1 100644 --- a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/BUILD +++ b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/BUILD @@ -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", diff --git a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift index 5493510405..6dfa8bb2d5 100644 --- a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift @@ -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? if product.storeProduct.id == context.component.selectedProductId { backgroundComponent = AnyComponent( - ItemLoadingComponent(color: environment.theme.list.itemAccentColor) + ItemShimmeringLoadingComponent(color: environment.theme.list.itemAccentColor) ) } else { backgroundComponent = nil diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift index 658dcf5d12..7248d88a8e 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -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) } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift index fd2bef3098..5f299827df 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift @@ -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 { diff --git a/submodules/TelegramUI/Components/TabSelectorComponent/BUILD b/submodules/TelegramUI/Components/TabSelectorComponent/BUILD index 87087a3713..e5a61ad4ba 100644 --- a/submodules/TelegramUI/Components/TabSelectorComponent/BUILD +++ b/submodules/TelegramUI/Components/TabSelectorComponent/BUILD @@ -13,6 +13,9 @@ swift_library( "//submodules/Display", "//submodules/ComponentFlow", "//submodules/TelegramUI/Components/PlainButtonComponent", + "//submodules/Components/MultilineTextWithEntitiesComponent", + "//submodules/TextFormat", + "//submodules/AccountContext" ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift b/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift index dc2815c8c0..486ab7df91 100644 --- a/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift +++ b/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift @@ -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 diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/BotCopy.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Message/BotCopy.imageset/Contents.json new file mode 100644 index 0000000000..ff2c7d7a8b --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/BotCopy.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "copy_10.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/BotCopy.imageset/copy_10.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Message/BotCopy.imageset/copy_10.pdf new file mode 100644 index 0000000000..e8ae82e284 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/BotCopy.imageset/copy_10.pdf @@ -0,0 +1,65 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Filter /FlateDecode + /Length 3 0 R + >> +stream +x͎$5{Fi_3]4Z Luiw3?˗z|ן>~}瓻?z|^||;™Xgټ,,X.gbg-z#յZ@|qQTbbUZ0̱yKBo'rO[ӆZ7Y,eoľJe²ZdIH5@\-[*`5eWJ.Z \UdS f`"8 YvKE bƲ)<>racIn<Ȯ] ڮ4H*:FEWUȅ yZ t[jB jq,MI(pQD;(;Sz[IJbCf+|HWbۗZN9c[z|;~E-L K,ww{aI}GZjn_dGye- +l'vlC̲YeT|Xɤ`GrjD!x# y_Dz3[0Qf鮦_QiYAXȗt5Cm'iRQ6ҤC[X"O)/Q)0Att +/LB $G+-$ ]$A|az?XP4u1 +JEu߁ LtYQ=Z^&D;"3tλҼ6{jM[G8Z pH"q+$z'؂*1-˗qkӲQ Kq':bpZ&"4oj̙B)萷J2~ +@N<-6o|R%p)ʧ U ekNPCU5nJJ?_ +wC `Nx]tʀ( PÖO/FTk! +sU^RC"N)3/L~+ i{-&TA(OIK)_LRRgݭ>Bst4@f|Ϝl_$6ʶ! dC6ըK|sCfˉj`C/C.BM1ʃv4Uv8!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 \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/GiftRibbon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Message/GiftRibbon.imageset/Contents.json new file mode 100644 index 0000000000..3f3ed2761d --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/GiftRibbon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "giftline_chat.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/GiftRibbon.imageset/giftline_chat.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Message/GiftRibbon.imageset/giftline_chat.pdf new file mode 100644 index 0000000000..3ef1493997 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Message/GiftRibbon.imageset/giftline_chat.pdf @@ -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 \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Premium/GiftRibbon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/GiftRibbon.imageset/Contents.json new file mode 100644 index 0000000000..bc85fccee1 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/GiftRibbon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "giftline_cell.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/GiftRibbon.imageset/giftline_cell.pdf b/submodules/TelegramUI/Images.xcassets/Premium/GiftRibbon.imageset/giftline_cell.pdf new file mode 100644 index 0000000000..68d0f7b17d --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/GiftRibbon.imageset/giftline_cell.pdf @@ -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 \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index e73b473521..c3b1aedc8d 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -2365,7 +2365,7 @@ private func extractAccountManagerState(records: AccountRecordsView take(1) |> deliverOnMainQueue @@ -2478,7 +2478,7 @@ private func extractAccountManagerState(records: AccountRecordsView 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) })) } diff --git a/submodules/TelegramUI/Sources/ApplicationContext.swift b/submodules/TelegramUI/Sources/ApplicationContext.swift index 245323180c..89264f0f67 100644 --- a/submodules/TelegramUI/Sources/ApplicationContext.swift +++ b/submodules/TelegramUI/Sources/ApplicationContext.swift @@ -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)) + } }) } } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift index b0374e4646..c899f6db8e 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift @@ -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 } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 8ab7dc49e5..b4b2369518 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -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 { diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index bd5bef6587..502b9a9c3a 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -351,7 +351,8 @@ private func extractAssociatedData( chatThemes: [TelegramTheme], deviceContactsNumbers: Set, 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 { diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 8d9f42b048..287f786bf2 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -165,7 +165,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu }, openLargeEmojiInfo: { _, _, _ in }, openJoinLink: { _ in }, openWebView: { _, _, _, _ in - }, activateAdAction: { _, _ in + }, activateAdAction: { _, _, _, _ in }, openRequestedPeerSelection: { _, _, _, _ in }, saveMediaToFiles: { _ in }, openNoAdsDemo: { diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index d31dfde970..70e5e549d7 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -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 { return MiniAppListScreen.initialData(context: context) } diff --git a/submodules/TelegramUI/Sources/SpotlightContacts.swift b/submodules/TelegramUI/Sources/SpotlightContacts.swift index 3437c5e1a4..699ac90b87 100644 --- a/submodules/TelegramUI/Sources/SpotlightContacts.swift +++ b/submodules/TelegramUI/Sources/SpotlightContacts.swift @@ -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) diff --git a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift index 45b3e874fa..86aa5d1b9a 100644 --- a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift +++ b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift @@ -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?