mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Add support for stars withdrawal timeout
This commit is contained in:
parent
a232ba765f
commit
c0489e251b
@ -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.";
|
||||
|
@ -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 {
|
||||
|
@ -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) }
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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()
|
||||
}))
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ import AccountContext
|
||||
import MultilineTextComponent
|
||||
import TelegramPresentationData
|
||||
import PresentationDataUtils
|
||||
import SolidRoundedButtonComponent
|
||||
|
||||
final class StarsOverviewItemComponent: Component {
|
||||
let theme: PresentationTheme
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user