Various improvements

This commit is contained in:
Isaac 2025-06-28 23:42:18 +02:00
parent 15c9af3ab2
commit 73218834ff
15 changed files with 772 additions and 60 deletions

View File

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

View File

@ -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):

View File

@ -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..<endIndex])
} else {
balanceText = String(balanceText[balanceText.startIndex..<balanceText.endIndex])

View File

@ -480,6 +480,7 @@ swift_library(
"//submodules/TelegramUI/Components/GifVideoLayer",
"//submodules/TelegramUI/Components/BatchVideoRendering",
"//submodules/TelegramUI/Components/ComposeTodoScreen",
"//submodules/TelegramUI/Components/SuggestedPostApproveAlert",
] + select({
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
"//build-system:ios_sim_arm64": [],

View File

@ -21,6 +21,12 @@ swift_library(
"//submodules/SolidRoundedButtonNode",
"//submodules/PresentationDataUtils",
"//submodules/UIKitRuntimeUtils",
"//submodules/ComponentFlow",
"//submodules/TelegramUI/Components/ToastComponent",
"//submodules/Markdown",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/ComponentDisplayAdapters",
],
visibility = [
"//visibility:public",

View File

@ -11,7 +11,7 @@ import TelegramPresentationData
public enum ChatScheduleTimeControllerMode {
case scheduledMessages(sendWhenOnlineAvailable: Bool)
case reminders
case suggestPost(needsTime: Bool)
case suggestPost(needsTime: Bool, isAdmin: Bool, funds: (amount: CurrencyAmount, commissionPermille: Int)?)
}
public enum ChatScheduleTimeControllerStyle {

View File

@ -10,6 +10,12 @@ import AccountContext
import SolidRoundedButtonNode
import PresentationDataUtils
import UIKitRuntimeUtils
import ComponentFlow
import ToastComponent
import Markdown
import LottieComponent
import MultilineTextComponent
import ComponentDisplayAdapters
class ChatScheduleTimeControllerNode: ViewControllerTracingNode, ASScrollViewDelegate {
private let context: AccountContext
@ -26,6 +32,7 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, ASScrollViewDel
private let backgroundNode: ASDisplayNode
private let contentBackgroundNode: ASDisplayNode
private let titleNode: ASTextNode
private let subtitleNode: ASTextNode?
private let textNode: ASTextNode?
private let cancelButton: HighlightableButtonNode
private let doneButton: SolidRoundedButtonNode
@ -36,6 +43,8 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, ASScrollViewDel
private var containerLayout: (ContainerViewLayout, CGFloat)?
private var toast: ComponentView<Empty>?
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<Empty>
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<Void>()
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))
}
}
}
}

View File

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

View File

@ -22,6 +22,7 @@ swift_library(
"//submodules/Components/SheetComponent",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramCore",
],
visibility = [
"//visibility:public",

View File

@ -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<Empty>()
private let button = ComponentView<Empty>()
private var cancelButton: ComponentView<Empty>?
private let closeButton = ComponentView<Empty>()
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<Empty>
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()
})
}

View File

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

View File

@ -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",
],
)

View File

@ -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<Empty>?
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<Empty>
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<Void>()
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
}

Binary file not shown.

View File

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