diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index cee7c241f6..32a87cb876 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1501,13 +1501,15 @@ public struct StarsSubscriptionConfiguration { return StarsSubscriptionConfiguration( maxFee: 2500, usdWithdrawRate: 1200, + tonUsdRate: 0, paidMessageMaxAmount: 10000, paidMessageCommissionPermille: 850, paidMessagesAvailable: false, starGiftResaleMinAmount: 125, starGiftResaleMaxAmount: 3500, starGiftCommissionPermille: 80, - channelMessageSuggestionCommissionPermille: 850, + channelMessageSuggestionStarsCommissionPermille: 850, + channelMessageSuggestionTonCommissionPermille: 850, channelMessageSuggestionMaxStarsAmount: 10000, channelMessageSuggestionMaxTonAmount: 10000000000000 ) @@ -1515,38 +1517,44 @@ public struct StarsSubscriptionConfiguration { public let maxFee: Int64 public let usdWithdrawRate: Int64 + public let tonUsdRate: Int64 public let paidMessageMaxAmount: Int64 public let paidMessageCommissionPermille: Int32 public let paidMessagesAvailable: Bool public let starGiftResaleMinAmount: Int64 public let starGiftResaleMaxAmount: Int64 public let starGiftCommissionPermille: Int32 - public let channelMessageSuggestionCommissionPermille: Int32 + public let channelMessageSuggestionStarsCommissionPermille: Int32 + public let channelMessageSuggestionTonCommissionPermille: Int32 public let channelMessageSuggestionMaxStarsAmount: Int64 public let channelMessageSuggestionMaxTonAmount: Int64 fileprivate init( maxFee: Int64, usdWithdrawRate: Int64, + tonUsdRate: Int64, paidMessageMaxAmount: Int64, paidMessageCommissionPermille: Int32, paidMessagesAvailable: Bool, starGiftResaleMinAmount: Int64, starGiftResaleMaxAmount: Int64, starGiftCommissionPermille: Int32, - channelMessageSuggestionCommissionPermille: Int32, + channelMessageSuggestionStarsCommissionPermille: Int32, + channelMessageSuggestionTonCommissionPermille: Int32, channelMessageSuggestionMaxStarsAmount: Int64, channelMessageSuggestionMaxTonAmount: Int64 ) { self.maxFee = maxFee self.usdWithdrawRate = usdWithdrawRate + self.tonUsdRate = tonUsdRate self.paidMessageMaxAmount = paidMessageMaxAmount self.paidMessageCommissionPermille = paidMessageCommissionPermille self.paidMessagesAvailable = paidMessagesAvailable self.starGiftResaleMinAmount = starGiftResaleMinAmount self.starGiftResaleMaxAmount = starGiftResaleMaxAmount self.starGiftCommissionPermille = starGiftCommissionPermille - self.channelMessageSuggestionCommissionPermille = channelMessageSuggestionCommissionPermille + self.channelMessageSuggestionStarsCommissionPermille = channelMessageSuggestionStarsCommissionPermille + self.channelMessageSuggestionTonCommissionPermille = channelMessageSuggestionTonCommissionPermille self.channelMessageSuggestionMaxStarsAmount = channelMessageSuggestionMaxStarsAmount self.channelMessageSuggestionMaxTonAmount = channelMessageSuggestionMaxTonAmount } @@ -1555,6 +1563,7 @@ public struct StarsSubscriptionConfiguration { if let data = appConfiguration.data { let maxFee = (data["stars_subscription_amount_max"] as? Double).flatMap(Int64.init) ?? StarsSubscriptionConfiguration.defaultValue.maxFee let usdWithdrawRate = (data["stars_usd_withdraw_rate_x1000"] as? Double).flatMap(Int64.init) ?? StarsSubscriptionConfiguration.defaultValue.usdWithdrawRate + let tonUsdRate = (data["ton_usd_rate"] as? Double).flatMap(Int64.init) ?? StarsSubscriptionConfiguration.defaultValue.tonUsdRate let paidMessageMaxAmount = (data["stars_paid_message_amount_max"] as? Double).flatMap(Int64.init) ?? StarsSubscriptionConfiguration.defaultValue.paidMessageMaxAmount let paidMessageCommissionPermille = (data["stars_paid_message_commission_permille"] as? Double).flatMap(Int32.init) ?? StarsSubscriptionConfiguration.defaultValue.paidMessageCommissionPermille let paidMessagesAvailable = (data["stars_paid_messages_available"] as? Bool) ?? StarsSubscriptionConfiguration.defaultValue.paidMessagesAvailable @@ -1562,20 +1571,23 @@ public struct StarsSubscriptionConfiguration { let starGiftResaleMaxAmount = (data["stars_stargift_resale_amount_max"] as? Double).flatMap(Int64.init) ?? StarsSubscriptionConfiguration.defaultValue.starGiftResaleMaxAmount let starGiftCommissionPermille = (data["stars_stargift_resale_commission_permille"] as? Double).flatMap(Int32.init) ?? StarsSubscriptionConfiguration.defaultValue.starGiftCommissionPermille - let channelMessageSuggestionCommissionPermille = (data["stars_suggested_post_commission_permille"] as? Double).flatMap(Int32.init) ?? StarsSubscriptionConfiguration.defaultValue.channelMessageSuggestionCommissionPermille + let channelMessageSuggestionStarsCommissionPermille = (data["stars_suggested_post_commission_permille"] as? Double).flatMap(Int32.init) ?? StarsSubscriptionConfiguration.defaultValue.channelMessageSuggestionStarsCommissionPermille + let channelMessageSuggestionTonCommissionPermille = (data["ton_suggested_post_commission_permille"] as? Double).flatMap(Int32.init) ?? StarsSubscriptionConfiguration.defaultValue.channelMessageSuggestionTonCommissionPermille let channelMessageSuggestionMaxStarsAmount = (data["stars_suggested_post_amount_max"] as? Double).flatMap(Int64.init) ?? StarsSubscriptionConfiguration.defaultValue.channelMessageSuggestionMaxStarsAmount let channelMessageSuggestionMaxTonAmount = (data["ton_suggested_post_amount_max"] as? Double).flatMap(Int64.init) ?? StarsSubscriptionConfiguration.defaultValue.channelMessageSuggestionMaxTonAmount return StarsSubscriptionConfiguration( maxFee: maxFee, usdWithdrawRate: usdWithdrawRate, + tonUsdRate: tonUsdRate, paidMessageMaxAmount: paidMessageMaxAmount, paidMessageCommissionPermille: paidMessageCommissionPermille, paidMessagesAvailable: paidMessagesAvailable, starGiftResaleMinAmount: starGiftResaleMinAmount, starGiftResaleMaxAmount: starGiftResaleMaxAmount, starGiftCommissionPermille: starGiftCommissionPermille, - channelMessageSuggestionCommissionPermille: channelMessageSuggestionCommissionPermille, + channelMessageSuggestionStarsCommissionPermille: channelMessageSuggestionStarsCommissionPermille, + channelMessageSuggestionTonCommissionPermille: channelMessageSuggestionTonCommissionPermille, channelMessageSuggestionMaxStarsAmount: channelMessageSuggestionMaxStarsAmount, channelMessageSuggestionMaxTonAmount: channelMessageSuggestionMaxTonAmount ) diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 11111d9eb3..b6cb987544 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -1487,7 +1487,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, amountString = "\(amount.amount.value) Stars" } case .ton: - amountString = "\(formatTonAmountText(amount.amount.value, dateTimeFormat: dateTimeFormat)) TON" + amountString = "\(formatTonAmountText(amount.amount.value, dateTimeFormat: dateTimeFormat, maxDecimalPositions: 3)) TON" } attributedString = parseMarkdownIntoAttributedString("**\(channelName)** received **\(amountString)** for publishing this post", attributes: MarkdownAttributes(body: bodyAttributes, bold: boldAttributes, link: bodyAttributes, linkAttribute: { _ in return nil })) case let .suggestedPostRefund(info): diff --git a/submodules/TelegramStringFormatting/Sources/TonFormat.swift b/submodules/TelegramStringFormatting/Sources/TonFormat.swift index 1b40073f81..b5a3151174 100644 --- a/submodules/TelegramStringFormatting/Sources/TonFormat.swift +++ b/submodules/TelegramStringFormatting/Sources/TonFormat.swift @@ -28,7 +28,7 @@ public func formatTonUsdValue(_ value: Int64, divide: Bool = true, rate: Double return "$\(formattedValue)" } -public func formatTonAmountText(_ value: Int64, dateTimeFormat: PresentationDateTimeFormat, showPlus: Bool = false) -> String { +public func formatTonAmountText(_ value: Int64, dateTimeFormat: PresentationDateTimeFormat, showPlus: Bool = false, maxDecimalPositions: Int = 2) -> String { var balanceText = "\(abs(value))" while balanceText.count < 10 { balanceText.insert("0", at: balanceText.startIndex) @@ -49,7 +49,7 @@ public func formatTonAmountText(_ value: Int64, dateTimeFormat: PresentationDate } if let dotIndex = balanceText.range(of: dateTimeFormat.decimalSeparator) { - if let endIndex = balanceText.index(dotIndex.upperBound, offsetBy: 2, limitedBy: balanceText.endIndex) { + if let endIndex = balanceText.index(dotIndex.upperBound, offsetBy: maxDecimalPositions, limitedBy: balanceText.endIndex) { balanceText = String(balanceText[balanceText.startIndex..? + var completion: ((Int32) -> Void)? var dismiss: (() -> Void)? var cancel: (() -> Void)? @@ -94,22 +103,43 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, ASScrollViewDel self.contentBackgroundNode.backgroundColor = backgroundColor let title: String + var subtitle: String? var text: String? switch mode { case .scheduledMessages: title = self.presentationData.strings.Conversation_ScheduleMessage_Title case .reminders: title = self.presentationData.strings.Conversation_SetReminder_Title - case let .suggestPost(needsTime): + case let .suggestPost(needsTime, isAdmin, funds): if needsTime { //TODO:localize - title = "Time" + title = "Accept Terms" text = "Set the date and time you want\nthis message to be published." } else { //TODO:localize title = "Time" text = "Set the date and time you want\nyour message to be published." } + + //TODO:localize + if let funds, isAdmin { + var commissionValue: String + commissionValue = "\(Double(funds.commissionPermille) * 0.1)" + if commissionValue.hasSuffix(".0") { + commissionValue = String(commissionValue[commissionValue.startIndex ..< commissionValue.index(commissionValue.endIndex, offsetBy: -2)]) + } else if commissionValue.hasSuffix(".00") { + commissionValue = String(commissionValue[commissionValue.startIndex ..< commissionValue.index(commissionValue.endIndex, offsetBy: -3)]) + } + + switch funds.amount.currency { + case .stars: + let displayAmount = funds.amount.amount.totalValue * Double(funds.commissionPermille) / 1000.0 + subtitle = "You will receive \(displayAmount) Stars (\(commissionValue)%)\nfor publishing this post" + case .ton: + let displayAmount = Double(funds.amount.amount.value) / 1000000000.0 * Double(funds.commissionPermille) / 1000.0 + subtitle = "You will receive \(displayAmount) TON (\(commissionValue)%)\nfor publishing this post" + } + } } self.titleNode = ASTextNode() @@ -130,6 +160,19 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, ASScrollViewDel self.textNode = nil } + if let subtitle { + let subtitleNode = ASTextNode() + subtitleNode.attributedText = NSAttributedString(string: subtitle, font: Font.regular(15.0), textColor: textColor) + subtitleNode.maximumNumberOfLines = 0 + subtitleNode.textAlignment = .center + subtitleNode.lineSpacing = 0.2 + subtitleNode.accessibilityLabel = text + subtitleNode.accessibilityTraits = [.staticText] + self.subtitleNode = subtitleNode + } else { + self.subtitleNode = nil + } + self.cancelButton = HighlightableButtonNode() self.cancelButton.setTitle(self.presentationData.strings.Common_Cancel, with: Font.regular(17.0), with: accentColor, for: .normal) self.cancelButton.accessibilityLabel = self.presentationData.strings.Common_Cancel @@ -139,7 +182,7 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, ASScrollViewDel self.onlineButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(backgroundColor: buttonColor, foregroundColor: buttonTextColor), font: .regular, height: 52.0, cornerRadius: 11.0, gloss: false) switch mode { - case let .suggestPost(needsTime): + case let .suggestPost(needsTime, _, _): //TODO:localize if needsTime { self.onlineButton.title = "Post Now" @@ -172,6 +215,9 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, ASScrollViewDel self.backgroundNode.addSubnode(self.effectNode) self.backgroundNode.addSubnode(self.contentBackgroundNode) self.contentContainerNode.addSubnode(self.titleNode) + if let subtitleNode = self.subtitleNode { + self.contentContainerNode.addSubnode(subtitleNode) + } if let textNode = self.textNode { self.contentContainerNode.addSubnode(textNode) } @@ -334,7 +380,7 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, ASScrollViewDel } else { self.doneButton.title = self.presentationData.strings.Conversation_SetReminder_RemindOn(self.dateFormatter.string(from: date), time).string } - case let .suggestPost(needsTime): + case let .suggestPost(needsTime, _, _): if needsTime { if calendar.isDateInToday(date) { self.doneButton.title = self.presentationData.strings.SuggestPost_Time_SendToday(time).string @@ -386,10 +432,14 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, ASScrollViewDel let targetBounds = self.bounds self.bounds = self.bounds.offsetBy(dx: 0.0, dy: -offset) self.dimNode.position = CGPoint(x: dimPosition.x, y: dimPosition.y - offset) - transition.animateView({ - self.bounds = targetBounds - self.dimNode.position = dimPosition - }) + + transition.updateBounds(layer: self.layer, bounds: targetBounds) + transition.updatePosition(layer: self.dimNode.layer, position: dimPosition) + + if let toastView = self.toast?.view { + toastView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + transition.animatePositionAdditive(layer: toastView.layer, offset: CGPoint(x: 0.0, y: -offset)) + } } func animateOut(completion: (() -> Void)? = nil) { @@ -415,6 +465,12 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, ASScrollViewDel offsetCompleted = true internalCompletion() }) + + if let toastView = self.toast?.view { + toastView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in + }) + toastView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -offset), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) + } } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { @@ -463,6 +519,17 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, ASScrollViewDel let textControlSpacing: CGFloat = -8.0 let textDoneSpacing: CGFloat = 21.0 + + let subtitleTopSpacing: CGFloat = 22.0 + let subtitleControlSpacing: CGFloat = 8.0 + + let subtitleSize = self.subtitleNode?.measure(CGSize(width: width, height: 1000.0)) + var controlOffset: CGFloat = 0.0 + if let subtitleSize { + contentHeight += subtitleSize.height + subtitleTopSpacing + subtitleControlSpacing + controlOffset += subtitleTopSpacing + subtitleControlSpacing + 20.0 + } + let textSize = self.textNode?.measure(CGSize(width: width, height: 1000.0)) if let textSize { contentHeight += textSize.height + textControlSpacing + textDoneSpacing @@ -486,6 +553,11 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, ASScrollViewDel let titleFrame = CGRect(origin: CGPoint(x: floor((contentFrame.width - titleSize.width) / 2.0), y: 16.0), size: titleSize) transition.updateFrame(node: self.titleNode, frame: titleFrame) + if let subtitleNode = self.subtitleNode, let subtitleSize { + let subtitleFrame = CGRect(origin: CGPoint(x: floor((contentFrame.width - subtitleSize.width) / 2.0), y: titleFrame.maxY + subtitleTopSpacing), size: subtitleSize) + transition.updateFrame(node: subtitleNode, frame: subtitleFrame) + } + let cancelSize = self.cancelButton.measure(CGSize(width: width, height: titleHeight)) let cancelFrame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: cancelSize) transition.updateFrame(node: self.cancelButton, frame: cancelFrame) @@ -503,8 +575,52 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, ASScrollViewDel let onlineButtonHeight = self.onlineButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition) transition.updateFrame(node: self.onlineButton, frame: CGRect(x: buttonInset, y: contentHeight - onlineButtonHeight - cleanInsets.bottom - 16.0, width: contentFrame.width, height: onlineButtonHeight)) - self.pickerView?.frame = CGRect(origin: CGPoint(x: 0.0, y: 54.0), size: CGSize(width: contentFrame.width, height: pickerHeight)) + self.pickerView?.frame = CGRect(origin: CGPoint(x: 0.0, y: 54.0 + controlOffset), size: CGSize(width: contentFrame.width, height: pickerHeight)) transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) + + if case let .suggestPost(_, isAdmin, funds) = self.mode, isAdmin, let funds, funds.amount.currency == .stars { + let toast: ComponentView + if let current = self.toast { + toast = current + } else { + toast = ComponentView() + self.toast = toast + } + let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) + let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) + //TODO:localize + let playOnce = ActionSlot() + let toastSize = toast.update( + transition: ComponentTransition(transition), + component: AnyComponent(ToastContentComponent( + icon: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: "anim_infotip"), + startingPosition: .begin, + size: CGSize(width: 32.0, height: 32.0), + playOnce: playOnce + )), + content: AnyComponent(VStack([ + AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( + text: .markdown(text: "Transactions in **Stars** may be reversed by the payment provider within **21** days. Only accept Stars from people you trust.", attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in nil })), + maximumNumberOfLines: 0 + ))) + ], alignment: .left, spacing: 6.0)), + insets: UIEdgeInsets(top: 10.0, left: 12.0, bottom: 10.0, right: 10.0), + iconSpacing: 12.0 + )), + environment: {}, + containerSize: CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - 12.0 * 2.0, height: 1000.0) + ) + let toastFrame = CGRect(origin: CGPoint(x: layout.safeInsets.left + 12.0, y: layout.insets(options: .statusBar).top + 4.0), size: toastSize) + if let toastView = toast.view { + if toastView.superview == nil { + self.view.addSubview(toastView) + playOnce.invoke(()) + } + transition.updatePosition(layer: toastView.layer, position: toastFrame.center) + transition.updateBounds(layer: toastView.layer, bounds: CGRect(origin: CGPoint(), size: toastFrame.size)) + } + } } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsSettingsScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsSettingsScreen.swift index 947cb43f32..9b525e8a41 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsSettingsScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsSettingsScreen.swift @@ -503,7 +503,7 @@ public final class PostSuggestionsSettingsScreen: ViewControllerComponentContain super.init(context: context, component: PostSuggestionsSettingsScreenComponent( context: context, usdWithdrawRate: configuration.usdWithdrawRate, - channelMessageSuggestionCommissionPermille: Int(configuration.channelMessageSuggestionCommissionPermille), + channelMessageSuggestionCommissionPermille: Int(configuration.paidMessageCommissionPermille), peer: peer, initialPrice: initialPrice, completion: completion diff --git a/submodules/TelegramUI/Components/Stars/BalanceNeededScreen/BUILD b/submodules/TelegramUI/Components/Stars/BalanceNeededScreen/BUILD index 4c9aad1501..1f4cce9b1f 100644 --- a/submodules/TelegramUI/Components/Stars/BalanceNeededScreen/BUILD +++ b/submodules/TelegramUI/Components/Stars/BalanceNeededScreen/BUILD @@ -22,6 +22,7 @@ swift_library( "//submodules/Components/SheetComponent", "//submodules/TelegramUI/Components/ButtonComponent", "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/TelegramCore", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stars/BalanceNeededScreen/Sources/BalanceNeededScreen.swift b/submodules/TelegramUI/Components/Stars/BalanceNeededScreen/Sources/BalanceNeededScreen.swift index c8df11eb3b..c3599c66b5 100644 --- a/submodules/TelegramUI/Components/Stars/BalanceNeededScreen/Sources/BalanceNeededScreen.swift +++ b/submodules/TelegramUI/Components/Stars/BalanceNeededScreen/Sources/BalanceNeededScreen.swift @@ -12,17 +12,25 @@ import BalancedTextComponent import Markdown import TelegramStringFormatting import BundleIconComponent +import TelegramCore +import TelegramPresentationData private final class BalanceNeededSheetContentComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment + let context: AccountContext + let amount: StarsAmount let action: () -> Void let dismiss: () -> Void init( + context: AccountContext, + amount: StarsAmount, action: @escaping () -> Void, dismiss: @escaping () -> Void ) { + self.context = context + self.amount = amount self.action = action self.dismiss = dismiss } @@ -37,11 +45,13 @@ private final class BalanceNeededSheetContentComponent: Component { private let text = ComponentView() private let button = ComponentView() - private var cancelButton: ComponentView? + private let closeButton = ComponentView() private var component: BalanceNeededSheetContentComponent? private weak var state: EmptyComponentState? + private var cachedCloseImage: (UIImage, PresentationTheme)? + override init(frame: CGRect) { super.init(frame: frame) } @@ -61,61 +71,64 @@ private final class BalanceNeededSheetContentComponent: Component { let sideInset: CGFloat = 16.0 - let cancelButton: ComponentView - if let current = self.cancelButton { - cancelButton = current + let closeImage: UIImage + if let (image, theme) = self.cachedCloseImage, theme === environment.theme { + closeImage = image } else { - cancelButton = ComponentView() - self.cancelButton = cancelButton + closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: environment.theme.actionSheet.inputClearButtonColor)! + self.cachedCloseImage = (closeImage, environment.theme) } - let cancelButtonSize = cancelButton.update( - transition: transition, + let closeButtonSize = self.closeButton.update( + transition: .immediate, component: AnyComponent(Button( - content: AnyComponent(Text(text: environment.strings.Common_Cancel, font: Font.regular(17.0), color: environment.theme.list.itemAccentColor)), + content: AnyComponent(Image(image: closeImage)), action: { [weak self] in guard let self, let component = self.component else { return } component.dismiss() } - ).minSize(CGSize(width: 8.0, height: 44.0))), + )), environment: {}, - containerSize: CGSize(width: 200.0, height: 100.0) + containerSize: CGSize(width: 30.0, height: 30.0) ) - if let cancelButtonView = cancelButton.view { - if cancelButtonView.superview == nil { - self.addSubview(cancelButtonView) + let closeButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - closeButtonSize.width - 16.0, y: 12.0), size: closeButtonSize) + if let closeButtonView = self.closeButton.view { + if closeButtonView.superview == nil { + self.addSubview(closeButtonView) } - transition.setFrame(view: cancelButtonView, frame: CGRect(origin: CGPoint(x: 16.0, y: 6.0), size: cancelButtonSize)) + transition.setFrame(view: closeButtonView, frame: closeButtonFrame) } var contentHeight: CGFloat = 0.0 contentHeight += 32.0 - let iconSize = self.icon.update( + let iconSize = CGSize(width: 120.0, height: 120.0) + let _ = self.icon.update( transition: transition, component: AnyComponent(LottieComponent( - content: LottieComponent.AppBundleContent(name: "StoryUpgradeSheet"), + content: LottieComponent.AppBundleContent(name: "TonLogo"), color: nil, startingPosition: .begin, - size: CGSize(width: 100.0, height: 100.0) + size: iconSize, + loop: true )), environment: {}, - containerSize: CGSize(width: 100.0, height: 100.0) + containerSize: iconSize ) if let iconView = self.icon.view { if iconView.superview == nil { self.addSubview(iconView) } - transition.setFrame(view: iconView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: 42.0), size: iconSize)) + transition.setFrame(view: iconView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: 16.0), size: iconSize)) } - contentHeight += 138.0 + contentHeight += 110.0 let titleSize = self.title.update( transition: transition, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: environment.strings.Story_UpgradeQuality_Title, font: Font.semibold(20.0), textColor: environment.theme.list.itemPrimaryTextColor)), + text: .plain(NSAttributedString(string: "\(formatTonAmountText(component.amount.value, dateTimeFormat: component.context.sharedContext.currentPresentationData.with({ $0 }).dateTimeFormat)) TON Needed", font: Font.bold(24.0), textColor: environment.theme.list.itemPrimaryTextColor)), horizontalAlignment: .center, maximumNumberOfLines: 0 )), @@ -131,10 +144,11 @@ private final class BalanceNeededSheetContentComponent: Component { contentHeight += titleSize.height contentHeight += 14.0 + //TODO:localize let textSize = self.text.update( transition: transition, component: AnyComponent(BalancedTextComponent( - text: .plain(NSAttributedString(string: environment.strings.Story_UpgradeQuality_Text, font: Font.regular(14.0), textColor: environment.theme.list.itemSecondaryTextColor)), + text: .plain(NSAttributedString(string: "You can add funds to your balance via the third-party platform Fragment.", font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor)), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.18 @@ -149,10 +163,9 @@ private final class BalanceNeededSheetContentComponent: Component { transition.setFrame(view: textView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - textSize.width) * 0.5), y: contentHeight), size: textSize)) } contentHeight += textSize.height - contentHeight += 12.0 - - contentHeight += 32.0 + contentHeight += 24.0 + //TODO:localize let buttonSize = self.button.update( transition: transition, component: AnyComponent(ButtonComponent( @@ -162,7 +175,7 @@ private final class BalanceNeededSheetContentComponent: Component { pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8) ), content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: "Add Funds via Fragment")) + text: .plain(NSAttributedString(string: "Add Funds via Fragment", font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor)) ))), isEnabled: true, allowActionWhenDisabled: true, @@ -190,7 +203,7 @@ private final class BalanceNeededSheetContentComponent: Component { if environment.safeInsets.bottom.isZero { contentHeight += 16.0 } else { - contentHeight += environment.safeInsets.bottom + 14.0 + contentHeight += environment.safeInsets.bottom + 8.0 } return CGSize(width: availableSize.width, height: contentHeight) @@ -210,13 +223,16 @@ private final class BalanceNeededScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext + let amount: StarsAmount let buttonAction: (() -> Void)? init( context: AccountContext, + amount: StarsAmount, buttonAction: (() -> Void)? ) { self.context = context + self.amount = amount self.buttonAction = buttonAction } @@ -268,6 +284,8 @@ private final class BalanceNeededScreenComponent: Component { transition: transition, component: AnyComponent(SheetComponent( content: AnyComponent(BalanceNeededSheetContentComponent( + context: component.context, + amount: component.amount, action: { [weak self] in guard let self else { return @@ -291,7 +309,7 @@ private final class BalanceNeededScreenComponent: Component { }) } )), - backgroundColor: .color(environment.theme.overallDarkAppearance ? environment.theme.list.itemBlocksBackgroundColor : environment.theme.list.blocksBackgroundColor), + backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), animateOut: self.sheetAnimateOut )), environment: { @@ -323,10 +341,12 @@ private final class BalanceNeededScreenComponent: Component { public class BalanceNeededScreen: ViewControllerComponentContainer { public init( context: AccountContext, + amount: StarsAmount, buttonAction: (() -> Void)? = nil ) { super.init(context: context, component: BalanceNeededScreenComponent( context: context, + amount: amount, buttonAction: buttonAction ), navigationBarAppearance: .none) @@ -359,3 +379,24 @@ public class BalanceNeededScreen: ViewControllerComponentContainer { self.wasDismissed?() } } + +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() + }) +} diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift index ba2a3c7a31..a01a1a9c65 100644 --- a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift @@ -229,6 +229,17 @@ private final class SheetContent: CombinedComponent { amountPlaceholder = "Price" minAmount = StarsAmount(value: 0, nanos: 0) + + if let usdWithdrawRate = withdrawConfiguration.usdWithdrawRate, let tonUsdRate = withdrawConfiguration.tonUsdRate, let amount = state.amount, amount > StarsAmount.zero { + switch state.currency { + case .stars: + let usdRate = Double(usdWithdrawRate) / 1000.0 / 100.0 + amountLabel = "≈\(formatTonUsdValue(amount.value, divide: false, rate: usdRate, dateTimeFormat: environment.dateTimeFormat))" + case .ton: + let usdRate = Double(tonUsdRate) / 1000.0 / 1000000.0 + amountLabel = "≈\(formatTonUsdValue(amount.value, divide: false, rate: usdRate, dateTimeFormat: environment.dateTimeFormat))" + } + } } let title = title.update( @@ -634,7 +645,7 @@ private final class SheetContent: CombinedComponent { let theme = environment.theme let minimalTime: Int32 = Int32(Date().timeIntervalSince1970) + 5 * 60 + 10 - let controller = ChatScheduleTimeController(context: state.context, updatedPresentationData: (state.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), state.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }), mode: .suggestPost(needsTime: false), style: .default, currentTime: state.timestamp, minimalTime: minimalTime, dismissByTapOutside: true, completion: { [weak state] time in + let controller = ChatScheduleTimeController(context: state.context, updatedPresentationData: (state.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), state.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }), mode: .suggestPost(needsTime: false, isAdmin: false, funds: nil), style: .default, currentTime: state.timestamp, minimalTime: minimalTime, dismissByTapOutside: true, completion: { [weak state] time in guard let state else { return } @@ -789,8 +800,20 @@ private final class SheetContent: CombinedComponent { } case .ton: if let balance = state.tonBalance, amount > balance { + let needed = amount - balance + var fragmentUrl = "https://fragment.com/ads/topup" + if let data = state.context.currentAppConfiguration.with({ $0 }).data, let value = data["ton_topup_url"] as? String { + fragmentUrl = value + } controller.push(BalanceNeededScreen( - context: state.context + context: state.context, + amount: needed, + buttonAction: { [weak state] in + guard let state else { + return + } + state.context.sharedContext.applicationBindings.openUrl(fragmentUrl) + } )) return } @@ -1641,17 +1664,19 @@ func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor private struct StarsWithdrawConfiguration { static var defaultValue: StarsWithdrawConfiguration { - return StarsWithdrawConfiguration(minWithdrawAmount: nil, maxPaidMediaAmount: nil, usdWithdrawRate: nil) + return StarsWithdrawConfiguration(minWithdrawAmount: nil, maxPaidMediaAmount: nil, usdWithdrawRate: nil, tonUsdRate: nil) } let minWithdrawAmount: Int64? let maxPaidMediaAmount: Int64? let usdWithdrawRate: Double? + let tonUsdRate: Double? - fileprivate init(minWithdrawAmount: Int64?, maxPaidMediaAmount: Int64?, usdWithdrawRate: Double?) { + fileprivate init(minWithdrawAmount: Int64?, maxPaidMediaAmount: Int64?, usdWithdrawRate: Double?, tonUsdRate: Double?) { self.minWithdrawAmount = minWithdrawAmount self.maxPaidMediaAmount = maxPaidMediaAmount self.usdWithdrawRate = usdWithdrawRate + self.tonUsdRate = tonUsdRate } static func with(appConfiguration: AppConfiguration) -> StarsWithdrawConfiguration { @@ -1668,8 +1693,12 @@ private struct StarsWithdrawConfiguration { if let value = data["stars_usd_withdraw_rate_x1000"] as? Double { usdWithdrawRate = value } + var tonUsdRate: Double? + if let value = data["ton_usd_rate"] as? Double { + tonUsdRate = value + } - return StarsWithdrawConfiguration(minWithdrawAmount: minWithdrawAmount, maxPaidMediaAmount: maxPaidMediaAmount, usdWithdrawRate: usdWithdrawRate) + return StarsWithdrawConfiguration(minWithdrawAmount: minWithdrawAmount, maxPaidMediaAmount: maxPaidMediaAmount, usdWithdrawRate: usdWithdrawRate, tonUsdRate: tonUsdRate) } else { return .defaultValue } diff --git a/submodules/TelegramUI/Components/SuggestedPostApproveAlert/BUILD b/submodules/TelegramUI/Components/SuggestedPostApproveAlert/BUILD new file mode 100644 index 0000000000..21fa6a5202 --- /dev/null +++ b/submodules/TelegramUI/Components/SuggestedPostApproveAlert/BUILD @@ -0,0 +1,26 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SuggestedPostApproveAlert", + module_name = "SuggestedPostApproveAlert", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/AsyncDisplayKit", + "//submodules/TelegramPresentationData", + "//submodules/ComponentFlow", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/Markdown", + "//submodules/TelegramUI/Components/ToastComponent", + "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/Components/MultilineTextComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/SuggestedPostApproveAlert/Sources/SuggestedPostApproveAlert.swift b/submodules/TelegramUI/Components/SuggestedPostApproveAlert/Sources/SuggestedPostApproveAlert.swift new file mode 100644 index 0000000000..76c1d318fc --- /dev/null +++ b/submodules/TelegramUI/Components/SuggestedPostApproveAlert/Sources/SuggestedPostApproveAlert.swift @@ -0,0 +1,441 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Markdown +import Display +import TelegramPresentationData +import ComponentFlow +import ToastComponent +import Markdown +import LottieComponent +import MultilineTextComponent +import ComponentDisplayAdapters + +private let alertWidth: CGFloat = 270.0 + +private final class SuggestedPostApproveAlertContentNode: AlertContentNode { + private var theme: AlertControllerTheme + private let actionLayout: TextAlertContentActionLayout + + private let titleNode: ImmediateTextNode? + private let textNode: ImmediateTextNode + + private let actionNodesSeparator: ASDisplayNode + private let actionNodes: [TextAlertContentActionNode] + private let actionVerticalSeparators: [ASDisplayNode] + + private var validLayout: CGSize? + + private let _dismissOnOutsideTap: Bool + override public var dismissOnOutsideTap: Bool { + return self._dismissOnOutsideTap + } + + private var highlightedItemIndex: Int? = nil + + public var textAttributeAction: (NSAttributedString.Key, (Any) -> Void)? { + didSet { + if let (attribute, textAttributeAction) = self.textAttributeAction { + self.textNode.highlightAttributeAction = { attributes in + if let _ = attributes[attribute] { + return attribute + } else { + return nil + } + } + self.textNode.tapAttributeAction = { attributes, _ in + if let value = attributes[attribute] { + textAttributeAction(value) + } + } + self.textNode.linkHighlightColor = self.theme.accentColor.withAlphaComponent(0.5) + } else { + self.textNode.highlightAttributeAction = nil + self.textNode.tapAttributeAction = nil + } + } + } + + public init(theme: AlertControllerTheme, title: NSAttributedString?, text: NSAttributedString, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout, dismissOnOutsideTap: Bool, linkAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil) { + self.theme = theme + self.actionLayout = actionLayout + self._dismissOnOutsideTap = dismissOnOutsideTap + if let title = title { + let titleNode = ImmediateTextNode() + titleNode.attributedText = title + titleNode.displaysAsynchronously = false + titleNode.isUserInteractionEnabled = false + titleNode.maximumNumberOfLines = 4 + titleNode.truncationType = .end + titleNode.isAccessibilityElement = true + titleNode.accessibilityLabel = title.string + self.titleNode = titleNode + } else { + self.titleNode = nil + } + + self.textNode = ImmediateTextNode() + self.textNode.maximumNumberOfLines = 0 + self.textNode.attributedText = text + self.textNode.displaysAsynchronously = false + self.textNode.isLayerBacked = false + self.textNode.isAccessibilityElement = true + self.textNode.accessibilityLabel = text.string + self.textNode.insets = UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0) + self.textNode.tapAttributeAction = linkAction + self.textNode.highlightAttributeAction = { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { + return NSAttributedString.Key(rawValue: "URL") + } else { + return nil + } + } + self.textNode.linkHighlightColor = theme.accentColor.withMultipliedAlpha(0.1) + if text.length != 0 { + if let paragraphStyle = text.attribute(.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle { + self.textNode.textAlignment = paragraphStyle.alignment + } + } + + self.actionNodesSeparator = ASDisplayNode() + self.actionNodesSeparator.isLayerBacked = true + self.actionNodesSeparator.backgroundColor = theme.separatorColor + + 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 + separatorNode.backgroundColor = theme.separatorColor + actionVerticalSeparators.append(separatorNode) + } + } + self.actionVerticalSeparators = actionVerticalSeparators + + super.init() + + if let titleNode = self.titleNode { + self.addSubnode(titleNode) + } + self.addSubnode(self.textNode) + + self.addSubnode(self.actionNodesSeparator) + + var i = 0 + for actionNode in self.actionNodes { + self.addSubnode(actionNode) + + let index = i + actionNode.highlightedUpdated = { [weak self] highlighted in + if highlighted { + self?.highlightedItemIndex = index + } + } + i += 1 + } + + for separatorNode in self.actionVerticalSeparators { + self.addSubnode(separatorNode) + } + } + + func setHighlightedItemIndex(_ index: Int?, update: Bool = false) { + self.highlightedItemIndex = index + + if update { + var i = 0 + for actionNode in self.actionNodes { + if i == index { + actionNode.setHighlighted(true, animated: false) + } else { + actionNode.setHighlighted(false, animated: false) + } + i += 1 + } + } + } + + override public func decreaseHighlightedIndex() { + let currentHighlightedIndex = self.highlightedItemIndex ?? 0 + + self.setHighlightedItemIndex(max(0, currentHighlightedIndex - 1), update: true) + } + + override public func increaseHighlightedIndex() { + let currentHighlightedIndex = self.highlightedItemIndex ?? -1 + + self.setHighlightedItemIndex(min(self.actionNodes.count - 1, currentHighlightedIndex + 1), update: true) + } + + override public func performHighlightedAction() { + guard let highlightedItemIndex = self.highlightedItemIndex else { + return + } + + var i = 0 + for itemNode in self.actionNodes { + if i == highlightedItemIndex { + itemNode.performAction() + return + } + i += 1 + } + } + + override public func updateTheme(_ theme: AlertControllerTheme) { + self.theme = theme + + if let titleNode = self.titleNode, let attributedText = titleNode.attributedText { + let updatedText = NSMutableAttributedString(attributedString: attributedText) + updatedText.addAttribute(NSAttributedString.Key.foregroundColor, value: theme.primaryColor, range: NSRange(location: 0, length: updatedText.length)) + titleNode.attributedText = updatedText + } + if let attributedText = self.textNode.attributedText { + let updatedText = NSMutableAttributedString(attributedString: attributedText) + updatedText.addAttribute(NSAttributedString.Key.foregroundColor, value: theme.primaryColor, range: NSRange(location: 0, length: updatedText.length)) + self.textNode.attributedText = updatedText + } + + 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 public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + self.validLayout = size + + let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0) + + var size = size + size.width = min(size.width, alertWidth) + + var titleSize: CGSize? + if let titleNode = self.titleNode { + titleSize = titleNode.updateLayout(CGSize(width: size.width - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude)) + } + let textSize = self.textNode.updateLayout(CGSize(width: size.width - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude)) + + 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 = self.actionLayout + 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 resultSize: CGSize + + var actionsHeight: CGFloat = 0.0 + switch effectiveActionLayout { + case .horizontal: + actionsHeight = actionButtonHeight + case .vertical: + actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) + } + + let contentWidth = alertWidth - insets.left - insets.right + if let titleNode = self.titleNode, let titleSize = titleSize { + let spacing: CGFloat = 6.0 + let titleFrame = CGRect(origin: CGPoint(x: insets.left + floor((contentWidth - titleSize.width) / 2.0), y: insets.top), size: titleSize) + transition.updateFrame(node: titleNode, frame: titleFrame) + + let textFrame = CGRect(origin: CGPoint(x: insets.left + floor((contentWidth - textSize.width) / 2.0), y: titleFrame.maxY + spacing), size: textSize) + transition.updateFrame(node: self.textNode, frame: textFrame.offsetBy(dx: -1.0, dy: -1.0)) + + resultSize = CGSize(width: contentWidth + insets.left + insets.right, height: titleSize.height + spacing + textSize.height + actionsHeight + insets.top + insets.bottom) + } else { + let textFrame = CGRect(origin: CGPoint(x: insets.left + floor((contentWidth - textSize.width) / 2.0), y: insets.top), size: textSize) + transition.updateFrame(node: self.textNode, frame: textFrame) + + resultSize = CGSize(width: contentWidth + insets.left + insets.right, height: textSize.height + actionsHeight + insets.top + insets.bottom) + } + + 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 + } +} + +private final class SuggestedPostAlertImpl: AlertController { + private let toastText: String? + private var toast: ComponentView? + + init(theme: AlertControllerTheme, contentNode: AlertContentNode, allowInputInset: Bool, toastText: String?) { + self.toastText = toastText + + super.init(theme: theme, contentNode: contentNode, allowInputInset: allowInputInset) + + self.willDismiss = { [weak self] in + guard let self else { + return + } + if let toastView = self.toast?.view { + toastView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + } + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + if let toastText = self.toastText { + let toast: ComponentView + if let current = self.toast { + toast = current + } else { + toast = ComponentView() + self.toast = toast + } + let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) + let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) + //TODO:localize + let playOnce = ActionSlot() + let toastSize = toast.update( + transition: ComponentTransition(transition), + component: AnyComponent(ToastContentComponent( + icon: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: "anim_infotip"), + startingPosition: .begin, + size: CGSize(width: 32.0, height: 32.0), + playOnce: playOnce + )), + content: AnyComponent(VStack([ + AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( + text: .markdown(text: toastText, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in nil })), + maximumNumberOfLines: 0 + ))) + ], alignment: .left, spacing: 6.0)), + insets: UIEdgeInsets(top: 10.0, left: 12.0, bottom: 10.0, right: 10.0), + iconSpacing: 12.0 + )), + environment: {}, + containerSize: CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - 12.0 * 2.0, height: 1000.0) + ) + let toastFrame = CGRect(origin: CGPoint(x: layout.safeInsets.left + 12.0, y: layout.insets(options: .statusBar).top + 4.0), size: toastSize) + if let toastView = toast.view { + if toastView.superview == nil { + self.view.addSubview(toastView) + playOnce.invoke(()) + } + transition.updatePosition(layer: toastView.layer, position: toastFrame.center) + transition.updateBounds(layer: toastView.layer, bounds: CGRect(origin: CGPoint(), size: toastFrame.size)) + } + } + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if let toastView = self.toast?.view { + toastView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + } + } + + override func dismissAnimated() { + super.dismissAnimated() + + if let toastView = self.toast?.view { + toastView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + } + } +} + +public func SuggestedPostApproveAlert(presentationData: PresentationData, title: String?, text: String, actions: [TextAlertAction], actionLayout: TextAlertContentActionLayout = .horizontal, allowInputInset: Bool = true, parseMarkdown: Bool = false, dismissOnOutsideTap: Bool = true, linkAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil, toastText: String?) -> AlertController { + let theme = AlertControllerTheme(presentationData: presentationData) + + var dismissImpl: (() -> Void)? + let attributedText: NSAttributedString + if parseMarkdown { + let font = title == nil ? Font.semibold(theme.baseFontSize) : Font.regular(floor(theme.baseFontSize * 13.0 / 17.0)) + let boldFont = title == nil ? Font.bold(theme.baseFontSize) : Font.semibold(floor(theme.baseFontSize * 13.0 / 17.0)) + let body = MarkdownAttributeSet(font: font, textColor: theme.primaryColor) + let bold = MarkdownAttributeSet(font: boldFont, textColor: theme.primaryColor) + let link = MarkdownAttributeSet(font: font, textColor: theme.accentColor) + attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { url in + return ("URL", url) + }), textAlignment: .center) + } else { + attributedText = NSAttributedString(string: text, font: title == nil ? Font.semibold(theme.baseFontSize) : Font.regular(floor(theme.baseFontSize * 13.0 / 17.0)), textColor: theme.primaryColor, paragraphAlignment: .center) + } + let controller = SuggestedPostAlertImpl(theme: theme, contentNode: TextAlertContentNode(theme: theme, title: title != nil ? NSAttributedString(string: title!, font: Font.semibold(theme.baseFontSize), textColor: theme.primaryColor, paragraphAlignment: .center) : nil, text: attributedText, actions: actions.map { action in + return TextAlertAction(type: action.type, title: action.title, action: { + dismissImpl?() + action.action() + }) + }, actionLayout: actionLayout, dismissOnOutsideTap: dismissOnOutsideTap, linkAction: linkAction), allowInputInset: allowInputInset, toastText: toastText) + dismissImpl = { [weak controller] in + controller?.dismissAnimated() + } + return controller +} diff --git a/submodules/TelegramUI/Resources/Animations/TonLogo.tgs b/submodules/TelegramUI/Resources/Animations/TonLogo.tgs new file mode 100644 index 0000000000..63d1896d9a Binary files /dev/null and b/submodules/TelegramUI/Resources/Animations/TonLogo.tgs differ diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 6763da6747..4dd22f4bf6 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -137,6 +137,7 @@ import TelegramCallsUI import QuickShareScreen import PostSuggestionsSettingsScreen import PromptUI +import SuggestedPostApproveAlert public final class ChatControllerOverlayPresentationData { public let expandData: (ASDisplayNode?, () -> Void) @@ -2362,8 +2363,19 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.present(promptController, in: .window(.root)) case 1: var timestamp: Int32? + var funds: (amount: CurrencyAmount, commissionPermille: Int)? + if let amount = attribute.amount { + let configuration = StarsSubscriptionConfiguration.with(appConfiguration: strongSelf.context.currentAppConfiguration.with { $0 }) + funds = (amount, amount.currency == .stars ? Int(configuration.channelMessageSuggestionStarsCommissionPermille) : Int(configuration.channelMessageSuggestionTonCommissionPermille)) + } + + var isAdmin = false + if let channel = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isMonoForum, let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = strongSelf.presentationInterfaceState.renderedPeer?.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.manageDirect) { + isAdmin = true + } + if attribute.timestamp == nil { - let controller = ChatScheduleTimeController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, mode: .suggestPost(needsTime: true), style: .default, currentTime: nil, minimalTime: nil, dismissByTapOutside: true, completion: { [weak strongSelf] time in + let controller = ChatScheduleTimeController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, mode: .suggestPost(needsTime: true, isAdmin: isAdmin, funds: funds), style: .default, currentTime: nil, minimalTime: nil, dismissByTapOutside: true, completion: { [weak strongSelf] time in guard let strongSelf else { return } @@ -2378,16 +2390,43 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G timestamp = Int32(Date().timeIntervalSince1970) + 1 * 60 * 60 } else { //TODO:localize - let textString = "Publish this message now?" - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: textString, actions: [ - TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), + var textString: String + if isAdmin { + textString = "Do you really want to publish this post from **\((message.author.flatMap(EnginePeer.init))?.compactDisplayTitle ?? "")**?" + + if let funds { + var commissionValue: String + commissionValue = "\(Double(funds.commissionPermille) * 0.1)" + if commissionValue.hasSuffix(".0") { + commissionValue = String(commissionValue[commissionValue.startIndex ..< commissionValue.index(commissionValue.endIndex, offsetBy: -2)]) + } else if commissionValue.hasSuffix(".00") { + commissionValue = String(commissionValue[commissionValue.startIndex ..< commissionValue.index(commissionValue.endIndex, offsetBy: -3)]) + } + + textString += "\n\n" + + switch funds.amount.currency { + case .stars: + let displayAmount = funds.amount.amount.totalValue * Double(funds.commissionPermille) / 1000.0 + textString += "You will receive \(displayAmount) Stars (\(commissionValue)%)\nfor publishing this post. It must remain visible for **24** hours after publication." + case .ton: + let displayAmount = Double(funds.amount.amount.value) / 1000000000.0 * Double(funds.commissionPermille) / 1000.0 + textString += "You will receive \(displayAmount) TON (\(commissionValue)%)\nfor publishing this post. It must remain visible for **24** hours after publication." + } + } + } else { + textString = "Do you really want to publish this post?" + } + + strongSelf.present(SuggestedPostApproveAlert(presentationData: strongSelf.presentationData, title: "Accept Terms", text: textString, actions: [ TextAlertAction(type: .defaultAction, title: "Publish", action: { [weak strongSelf] in guard let strongSelf else { return } let _ = strongSelf.context.engine.messages.monoforumPerformSuggestedPostAction(id: message.id, action: .approve(timestamp: timestamp)).startStandalone() - }) - ]), in: .window(.root)) + }), + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}) + ], actionLayout: .vertical, parseMarkdown: true, toastText: funds?.amount.currency == .ton ? "Transactions in **Stars** may be reversed by the payment provider within **21** days. Only accept Stars from people you trust." : nil), in: .window(.root)) } case 2: strongSelf.interfaceInteraction?.openSuggestPost(message, .default)