Add support for stars withdrawal timeout

This commit is contained in:
Ilya Laktyushin 2024-06-16 15:51:21 +04:00
parent a232ba765f
commit c0489e251b
19 changed files with 346 additions and 84 deletions

View File

@ -12323,6 +12323,7 @@ Sorry for the inconvenience.";
"Story.ViewLink" = "Open Link";
"PeerInfo.Bot.Username" = "Username";
"PeerInfo.Bot.Balance" = "Balance";
"PeerInfo.Bot.Balance.Stars_1" = "%@ Star";
"PeerInfo.Bot.Balance.Stars_any" = "%@ Stars";
@ -12349,10 +12350,12 @@ Sorry for the inconvenience.";
"Stars.Withdraw.AmountPlaceholder" = "Stars Amount";
"Stars.Withdraw.Withdraw" = "Withdraw";
"Stars.Withdraw.Withdraw.ErrorMinimum" = "You cannot withdraw less than %@";
"Stars.Withdraw.Withdraw.ErrorMinimum" = "You cannot withdraw less than [%@]().";
"Stars.Withdraw.Withdraw.ErrorMinimum.Stars_1" = "%@ Star";
"Stars.Withdraw.Withdraw.ErrorMinimum.Stars_any" = "%@ Stars";
"Stars.Withdraw.Withdraw.ErrorTimeout" = "Next withdrawal will be available in **%@**.";
"Stars.PaidContent.Title" = "Paid Content";
"Stars.PaidContent.AmountTitle" = "ENTER UNLOCK COST";
"Stars.PaidContent.AmountPlaceholder" = "Stars to Unlock";
@ -12369,3 +12372,7 @@ Sorry for the inconvenience.";
"MediaEditor.Link.LinkName.Placeholder" = "Enter a Name";
"Story.Editor.TooltipLinkPremium" = "Subscribe to [Telegram Premium]() to add links.";
"Story.Editor.TooltipLinkLimitValue_1" = "**%@** link";
"Story.Editor.TooltipLinkLimitValue_any" = "**%@** links";
"Story.Editor.TooltipReachedLinkLimitText" = "You can't add more than %@ to a story.";

View File

@ -3088,6 +3088,7 @@ public final class DrawingToolsInteraction {
var isVideo = false
var isAdditional = false
var isMessage = false
var isLink = false
if let entity = entityView.entity as? DrawingStickerEntity {
if case let .dualVideoReference(isAdditionalValue) = entity.content {
isVideo = true
@ -3095,6 +3096,8 @@ public final class DrawingToolsInteraction {
} else if case .message = entity.content {
isMessage = true
}
} else if entityView.entity is DrawingLinkEntity {
isLink = true
}
guard (!isVideo || isAdditional) && (!isMessage || !isTopmost) else {
@ -3140,7 +3143,7 @@ public final class DrawingToolsInteraction {
}
}))
}
if !isVideo && !isMessage {
if !isVideo && !isMessage && !isLink {
if let stickerEntity = entityView.entity as? DrawingStickerEntity, case let .file(_, type) = stickerEntity.content, case .reaction = type {
} else {

View File

@ -518,7 +518,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[340088945] = { return Api.MediaArea.parse_mediaAreaSuggestedReaction($0) }
dict[926421125] = { return Api.MediaArea.parse_mediaAreaUrl($0) }
dict[-1098720356] = { return Api.MediaArea.parse_mediaAreaVenue($0) }
dict[64088654] = { return Api.MediaAreaCoordinates.parse_mediaAreaCoordinates($0) }
dict[-808853502] = { return Api.MediaAreaCoordinates.parse_mediaAreaCoordinates($0) }
dict[-1808510398] = { return Api.Message.parse_message($0) }
dict[-1868117372] = { return Api.Message.parse_messageEmpty($0) }
dict[721967202] = { return Api.Message.parse_messageService($0) }
@ -872,7 +872,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-425595208] = { return Api.SmsJob.parse_smsJob($0) }
dict[-1108478618] = { return Api.SponsoredMessage.parse_sponsoredMessage($0) }
dict[1124938064] = { return Api.SponsoredMessageReportOption.parse_sponsoredMessageReportOption($0) }
dict[-407138204] = { return Api.StarsRevenueStatus.parse_starsRevenueStatus($0) }
dict[2033461574] = { return Api.StarsRevenueStatus.parse_starsRevenueStatus($0) }
dict[198776256] = { return Api.StarsTopupOption.parse_starsTopupOption($0) }
dict[-1442789224] = { return Api.StarsTransaction.parse_starsTransaction($0) }
dict[-670195363] = { return Api.StarsTransactionPeer.parse_starsTransactionPeer($0) }

View File

@ -300,33 +300,35 @@ public extension Api {
}
public extension Api {
enum MediaAreaCoordinates: TypeConstructorDescription {
case mediaAreaCoordinates(x: Double, y: Double, w: Double, h: Double, rotation: Double)
case mediaAreaCoordinates(flags: Int32, x: Double, y: Double, w: Double, h: Double, rotation: Double, radius: Double?)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .mediaAreaCoordinates(let x, let y, let w, let h, let rotation):
case .mediaAreaCoordinates(let flags, let x, let y, let w, let h, let rotation, let radius):
if boxed {
buffer.appendInt32(64088654)
buffer.appendInt32(-808853502)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeDouble(x, buffer: buffer, boxed: false)
serializeDouble(y, buffer: buffer, boxed: false)
serializeDouble(w, buffer: buffer, boxed: false)
serializeDouble(h, buffer: buffer, boxed: false)
serializeDouble(rotation, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {serializeDouble(radius!, buffer: buffer, boxed: false)}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .mediaAreaCoordinates(let x, let y, let w, let h, let rotation):
return ("mediaAreaCoordinates", [("x", x as Any), ("y", y as Any), ("w", w as Any), ("h", h as Any), ("rotation", rotation as Any)])
case .mediaAreaCoordinates(let flags, let x, let y, let w, let h, let rotation, let radius):
return ("mediaAreaCoordinates", [("flags", flags as Any), ("x", x as Any), ("y", y as Any), ("w", w as Any), ("h", h as Any), ("rotation", rotation as Any), ("radius", radius as Any)])
}
}
public static func parse_mediaAreaCoordinates(_ reader: BufferReader) -> MediaAreaCoordinates? {
var _1: Double?
_1 = reader.readDouble()
var _1: Int32?
_1 = reader.readInt32()
var _2: Double?
_2 = reader.readDouble()
var _3: Double?
@ -335,13 +337,19 @@ public extension Api {
_4 = reader.readDouble()
var _5: Double?
_5 = reader.readDouble()
var _6: Double?
_6 = reader.readDouble()
var _7: Double?
if Int(_1!) & Int(1 << 0) != 0 {_7 = reader.readDouble() }
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = _4 != nil
let _c5 = _5 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 {
return Api.MediaAreaCoordinates.mediaAreaCoordinates(x: _1!, y: _2!, w: _3!, h: _4!, rotation: _5!)
let _c6 = _6 != nil
let _c7 = (Int(_1!) & Int(1 << 0) == 0) || _7 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 {
return Api.MediaAreaCoordinates.mediaAreaCoordinates(flags: _1!, x: _2!, y: _3!, w: _4!, h: _5!, rotation: _6!, radius: _7)
}
else {
return nil

View File

@ -604,26 +604,27 @@ public extension Api {
}
public extension Api {
enum StarsRevenueStatus: TypeConstructorDescription {
case starsRevenueStatus(flags: Int32, currentBalance: Int64, availableBalance: Int64, overallRevenue: Int64)
case starsRevenueStatus(flags: Int32, currentBalance: Int64, availableBalance: Int64, overallRevenue: Int64, nextWithdrawalAt: Int32?)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .starsRevenueStatus(let flags, let currentBalance, let availableBalance, let overallRevenue):
case .starsRevenueStatus(let flags, let currentBalance, let availableBalance, let overallRevenue, let nextWithdrawalAt):
if boxed {
buffer.appendInt32(-407138204)
buffer.appendInt32(2033461574)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt64(currentBalance, buffer: buffer, boxed: false)
serializeInt64(availableBalance, buffer: buffer, boxed: false)
serializeInt64(overallRevenue, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 1) != 0 {serializeInt32(nextWithdrawalAt!, buffer: buffer, boxed: false)}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .starsRevenueStatus(let flags, let currentBalance, let availableBalance, let overallRevenue):
return ("starsRevenueStatus", [("flags", flags as Any), ("currentBalance", currentBalance as Any), ("availableBalance", availableBalance as Any), ("overallRevenue", overallRevenue as Any)])
case .starsRevenueStatus(let flags, let currentBalance, let availableBalance, let overallRevenue, let nextWithdrawalAt):
return ("starsRevenueStatus", [("flags", flags as Any), ("currentBalance", currentBalance as Any), ("availableBalance", availableBalance as Any), ("overallRevenue", overallRevenue as Any), ("nextWithdrawalAt", nextWithdrawalAt as Any)])
}
}
@ -636,12 +637,15 @@ public extension Api {
_3 = reader.readInt64()
var _4: Int64?
_4 = reader.readInt64()
var _5: Int32?
if Int(_1!) & Int(1 << 1) != 0 {_5 = reader.readInt32() }
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
let _c4 = _4 != nil
if _c1 && _c2 && _c3 && _c4 {
return Api.StarsRevenueStatus.starsRevenueStatus(flags: _1!, currentBalance: _2!, availableBalance: _3!, overallRevenue: _4!)
let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 {
return Api.StarsRevenueStatus.starsRevenueStatus(flags: _1!, currentBalance: _2!, availableBalance: _3!, overallRevenue: _4!, nextWithdrawalAt: _5)
}
else {
return nil

View File

@ -473,8 +473,8 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI
func mediaAreaFromApiMediaArea(_ mediaArea: Api.MediaArea) -> MediaArea? {
func coodinatesFromApiMediaAreaCoordinates(_ coordinates: Api.MediaAreaCoordinates) -> MediaArea.Coordinates {
switch coordinates {
case let .mediaAreaCoordinates(x, y, width, height, rotation):
return MediaArea.Coordinates(x: x, y: y, width: width, height: height, rotation: rotation)
case let .mediaAreaCoordinates(_, x, y, width, height, rotation, radius):
return MediaArea.Coordinates(x: x, y: y, width: width, height: height, rotation: rotation, cornerRadius: radius)
}
}
switch mediaArea {
@ -551,7 +551,11 @@ func apiMediaAreasFromMediaAreas(_ mediaAreas: [MediaArea], transaction: Transac
var apiMediaAreas: [Api.MediaArea] = []
for area in mediaAreas {
let coordinates = area.coordinates
let inputCoordinates = Api.MediaAreaCoordinates.mediaAreaCoordinates(x: coordinates.x, y: coordinates.y, w: coordinates.width, h: coordinates.height, rotation: coordinates.rotation)
var flags: Int32 = 0
if let _ = coordinates.cornerRadius {
flags |= (1 << 0)
}
let inputCoordinates = Api.MediaAreaCoordinates.mediaAreaCoordinates(flags: flags, x: coordinates.x, y: coordinates.y, w: coordinates.width, h: coordinates.height, rotation: coordinates.rotation, radius: coordinates.cornerRadius)
switch area {
case let .venue(_, venue):
if let queryId = venue.queryId, let resultId = venue.resultId {

View File

@ -10,6 +10,7 @@ public struct StarsRevenueStats: Equatable {
public let availableBalance: Int64
public let overallRevenue: Int64
public let withdrawEnabled: Bool
public let nextWithdrawalTimestamp: Int32?
}
public let revenueGraph: StatsGraph
@ -58,8 +59,8 @@ extension StarsRevenueStats {
extension StarsRevenueStats.Balances {
init(apiStarsRevenueStatus: Api.StarsRevenueStatus) {
switch apiStarsRevenueStatus {
case let .starsRevenueStatus(flags, currentBalance, availableBalance, overallRevenue):
self.init(currentBalance: currentBalance, availableBalance: availableBalance, overallRevenue: overallRevenue, withdrawEnabled: ((flags & (1 << 0)) != 0))
case let .starsRevenueStatus(flags, currentBalance, availableBalance, overallRevenue, nextWithdrawalAt):
self.init(currentBalance: currentBalance, availableBalance: availableBalance, overallRevenue: overallRevenue, withdrawEnabled: ((flags & (1 << 0)) != 0), nextWithdrawalTimestamp: nextWithdrawalAt)
}
}
}

View File

@ -16,6 +16,7 @@ public enum MediaArea: Codable, Equatable {
case width
case height
case rotation
case cornerRadius
}
public var x: Double
@ -23,19 +24,22 @@ public enum MediaArea: Codable, Equatable {
public var width: Double
public var height: Double
public var rotation: Double
public var cornerRadius: Double?
public init(
x: Double,
y: Double,
width: Double,
height: Double,
rotation: Double
rotation: Double,
cornerRadius: Double?
) {
self.x = x
self.y = y
self.width = width
self.height = height
self.rotation = rotation
self.cornerRadius = cornerRadius
}
public init(from decoder: Decoder) throws {
@ -46,6 +50,7 @@ public enum MediaArea: Codable, Equatable {
self.width = try container.decode(Double.self, forKey: .width)
self.height = try container.decode(Double.self, forKey: .height)
self.rotation = try container.decode(Double.self, forKey: .rotation)
self.cornerRadius = try container.decodeIfPresent(Double.self, forKey: .cornerRadius)
}
public func encode(to encoder: Encoder) throws {
@ -56,6 +61,7 @@ public enum MediaArea: Codable, Equatable {
try container.encode(self.width, forKey: .width)
try container.encode(self.height, forKey: .height)
try container.encode(self.rotation, forKey: .rotation)
try container.encodeIfPresent(self.cornerRadius, forKey: .cornerRadius)
}
}

View File

@ -110,7 +110,8 @@ public enum CodableDrawingEntity: Equatable {
y: position.y / 1920.0 * 100.0,
width: size.width * scale / 1080.0 * 100.0,
height: size.height * scale / 1920.0 * 100.0,
rotation: rotation / .pi * 180.0
rotation: rotation / .pi * 180.0,
cornerRadius: nil
)
}

View File

@ -260,7 +260,7 @@ private final class SheetContent: CombinedComponent {
placeholderColor: theme.list.itemPlaceholderTextColor,
text: state.name,
link: false,
placeholderText: strings.MediaEditor_Link_LinkTo_Placeholder,
placeholderText: strings.MediaEditor_Link_LinkName_Placeholder,
textUpdated: { [weak state] text in
state?.name = text
}

View File

@ -4485,6 +4485,20 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
return
}
if existingEntity == nil {
let maxLinkCount = 3
var currentLinkCount = 0
self.entitiesView.eachView { entityView in
if entityView.entity is DrawingLinkEntity {
currentLinkCount += 1
}
}
if currentLinkCount >= maxLinkCount {
controller.presentLinkLimitTooltip()
return
}
}
var link: CreateLinkScreen.Link?
if let existingEntity {
link = CreateLinkScreen.Link(
@ -5977,6 +5991,29 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
self.present(controller, in: .current)
}
fileprivate func presentLinkLimitTooltip() {
self.hapticFeedback.impact(.light)
self.dismissAllTooltips()
let context = self.context
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let limit: Int32 = 3
let value = presentationData.strings.Story_Editor_TooltipLinkLimitValue(limit)
let content: UndoOverlayContent = .info(
title: nil,
text: presentationData.strings.Story_Editor_TooltipReachedLinkLimitText(value).string,
timeout: nil,
customUndoText: nil
)
let controller = UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: true, position: .top, animateInAsReplacement: false, action: { _ in
return true
})
self.present(controller, in: .window(.root))
}
func maybePresentDiscardAlert() {
self.hapticFeedback.impact(.light)
if !self.isEligibleForDraft() {

View File

@ -1729,8 +1729,7 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL
let ItemBotInfo = 10
if let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) {
//TODO:localize
items[.peerDataSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemUsername, label: .text("@\(user.addressName ?? "")"), text: "Username", icon: PresentationResourcesSettings.bot, action: {
items[.peerDataSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemUsername, label: .text("@\(user.addressName ?? "")"), text: presentationData.strings.PeerInfo_Bot_Username, icon: PresentationResourcesSettings.bot, action: {
interaction.editingOpenPublicLinkSetup()
}))

View File

@ -7,7 +7,9 @@ import AccountContext
import MultilineTextComponent
import TelegramPresentationData
import PresentationDataUtils
import SolidRoundedButtonComponent
import ButtonComponent
import BundleIconComponent
import TelegramStringFormatting
final class StarsBalanceComponent: Component {
let theme: PresentationTheme
@ -17,6 +19,8 @@ final class StarsBalanceComponent: Component {
let rate: Double?
let actionTitle: String
let actionAvailable: Bool
let actionIsEnabled: Bool
let actionCooldownUntilTimestamp: Int32?
let buy: () -> Void
init(
@ -27,6 +31,8 @@ final class StarsBalanceComponent: Component {
rate: Double?,
actionTitle: String,
actionAvailable: Bool,
actionIsEnabled: Bool,
actionCooldownUntilTimestamp: Int32? = nil,
buy: @escaping () -> Void
) {
self.theme = theme
@ -36,6 +42,8 @@ final class StarsBalanceComponent: Component {
self.rate = rate
self.actionTitle = actionTitle
self.actionAvailable = actionAvailable
self.actionIsEnabled = actionIsEnabled
self.actionCooldownUntilTimestamp = actionCooldownUntilTimestamp
self.buy = buy
}
@ -55,6 +63,12 @@ final class StarsBalanceComponent: Component {
if lhs.actionAvailable != rhs.actionAvailable {
return false
}
if lhs.actionIsEnabled != rhs.actionIsEnabled {
return false
}
if lhs.actionCooldownUntilTimestamp != rhs.actionCooldownUntilTimestamp {
return false
}
if lhs.count != rhs.count {
return false
}
@ -71,6 +85,9 @@ final class StarsBalanceComponent: Component {
private var button = ComponentView<Empty>()
private var component: StarsBalanceComponent?
private weak var state: EmptyComponentState?
private var timer: Timer?
override init(frame: CGRect) {
super.init(frame: frame)
@ -86,6 +103,29 @@ final class StarsBalanceComponent: Component {
func update(component: StarsBalanceComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
var remainingCooldownSeconds: Int32 = 0
if let cooldownUntilTimestamp = component.actionCooldownUntilTimestamp {
remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970)
remainingCooldownSeconds = max(0, remainingCooldownSeconds)
}
if remainingCooldownSeconds > 0 {
if self.timer == nil {
self.timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { [weak self] _ in
guard let self else {
return
}
self.state?.updated(transition: .immediate)
})
}
} else {
if let timer = self.timer {
self.timer = nil
timer.invalidate()
}
}
let sideInset: CGFloat = 16.0
var contentHeight: CGFloat = sideInset
@ -147,19 +187,40 @@ final class StarsBalanceComponent: Component {
if component.actionAvailable {
contentHeight += 12.0
let content: AnyComponentWithIdentity<Empty>
if remainingCooldownSeconds > 0 {
content = AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent(
VStack([
AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent(Text(text: component.actionTitle, font: Font.semibold(17.0), color: component.theme.list.itemCheckColors.foregroundColor))),
AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(HStack([
AnyComponentWithIdentity(id: 1, component: AnyComponent(BundleIconComponent(name: "Chat List/StatusLockIcon", tintColor: component.theme.list.itemCheckColors.fillColor.mixedWith(component.theme.list.itemCheckColors.foregroundColor, alpha: 0.7)))),
AnyComponentWithIdentity(id: 0, component: AnyComponent(Text(text: stringForRemainingTime(remainingCooldownSeconds), font: Font.with(size: 11.0, weight: .medium, traits: [.monospacedNumbers]), color: component.theme.list.itemCheckColors.fillColor.mixedWith(component.theme.list.itemCheckColors.foregroundColor, alpha: 0.7))))
], spacing: 3.0)))
], spacing: 1.0)
))
} else {
content = AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(Text(text: component.actionTitle, font: Font.semibold(17.0), color: component.theme.list.itemCheckColors.foregroundColor)))
}
let buttonSize = self.button.update(
transition: .immediate,
component: AnyComponent(
SolidRoundedButtonComponent(
title: component.actionTitle,
theme: SolidRoundedButtonComponent.Theme(theme: component.theme),
height: 50.0,
cornerRadius: 11.0,
action: { [weak self] in
self?.component?.buy()
transition: transition,
component: AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
color: component.theme.list.itemCheckColors.fillColor,
foreground: component.theme.list.itemCheckColors.foregroundColor,
pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8)
),
content: content,
isEnabled: component.actionIsEnabled,
allowActionWhenDisabled: false,
displaysProgress: false,
action: { [weak self] in
guard let self, let component = self.component else {
return
}
)
),
component.buy()
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0)
)
@ -186,3 +247,16 @@ final class StarsBalanceComponent: Component {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
func stringForRemainingTime(_ duration: Int32) -> String {
let hours = duration / 3600
let minutes = duration / 60 % 60
let seconds = duration % 60
let durationString: String
if hours > 0 {
durationString = String(format: "%d:%02d", hours, minutes)
} else {
durationString = String(format: "%02d:%02d", minutes, seconds)
}
return durationString
}

View File

@ -7,7 +7,6 @@ import AccountContext
import MultilineTextComponent
import TelegramPresentationData
import PresentationDataUtils
import SolidRoundedButtonComponent
final class StarsOverviewItemComponent: Component {
let theme: PresentationTheme

View File

@ -29,19 +29,22 @@ final class StarsStatisticsScreenComponent: Component {
let revenueContext: StarsRevenueStatsContext
let openTransaction: (StarsContext.State.Transaction) -> Void
let buy: () -> Void
let showTimeoutTooltip: (Int32) -> Void
init(
context: AccountContext,
peerId: EnginePeer.Id,
revenueContext: StarsRevenueStatsContext,
openTransaction: @escaping (StarsContext.State.Transaction) -> Void,
buy: @escaping () -> Void
buy: @escaping () -> Void,
showTimeoutTooltip: @escaping (Int32) -> Void
) {
self.context = context
self.peerId = peerId
self.revenueContext = revenueContext
self.openTransaction = openTransaction
self.buy = buy
self.showTimeoutTooltip = showTimeoutTooltip
}
static func ==(lhs: StarsStatisticsScreenComponent, rhs: StarsStatisticsScreenComponent) -> Bool {
@ -459,11 +462,25 @@ final class StarsStatisticsScreenComponent: Component {
rate: 0.2,
actionTitle: strings.Stars_BotRevenue_Withdraw_Withdraw,
actionAvailable: true,
actionIsEnabled: self.starsState?.balances.withdrawEnabled ?? true,
actionCooldownUntilTimestamp: self.starsState?.balances.nextWithdrawalTimestamp,
buy: { [weak self] in
guard let self, let component = self.component else {
return
}
component.buy()
var remainingCooldownSeconds: Int32 = 0
if let cooldownUntilTimestamp = self.starsState?.balances.nextWithdrawalTimestamp {
remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970)
remainingCooldownSeconds = max(0, remainingCooldownSeconds)
if remainingCooldownSeconds > 0 {
component.showTimeoutTooltip(cooldownUntilTimestamp)
} else {
component.buy()
}
} else {
component.buy()
}
}
)
))]
@ -597,13 +614,17 @@ public final class StarsStatisticsScreen: ViewControllerComponentContainer {
private let context: AccountContext
private let peerId: EnginePeer.Id
private let revenueContext: StarsRevenueStatsContext
private weak var tooltipScreen: UndoOverlayController?
private var timer: Foundation.Timer?
public init(context: AccountContext, peerId: EnginePeer.Id, revenueContext: StarsRevenueStatsContext) {
self.context = context
self.peerId = peerId
self.revenueContext = revenueContext
var withdrawImpl: (() -> Void)?
var showTimeoutTooltipImpl: ((Int32) -> Void)?
var openTransactionImpl: ((StarsContext.State.Transaction) -> Void)?
super.init(context: context, component: StarsStatisticsScreenComponent(
context: context,
@ -614,6 +635,9 @@ public final class StarsStatisticsScreen: ViewControllerComponentContainer {
},
buy: {
withdrawImpl?()
},
showTimeoutTooltip: { timestamp in
showTimeoutTooltipImpl?(timestamp)
}
), navigationBarAppearance: .transparent)
@ -654,6 +678,10 @@ public final class StarsStatisticsScreen: ViewControllerComponentContainer {
}, completion: { url in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {})
Queue.mainQueue().after(2.0) {
revenueContext.reload()
}
})
self.present(controller, in: .window(.root))
})
@ -669,6 +697,59 @@ public final class StarsStatisticsScreen: ViewControllerComponentContainer {
}
})
}
showTimeoutTooltipImpl = { [weak self] cooldownUntilTimestamp in
guard let self, self.tooltipScreen == nil else {
return
}
let remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970)
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let content: UndoOverlayContent = .universal(
animation: "anim_clock",
scale: 0.058,
colors: [:],
title: nil,
text: presentationData.strings.Stars_Withdraw_Withdraw_ErrorTimeout(stringForRemainingTime(remainingCooldownSeconds)).string,
customUndoText: nil,
timeout: nil
)
let controller = UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in
return true
})
self.tooltipScreen = controller
self.present(controller, in: .window(.root))
if remainingCooldownSeconds < 3600 {
if self.timer == nil {
self.timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { [weak self] _ in
guard let self else {
return
}
if let tooltipScreen = self.tooltipScreen {
let remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970)
let content: UndoOverlayContent = .universal(
animation: "anim_clock",
scale: 0.058,
colors: [:],
title: nil,
text: presentationData.strings.Stars_Withdraw_Withdraw_ErrorTimeout(stringForRemainingTime(remainingCooldownSeconds)).string,
customUndoText: nil,
timeout: nil
)
tooltipScreen.content = content
} else {
if let timer = self.timer {
self.timer = nil
timer.invalidate()
}
}
})
}
}
}
}
required public init(coder aDecoder: NSCoder) {

View File

@ -524,6 +524,7 @@ final class StarsTransactionsScreenComponent: Component {
rate: nil,
actionTitle: environment.strings.Stars_Intro_Buy,
actionAvailable: !premiumConfiguration.areStarsDisabled,
actionIsEnabled: true,
buy: { [weak self] in
guard let self, let component = self.component else {
return

View File

@ -20,6 +20,7 @@ import AccountContext
import PresentationDataUtils
import ListSectionComponent
import TelegramStringFormatting
import UndoUI
private let amountTag = GenericComponentViewTag()
@ -286,8 +287,12 @@ private final class SheetContent: CombinedComponent {
displaysProgress: false,
action: { [weak state] in
if let controller = controller() as? StarsWithdrawScreen, let amount = state?.amount {
controller.completion(amount)
controller.dismissAnimated()
if let minAmount, amount < minAmount {
controller.presentMinAmountTooltip(minAmount)
} else {
controller.completion(amount)
controller.dismissAnimated()
}
}
}
),
@ -300,8 +305,8 @@ private final class SheetContent: CombinedComponent {
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0))
)
contentSize.height += button.size.height
contentSize.height += 30.0
contentSize.height += 15.0
contentSize.height += max(environment.inputHeight, environment.safeInsets.bottom)
return contentSize
@ -469,6 +474,26 @@ public final class StarsWithdrawScreen: ViewControllerComponentContainer {
}
}
}
func presentMinAmountTooltip(_ minAmount: Int64) {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let resultController = UndoOverlayController(
presentationData: presentationData,
content: .image(
image: UIImage(bundleImageName: "Premium/Stars/StarLarge")!,
title: nil,
text: presentationData.strings.Stars_Withdraw_Withdraw_ErrorMinimum(presentationData.strings.Stars_Withdraw_Withdraw_ErrorMinimum_Stars(Int32(minAmount))).string,
round: false,
undoText: nil
),
elevatedLayout: false,
action: { _ in return true})
self.present(resultController, in: .window(.root))
if let view = self.node.hostView.findTaggedView(tag: amountTag) as? AmountFieldComponent.View {
view.animateError()
}
}
public func dismissAnimated() {
if let view = self.node.hostView.findTaggedView(tag: SheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
@ -593,14 +618,7 @@ private final class AmountFieldComponent: Component {
if let amount, let maxAmount = component.maxValue, amount > maxAmount {
textField.text = "\(maxAmount)"
textField.layer.addShakeAnimation()
let hapticFeedback = HapticFeedback()
hapticFeedback.error()
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0, execute: {
let _ = hapticFeedback
})
self.animateError()
return false
}
}
@ -615,6 +633,15 @@ private final class AmountFieldComponent: Component {
self.textField.selectAll(nil)
}
func animateError() {
self.textField.layer.addShakeAnimation()
let hapticFeedback = HapticFeedback()
hapticFeedback.error()
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0, execute: {
let _ = hapticFeedback
})
}
func update(component: AmountFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.textField.textColor = component.textColor
if let value = component.value {

File diff suppressed because one or more lines are too long

View File

@ -1108,7 +1108,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
let link = MarkdownAttributeSet(font: Font.regular(14.0), textColor: undoTextColor)
let link = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: undoTextColor)
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { contents in
return ("URL", contents)
}), textAlignment: .natural)
@ -1417,32 +1417,41 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
func updateContent(_ content: UndoOverlayContent) {
self.content = content
var undoTextColor = self.presentationData.theme.list.itemAccentColor.withMultiplied(hue: 0.933, saturation: 0.61, brightness: 1.0)
switch content {
case let .image(image, title, text, _, _):
self.iconNode?.image = image
if let title = title {
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white)
} else {
self.titleNode.attributedText = nil
}
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: .white)
self.renewWithCurrentContent()
case let .actionSucceeded(title, text, _, destructive):
var undoTextColor = self.presentationData.theme.list.itemAccentColor.withMultiplied(hue: 0.933, saturation: 0.61, brightness: 1.0)
if destructive {
undoTextColor = UIColor(rgb: 0xff7b74)
}
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
let link = MarkdownAttributeSet(font: Font.regular(14.0), textColor: undoTextColor)
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { _ in return nil }), textAlignment: .natural)
if let title {
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white)
}
self.textNode.attributedText = attributedText
default:
break
case let .info(title, text, _, _), let .universal(_, _, _, title, text, _, _):
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
let link = MarkdownAttributeSet(font: Font.regular(14.0), textColor: undoTextColor)
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { _ in return nil }), textAlignment: .natural)
if let title {
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white)
}
self.textNode.attributedText = attributedText
case let .image(image, title, text, _, _):
self.iconNode?.image = image
if let title = title {
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white)
} else {
self.titleNode.attributedText = nil
}
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: .white)
self.renewWithCurrentContent()
case let .actionSucceeded(title, text, _, destructive):
if destructive {
undoTextColor = UIColor(rgb: 0xff7b74)
}
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
let link = MarkdownAttributeSet(font: Font.regular(14.0), textColor: undoTextColor)
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { _ in return nil }), textAlignment: .natural)
if let title {
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white)
}
self.textNode.attributedText = attributedText
default:
break
}
if let validLayout = self.validLayout {