diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index cac42787f3..0d58780b9d 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12217,3 +12217,18 @@ Sorry for the inconvenience."; "Conversation.ContextMenuAddFactCheck" = "Add Fact Check"; "Conversation.ContextMenuEditFactCheck" = "Edit Fact Check"; + +"Stars.Purchase.GetStars" = "Get Stars"; +"Stars.Purchase.GetStarsInfo" = "Choose how many Stars you would like to buy."; + +"Stars.Purchase.Balance" = "Balance"; + +"Stars.Purchase.StarsNeeded_1" = "%@ Star Needed"; +"Stars.Purchase.StarsNeeded_any" = "%@ Stars Needed"; +"Stars.Purchase.StarsNeededInfo" = "Buy Stars to use them on **%@** and other miniapps."; + +"Stars.Purchase.Stars_1" = "%@ Star"; +"Stars.Purchase.Stars_any" = "%@ Stars"; +"Stars.Purchase.ShowMore" = "Show More Options"; +"Stars.Purchase.Info" = "By proceeding and purchasing Stars, you agree with [Terms and Conditions]()."; +"Stars.Purchase.Terms_URL" = "https://telegram.org/tos"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 67a5a95cef..dbdee4588f 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1042,9 +1042,10 @@ public protocol SharedAccountContext: AnyObject { func makeStoryStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: EnginePeer.Id, storyId: Int32, storyItem: EngineStoryItem, fromStory: Bool) -> ViewController func makeStarsTransactionsScreen(context: AccountContext, starsContext: StarsContext) -> ViewController - func makeStarsPurchaseScreen(context: AccountContext, starsContext: StarsContext, options: [StarsTopUpOption], peerId: EnginePeer.Id?, requiredStars: Int32?) -> ViewController - func makeStarsTransferScreen(context: AccountContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>) -> ViewController - + func makeStarsPurchaseScreen(context: AccountContext, starsContext: StarsContext, options: [StarsTopUpOption], peerId: EnginePeer.Id?, requiredStars: Int64?, completion: @escaping (Int64) -> Void) -> ViewController + func makeStarsTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>) -> ViewController + func makeStarsTransactionScreen(context: AccountContext, transaction: StarsContext.State.Transaction) -> ViewController + func makeDebugSettingsController(context: AccountContext?) -> ViewController? func navigateToCurrentCall() diff --git a/submodules/Components/SheetComponent/Sources/SheetComponent.swift b/submodules/Components/SheetComponent/Sources/SheetComponent.swift index 2626cccf9d..7e6678290e 100644 --- a/submodules/Components/SheetComponent/Sources/SheetComponent.swift +++ b/submodules/Components/SheetComponent/Sources/SheetComponent.swift @@ -62,6 +62,7 @@ public final class SheetComponent: Component { public let content: AnyComponent public let backgroundColor: BackgroundColor public let followContentSizeChanges: Bool + public let clipsContent: Bool public let externalState: ExternalState? public let animateOut: ActionSlot> @@ -69,12 +70,14 @@ public final class SheetComponent: Component { content: AnyComponent, backgroundColor: BackgroundColor, followContentSizeChanges: Bool = false, + clipsContent: Bool = false, externalState: ExternalState? = nil, animateOut: ActionSlot> ) { self.content = content self.backgroundColor = backgroundColor self.followContentSizeChanges = followContentSizeChanges + self.clipsContent = clipsContent self.externalState = externalState self.animateOut = animateOut } @@ -349,6 +352,7 @@ public final class SheetComponent: Component { if contentView.superview == nil { self.scrollView.addSubview(contentView) } + contentView.clipsToBounds = component.clipsContent if sheetEnvironment.isCentered { let y: CGFloat = floorToScreenPixels((availableSize.height - contentSize.height) / 2.0) transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) / 2.0), y: -y), size: contentSize), completion: nil) diff --git a/submodules/Display/Source/TextAlertController.swift b/submodules/Display/Source/TextAlertController.swift index 54437bb0f6..252d5efc54 100644 --- a/submodules/Display/Source/TextAlertController.swift +++ b/submodules/Display/Source/TextAlertController.swift @@ -9,6 +9,7 @@ public enum TextAlertActionType { case genericAction case defaultAction case destructiveAction + case defaultDestructiveAction } public struct TextAlertAction { @@ -25,7 +26,11 @@ public struct TextAlertAction { public final class TextAlertContentActionNode: HighlightableButtonNode { private var theme: AlertControllerTheme - let action: TextAlertAction + public var action: TextAlertAction { + didSet { + self.updateTitle() + } + } private let backgroundNode: ASDisplayNode @@ -110,11 +115,11 @@ public final class TextAlertContentActionNode: HighlightableButtonNode { switch self.action.type { case .defaultAction, .genericAction: color = self.actionEnabled ? self.theme.accentColor : self.theme.disabledColor - case .destructiveAction: + case .destructiveAction, .defaultDestructiveAction: color = self.actionEnabled ? self.theme.destructiveColor : self.theme.disabledColor } switch self.action.type { - case .defaultAction: + case .defaultAction, .defaultDestructiveAction: font = Font.semibold(theme.baseFontSize) case .destructiveAction, .genericAction: break diff --git a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift index df878b2a41..91671972a0 100644 --- a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift @@ -1107,7 +1107,7 @@ private final class DemoSheetContent: CombinedComponent { id: "background", component: AnyComponent( BlurredBackgroundComponent( - color: UIColor(rgb: 0x888888, alpha: 0.1) + color: UIColor(rgb: 0x888888, alpha: 0.1) ) ) ), diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 8871eff5c1..43b2b98045 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -2018,7 +2018,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { backgroundColor: gradientColors[i], foregroundColor: .white, iconName: perk.iconName - )))), + ))), false), action: { [weak state] _ in var demoSubject: PremiumDemoScreen.Subject switch perk { @@ -2185,7 +2185,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { backgroundColor: gradientColors[min(i, gradientColors.count - 1)], foregroundColor: .white, iconName: perk.iconName - )))), + ))), false), action: { [weak state] _ in let isPremium = state?.isPremium == true if isPremium { @@ -2369,7 +2369,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { backgroundColor: UIColor(rgb: 0x676bff), foregroundColor: .white, iconName: "Premium/BusinessPerk/Status" - )))), + ))), false), icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent( context: context.component.context, color: accentColor, @@ -2410,7 +2410,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { backgroundColor: UIColor(rgb: 0x4492ff), foregroundColor: .white, iconName: "Premium/BusinessPerk/Tag" - )))), + ))), false), action: { _ in push(accountContext.sharedContext.makeFilterSettingsController(context: accountContext, modal: false, scrollToTags: true, dismissed: nil)) } @@ -2441,7 +2441,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { backgroundColor: UIColor(rgb: 0x41a6a5), foregroundColor: .white, iconName: "Premium/Perk/Stories" - )))), + ))), false), action: { _ in push(accountContext.sharedContext.makeMyStoriesController(context: accountContext, isArchive: false)) } diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 7bfe51dc5c..c008fca8c4 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -564,7 +564,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1205698681] = { return Api.MessageAction.parse_messageActionWebViewDataSentMe($0) } dict[546203849] = { return Api.MessageEntity.parse_inputMessageEntityMentionName($0) } dict[1981704948] = { return Api.MessageEntity.parse_messageEntityBankCard($0) } - dict[34469328] = { return Api.MessageEntity.parse_messageEntityBlockquote($0) } + dict[-238245204] = { return Api.MessageEntity.parse_messageEntityBlockquote($0) } dict[-1117713463] = { return Api.MessageEntity.parse_messageEntityBold($0) } dict[1827637959] = { return Api.MessageEntity.parse_messageEntityBotCommand($0) } dict[1280209983] = { return Api.MessageEntity.parse_messageEntityCashtag($0) } @@ -875,6 +875,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1269320843] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerAppStore($0) } dict[-382740222] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerFragment($0) } dict[2069236235] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerPlayMarket($0) } + dict[621656824] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerPremiumBot($0) } + dict[-1779253276] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerUnsupported($0) } dict[-884757282] = { return Api.StatsAbsValueAndPrev.parse_statsAbsValueAndPrev($0) } dict[-1237848657] = { return Api.StatsDateRangeDays.parse_statsDateRangeDays($0) } dict[-1901828938] = { return Api.StatsGraph.parse_statsGraph($0) } diff --git a/submodules/TelegramApi/Sources/Api15.swift b/submodules/TelegramApi/Sources/Api15.swift index f82c179069..3924397ce3 100644 --- a/submodules/TelegramApi/Sources/Api15.swift +++ b/submodules/TelegramApi/Sources/Api15.swift @@ -2,7 +2,7 @@ public extension Api { indirect enum MessageEntity: TypeConstructorDescription { case inputMessageEntityMentionName(offset: Int32, length: Int32, userId: Api.InputUser) case messageEntityBankCard(offset: Int32, length: Int32) - case messageEntityBlockquote(offset: Int32, length: Int32) + case messageEntityBlockquote(flags: Int32, offset: Int32, length: Int32) case messageEntityBold(offset: Int32, length: Int32) case messageEntityBotCommand(offset: Int32, length: Int32) case messageEntityCashtag(offset: Int32, length: Int32) @@ -39,10 +39,11 @@ public extension Api { serializeInt32(offset, buffer: buffer, boxed: false) serializeInt32(length, buffer: buffer, boxed: false) break - case .messageEntityBlockquote(let offset, let length): + case .messageEntityBlockquote(let flags, let offset, let length): if boxed { - buffer.appendInt32(34469328) + buffer.appendInt32(-238245204) } + serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(offset, buffer: buffer, boxed: false) serializeInt32(length, buffer: buffer, boxed: false) break @@ -185,8 +186,8 @@ public extension Api { return ("inputMessageEntityMentionName", [("offset", offset as Any), ("length", length as Any), ("userId", userId as Any)]) case .messageEntityBankCard(let offset, let length): return ("messageEntityBankCard", [("offset", offset as Any), ("length", length as Any)]) - case .messageEntityBlockquote(let offset, let length): - return ("messageEntityBlockquote", [("offset", offset as Any), ("length", length as Any)]) + case .messageEntityBlockquote(let flags, let offset, let length): + return ("messageEntityBlockquote", [("flags", flags as Any), ("offset", offset as Any), ("length", length as Any)]) case .messageEntityBold(let offset, let length): return ("messageEntityBold", [("offset", offset as Any), ("length", length as Any)]) case .messageEntityBotCommand(let offset, let length): @@ -264,10 +265,13 @@ public extension Api { _1 = reader.readInt32() var _2: Int32? _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() let _c1 = _1 != nil let _c2 = _2 != nil - if _c1 && _c2 { - return Api.MessageEntity.messageEntityBlockquote(offset: _1!, length: _2!) + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.MessageEntity.messageEntityBlockquote(flags: _1!, offset: _2!, length: _3!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api23.swift b/submodules/TelegramApi/Sources/Api23.swift index 9f3e4a40be..a5b2dfc96c 100644 --- a/submodules/TelegramApi/Sources/Api23.swift +++ b/submodules/TelegramApi/Sources/Api23.swift @@ -710,6 +710,8 @@ public extension Api { case starsTransactionPeerAppStore case starsTransactionPeerFragment case starsTransactionPeerPlayMarket + case starsTransactionPeerPremiumBot + case starsTransactionPeerUnsupported public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -736,6 +738,18 @@ public extension Api { buffer.appendInt32(2069236235) } + break + case .starsTransactionPeerPremiumBot: + if boxed { + buffer.appendInt32(621656824) + } + + break + case .starsTransactionPeerUnsupported: + if boxed { + buffer.appendInt32(-1779253276) + } + break } } @@ -750,6 +764,10 @@ public extension Api { return ("starsTransactionPeerFragment", []) case .starsTransactionPeerPlayMarket: return ("starsTransactionPeerPlayMarket", []) + case .starsTransactionPeerPremiumBot: + return ("starsTransactionPeerPremiumBot", []) + case .starsTransactionPeerUnsupported: + return ("starsTransactionPeerUnsupported", []) } } @@ -775,85 +793,11 @@ public extension Api { public static func parse_starsTransactionPeerPlayMarket(_ reader: BufferReader) -> StarsTransactionPeer? { return Api.StarsTransactionPeer.starsTransactionPeerPlayMarket } - - } -} -public extension Api { - enum StatsAbsValueAndPrev: TypeConstructorDescription { - case statsAbsValueAndPrev(current: Double, previous: Double) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .statsAbsValueAndPrev(let current, let previous): - if boxed { - buffer.appendInt32(-884757282) - } - serializeDouble(current, buffer: buffer, boxed: false) - serializeDouble(previous, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .statsAbsValueAndPrev(let current, let previous): - return ("statsAbsValueAndPrev", [("current", current as Any), ("previous", previous as Any)]) - } - } - - public static func parse_statsAbsValueAndPrev(_ reader: BufferReader) -> StatsAbsValueAndPrev? { - var _1: Double? - _1 = reader.readDouble() - var _2: Double? - _2 = reader.readDouble() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.StatsAbsValueAndPrev.statsAbsValueAndPrev(current: _1!, previous: _2!) - } - else { - return nil - } - } - - } -} -public extension Api { - enum StatsDateRangeDays: TypeConstructorDescription { - case statsDateRangeDays(minDate: Int32, maxDate: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .statsDateRangeDays(let minDate, let maxDate): - if boxed { - buffer.appendInt32(-1237848657) - } - serializeInt32(minDate, buffer: buffer, boxed: false) - serializeInt32(maxDate, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .statsDateRangeDays(let minDate, let maxDate): - return ("statsDateRangeDays", [("minDate", minDate as Any), ("maxDate", maxDate as Any)]) - } - } - - public static func parse_statsDateRangeDays(_ reader: BufferReader) -> StatsDateRangeDays? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.StatsDateRangeDays.statsDateRangeDays(minDate: _1!, maxDate: _2!) - } - else { - return nil - } + public static func parse_starsTransactionPeerPremiumBot(_ reader: BufferReader) -> StarsTransactionPeer? { + return Api.StarsTransactionPeer.starsTransactionPeerPremiumBot + } + public static func parse_starsTransactionPeerUnsupported(_ reader: BufferReader) -> StarsTransactionPeer? { + return Api.StarsTransactionPeer.starsTransactionPeerUnsupported } } diff --git a/submodules/TelegramApi/Sources/Api24.swift b/submodules/TelegramApi/Sources/Api24.swift index 51256e9e02..f42e53ce09 100644 --- a/submodules/TelegramApi/Sources/Api24.swift +++ b/submodules/TelegramApi/Sources/Api24.swift @@ -1,3 +1,83 @@ +public extension Api { + enum StatsAbsValueAndPrev: TypeConstructorDescription { + case statsAbsValueAndPrev(current: Double, previous: Double) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .statsAbsValueAndPrev(let current, let previous): + if boxed { + buffer.appendInt32(-884757282) + } + serializeDouble(current, buffer: buffer, boxed: false) + serializeDouble(previous, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .statsAbsValueAndPrev(let current, let previous): + return ("statsAbsValueAndPrev", [("current", current as Any), ("previous", previous as Any)]) + } + } + + public static func parse_statsAbsValueAndPrev(_ reader: BufferReader) -> StatsAbsValueAndPrev? { + var _1: Double? + _1 = reader.readDouble() + var _2: Double? + _2 = reader.readDouble() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.StatsAbsValueAndPrev.statsAbsValueAndPrev(current: _1!, previous: _2!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum StatsDateRangeDays: TypeConstructorDescription { + case statsDateRangeDays(minDate: Int32, maxDate: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .statsDateRangeDays(let minDate, let maxDate): + if boxed { + buffer.appendInt32(-1237848657) + } + serializeInt32(minDate, buffer: buffer, boxed: false) + serializeInt32(maxDate, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .statsDateRangeDays(let minDate, let maxDate): + return ("statsDateRangeDays", [("minDate", minDate as Any), ("maxDate", maxDate as Any)]) + } + } + + public static func parse_statsDateRangeDays(_ reader: BufferReader) -> StatsDateRangeDays? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.StatsDateRangeDays.statsDateRangeDays(minDate: _1!, maxDate: _2!) + } + else { + return nil + } + } + + } +} public extension Api { enum StatsGraph: TypeConstructorDescription { case statsGraph(flags: Int32, json: Api.DataJSON, zoomToken: String?) @@ -1340,89 +1420,3 @@ public extension Api { } } -public extension Api { - enum Timezone: TypeConstructorDescription { - case timezone(id: String, name: String, utcOffset: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .timezone(let id, let name, let utcOffset): - if boxed { - buffer.appendInt32(-7173643) - } - serializeString(id, buffer: buffer, boxed: false) - serializeString(name, buffer: buffer, boxed: false) - serializeInt32(utcOffset, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .timezone(let id, let name, let utcOffset): - return ("timezone", [("id", id as Any), ("name", name as Any), ("utcOffset", utcOffset as Any)]) - } - } - - public static func parse_timezone(_ reader: BufferReader) -> Timezone? { - var _1: String? - _1 = parseString(reader) - var _2: String? - _2 = parseString(reader) - var _3: Int32? - _3 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.Timezone.timezone(id: _1!, name: _2!, utcOffset: _3!) - } - else { - return nil - } - } - - } -} -public extension Api { - enum TopPeer: TypeConstructorDescription { - case topPeer(peer: Api.Peer, rating: Double) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .topPeer(let peer, let rating): - if boxed { - buffer.appendInt32(-305282981) - } - peer.serialize(buffer, true) - serializeDouble(rating, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .topPeer(let peer, let rating): - return ("topPeer", [("peer", peer as Any), ("rating", rating as Any)]) - } - } - - public static func parse_topPeer(_ reader: BufferReader) -> TopPeer? { - var _1: Api.Peer? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.Peer - } - var _2: Double? - _2 = reader.readDouble() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.TopPeer.topPeer(peer: _1!, rating: _2!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api25.swift b/submodules/TelegramApi/Sources/Api25.swift index 283d8da677..ffad9c1ca7 100644 --- a/submodules/TelegramApi/Sources/Api25.swift +++ b/submodules/TelegramApi/Sources/Api25.swift @@ -1,3 +1,89 @@ +public extension Api { + enum Timezone: TypeConstructorDescription { + case timezone(id: String, name: String, utcOffset: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .timezone(let id, let name, let utcOffset): + if boxed { + buffer.appendInt32(-7173643) + } + serializeString(id, buffer: buffer, boxed: false) + serializeString(name, buffer: buffer, boxed: false) + serializeInt32(utcOffset, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .timezone(let id, let name, let utcOffset): + return ("timezone", [("id", id as Any), ("name", name as Any), ("utcOffset", utcOffset as Any)]) + } + } + + public static func parse_timezone(_ reader: BufferReader) -> Timezone? { + var _1: String? + _1 = parseString(reader) + var _2: String? + _2 = parseString(reader) + var _3: Int32? + _3 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.Timezone.timezone(id: _1!, name: _2!, utcOffset: _3!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum TopPeer: TypeConstructorDescription { + case topPeer(peer: Api.Peer, rating: Double) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .topPeer(let peer, let rating): + if boxed { + buffer.appendInt32(-305282981) + } + peer.serialize(buffer, true) + serializeDouble(rating, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .topPeer(let peer, let rating): + return ("topPeer", [("peer", peer as Any), ("rating", rating as Any)]) + } + } + + public static func parse_topPeer(_ reader: BufferReader) -> TopPeer? { + var _1: Api.Peer? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.Peer + } + var _2: Double? + _2 = reader.readDouble() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.TopPeer.topPeer(peer: _1!, rating: _2!) + } + else { + return nil + } + } + + } +} public extension Api { enum TopPeerCategory: TypeConstructorDescription { case topPeerCategoryBotsInline diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index 11cbedfba1..49d0d174f8 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -593,7 +593,7 @@ func messageTextEntitiesFromApiEntities(_ entities: [Api.MessageEntity]) -> [Mes result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Underline)) case let .messageEntityStrike(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Strikethrough)) - case let .messageEntityBlockquote(offset, length): + case let .messageEntityBlockquote(_, offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .BlockQuote)) case let .messageEntityBankCard(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .BankCard)) diff --git a/submodules/TelegramCore/Sources/ApiUtils/TextEntitiesMessageAttribute.swift b/submodules/TelegramCore/Sources/ApiUtils/TextEntitiesMessageAttribute.swift index 15e1f0d005..391c2d3bec 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TextEntitiesMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TextEntitiesMessageAttribute.swift @@ -41,7 +41,7 @@ func apiEntitiesFromMessageTextEntities(_ entities: [MessageTextEntity], associa case .Strikethrough: apiEntities.append(.messageEntityStrike(offset: offset, length: length)) case .BlockQuote: - apiEntities.append(.messageEntityBlockquote(offset: offset, length: length)) + apiEntities.append(.messageEntityBlockquote(flags: 0, offset: offset, length: length)) case .Underline: apiEntities.append(.messageEntityUnderline(offset: offset, length: length)) case .BankCard: diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index be4b0354af..49b340a520 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -3872,6 +3872,13 @@ func replayFinalState( } } + if let previousFactCheckAttribute = previousMessage.attributes.first(where: { $0 is FactCheckMessageAttribute }) as? FactCheckMessageAttribute, let updatedFactCheckAttribute = message.attributes.first(where: { $0 is FactCheckMessageAttribute }) as? FactCheckMessageAttribute { + if case .Pending = updatedFactCheckAttribute.content, updatedFactCheckAttribute.hash == previousFactCheckAttribute.hash { + updatedAttributes.removeAll(where: { $0 is FactCheckMessageAttribute }) + updatedAttributes.append(previousFactCheckAttribute) + } + } + if let message = locallyRenderedMessage(message: message, peers: peers) { generatedEvent = reactionGeneratedEvent(previousMessage.reactionsAttribute, message.reactionsAttribute, message: message, transaction: transaction) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index 7eb372692b..710a63d477 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -225,6 +225,10 @@ private extension StarsContext.State.Transaction { parsedPeer = .playMarket case .starsTransactionPeerFragment: parsedPeer = .fragment + case .starsTransactionPeerPremiumBot: + parsedPeer = .premiumBot + case .starsTransactionPeerUnsupported: + parsedPeer = .unsupported case let .starsTransactionPeer(apiPeer): guard let peer = transaction.getPeer(apiPeer.peerId) else { return nil @@ -243,6 +247,8 @@ public final class StarsContext { case appStore case playMarket case fragment + case premiumBot + case unsupported case peer(EnginePeer) } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift index 7bc7974568..c5b5b9efad 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift @@ -313,12 +313,10 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode let titleBadgeString = NSAttributedString(string: item.presentationData.strings.Message_FactCheck_WhatIsThis, font: badgeFont, textColor: mainColor) let (titleBadgeLayout, titleBadgeApply) = titleBadgeLayout(TextNodeLayoutArguments(attributedString: titleBadgeString, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize)) - var finalAttributedText = attributedText - if "".isEmpty { - finalAttributedText = stringWithAppliedEntities(rawText + "\u{00A0}\u{00A0}\u{00A0}", entities: rawEntities, baseColor: messageTheme.primaryTextColor, linkColor: messageTheme.linkTextColor, baseFont: textFont, linkFont: textFont, boldFont: textBoldFont, italicFont: textItalicFont, boldItalicFont: textBoldItalicFont, fixedFont: textFixedFont, blockQuoteFont: textBlockQuoteFont, message: nil) - } + let finalAttributedText = stringWithAppliedEntities(rawText, entities: rawEntities, baseColor: messageTheme.primaryTextColor, linkColor: messageTheme.linkTextColor, baseFont: textFont, linkFont: textFont, boldFont: textBoldFont, italicFont: textItalicFont, boldItalicFont: textBoldItalicFont, fixedFont: textFixedFont, blockQuoteFont: textBlockQuoteFont, message: nil) as! NSMutableAttributedString + finalAttributedText.append(NSAttributedString(string: "__", font: textFont, textColor: .clear)) - let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: finalAttributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets, lineColor: messageTheme.accentControlColor, customTruncationToken: NSAttributedString(string: "", font: textFont, textColor: .clear))) + let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: finalAttributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets, lineColor: messageTheme.accentControlColor)) var canExpand = false var clippedTextHeight: CGFloat = textLayout.size.height @@ -336,6 +334,7 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode var titleFrameWithoutInsets = CGRect(origin: CGPoint(x: titleFrame.origin.x + textInsets.left, y: titleFrame.origin.y + textInsets.top), size: CGSize(width: titleFrame.width - textInsets.left - textInsets.right, height: titleFrame.height - textInsets.top - textInsets.bottom)) titleFrameWithoutInsets = titleFrameWithoutInsets.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top) + let topInset: CGFloat = 5.0 let textSpacing: CGFloat = 3.0 let textFrame = CGRect(origin: CGPoint(x: titleFrame.origin.x, y: -textInsets.top + titleFrameWithoutInsets.height + textSpacing), size: textLayout.size) @@ -386,7 +385,7 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode let statusSizeAndApply = statusSuggestedWidthAndContinue?.1(boundingWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right) - boundingSize = CGSize(width: boundingWidth, height: titleFrameWithoutInsets.height + textFrameWithoutInsets.size.height + textSpacing) + boundingSize = CGSize(width: boundingWidth, height: topInset + titleFrameWithoutInsets.height + textFrameWithoutInsets.size.height + textSpacing) if let statusSizeAndApply = statusSizeAndApply { boundingSize.height += statusSizeAndApply.0.height } @@ -460,13 +459,13 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode } let _ = titleApply() - strongSelf.titleNode.frame = titleFrame + strongSelf.titleNode.frame = titleFrame.offsetBy(dx: 0.0, dy: topInset) let _ = titleBadgeApply() let _ = textApply() strongSelf.textNode.frame = CGRect(origin: .zero, size: textFrame.size) - var clippingTextFrame = textFrame + var clippingTextFrame = textFrame.offsetBy(dx: 0.0, dy: topInset) clippingTextFrame.size.height = clippedTextHeight - 3.0 if canExpand { @@ -523,7 +522,7 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode titleLineWidth = titleFrame.width } - let titleBadgeFrame = CGRect(origin: CGPoint(x: titleFrame.minX + titleLineWidth + titleBadgeSpacing + titleBadgePadding, y: floorToScreenPixels(titleFrame.midY - titleBadgeLayout.size.height / 2.0) - 1.0), size: titleBadgeLayout.size) + let titleBadgeFrame = CGRect(origin: CGPoint(x: titleFrame.minX + titleLineWidth + titleBadgeSpacing + titleBadgePadding, y: topInset + floorToScreenPixels(titleFrame.midY - titleBadgeLayout.size.height / 2.0) - 1.0), size: titleBadgeLayout.size) let badgeBackgroundFrame = titleBadgeFrame.insetBy(dx: -titleBadgePadding, dy: -1.0 + UIScreenPixel) strongSelf.titleBadgeLabel.frame = titleBadgeFrame @@ -560,7 +559,7 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode titleBadgeButton.setBackgroundImage(generateFilledCircleImage(diameter: badgeBackgroundFrame.height, color: mainColor.withMultipliedAlpha(0.1))?.stretchableImage(withLeftCapWidth: Int(badgeBackgroundFrame.height / 2), topCapHeight: Int(badgeBackgroundFrame.height / 2)), for: .normal) } - let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: boundingWidth - backgroundInsets.left - backgroundInsets.right, height: titleFrameWithoutInsets.height + textSpacing + textFrameWithoutInsets.height + textSpacing)) + let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top + topInset), size: CGSize(width: boundingWidth - backgroundInsets.left - backgroundInsets.right, height: titleFrameWithoutInsets.height + textSpacing + textFrameWithoutInsets.height + textSpacing)) if isFirstTime { strongSelf.textClippingNode.frame = clippingTextFrame @@ -595,7 +594,7 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode item.controllerInteraction.openMessageReactionContextMenu(item.topMessage, sourceNode, gesture, value) } - let statusFrame = CGRect(origin: CGPoint(x: boundingWidth - layoutConstants.text.bubbleInsets.right - statusSizeAndApply.0.width, y: textFrameWithoutInsets.maxY), size: statusSizeAndApply.0) + let statusFrame = CGRect(origin: CGPoint(x: boundingWidth - layoutConstants.text.bubbleInsets.right - statusSizeAndApply.0.width, y: topInset + textFrameWithoutInsets.maxY), size: statusSizeAndApply.0) if isFirstTime { strongSelf.statusNode.frame = statusFrame } else { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInvoiceBubbleContentNode/Sources/ChatMessageInvoiceBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInvoiceBubbleContentNode/Sources/ChatMessageInvoiceBubbleContentNode.swift index ab5480aea8..02855854c9 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInvoiceBubbleContentNode/Sources/ChatMessageInvoiceBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInvoiceBubbleContentNode/Sources/ChatMessageInvoiceBubbleContentNode.swift @@ -62,7 +62,7 @@ public final class ChatMessageInvoiceBubbleContentNode: ChatMessageBubbleContent if let image = invoice.photo { automaticDownloadSettings = MediaAutoDownloadSettings.defaultSettings mediaAndFlags = ([image], [.preferMediaBeforeText]) - } else { + } else if invoice.currency != "XTR" { let invoiceLabel = item.presentationData.strings.Message_InvoiceLabel var invoiceText = "\(formatCurrencyAmount(invoice.totalAmount, currency: invoice.currency)) " invoiceText += invoiceLabel diff --git a/submodules/TelegramUI/Components/Chat/FactCheckAlertController/Sources/FactCheckAlertController.swift b/submodules/TelegramUI/Components/Chat/FactCheckAlertController/Sources/FactCheckAlertController.swift index 758560fd94..023ad26a55 100644 --- a/submodules/TelegramUI/Components/Chat/FactCheckAlertController/Sources/FactCheckAlertController.swift +++ b/submodules/TelegramUI/Components/Chat/FactCheckAlertController/Sources/FactCheckAlertController.swift @@ -20,6 +20,7 @@ private final class FactCheckAlertContentNode: AlertContentNode { private var presentationTheme: PresentationTheme private let strings: PresentationStrings private let text: String + private let initialValue: String private let titleView = ComponentView() @@ -58,6 +59,7 @@ private final class FactCheckAlertContentNode: AlertContentNode { self.presentationTheme = presentationTheme self.strings = strings self.text = text + self.initialValue = value if !value.isEmpty { self.inputFieldExternalState.initialText = chatInputStateStringWithAppliedEntities(value, entities: entities) @@ -271,7 +273,23 @@ private final class FactCheckAlertContentNode: AlertContentNode { } if let lastActionNode = self.actionNodes.last { - lastActionNode.actionEnabled = self.inputFieldExternalState.hasText + if self.initialValue.isEmpty { + lastActionNode.actionEnabled = self.inputFieldExternalState.hasText + } else { + if self.inputFieldExternalState.hasText { + lastActionNode.action = TextAlertAction( + type: .defaultAction, + title: self.strings.Common_Done, + action: lastActionNode.action.action + ) + } else { + lastActionNode.action = TextAlertAction( + type: .defaultDestructiveAction, + title: self.strings.FactCheck_Remove, + action: lastActionNode.action.action + ) + } + } } let resultSize = CGSize(width: resultWidth, height: titleSize.height + spacing + inputFieldSize.height + 17.0 + actionsHeight + insets.top + insets.bottom) diff --git a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift index 2e45b46a04..12ca89591c 100644 --- a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift +++ b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift @@ -105,7 +105,7 @@ public final class ListActionItemComponent: Component { } case check(Check) - case custom(AnyComponentWithIdentity) + case custom(AnyComponentWithIdentity, Bool) } public enum Highlighting { @@ -119,6 +119,7 @@ public final class ListActionItemComponent: Component { } public let theme: PresentationTheme + public let background: AnyComponent? public let title: AnyComponent public let titleAlignment: Alignment public let contentInsets: UIEdgeInsets @@ -127,9 +128,11 @@ public final class ListActionItemComponent: Component { public let accessory: Accessory? public let action: ((UIView) -> Void)? public let highlighting: Highlighting + public let updateIsHighlighted: ((UIView, Bool) -> Void)? public init( theme: PresentationTheme, + background: AnyComponent? = nil, title: AnyComponent, titleAlignment: Alignment = .default, contentInsets: UIEdgeInsets = UIEdgeInsets(top: 12.0, left: 0.0, bottom: 12.0, right: 0.0), @@ -137,9 +140,11 @@ public final class ListActionItemComponent: Component { icon: Icon? = nil, accessory: Accessory? = .arrow, action: ((UIView) -> Void)?, - highlighting: Highlighting = .default + highlighting: Highlighting = .default, + updateIsHighlighted: ((UIView, Bool) -> Void)? = nil ) { self.theme = theme + self.background = background self.title = title self.titleAlignment = titleAlignment self.contentInsets = contentInsets @@ -148,12 +153,16 @@ public final class ListActionItemComponent: Component { self.accessory = accessory self.action = action self.highlighting = highlighting + self.updateIsHighlighted = updateIsHighlighted } public static func ==(lhs: ListActionItemComponent, rhs: ListActionItemComponent) -> Bool { if lhs.theme !== rhs.theme { return false } + if lhs.background != rhs.background { + return false + } if lhs.title != rhs.title { return false } @@ -255,6 +264,7 @@ public final class ListActionItemComponent: Component { } public final class View: HighlightTrackingButton, ListSectionComponent.ChildView { + private var background: ComponentView? private let title = ComponentView() private var leftIcon: ComponentView? private var leftCheckView: CheckView? @@ -287,6 +297,7 @@ public final class ListActionItemComponent: Component { guard let self, let component = self.component, component.action != nil else { return } + component.updateIsHighlighted?(self, isHighlighted) if isHighlighted, component.highlighting == .disabled { return } @@ -389,14 +400,8 @@ public final class ListActionItemComponent: Component { contentLeftInset = floor((availableSize.width - titleSize.width) / 2.0) } - let titleFrame = CGRect(origin: CGPoint(x: contentLeftInset, y: contentHeight), size: titleSize) - if let titleView = self.title.view { - if titleView.superview == nil { - titleView.isUserInteractionEnabled = false - self.addSubview(titleView) - } - transition.setFrame(view: titleView, frame: titleFrame) - } + + let titleY = contentHeight contentHeight += titleSize.height contentHeight += component.contentInsets.bottom @@ -500,9 +505,9 @@ public final class ListActionItemComponent: Component { transition.setBounds(view: leftCheckView, bounds: CGRect(origin: CGPoint(), size: checkFrame.size)) leftCheckView.update(size: checkFrame.size, theme: component.theme, isSelected: check.isSelected, transition: transition) } - case let .custom(customLeftIcon): + case let .custom(customLeftIcon, adjustLeftInset): var resetLeftIcon = false - if case let .custom(previousCustomLeftIcon) = previousComponent?.leftIcon { + if case let .custom(previousCustomLeftIcon, _) = previousComponent?.leftIcon { if previousCustomLeftIcon.id != customLeftIcon.id { resetLeftIcon = true } @@ -542,7 +547,13 @@ public final class ListActionItemComponent: Component { environment: {}, containerSize: CGSize(width: availableSize.width, height: availableSize.height) ) - let leftIconFrame = CGRect(origin: CGPoint(x: floor((contentLeftInset - leftIconSize.width) * 0.5), y: floor((min(60.0, contentHeight) - leftIconSize.height) * 0.5)), size: leftIconSize) + let leftIconX: CGFloat + if adjustLeftInset { + leftIconX = 15.0 + } else { + leftIconX = floor((contentLeftInset - leftIconSize.width) * 0.5) + } + let leftIconFrame = CGRect(origin: CGPoint(x: leftIconX, y: floor((min(60.0, contentHeight) - leftIconSize.height) * 0.5)), size: leftIconSize) if let leftIconView = leftIcon.view { if leftIconView.superview == nil { leftIconView.isUserInteractionEnabled = false @@ -551,6 +562,9 @@ public final class ListActionItemComponent: Component { } leftIconTransition.setFrame(view: leftIconView, frame: leftIconFrame) } + if adjustLeftInset { + contentLeftInset = 22.0 + leftIconSize.width + } } } else { if let leftIcon = self.leftIcon { @@ -738,8 +752,54 @@ public final class ListActionItemComponent: Component { } } + let titleFrame = CGRect(origin: CGPoint(x: contentLeftInset, y: titleY), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + self.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: titleFrame) + } + self.separatorInset = contentLeftInset + if let backgroundComponent = component.background { + var backgroundTransition = transition + let background: ComponentView + if let current = self.background { + background = current + } else { + backgroundTransition = backgroundTransition.withAnimation(.none) + background = ComponentView() + self.background = background + } + + let backgroundSize = background.update( + transition: backgroundTransition, + component: backgroundComponent, + environment: {}, + containerSize: CGSize(width: availableSize.width, height: contentHeight) + ) + let backgroundFrame = CGRect(origin: .zero, size: backgroundSize) + if let backgroundView = background.view { + if backgroundView.superview == nil { + backgroundView.isUserInteractionEnabled = false + self.addSubview(backgroundView) + transition.animateAlpha(view: backgroundView, from: 0.0, to: 1.0) + } + backgroundTransition.setFrame(view: backgroundView, frame: backgroundFrame) + } + } else { + if let background = self.background { + self.background = nil + if let backgroundView = background.view { + transition.setAlpha(view: backgroundView, alpha: 0.0, completion: { [weak backgroundView] _ in + backgroundView?.removeFromSuperview() + }) + } + } + } + return CGSize(width: availableSize.width, height: contentHeight) } } diff --git a/submodules/TelegramUI/Components/Premium/PremiumStarComponent/Sources/GiftAvatarComponent.swift b/submodules/TelegramUI/Components/Premium/PremiumStarComponent/Sources/GiftAvatarComponent.swift index 60e8191424..2890dcbd85 100644 --- a/submodules/TelegramUI/Components/Premium/PremiumStarComponent/Sources/GiftAvatarComponent.swift +++ b/submodules/TelegramUI/Components/Premium/PremiumStarComponent/Sources/GiftAvatarComponent.swift @@ -20,25 +20,29 @@ public final class GiftAvatarComponent: Component { let context: AccountContext let theme: PresentationTheme let peers: [EnginePeer] + let photo: TelegramMediaWebFile? let isVisible: Bool let hasIdleAnimations: Bool let hasScaleAnimation: Bool + let avatarSize: CGFloat let color: UIColor? let offset: CGFloat? - public init(context: AccountContext, theme: PresentationTheme, peers: [EnginePeer], isVisible: Bool, hasIdleAnimations: Bool, hasScaleAnimation: Bool = true, color: UIColor? = nil, offset: CGFloat? = nil) { + public init(context: AccountContext, theme: PresentationTheme, peers: [EnginePeer], photo: TelegramMediaWebFile? = nil, isVisible: Bool, hasIdleAnimations: Bool, hasScaleAnimation: Bool = true, avatarSize: CGFloat = 100.0, color: UIColor? = nil, offset: CGFloat? = nil) { self.context = context self.theme = theme self.peers = peers + self.photo = photo self.isVisible = isVisible self.hasIdleAnimations = hasIdleAnimations self.hasScaleAnimation = hasScaleAnimation + self.avatarSize = avatarSize self.color = color self.offset = offset } public static func ==(lhs: GiftAvatarComponent, rhs: GiftAvatarComponent) -> Bool { - return lhs.peers == rhs.peers && lhs.theme === rhs.theme && lhs.isVisible == rhs.isVisible && lhs.hasIdleAnimations == rhs.hasIdleAnimations && lhs.hasScaleAnimation == rhs.hasScaleAnimation && lhs.offset == rhs.offset + return lhs.peers == rhs.peers && lhs.photo == rhs.photo && lhs.theme === rhs.theme && lhs.isVisible == rhs.isVisible && lhs.hasIdleAnimations == rhs.hasIdleAnimations && lhs.hasScaleAnimation == rhs.hasScaleAnimation && lhs.avatarSize == rhs.avatarSize && lhs.offset == rhs.offset } public final class View: UIView, SCNSceneRendererDelegate, ComponentTaggedView { @@ -316,7 +320,7 @@ public final class GiftAvatarComponent: Component { self.mergedAvatarsNode = nil self.avatarNode.isHidden = false - let avatarSize = CGSize(width: 100.0, height: 100.0) + let avatarSize = CGSize(width: component.avatarSize, height: component.avatarSize) if let peer = component.peers.first { self.avatarNode.setSignal(peerAvatarCompleteImage(account: component.context.account, peer: peer, size: avatarSize, font: avatarPlaceholderFont(size: 43.0), fullSize: true)) } diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift index 2db55b4b3b..44d712798c 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift @@ -856,7 +856,7 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component { leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent( name: "Chat List/ComposeIcon", tintColor: environment.theme.list.itemAccentColor - )))), + ))), false), accessory: nil, action: { [weak self] _ in guard let self else { @@ -933,7 +933,7 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component { image: checkIcon, tintColor: !isSelected ? .clear : environment.theme.list.itemAccentColor, contentMode: .center - )))), + ))), false), accessory: nil, action: { [weak self] _ in guard let self else { @@ -1203,7 +1203,7 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component { image: checkIcon, tintColor: !self.hasAccessToAllChatsByDefault ? .clear : environment.theme.list.itemAccentColor, contentMode: .center - )))), + ))), false), accessory: nil, action: { [weak self] _ in guard let self else { @@ -1233,7 +1233,7 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component { image: checkIcon, tintColor: self.hasAccessToAllChatsByDefault ? .clear : environment.theme.list.itemAccentColor, contentMode: .center - )))), + ))), false), accessory: nil, action: { [weak self] _ in guard let self else { @@ -1280,7 +1280,7 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component { leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent( name: "Chat List/AddIcon", tintColor: environment.theme.list.itemAccentColor - )))), + ))), false), accessory: nil, action: { [weak self] _ in guard let self else { diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinksSetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinksSetupScreen.swift index 83f55acc75..86ea07efa8 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinksSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinksSetupScreen.swift @@ -416,7 +416,7 @@ final class BusinessLinksSetupScreenComponent: Component { leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent( name: "Item List/AddLinkIcon", tintColor: environment.theme.list.itemAccentColor - )))), + ))), false), accessory: nil, action: { [weak self] _ in guard let self else { diff --git a/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessDaySetupScreen.swift b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessDaySetupScreen.swift index c258c60fb4..35689013c9 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessDaySetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessDaySetupScreen.swift @@ -531,7 +531,7 @@ final class BusinessDaySetupScreenComponent: Component { leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent( name: "Item List/AddTimeIcon", tintColor: environment.theme.list.itemAccentColor - )))), + ))), false), accessory: nil, action: { [weak self] _ in guard let self else { diff --git a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/BusinessRecipientListScreen.swift b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/BusinessRecipientListScreen.swift index cd44260599..7f82689d75 100644 --- a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/BusinessRecipientListScreen.swift +++ b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/BusinessRecipientListScreen.swift @@ -425,7 +425,7 @@ final class BusinessRecipientListScreenComponent: Component { leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent( name: "Chat List/AddIcon", tintColor: environment.theme.list.itemAccentColor - )))), + ))), false), accessory: nil, action: { [weak self] _ in guard let self else { diff --git a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift index 7709c95048..4571ab2149 100644 --- a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift @@ -774,7 +774,7 @@ final class ChatbotSetupScreenComponent: Component { image: checkIcon, tintColor: !self.hasAccessToAllChatsByDefault ? .clear : environment.theme.list.itemAccentColor, contentMode: .center - )))), + ))), false), accessory: nil, action: { [weak self] _ in guard let self else { @@ -804,7 +804,7 @@ final class ChatbotSetupScreenComponent: Component { image: checkIcon, tintColor: self.hasAccessToAllChatsByDefault ? .clear : environment.theme.list.itemAccentColor, contentMode: .center - )))), + ))), false), accessory: nil, action: { [weak self] _ in guard let self else { diff --git a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/BUILD b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/BUILD index 91f04b157a..dd5b8cbbb6 100644 --- a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/BUILD +++ b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/BUILD @@ -32,6 +32,7 @@ swift_library( "//submodules/TelegramUI/Components/ListSectionComponent", "//submodules/TelegramUI/Components/ListActionItemComponent", "//submodules/TelegramUI/Components/ScrollComponent", + "//submodules/TelegramUI/Components/TextLoadingEffect", "//submodules/TelegramUI/Components/Premium/PremiumStarComponent", "//submodules/Components/BlurredBackgroundComponent", "//submodules/Components/BundleIconComponent", diff --git a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/ItemLoadingComponent.swift b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/ItemLoadingComponent.swift new file mode 100644 index 0000000000..24905d0bf9 --- /dev/null +++ b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/ItemLoadingComponent.swift @@ -0,0 +1,92 @@ +import Foundation +import UIKit +import Display +import AppBundle +import HierarchyTrackingLayer +import ComponentFlow +import TextLoadingEffect + +final class ItemLoadingComponent: Component { + private let color: UIColor + + public init(color: UIColor) { + self.color = color + } + + public static func ==(lhs: ItemLoadingComponent, rhs: ItemLoadingComponent) -> Bool { + if !lhs.color.isEqual(rhs.color) { + return false + } + return true + } + + public final class View: UIView { + private let loadingView = TextLoadingEffectView() + private let borderView = UIImageView() + + private let borderMaskView = UIView() + private let borderMaskGradientView = UIImageView() + private let borderMaskFillView = UIImageView() + + private var component: ItemLoadingComponent? + + override public init(frame: CGRect) { + super.init(frame: frame) + + self.addSubview(self.loadingView) + self.addSubview(self.borderView) + + self.borderView.image = generateFilledRoundedRectImage(size: CGSize(width: 24.0, height: 24.0), cornerRadius: 10.0, color: nil, strokeColor: .white, strokeWidth: 1.0 + UIScreenPixel, backgroundColor: nil)?.stretchableImage(withLeftCapWidth: 10, topCapHeight: 10).withRenderingMode(.alwaysTemplate) + + self.borderMaskView.backgroundColor = .clear + self.borderMaskFillView.backgroundColor = .white + + self.borderMaskView.addSubview(self.borderMaskFillView) + self.borderMaskFillView.addSubview(self.borderMaskGradientView) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func playAppearanceAnimation() { + self.borderView.mask = self.borderMaskView + + let gradientWidth = self.borderView.bounds.width * 0.4 + self.borderMaskGradientView.image = generateGradientImage(size: CGSize(width: gradientWidth, height: 24.0), colors: [UIColor.white, UIColor.white.withAlphaComponent(0.0)], locations: [0.0, 1.0], direction: .horizontal) + + self.borderMaskGradientView.frame = CGRect(origin: CGPoint(x: self.borderView.bounds.width, y: 0.0), size: CGSize(width: gradientWidth, height: self.borderView.bounds.height)) + self.borderMaskFillView.frame = CGRect(origin: .zero, size: self.borderView.bounds.size) + + self.borderMaskFillView.layer.animatePosition(from: CGPoint(x: -self.borderView.bounds.width, y: 0.0), to: .zero, duration: 1.0, removeOnCompletion: false, additive: true, completion: { _ in + self.borderView.mask = nil + }) + } + + func update(component: ItemLoadingComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let isFirstTime = self.component == nil + + self.component = component + + self.borderView.tintColor = component.color + + self.loadingView.update(color: component.color, rect: CGRect(origin: .zero, size: availableSize)) + transition.setFrame(view: self.borderView, frame: CGRect(origin: .zero, size: availableSize)) + self.borderMaskView.frame = self.borderView.bounds + + if isFirstTime { + self.playAppearanceAnimation() + } + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift index a4791b830e..ecddb0f63b 100644 --- a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift @@ -45,7 +45,8 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { let context: AccountContext let options: [StarsTopUpOption] let peerId: EnginePeer.Id? - let requiredStars: Int32? + let requiredStars: Int64? + let selectedProductId: String? let forceDark: Bool let products: [StarsProduct]? let expanded: Bool @@ -56,7 +57,8 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { context: AccountContext, options: [StarsTopUpOption], peerId: EnginePeer.Id?, - requiredStars: Int32?, + requiredStars: Int64?, + selectedProductId: String?, forceDark: Bool, products: [StarsProduct]?, expanded: Bool, @@ -67,6 +69,7 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { self.options = options self.peerId = peerId self.requiredStars = requiredStars + self.selectedProductId = selectedProductId self.forceDark = forceDark self.products = products self.expanded = expanded @@ -87,6 +90,9 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { if lhs.requiredStars != rhs.requiredStars { return false } + if lhs.selectedProductId != rhs.selectedProductId { + return false + } if lhs.forceDark != rhs.forceDark { return false } @@ -157,6 +163,7 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { state.products = context.component.products let theme = environment.theme + let strings = environment.strings let presentationData = context.component.context.sharedContext.currentPresentationData.with { $0 } let availableWidth = context.availableSize.width @@ -202,13 +209,12 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { let textFont = Font.regular(15.0) let boldTextFont = Font.semibold(15.0) - //TODO:localize let textString: String -// if let peer = state.peer, let requiredStars = context.component.requiredStars { -// textString = "\(peer.compactDisplayTitle) requests \(requiredStars) Stars.\n\nAvailable balance: **1000 Stars**.\n\nBuy **Stars** to unlock **content and services** in miniapps on **Telegram**." -// } else { - textString = "Choose how many Stars you would like to buy." -// } + if let _ = context.component.requiredStars { + textString = strings.Stars_Purchase_StarsNeededInfo(state.peer?.compactDisplayTitle ?? "").string + } else { + textString = strings.Stars_Purchase_GetStarsInfo + } let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: accentColor), linkAttribute: { contents in return (TelegramTextAttributes.URL, contents) @@ -272,21 +278,23 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { ] let externalStateUpdated = context.component.stateUpdated - let layoutPerks = { - size.height += 8.0 - - var i = 0 - var items: [AnyComponentWithIdentity] = [] - - guard let products = state.products else { - return - } + + size.height += 8.0 + + var i = 0 + var items: [AnyComponentWithIdentity] = [] + + if let products = state.products { for product in products { + if let requiredStars = context.component.requiredStars, requiredStars > product.option.count { + continue + } + if !context.component.expanded && !initialValues.contains(product.option.count) { continue } - - let title = "\(product.option.count) Stars" + + let title = strings.Stars_Purchase_Stars(Int32(product.option.count)) let price = product.price let titleComponent = AnyComponent(MultilineTextComponent( @@ -298,6 +306,15 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { maximumNumberOfLines: 0 )) + let backgroundComponent: AnyComponent? + if product.storeProduct.id == context.component.selectedProductId { + backgroundComponent = AnyComponent( + ItemLoadingComponent(color: environment.theme.list.itemAccentColor) + ) + } else { + backgroundComponent = nil + } + let buy = context.component.buy items.append(AnyComponentWithIdentity( id: product.id, @@ -307,11 +324,12 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { footer: nil, items: [AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( theme: environment.theme, + background: backgroundComponent, title: titleComponent, contentInsets: UIEdgeInsets(top: 12.0, left: -6.0, bottom: 12.0, right: 0.0), leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(StarsIconComponent( count: stars[product.option.count] ?? 1 - )))), + ))), true), accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: price, @@ -322,66 +340,71 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { ))), insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 16.0))), action: { _ in buy(product) + }, + highlighting: .disabled, + updateIsHighlighted: { view, isHighlighted in + let transition: Transition = .easeInOut(duration: 0.25) + if let superview = view.superview { + transition.setScale(view: superview, scale: isHighlighted ? 0.9 : 1.0) + } } )))] )) )) i += 1 } - - if !context.component.expanded { - let titleComponent = AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: "Show More Options", - font: Font.regular(presentationData.listsFontSize.baseDisplaySize), - textColor: environment.theme.list.itemAccentColor - )), - horizontalAlignment: .center, - maximumNumberOfLines: 0 - )) - - let titleCombinedComponent = AnyComponent(HStack([ - AnyComponentWithIdentity(id: AnyHashable(0), component: titleComponent), - AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(BundleIconComponent(name: "Chat/Input/Search/DownButton", tintColor: environment.theme.list.itemAccentColor))) - ], spacing: 1.0)) - - items.append(AnyComponentWithIdentity( - id: items.count, - component: AnyComponent(ListSectionComponent( - theme: environment.theme, - header: nil, - footer: nil, - items: [AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( - theme: environment.theme, - title: titleCombinedComponent, - titleAlignment: .center, - contentInsets: UIEdgeInsets(top: 7.0, left: 0.0, bottom: 7.0, right: 0.0), - leftIcon: nil, - accessory: .none, - action: { _ in - externalStateUpdated(.easeInOut(duration: 0.3)) - } - )))] - )) - )) - } - - let list = list.update( - component: VStack(items, spacing: 16.0), - environment: {}, - availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude), - transition: context.transition - ) - context.add(list - .position(CGPoint(x: availableWidth / 2.0, y: size.height + list.size.height / 2.0)) - ) - size.height += list.size.height - - size.height += 23.0 } - layoutPerks() + if !context.component.expanded { + let titleComponent = AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.Stars_Purchase_ShowMore, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemAccentColor + )), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + )) + + let titleCombinedComponent = AnyComponent(HStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: titleComponent), + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(BundleIconComponent(name: "Chat/Input/Search/DownButton", tintColor: environment.theme.list.itemAccentColor))) + ], spacing: 1.0)) + + items.append(AnyComponentWithIdentity( + id: items.count, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: nil, + items: [AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: titleCombinedComponent, + titleAlignment: .center, + contentInsets: UIEdgeInsets(top: 7.0, left: 0.0, bottom: 7.0, right: 0.0), + leftIcon: nil, + accessory: .none, + action: { _ in + externalStateUpdated(.easeInOut(duration: 0.3)) + } + )))] + )) + )) + } + + let list = list.update( + component: VStack(items, spacing: 16.0), + environment: {}, + availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude), + transition: context.transition + ) + context.add(list + .position(CGPoint(x: availableWidth / 2.0, y: size.height + list.size.height / 2.0)) + ) + size.height += list.size.height + size.height += 23.0 + let termsFont = Font.regular(13.0) let termsTextColor = environment.theme.list.freeTextColor @@ -390,9 +413,10 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { }) let textSideInset: CGFloat = 16.0 + let component = context.component let termsText = termsText.update( component: BalancedTextComponent( - text: .markdown(text: "By proceeding and purchasing Stars, you agree with [Terms and Conditions]().", attributes: termsMarkdownAttributes), + text: .markdown(text: strings.Stars_Purchase_Info, attributes: termsMarkdownAttributes), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.2, @@ -405,7 +429,7 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { } }, tapAction: { attributes, _ in - + component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Stars_Purchase_Terms_URL, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) } ), environment: {}, @@ -432,22 +456,22 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { let starsContext: StarsContext let options: [StarsTopUpOption] let peerId: EnginePeer.Id? - let requiredStars: Int32? + let requiredStars: Int64? let forceDark: Bool let updateInProgress: (Bool) -> Void let present: (ViewController) -> Void - let completion: () -> Void + let completion: (Int64) -> Void init( context: AccountContext, starsContext: StarsContext, options: [StarsTopUpOption], peerId: EnginePeer.Id?, - requiredStars: Int32?, + requiredStars: Int64?, forceDark: Bool, updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, - completion: @escaping () -> Void + completion: @escaping (Int64) -> Void ) { self.context = context self.starsContext = starsContext @@ -486,14 +510,14 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { private let context: AccountContext private let updateInProgress: (Bool) -> Void private let present: (ViewController) -> Void - private let completion: () -> Void + private let completion: (Int64) -> Void var topContentOffset: CGFloat? var bottomContentOffset: CGFloat? var hasIdleAnimations = true - var inProgress = false + var progressProduct: StarsProduct? private(set) var promoConfiguration: PremiumPromoConfiguration? @@ -514,7 +538,7 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { initialOptions: [StarsTopUpOption], updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, - completion: @escaping () -> Void + completion: @escaping (Int64) -> Void ) { self.context = context self.updateInProgress = updateInProgress @@ -569,13 +593,13 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { } func buy(product: StarsProduct) { - guard let inAppPurchaseManager = self.context.inAppPurchaseManager, !self.inProgress else { + guard let inAppPurchaseManager = self.context.inAppPurchaseManager, self.progressProduct == nil else { return } - self.inProgress = true + self.progressProduct = product self.updateInProgress(true) - self.updated(transition: .immediate) + self.updated(transition: .easeInOut(duration: 0.2)) let (currency, amount) = product.storeProduct.priceCurrencyAndAmount let purpose: AppStoreTransactionPurpose = .stars(count: product.option.count, currency: currency, amount: amount) @@ -588,17 +612,16 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { strongSelf.paymentDisposable.set((inAppPurchaseManager.buyProduct(product.storeProduct, purpose: purpose) |> deliverOnMainQueue).start(next: { [weak self] status in if let self, case .purchased = status { - self.inProgress = false self.updateInProgress(false) - self.updated(transition: .easeInOut(duration: 0.25)) - self.completion() + self.updated(transition: .easeInOut(duration: 0.2)) + self.completion(product.option.count) } }, error: { [weak self] error in if let strongSelf = self { - strongSelf.inProgress = false + strongSelf.progressProduct = nil strongSelf.updateInProgress(false) - strongSelf.updated(transition: .immediate) + strongSelf.updated(transition: .easeInOut(duration: 0.2)) var errorText: String? switch error { @@ -623,9 +646,9 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { } })) } else { - strongSelf.inProgress = false + strongSelf.progressProduct = nil strongSelf.updateInProgress(false) - strongSelf.updated(transition: .immediate) + strongSelf.updated(transition: .easeInOut(duration: 0.2)) } } }) @@ -657,6 +680,8 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { return { context in let environment = context.environment[EnvironmentType.self].value let state = context.state + + let strings = environment.strings let background = background.update(component: Rectangle(color: environment.theme.list.blocksBackgroundColor), environment: {}, availableSize: context.availableSize, transition: context.transition) @@ -697,10 +722,16 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { transition: context.transition ) - //TODO:localize + let titleText: String + if let requiredStars = context.component.requiredStars { + titleText = strings.Stars_Purchase_StarsNeeded(Int32(requiredStars)) + } else { + titleText = strings.Stars_Purchase_GetStars + } + let title = title.update( component: MultilineTextComponent( - text: .plain(NSAttributedString(string: "Get Stars", font: Font.bold(28.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)), + text: .plain(NSAttributedString(string: titleText, font: Font.bold(28.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)), horizontalAlignment: .center, truncationType: .end, maximumNumberOfLines: 1 @@ -722,11 +753,11 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: UIColor(rgb: 0xf09903))!, environment.theme) } - let balanceAttributedString = parseMarkdownIntoAttributedString("Balance: * **\(state.starsState?.balance ?? 0)**", attributes: markdownAttributes).mutableCopy() as! NSMutableAttributedString - if let range = balanceAttributedString.string.range(of: "*"), let chevronImage = state.cachedChevronImage?.0 { + let balanceAttributedString = parseMarkdownIntoAttributedString(" \(strings.Stars_Purchase_Balance)\n # **\(state.starsState?.balance ?? 0)**", attributes: markdownAttributes, textAlignment: .right).mutableCopy() as! NSMutableAttributedString + if let range = balanceAttributedString.string.range(of: "#"), let chevronImage = state.cachedChevronImage?.0 { balanceAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: balanceAttributedString.string)) balanceAttributedString.addAttribute(.foregroundColor, value: UIColor(rgb: 0xf09903), range: NSRange(range, in: balanceAttributedString.string)) - balanceAttributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: balanceAttributedString.string)) + balanceAttributedString.addAttribute(.baselineOffset, value: 2.0, range: NSRange(range, in: balanceAttributedString.string)) } let balanceText = balanceText.update( component: MultilineTextComponent( @@ -745,11 +776,12 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { options: context.component.options, peerId: context.component.peerId, requiredStars: context.component.requiredStars, + selectedProductId: state.progressProduct?.storeProduct.id, forceDark: context.component.forceDark, products: state.products, expanded: state.isExpanded, stateUpdated: { [weak state] transition in - scrollAction.invoke(CGPoint(x: 0.0, y: 176.0)) + scrollAction.invoke(CGPoint(x: 0.0, y: 170.0)) state?.isExpanded = true state?.updated(transition: transition) }, @@ -768,8 +800,8 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { contentOffsetWillCommit: { targetContentOffset in if targetContentOffset.pointee.y < 100.0 { targetContentOffset.pointee = CGPoint(x: 0.0, y: 0.0) - } else if targetContentOffset.pointee.y < 176.0 { - targetContentOffset.pointee = CGPoint(x: 0.0, y: 176.0) + } else if targetContentOffset.pointee.y < 170.0 { + targetContentOffset.pointee = CGPoint(x: 0.0, y: 170.0) } }, resetScroll: scrollAction @@ -855,9 +887,10 @@ public final class StarsPurchaseScreen: ViewControllerComponentContainer { starsContext: StarsContext, options: [StarsTopUpOption], peerId: EnginePeer.Id?, - requiredStars: Int32?, + requiredStars: Int64?, modal: Bool = true, - forceDark: Bool = false + forceDark: Bool = false, + completion: @escaping (Int64) -> Void = { _ in } ) { self.context = context self.starsContext = starsContext @@ -865,7 +898,7 @@ public final class StarsPurchaseScreen: ViewControllerComponentContainer { var updateInProgressImpl: ((Bool) -> Void)? var presentImpl: ((ViewController) -> Void)? - var completionImpl: (() -> Void)? + var completionImpl: ((Int64) -> Void)? super.init(context: context, component: StarsPurchaseScreenComponent( context: context, starsContext: starsContext, @@ -879,8 +912,8 @@ public final class StarsPurchaseScreen: ViewControllerComponentContainer { present: { c in presentImpl?(c) }, - completion: { - completionImpl?() + completion: { stars in + completionImpl?(stars) } ), navigationBarAppearance: .transparent, presentationMode: modal ? .modal : .default, theme: forceDark ? .dark : .default) @@ -906,9 +939,11 @@ public final class StarsPurchaseScreen: ViewControllerComponentContainer { self.present(c, in: .window(.root)) } } - completionImpl = { [weak self] in + completionImpl = { [weak self] stars in if let self { self.animateSuccess() + + completion(stars) } } } @@ -996,12 +1031,13 @@ func generateStarsIcon(count: Int) -> UIImage { var originX = floorToScreenPixels((size.width - totalWidth) / 2.0) - if let cgImage = image.cgImage, let partCGImage = partImage.cgImage { + let mainImage = UIImage(bundleImageName: "Premium/Stars/Star") + if let cgImage = mainImage?.cgImage, let partCGImage = partImage.cgImage { context.draw(cgImage, in: CGRect(origin: CGPoint(x: originX, y: 0.0), size: imageSize), byTiling: false) originX += spacing for _ in 0 ..< count - 1 { - context.draw(partCGImage, in: CGRect(origin: CGPoint(x: originX, y: 0.0), size: imageSize), byTiling: false) + context.draw(partCGImage, in: CGRect(origin: CGPoint(x: originX, y: UIScreenPixel), size: imageSize), byTiling: false) originX += spacing } } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift index 28ac40019e..e1371b6504 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift @@ -62,6 +62,7 @@ final class StarsBalanceComponent: Component { } func update(component: StarsBalanceComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let isFirstTime = self.component == nil self.component = component let sideInset: CGFloat = 16.0 @@ -76,17 +77,13 @@ final class StarsBalanceComponent: Component { )) let titleSize = self.title.update( - transition: .easeInOut(duration: 0.2), + transition: isFirstTime ? .immediate : .easeInOut(duration: 0.2), component: AnyComponent( AnimatedTextComponent( font: Font.with(size: 48.0, design: .round, weight: .semibold), color: component.theme.list.itemPrimaryTextColor, items: animatedTextItems ) -// MultilineTextComponent( -// text: .plain(NSAttributedString(string: "\(component.count)", font: Font.with(size: 48.0, design: .round, weight: .semibold), textColor: component.theme.list.itemPrimaryTextColor)), -// horizontalAlignment: .center -// ) ), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionScreen.swift new file mode 100644 index 0000000000..19f371a0bb --- /dev/null +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionScreen.swift @@ -0,0 +1,885 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import TelegramCore +import SwiftSignalKit +import AccountContext +import TelegramPresentationData +import PresentationDataUtils +import ComponentFlow +import ViewControllerComponent +import SheetComponent +import MultilineTextComponent +import BundleIconComponent +import SolidRoundedButtonComponent +import Markdown +import BalancedTextComponent +import AvatarNode +import TextFormat +import TelegramStringFormatting +import UndoUI +import PremiumStarComponent + +private final class StarsTransactionSheetContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let subject: StarsTransactionScreen.Subject + let action: () -> Void + let cancel: (Bool) -> Void + let openPeer: (EnginePeer) -> Void + + init( + context: AccountContext, + subject: StarsTransactionScreen.Subject, + action: @escaping () -> Void, + cancel: @escaping (Bool) -> Void, + openPeer: @escaping (EnginePeer) -> Void + ) { + self.context = context + self.subject = subject + self.action = action + self.cancel = cancel + self.openPeer = openPeer + } + + static func ==(lhs: StarsTransactionSheetContent, rhs: StarsTransactionSheetContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.subject != rhs.subject { + return false + } + return true + } + + final class State: ComponentState { + private let context: AccountContext + private var disposable: Disposable? + var initialized = false + + var peerMap: [EnginePeer.Id: EnginePeer] = [:] + + var cachedCloseImage: (UIImage, PresentationTheme)? + + var inProgress = false + + init(context: AccountContext, subject: StarsTransactionScreen.Subject) { + self.context = context + + super.init() + + var peerIds: [EnginePeer.Id] = [] + switch subject { + case let .transaction(transaction): + if case let .peer(peer) = transaction.peer { + peerIds.append(peer.id) + } + } + + self.disposable = (context.engine.data.get( + EngineDataMap( + peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.Peer in + return TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) + } + ) + ) |> deliverOnMainQueue).startStrict(next: { [weak self] peers in + if let strongSelf = self { + var peersMap: [EnginePeer.Id: EnginePeer] = [:] + for peerId in peerIds { + if let maybePeer = peers[peerId], let peer = maybePeer { + peersMap[peerId] = peer + } + } + strongSelf.peerMap = peersMap + strongSelf.initialized = true + + strongSelf.updated(transition: .immediate) + } + }) + } + + deinit { + self.disposable?.dispose() + } + } + + func makeState() -> State { + return State(context: self.context, subject: self.subject) + } + + static var body: Body { + let closeButton = Child(Button.self) + let title = Child(MultilineTextComponent.self) + let star = Child(GiftAvatarComponent.self) + let description = Child(BalancedTextComponent.self) + let table = Child(TableComponent.self) + let additional = Child(BalancedTextComponent.self) + let button = Child(SolidRoundedButtonComponent.self) + + return { context in + let environment = context.environment[ViewControllerComponentContainer.Environment.self].value + let component = context.component + let theme = environment.theme + let strings = environment.strings + let dateTimeFormat = environment.dateTimeFormat + let accountContext = context.component.context + + let state = context.state + let subject = component.subject + + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + let textSideInset: CGFloat = 32.0 + environment.safeInsets.left + + let closeImage: UIImage + if let (image, theme) = state.cachedCloseImage, theme === environment.theme { + closeImage = image + } else { + closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: theme.actionSheet.inputClearButtonColor)! + state.cachedCloseImage = (closeImage, theme) + } + + let closeButton = closeButton.update( + component: Button( + content: AnyComponent(Image(image: closeImage)), + action: { [weak component] in + component?.cancel(true) + } + ), + availableSize: CGSize(width: 30.0, height: 30.0), + transition: .immediate + ) + + let titleText: String + let descriptionText: String + let additionalText: String + let buttonText: String + + let transactionId: String + let date: Int32 + let toPeer: EnginePeer? + + let gloss = false + switch subject { + case let .transaction(transaction): + titleText = "Product Title" + if transaction.count < 0 { + descriptionText = "- \(transaction.count * -1) ⭐️" + } else { + descriptionText = "+ \(transaction.count) ⭐️" + } + additionalText = "You can dispute this transaction [here]()." + buttonText = "OK" + + transactionId = transaction.id + date = transaction.date + if case let .peer(peer) = transaction.peer { + toPeer = peer + } else { + toPeer = nil + } + } + + let title = title.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: titleText, + font: Font.semibold(24.0), + textColor: theme.actionSheet.primaryTextColor, + paragraphAlignment: .center + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + + let star = star.update( + component: GiftAvatarComponent( + context: component.context, + theme: theme, + peers: toPeer.flatMap { [$0] } ?? [], + isVisible: true, + hasIdleAnimations: true, + hasScaleAnimation: false, + avatarSize: 90.0, + color: UIColor(rgb: 0xf7ab04) + ), +// PremiumStarComponent(isIntro: false, isVisible: true, hasIdleAnimations: true), + availableSize: CGSize(width: context.availableSize.width, height: 200.0), + transition: .immediate + ) + + let textFont = Font.regular(15.0) + let boldTextFont = Font.semibold(15.0) + let textColor = theme.actionSheet.primaryTextColor + let linkColor = theme.actionSheet.controlAccentColor + let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + }) + let description = description.update( + component: BalancedTextComponent( + text: .plain(NSAttributedString(string: descriptionText, font: boldTextFont, textColor: descriptionText.hasPrefix("-") ? theme.list.itemDestructiveColor : theme.list.itemDisclosureActions.constructive.fillColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + ), + availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + + let tableFont = Font.regular(15.0) + let tableTextColor = theme.list.itemPrimaryTextColor + let tableLinkColor = theme.list.itemAccentColor + var tableItems: [TableComponent.Item] = [] + + if let toPeer { + tableItems.append(.init( + id: "to", + title: strings.GiftLink_To, + component: AnyComponent( + Button( + content: AnyComponent(PeerCellComponent(context: context.component.context, textColor: tableLinkColor, peer: toPeer)), + action: { + if toPeer.id != accountContext.account.peerId { + component.openPeer(toPeer) + Queue.mainQueue().after(1.0, { + component.cancel(false) + }) + } + } + ) + ) + )) + } + + tableItems.append(.init( + id: "transaction", + title: "Transaction ID", + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: transactionId, font: tableFont, textColor: tableTextColor)), truncationType: .middle) + ) + )) + + tableItems.append(.init( + id: "date", + title: strings.GiftLink_Date, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) + ) + )) + + let table = table.update( + component: TableComponent( + theme: environment.theme, + items: tableItems + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude), + transition: .immediate + ) + + let additional = additional.update( + component: BalancedTextComponent( + text: .markdown(text: additionalText, attributes: markdownAttributes), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.1, + highlightColor: linkColor.withAlphaComponent(0.2), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { attributes, _ in + + } + ), + availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + + let button = button.update( + component: SolidRoundedButtonComponent( + title: buttonText, + theme: SolidRoundedButtonComponent.Theme(theme: theme), + font: .bold, + fontSize: 17.0, + height: 50.0, + cornerRadius: 10.0, + gloss: gloss, + iconName: nil, + animationName: nil, + iconPosition: .left, + isLoading: state.inProgress, + action: { [weak state] in + if gloss { + component.action() + if let state { + state.inProgress = true + state.updated() + } + } else { + component.cancel(true) + } + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + transition: context.transition + ) + + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: 28.0 + 125.0)) + ) + + context.add(star + .position(CGPoint(x: context.availableSize.width / 2.0, y: star.size.height / 2.0 - 32.0)) + ) + + var originY: CGFloat = 0.0 + originY += star.size.height - 32.0 + + context.add(description + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + description.size.height / 2.0)) + ) + originY += description.size.height + 21.0 + + context.add(table + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + table.size.height / 2.0)) + ) + originY += table.size.height + 23.0 + + context.add(additional + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + additional.size.height / 2.0)) + ) + originY += additional.size.height + 23.0 + + let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: originY), size: button.size) + context.add(button + .position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY)) + ) + + context.add(closeButton + .position(CGPoint(x: context.availableSize.width - environment.safeInsets.left - closeButton.size.width, y: 28.0)) + ) + + let contentSize = CGSize(width: context.availableSize.width, height: buttonFrame.maxY + 5.0 + environment.safeInsets.bottom) + + return contentSize + } + } +} + +private final class StarsTransactionSheetComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let subject: StarsTransactionScreen.Subject + let action: () -> Void + let openPeer: (EnginePeer) -> Void + + init( + context: AccountContext, + subject: StarsTransactionScreen.Subject, + action: @escaping () -> Void, + openPeer: @escaping (EnginePeer) -> Void + ) { + self.context = context + self.subject = subject + self.action = action + self.openPeer = openPeer + } + + static func ==(lhs: StarsTransactionSheetComponent, rhs: StarsTransactionSheetComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.subject != rhs.subject { + return false + } + return true + } + + static var body: Body { + let sheet = Child(SheetComponent.self) + let animateOut = StoredActionSlot(Action.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + let controller = environment.controller + + let sheet = sheet.update( + component: SheetComponent( + content: AnyComponent(StarsTransactionSheetContent( + context: context.component.context, + subject: context.component.subject, + action: context.component.action, + cancel: { animate in + if animate { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } else if let controller = controller() { + controller.dismiss(animated: false, completion: nil) + } + }, + openPeer: context.component.openPeer + )), + backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), + followContentSizeChanges: true, + clipsContent: true, + animateOut: animateOut + ), + environment: { + environment + SheetComponentEnvironment( + isDisplaying: environment.value.isVisible, + isCentered: environment.metrics.widthClass == .regular, + hasInputHeight: !environment.inputHeight.isZero, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { animated in + if animated { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } else { + if let controller = controller() { + controller.dismiss(completion: nil) + } + } + } + ) + }, + availableSize: context.availableSize, + transition: context.transition + ) + + context.add(sheet + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + return context.availableSize + } + } +} + +public class StarsTransactionScreen: ViewControllerComponentContainer { + public enum Subject: Equatable { + case transaction(StarsContext.State.Transaction) + } + + private let context: AccountContext + public var disposed: () -> Void = {} + + private let hapticFeedback = HapticFeedback() + + public init( + context: AccountContext, + subject: StarsTransactionScreen.Subject, + forceDark: Bool = false, + action: @escaping () -> Void + ) { + self.context = context + + var openPeerImpl: ((EnginePeer) -> Void)? + super.init( + context: context, + component: StarsTransactionSheetComponent( + context: context, + subject: subject, + action: action, + openPeer: { peerId in + openPeerImpl?(peerId) + } + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: forceDark ? .dark : .default + ) + + self.navigationPresentation = .flatModal + + openPeerImpl = { [weak self] peer in + guard let self, let navigationController = self.navigationController as? NavigationController else { + return + } + let _ = (context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id) + ) + |> deliverOnMainQueue).start(next: { peer in + guard let peer else { + return + } + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, chatController: nil, context: context, chatLocation: .peer(peer), subject: nil, botStart: nil, updateTextInputState: nil, keepStack: .always, useExisting: false, purposefulAction: nil, scrollToEndIfExists: false, activateMessageSearch: nil, animated: true)) + }) + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.disposed() + } + + public override func viewDidLoad() { + super.viewDidLoad() + + self.view.disablesInteractiveModalDismiss = true + } + + public override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + self.dismissAllTooltips() + } + + public func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { + view.dismissAnimated() + } + } + + fileprivate func dismissAllTooltips() { + self.window?.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismiss() + } + }) + self.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismiss() + } + return true + }) + } +} + +private final class TableComponent: CombinedComponent { + class Item: Equatable { + public let id: AnyHashable + public let title: String + public let component: AnyComponent + + public init(id: IdType, title: String, component: AnyComponent) { + self.id = AnyHashable(id) + self.title = title + self.component = component + } + + public static func == (lhs: Item, rhs: Item) -> Bool { + if lhs.id != rhs.id { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.component != rhs.component { + return false + } + return true + } + } + + private let theme: PresentationTheme + private let items: [Item] + + public init(theme: PresentationTheme, items: [Item]) { + self.theme = theme + self.items = items + } + + public static func ==(lhs: TableComponent, rhs: TableComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.items != rhs.items { + return false + } + return true + } + + final class State: ComponentState { + var cachedBorderImage: (UIImage, PresentationTheme)? + } + + func makeState() -> State { + return State() + } + + public static var body: Body { + let leftColumnBackground = Child(Rectangle.self) + let verticalBorder = Child(Rectangle.self) + let titleChildren = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) + let valueChildren = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) + let borderChildren = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) + let outerBorder = Child(Image.self) + + return { context in + let verticalPadding: CGFloat = 11.0 + let horizontalPadding: CGFloat = 12.0 + let borderWidth: CGFloat = 1.0 + + let backgroundColor = context.component.theme.actionSheet.opaqueItemBackgroundColor + let borderColor = backgroundColor.mixedWith(context.component.theme.list.itemBlocksSeparatorColor, alpha: 0.6) + + var leftColumnWidth: CGFloat = 0.0 + + var updatedTitleChildren: [_UpdatedChildComponent] = [] + var updatedValueChildren: [_UpdatedChildComponent] = [] + var updatedBorderChildren: [_UpdatedChildComponent] = [] + + for item in context.component.items { + let titleChild = titleChildren[item.id].update( + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: item.title, font: Font.regular(15.0), textColor: context.component.theme.list.itemPrimaryTextColor)) + )), + availableSize: context.availableSize, + transition: context.transition + ) + updatedTitleChildren.append(titleChild) + + if titleChild.size.width > leftColumnWidth { + leftColumnWidth = titleChild.size.width + } + } + + leftColumnWidth = max(100.0, leftColumnWidth + horizontalPadding * 2.0) + let rightColumnWidth = context.availableSize.width - leftColumnWidth + + var i = 0 + var rowHeights: [Int: CGFloat] = [:] + var totalHeight: CGFloat = 0.0 + + for item in context.component.items { + let titleChild = updatedTitleChildren[i] + let valueChild = valueChildren[item.id].update( + component: item.component, + availableSize: CGSize(width: rightColumnWidth - horizontalPadding * 2.0, height: context.availableSize.height), + transition: context.transition + ) + updatedValueChildren.append(valueChild) + + let rowHeight = max(40.0, max(titleChild.size.height, valueChild.size.height) + verticalPadding * 2.0) + rowHeights[i] = rowHeight + totalHeight += rowHeight + + if i < context.component.items.count - 1 { + let borderChild = borderChildren[item.id].update( + component: AnyComponent(Rectangle(color: borderColor)), + availableSize: CGSize(width: context.availableSize.width, height: borderWidth), + transition: context.transition + ) + updatedBorderChildren.append(borderChild) + } + + i += 1 + } + + let leftColumnBackground = leftColumnBackground.update( + component: Rectangle(color: context.component.theme.list.itemInputField.backgroundColor), + availableSize: CGSize(width: leftColumnWidth, height: totalHeight), + transition: context.transition + ) + context.add( + leftColumnBackground + .position(CGPoint(x: leftColumnWidth / 2.0, y: totalHeight / 2.0)) + ) + + let borderImage: UIImage + if let (currentImage, theme) = context.state.cachedBorderImage, theme === context.component.theme { + borderImage = currentImage + } else { + let borderRadius: CGFloat = 5.0 + borderImage = generateImage(CGSize(width: 16.0, height: 16.0), rotatedContext: { size, context in + let bounds = CGRect(origin: .zero, size: size) + context.setFillColor(backgroundColor.cgColor) + context.fill(bounds) + + let path = CGPath(roundedRect: bounds.insetBy(dx: borderWidth / 2.0, dy: borderWidth / 2.0), cornerWidth: borderRadius, cornerHeight: borderRadius, transform: nil) + context.setBlendMode(.clear) + context.addPath(path) + context.fillPath() + + context.setBlendMode(.normal) + context.setStrokeColor(borderColor.cgColor) + context.setLineWidth(borderWidth) + context.addPath(path) + context.strokePath() + })!.stretchableImage(withLeftCapWidth: 5, topCapHeight: 5) + context.state.cachedBorderImage = (borderImage, context.component.theme) + } + + let outerBorder = outerBorder.update( + component: Image(image: borderImage), + availableSize: CGSize(width: context.availableSize.width, height: totalHeight), + transition: context.transition + ) + context.add(outerBorder + .position(CGPoint(x: context.availableSize.width / 2.0, y: totalHeight / 2.0)) + ) + + let verticalBorder = verticalBorder.update( + component: Rectangle(color: borderColor), + availableSize: CGSize(width: borderWidth, height: totalHeight), + transition: context.transition + ) + context.add( + verticalBorder + .position(CGPoint(x: leftColumnWidth - borderWidth / 2.0, y: totalHeight / 2.0)) + ) + + i = 0 + var originY: CGFloat = 0.0 + for (titleChild, valueChild) in zip(updatedTitleChildren, updatedValueChildren) { + let rowHeight = rowHeights[i] ?? 0.0 + + let titleFrame = CGRect(origin: CGPoint(x: horizontalPadding, y: originY + verticalPadding), size: titleChild.size) + let valueFrame = CGRect(origin: CGPoint(x: leftColumnWidth + horizontalPadding, y: originY + verticalPadding), size: valueChild.size) + + context.add(titleChild + .position(titleFrame.center) + ) + + context.add(valueChild + .position(valueFrame.center) + ) + + if i < updatedBorderChildren.count { + let borderChild = updatedBorderChildren[i] + context.add(borderChild + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + rowHeight - borderWidth / 2.0)) + ) + } + + originY += rowHeight + i += 1 + } + + return CGSize(width: context.availableSize.width, height: totalHeight) + } + } +} + +private final class PeerCellComponent: Component { + let context: AccountContext + let textColor: UIColor + let peer: EnginePeer? + + init(context: AccountContext, textColor: UIColor, peer: EnginePeer?) { + self.context = context + self.textColor = textColor + self.peer = peer + } + + static func ==(lhs: PeerCellComponent, rhs: PeerCellComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.textColor !== rhs.textColor { + return false + } + if lhs.peer != rhs.peer { + return false + } + return true + } + + final class View: UIView { + private let avatarNode: AvatarNode + private let text = ComponentView() + + private var component: PeerCellComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 13.0)) + + super.init(frame: frame) + + self.addSubnode(self.avatarNode) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: PeerCellComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + self.avatarNode.setPeer( + context: component.context, + theme: component.context.sharedContext.currentPresentationData.with({ $0 }).theme, + peer: component.peer, + synchronousLoad: true + ) + + let avatarSize = CGSize(width: 22.0, height: 22.0) + let spacing: CGFloat = 6.0 + + let textSize = self.text.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: component.peer?.compactDisplayTitle ?? "", font: Font.regular(15.0), textColor: component.textColor, paragraphAlignment: .left)) + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - avatarSize.width - spacing, height: availableSize.height) + ) + + let size = CGSize(width: avatarSize.width + textSize.width + spacing, height: textSize.height) + + let avatarFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - avatarSize.height) / 2.0)), size: avatarSize) + self.avatarNode.frame = avatarFrame + + if let view = self.text.view { + if view.superview == nil { + self.addSubview(view) + } + let textFrame = CGRect(origin: CGPoint(x: avatarSize.width + spacing, y: floorToScreenPixels((size.height - textSize.height) / 2.0)), size: textSize) + transition.setFrame(view: view, frame: textFrame) + } + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + + +private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { + return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(backgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + context.setLineWidth(2.0) + context.setLineCap(.round) + context.setStrokeColor(foregroundColor.cgColor) + + context.move(to: CGPoint(x: 10.0, y: 10.0)) + context.addLine(to: CGPoint(x: 20.0, y: 20.0)) + context.strokePath() + + context.move(to: CGPoint(x: 20.0, y: 10.0)) + context.addLine(to: CGPoint(x: 10.0, y: 20.0)) + context.strokePath() + }) +} diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift index b51cac1047..1e5c355b07 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift @@ -226,6 +226,12 @@ final class StarsTransactionsListPanelComponent: Component { case .fragment: itemTitle = "Fragment" itemLabel = NSAttributedString(string: "+ \(item.transaction.count)", font: Font.medium(fontBaseDisplaySize), textColor: environment.theme.list.itemDisclosureActions.constructive.fillColor) + case .premiumBot: + itemTitle = "Premium Bot" + itemLabel = NSAttributedString(string: "+ \(item.transaction.count)", font: Font.medium(fontBaseDisplaySize), textColor: environment.theme.list.itemDisclosureActions.constructive.fillColor) + case .unsupported: + itemTitle = "Unsupported" + itemLabel = NSAttributedString(string: "+ \(item.transaction.count)", font: Font.medium(fontBaseDisplaySize), textColor: environment.theme.list.itemDisclosureActions.constructive.fillColor) } itemSubtitle = stringForMediumCompactDate(timestamp: item.transaction.date, strings: environment.strings, dateTimeFormat: environment.dateTimeFormat) @@ -245,15 +251,15 @@ final class StarsTransactionsListPanelComponent: Component { AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: itemSubtitle, - font: Font.regular(floor(fontBaseDisplaySize * 14.0 / 17.0)), + font: Font.regular(floor(fontBaseDisplaySize * 15.0 / 17.0)), textColor: environment.theme.list.itemSecondaryTextColor )), maximumNumberOfLines: 0, lineSpacing: 0.18 ))) - ], alignment: .left, spacing: 2.0)), - contentInsets: UIEdgeInsets(top: 11.0, left: 0.0, bottom: 11.0, right: 0.0), - leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(AvatarComponent(context: component.context, theme: environment.theme, peer: item.transaction.peer)))), + ], alignment: .left, spacing: 3.0)), + contentInsets: UIEdgeInsets(top: 9.0, left: 0.0, bottom: 8.0, right: 0.0), + leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(AvatarComponent(context: component.context, theme: environment.theme, peer: item.transaction.peer))), false), icon: nil, accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: "label", component: AnyComponent(LabelComponent(text: itemLabel))), insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16.0))), action: { [weak self] _ in @@ -324,13 +330,14 @@ final class StarsTransactionsListPanelComponent: Component { AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: "abc", - font: Font.regular(floor(fontBaseDisplaySize * 13.0 / 17.0)), + font: Font.regular(floor(fontBaseDisplaySize * 15.0 / 17.0)), textColor: environment.theme.list.itemSecondaryTextColor )), maximumNumberOfLines: 0, lineSpacing: 0.18 ))) - ], alignment: .left, spacing: 2.0)), + ], alignment: .left, spacing: 3.0)), + contentInsets: UIEdgeInsets(top: 9.0, left: 0.0, bottom: 8.0, right: 0.0), leftIcon: nil, icon: nil, accessory: nil, @@ -479,6 +486,16 @@ private final class AvatarComponent: Component { self.backgroundView.isHidden = false self.iconView.isHidden = false self.avatarNode.isHidden = true + case .premiumBot: + self.backgroundView.image = gradientImage + self.backgroundView.isHidden = false + self.iconView.isHidden = false + self.avatarNode.isHidden = true + case .unsupported: + self.backgroundView.image = gradientImage + self.backgroundView.isHidden = false + self.iconView.isHidden = false + self.avatarNode.isHidden = true } self.avatarNode.frame = CGRect(origin: .zero, size: size) diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift index 5009a08b8d..4522f76b66 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -22,15 +22,18 @@ final class StarsTransactionsScreenComponent: Component { let context: AccountContext let starsContext: StarsContext + let openTransaction: (StarsContext.State.Transaction) -> Void let buy: () -> Void init( context: AccountContext, starsContext: StarsContext, + openTransaction: @escaping (StarsContext.State.Transaction) -> Void, buy: @escaping () -> Void ) { self.context = context self.starsContext = starsContext + self.openTransaction = openTransaction self.buy = buy } @@ -243,38 +246,6 @@ final class StarsTransactionsScreenComponent: Component { } } - -// if let headerView = self.headerView.view, let navigationMetrics = self.navigationMetrics { -// var headerOffset: CGFloat = scrollBounds.minY -// -// let minY = navigationMetrics.statusBarHeight + floor((navigationMetrics.navigationHeight - navigationMetrics.statusBarHeight) / 2.0) -// -// let minOffset = headerView.center.y - minY -// -// headerOffset = min(headerOffset, minOffset) -// -// let animatedTransition = Transition(animation: .curve(duration: 0.18, curve: .easeInOut)) -// let navigationBackgroundAlpha: CGFloat = abs(headerOffset - minOffset) < 4.0 ? 1.0 : 0.0 -// -// animatedTransition.setAlpha(view: self.navigationBackgroundView, alpha: navigationBackgroundAlpha) -// animatedTransition.setAlpha(layer: self.navigationSeparatorLayerContainer, alpha: navigationBackgroundAlpha) -// -// let expansionDistance: CGFloat = 32.0 -// var expansionDistanceFactor: CGFloat = abs(scrollBounds.maxY - self.scrollView.contentSize.height) / expansionDistance -// expansionDistanceFactor = max(0.0, min(1.0, expansionDistanceFactor)) -// -// transition.setAlpha(layer: self.navigationSeparatorLayer, alpha: expansionDistanceFactor) -// if let panelContainerView = self.panelContainer.view as? StarsTransactionsPanelContainerComponent.View { -// panelContainerView.updateNavigationMergeFactor(value: 1.0 - expansionDistanceFactor, transition: transition) -// } -// -// var offsetFraction: CGFloat = abs(headerOffset - minOffset) / 60.0 -// offsetFraction = min(1.0, max(0.0, offsetFraction)) -// transition.setScale(view: headerView, scale: 1.0 * offsetFraction + 0.8 * (1.0 - offsetFraction)) -// -// transition.setBounds(view: self.headerOffsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: headerOffset), size: self.headerOffsetContainer.bounds.size)) -// } - let _ = self.panelContainer.updateEnvironment( transition: transition, environment: { @@ -409,12 +380,12 @@ final class StarsTransactionsScreenComponent: Component { environment: {}, containerSize: CGSize(width: min(414.0, availableSize.width), height: 220.0) ) - let starFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: starSize) + let starFrame = CGRect(origin: .zero, size: starSize) if let starView = self.starView.view { if starView.superview == nil { self.insertSubview(starView, aboveSubview: self.scrollView) } - starTransition.setFrame(view: starView, frame: starFrame) + starTransition.setBounds(view: starView, bounds: starFrame) } let titleSize = self.titleView.update( @@ -543,38 +514,45 @@ final class StarsTransactionsScreenComponent: Component { ) var panelItems: [StarsTransactionsPanelContainerComponent.Item] = [] - panelItems.append(StarsTransactionsPanelContainerComponent.Item( - id: "all", - title: "All Transactions", - panel: AnyComponent(StarsTransactionsListPanelComponent( - context: component.context, - items: allItems, - action: { _ in - } + if !allItems.items.isEmpty { + panelItems.append(StarsTransactionsPanelContainerComponent.Item( + id: "all", + title: "All Transactions", + panel: AnyComponent(StarsTransactionsListPanelComponent( + context: component.context, + items: allItems, + action: { transaction in + component.openTransaction(transaction) + } + )) )) - )) - - panelItems.append(StarsTransactionsPanelContainerComponent.Item( - id: "incoming", - title: "Incoming", - panel: AnyComponent(StarsTransactionsListPanelComponent( - context: component.context, - items: incomingItems, - action: { _ in - } - )) - )) - - panelItems.append(StarsTransactionsPanelContainerComponent.Item( - id: "outgoing", - title: "Outgoing", - panel: AnyComponent(StarsTransactionsListPanelComponent( - context: component.context, - items: outgoingItems, - action: { _ in - } - )) - )) + + if !outgoingItems.items.isEmpty { + panelItems.append(StarsTransactionsPanelContainerComponent.Item( + id: "incoming", + title: "Incoming", + panel: AnyComponent(StarsTransactionsListPanelComponent( + context: component.context, + items: incomingItems, + action: { transaction in + component.openTransaction(transaction) + } + )) + )) + + panelItems.append(StarsTransactionsPanelContainerComponent.Item( + id: "outgoing", + title: "Outgoing", + panel: AnyComponent(StarsTransactionsListPanelComponent( + context: component.context, + items: outgoingItems, + action: { transaction in + component.openTransaction(transaction) + } + )) + )) + } + } var panelTransition = transition if balanceUpdated { @@ -655,30 +633,53 @@ final class StarsTransactionsScreenComponent: Component { public final class StarsTransactionsScreen: ViewControllerComponentContainer { private let context: AccountContext + private let starsContext: StarsContext private let options = Promise<[StarsTopUpOption]>() public init(context: AccountContext, starsContext: StarsContext, forceDark: Bool = false) { self.context = context + self.starsContext = starsContext var buyImpl: (() -> Void)? - super.init(context: context, component: StarsTransactionsScreenComponent(context: context, starsContext: starsContext, buy: { - buyImpl?() - }), navigationBarAppearance: .transparent) + var openTransactionImpl: ((StarsContext.State.Transaction) -> Void)? + super.init(context: context, component: StarsTransactionsScreenComponent( + context: context, + starsContext: starsContext, + openTransaction: { transaction in + openTransactionImpl?(transaction) + }, + buy: { + buyImpl?() + } + ), navigationBarAppearance: .transparent) self.options.set(.single([]) |> then(context.engine.payments.starsTopUpOptions())) + openTransactionImpl = { [weak self] transaction in + guard let self else { + return + } + let controller = context.sharedContext.makeStarsTransactionScreen(context: context, transaction: transaction) + self.push(controller) + } + buyImpl = { [weak self] in guard let self else { return } let _ = (self.options.get() - |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] options in + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] options in guard let self else { return } - let controller = context.sharedContext.makeStarsPurchaseScreen(context: context, starsContext: starsContext, options: options, peerId: nil, requiredStars: nil) + let controller = context.sharedContext.makeStarsPurchaseScreen(context: context, starsContext: starsContext, options: options, peerId: nil, requiredStars: nil, completion: { [weak self] stars in + guard let self else { + return + } + self.starsContext.add(balance: stars) + }) self.push(controller) }) } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift index 6981b5290e..f09f5e029c 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift @@ -21,20 +21,26 @@ private final class SheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext + let starsContext: StarsContext let invoice: TelegramMediaInvoice let source: BotPaymentInvoiceSource let inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError> + let dismiss: () -> Void init( context: AccountContext, + starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, - inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError> + inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>, + dismiss: @escaping () -> Void ) { self.context = context + self.starsContext = starsContext self.invoice = invoice self.source = source self.inputData = inputData + self.dismiss = dismiss } static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool { @@ -54,21 +60,27 @@ private final class SheetContent: CombinedComponent { private let context: AccountContext private let source: BotPaymentInvoiceSource + private let invoice: TelegramMediaInvoice - var peer: EnginePeer? - var peerDisposable: Disposable? - var balance: Int64? - var form: BotPaymentForm? + private(set) var peer: EnginePeer? + private var peerDisposable: Disposable? + private(set) var balance: Int64? + private(set) var form: BotPaymentForm? + + private var optionsDisposable: Disposable? + private(set) var options: [StarsTopUpOption] = [] var inProgress = false init( context: AccountContext, source: BotPaymentInvoiceSource, + invoice: TelegramMediaInvoice, inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError> ) { self.context = context self.source = source + self.invoice = invoice super.init() @@ -81,29 +93,51 @@ private final class SheetContent: CombinedComponent { self.form = inputData?.1 self.peer = inputData?.2 self.updated(transition: .immediate) + + if self.optionsDisposable != nil { + self.optionsDisposable = (context.engine.payments.starsTopUpOptions() + |> deliverOnMainQueue).start(next: { [weak self] options in + guard let self else { + return + } + self.options = options + }) + } }) } deinit { self.peerDisposable?.dispose() + self.optionsDisposable?.dispose() } - func buy(completion: @escaping () -> Void) { - guard let form else { + func buy(requestTopUp: (@escaping () -> Void) -> Void, completion: @escaping () -> Void) { + guard let form, let balance else { return } - self.inProgress = true - self.updated() - let _ = (self.context.engine.payments.sendStarsPaymentForm(formId: form.id, source: self.source) - |> deliverOnMainQueue).start(next: { _ in - completion() - }) + let action = { + self.inProgress = true + self.updated() + + let _ = (self.context.engine.payments.sendStarsPaymentForm(formId: form.id, source: self.source) + |> deliverOnMainQueue).start(next: { _ in + completion() + }) + } + + if balance < self.invoice.totalAmount { + requestTopUp({ + action() + }) + } else { + action() + } } } func makeState() -> State { - return State(context: self.context, source: self.source, inputData: self.inputData) + return State(context: self.context, source: self.source, invoice: self.invoice, inputData: self.inputData) } static var body: Body { @@ -143,11 +177,12 @@ private final class SheetContent: CombinedComponent { context: context.component.context, theme: environment.theme, peers: [peer], + photo: component.invoice.photo, isVisible: true, hasIdleAnimations: true, hasScaleAnimation: false, - color: UIColor(rgb: 0xf7ab04), - offset: 40.0 + avatarSize: 90.0, + color: UIColor(rgb: 0xf7ab04) ), availableSize: CGSize(width: min(414.0, context.availableSize.width), height: 220.0), transition: context.transition @@ -169,7 +204,7 @@ private final class SheetContent: CombinedComponent { component: Button( content: AnyComponent(Image(image: closeImage)), action: { -// component.dismiss() + component.dismiss() } ), availableSize: CGSize(width: 30.0, height: 30.0), @@ -205,7 +240,7 @@ private final class SheetContent: CombinedComponent { let amount = component.invoice.totalAmount let text = text.update( component: BalancedTextComponent( - text: .markdown(text: "Do you want to buy **\(component.invoice.title)** in **\(state.peer?.compactDisplayTitle ?? "levlam_bot")** for **\(amount) Stars**?", attributes: markdownAttributes), + text: .markdown(text: "Do you want to buy **\(component.invoice.title)** in **\(state.peer?.compactDisplayTitle ?? "")** for **\(amount) Stars**?", attributes: markdownAttributes), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.2 @@ -220,11 +255,11 @@ private final class SheetContent: CombinedComponent { contentSize.height += 24.0 if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== theme { - state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: UIColor(rgb: 0xf09903))!, theme) + state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Premium/Stars/Star"), color: UIColor(rgb: 0xf09903))!, theme) } - let balanceAttributedString = parseMarkdownIntoAttributedString("Balance\n > **\(state.balance ?? 0)**", attributes: markdownAttributes).mutableCopy() as! NSMutableAttributedString - if let range = balanceAttributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 { + let balanceAttributedString = parseMarkdownIntoAttributedString("Balance\n # **\(state.balance ?? 0)**", attributes: markdownAttributes).mutableCopy() as! NSMutableAttributedString + if let range = balanceAttributedString.string.range(of: "#"), let chevronImage = state.cachedChevronImage?.0 { balanceAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: balanceAttributedString.string)) balanceAttributedString.addAttribute(.foregroundColor, value: UIColor(rgb: 0xf09903), range: NSRange(range, in: balanceAttributedString.string)) balanceAttributedString.addAttribute(.baselineOffset, value: 2.0, range: NSRange(range, in: balanceAttributedString.string)) @@ -233,8 +268,7 @@ private final class SheetContent: CombinedComponent { component: MultilineTextComponent( text: .plain(balanceAttributedString), horizontalAlignment: .left, - maximumNumberOfLines: 0, - lineSpacing: 0.2 + maximumNumberOfLines: 0 ), availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height), transition: .immediate @@ -255,10 +289,10 @@ private final class SheetContent: CombinedComponent { } let controller = environment.controller() as? StarsTransferScreen - + let accountContext = component.context + let starsContext = component.starsContext let botTitle = state.peer?.compactDisplayTitle ?? "" - let invoice = component.invoice let button = button.update( component: ButtonComponent( @@ -275,7 +309,17 @@ private final class SheetContent: CombinedComponent { isEnabled: true, displaysProgress: state.inProgress, action: { [weak state, weak controller] in - state?.buy(completion: { [weak controller] in + state?.buy(requestTopUp: { [weak controller] _ in + let purchaseController = accountContext.sharedContext.makeStarsPurchaseScreen( + context: accountContext, + starsContext: starsContext, + options: state?.options ?? [], + peerId: state?.peer?.id, + requiredStars: invoice.totalAmount, + completion: { _ in } + ) + controller?.push(purchaseController) + }, completion: { [weak controller] in let presentationData = accountContext.sharedContext.currentPresentationData.with { $0 } let resultController = UndoOverlayController( presentationData: presentationData, @@ -308,17 +352,20 @@ private final class StarsTransferSheetComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment private let context: AccountContext + private let starsContext: StarsContext private let invoice: TelegramMediaInvoice private let source: BotPaymentInvoiceSource private let inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError> init( context: AccountContext, + starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError> ) { self.context = context + self.starsContext = starsContext self.invoice = invoice self.source = source self.inputData = inputData @@ -347,12 +394,21 @@ private final class StarsTransferSheetComponent: CombinedComponent { component: SheetComponent( content: AnyComponent(SheetContent( context: context.component.context, + starsContext: context.component.starsContext, invoice: context.component.invoice, source: context.component.source, - inputData: context.component.inputData + inputData: context.component.inputData, + dismiss: { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } )), backgroundColor: .blur(.light), followContentSizeChanges: true, + clipsContent: true, animateOut: animateOut ), environment: { @@ -395,6 +451,7 @@ public final class StarsTransferScreen: ViewControllerComponentContainer { public init( context: AccountContext, + starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError> @@ -405,6 +462,7 @@ public final class StarsTransferScreen: ViewControllerComponentContainer { context: context, component: StarsTransferSheetComponent( context: context, + starsContext: starsContext, invoice: invoice, source: source, inputData: inputData diff --git a/submodules/TelegramUI/Components/TextLoadingEffect/Sources/TextLoadingEffect.swift b/submodules/TelegramUI/Components/TextLoadingEffect/Sources/TextLoadingEffect.swift index e89d933895..3b4af64ec1 100644 --- a/submodules/TelegramUI/Components/TextLoadingEffect/Sources/TextLoadingEffect.swift +++ b/submodules/TelegramUI/Components/TextLoadingEffect/Sources/TextLoadingEffect.swift @@ -20,8 +20,8 @@ public final class TextLoadingEffectView: UIView { private let backgroundView: UIImageView private let borderBackgroundView: UIImageView - private let duration: Double - private let gradientWidth: CGFloat + private var duration: Double + private var gradientWidth: CGFloat private var size: CGSize? @@ -112,6 +112,33 @@ public final class TextLoadingEffectView: UIView { self.borderBackgroundView.layer.add(animation, forKey: "shimmer") } + public func update(color: UIColor, rect: CGRect) { + let maskFrame = CGRect(origin: CGPoint(), size: rect.size).insetBy(dx: -4.0, dy: -4.0) + + self.gradientWidth = 260.0 + self.duration = 1.2 + + self.maskContentsView.backgroundColor = .clear + + self.backgroundView.alpha = 0.25 + self.backgroundView.tintColor = color + + self.maskContentsView.frame = maskFrame + + let rectsSet: [CGRect] = [rect] + + self.maskHighlightNode.updateRects(rectsSet) + self.maskHighlightNode.frame = CGRect(origin: CGPoint(x: -maskFrame.minX, y: -maskFrame.minY), size: CGSize()) + + if self.size != maskFrame.size { + self.size = maskFrame.size + + self.backgroundView.frame = CGRect(origin: CGPoint(x: -self.gradientWidth, y: 0.0), size: CGSize(width: self.gradientWidth, height: maskFrame.height)) + + self.updateAnimations(size: maskFrame.size) + } + } + public func update(color: UIColor, textNode: TextNode, range: NSRange) { var rectsSet: [CGRect] = [] if let cachedLayout = textNode.cachedLayout { diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/Android.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Stars/Android.imageset/Contents.json new file mode 100644 index 0000000000..114b9d9975 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Stars/Android.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "android.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/Android.imageset/android.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Stars/Android.imageset/android.pdf new file mode 100644 index 0000000000..16f23def6e Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/Stars/Android.imageset/android.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/Apple.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Stars/Apple.imageset/Contents.json new file mode 100644 index 0000000000..097594087a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Stars/Apple.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "apple.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/Apple.imageset/apple.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Stars/Apple.imageset/apple.pdf new file mode 100644 index 0000000000..c057740f51 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Stars/Apple.imageset/apple.pdf @@ -0,0 +1,159 @@ +%PDF-1.7 + +1 0 obj + << /Type /XObject + /Length 2 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 40.000000 40.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 9.437500 7.750366 cm +1.000000 1.000000 1.000000 scn +17.680914 12.434765 m +17.651037 15.724161 20.369793 17.304625 20.492287 17.382303 c +18.962614 19.620049 16.584450 19.924788 15.735958 19.960640 c +13.713324 20.166788 11.786292 18.768570 10.758543 18.768570 c +9.730793 18.768570 8.147343 19.930763 6.468287 19.897900 c +4.260418 19.865036 2.225832 18.616199 1.087540 16.638380 c +-1.203983 12.664813 0.501962 6.770193 2.736719 3.540550 c +3.830197 1.963074 5.129822 0.185425 6.841742 0.251152 c +8.490921 0.316881 9.112350 1.317741 11.102121 1.317741 c +13.091892 1.317741 13.653570 0.251154 15.392379 0.284018 c +17.164051 0.319870 18.287407 1.897346 19.371922 3.480797 c +20.623743 5.312222 21.140604 7.086883 21.170481 7.179500 c +21.131641 7.194438 17.716764 8.503026 17.680914 12.434765 c +f +n +Q + +endstream +endobj + +2 0 obj + 859 +endobj + +3 0 obj + << /Type /XObject + /Length 4 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 40.000000 40.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 9.437500 7.998535 cm +0.000000 0.000000 0.000000 scn +0.000000 26.001465 m +21.173431 26.001465 l +21.173431 0.000002 l +0.000000 0.000002 l +0.000000 26.001465 l +h +f +n +Q + +endstream +endobj + +4 0 obj + 232 +endobj + +5 0 obj + << /XObject << /X1 1 0 R >> + /ExtGState << /E1 << /SMask << /Type /Mask + /G 3 0 R + /S /Alpha + >> + /Type /ExtGState + >> >> + >> +endobj + +6 0 obj + << /Length 7 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +/E1 gs +/X1 Do +Q +q +1.000000 0.000000 -0.000000 1.000000 19.959717 27.893250 cm +1.000000 1.000000 1.000000 scn +3.887190 1.953926 m +4.795434 3.053379 5.407901 4.583052 5.240592 6.106750 c +3.932005 6.052973 2.348554 5.234358 1.410434 4.134906 c +0.570906 3.163921 -0.167042 1.607359 0.033130 0.113537 c +1.488113 0.000007 2.978947 0.857460 3.887190 1.953926 c +h +f +n +Q + +endstream +endobj + +7 0 obj + 392 +endobj + +8 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 40.000000 40.000000 ] + /Resources 5 0 R + /Contents 6 0 R + /Parent 9 0 R + >> +endobj + +9 0 obj + << /Kids [ 8 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +10 0 obj + << /Pages 9 0 R + /Type /Catalog + >> +endobj + +xref +0 11 +0000000000 65535 f +0000000010 00000 n +0000001117 00000 n +0000001139 00000 n +0000001619 00000 n +0000001641 00000 n +0000001939 00000 n +0000002387 00000 n +0000002409 00000 n +0000002582 00000 n +0000002656 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 10 0 R + /Size 11 +>> +startxref +2716 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/Fragment.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Stars/Fragment.imageset/Contents.json new file mode 100644 index 0000000000..b82158fd85 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Stars/Fragment.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "fragment.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/Fragment.imageset/fragment.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Stars/Fragment.imageset/fragment.pdf new file mode 100644 index 0000000000..e29adc31a0 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/Stars/Fragment.imageset/fragment.pdf differ diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 812bf54605..55f308f31c 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -2915,11 +2915,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return .single(nil) }) if invoice.currency == "XTR" { - let statePromise = Promise() - statePromise.set(strongSelf.context.engine.payments.peerStarsState(peerId: strongSelf.context.account.peerId)) + let starsContext = strongSelf.context.engine.payments.peerStarsContext(peerId: strongSelf.context.account.peerId) let starsInputData = combineLatest( inputData.get(), - statePromise.get() + starsContext.state ) |> map { data, state -> (StarsContext.State, BotPaymentForm, EnginePeer?)? in if let data, let state { @@ -2928,11 +2927,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return nil } } - let _ = (starsInputData |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in + let _ = (starsInputData |> filter { $0 != nil } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in guard let strongSelf = self else { return } - let controller = strongSelf.context.sharedContext.makeStarsTransferScreen(context: strongSelf.context, invoice: invoice, source: .message(messageId), inputData: starsInputData) + let controller = strongSelf.context.sharedContext.makeStarsTransferScreen(context: strongSelf.context, starsContext: starsContext, invoice: invoice, source: .message(messageId), inputData: starsInputData) strongSelf.push(controller) }) } else { @@ -4684,28 +4683,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } phoneData.progress?.set(.single(true)) - let context = self.context - let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) - |> mapToSignal { peer -> Signal<(String, EnginePeer?), NoError> in - guard let peer, case let .user(user) = peer else { - return .complete() - } - var normalizedNumber = phoneData.number - if normalizedNumber.hasPrefix("0"), let accountPhone = user.phone, !accountPhone.hasPrefix("888") { - normalizedNumber = enhancePhoneNumberWithCodeFromNumber(normalizedNumber, otherPhoneNumber: accountPhone, configuration: context.currentCountriesConfiguration.with { $0 }) - } - normalizedNumber = formatPhoneNumber(context: context, number: cleanPhoneNumber(normalizedNumber)) - return self.context.engine.peers.resolvePeerByPhone(phone: normalizedNumber) - |> map { peer -> (String, EnginePeer?) in - return (normalizedNumber, peer) - } - } |> deliverOnMainQueue).start(next: { [weak self] number, peer in + let _ = (self.context.engine.peers.resolvePeerByPhone(phone: phoneData.number) + |> deliverOnMainQueue).start(next: { [weak self] peer in guard let self else { return } phoneData.progress?.set(.single(false)) - self.openPhoneContextMenu(number: number, peer: peer, message: phoneData.message, contentNode: phoneData.contentNode, messageNode: phoneData.messageNode, frame: phoneData.messageNode.bounds, anyRecognizer: nil, location: nil) + self.openPhoneContextMenu(number: phoneData.number, peer: peer, message: phoneData.message, contentNode: phoneData.contentNode, messageNode: phoneData.messageNode, frame: phoneData.messageNode.bounds, anyRecognizer: nil, location: nil) }) }, openAgeRestrictedMessageMedia: { [weak self] message, reveal in guard let self else { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 80d11950b0..f96d735bbf 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -1723,8 +1723,8 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/FactCheck"), color: theme.actionSheet.primaryTextColor) }, action: { c, f in c?.dismiss(completion: { + controllerInteraction.editMessageFactCheck(messages[0].id) }) - controllerInteraction.editMessageFactCheck(messages[0].id) }))) } // if message.id.peerId.isGroupOrChannel { diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 03ea02cfba..c117929b1c 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -825,11 +825,10 @@ func openResolvedUrlImpl( return .single(nil) }) if invoice.currency == "XTR" { - let statePromise = Promise() - statePromise.set(context.engine.payments.peerStarsState(peerId: context.account.peerId)) + let starsContext = context.engine.payments.peerStarsContext(peerId: context.account.peerId) let starsInputData = combineLatest( inputData.get(), - statePromise.get() + starsContext.state ) |> map { data, state -> (StarsContext.State, BotPaymentForm, EnginePeer?)? in if let data, let state { @@ -838,8 +837,8 @@ func openResolvedUrlImpl( return nil } } - let _ = (starsInputData |> take(1) |> deliverOnMainQueue).start(next: { _ in - let controller = context.sharedContext.makeStarsTransferScreen(context: context, invoice: invoice, source: .slug(slug), inputData: starsInputData) + let _ = (starsInputData |> filter { $0 != nil } |> take(1) |> deliverOnMainQueue).start(next: { _ in + let controller = context.sharedContext.makeStarsTransferScreen(context: context, starsContext: starsContext, invoice: invoice, source: .slug(slug), inputData: starsInputData) navigationController.pushViewController(controller) }) } else { diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 9e39f17eed..8b955694ad 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2622,12 +2622,16 @@ public final class SharedAccountContextImpl: SharedAccountContext { return StarsTransactionsScreen(context: context, starsContext: starsContext) } - public func makeStarsPurchaseScreen(context: AccountContext, starsContext: StarsContext, options: [StarsTopUpOption], peerId: EnginePeer.Id?, requiredStars: Int32?) -> ViewController { - return StarsPurchaseScreen(context: context, starsContext: starsContext, options: options, peerId: peerId, requiredStars: requiredStars, modal: true) + public func makeStarsPurchaseScreen(context: AccountContext, starsContext: StarsContext, options: [StarsTopUpOption], peerId: EnginePeer.Id?, requiredStars: Int64?, completion: @escaping (Int64) -> Void) -> ViewController { + return StarsPurchaseScreen(context: context, starsContext: starsContext, options: options, peerId: peerId, requiredStars: requiredStars, modal: true, completion: completion) } - public func makeStarsTransferScreen(context: AccountContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>) -> ViewController { - return StarsTransferScreen(context: context, invoice: invoice, source: source, inputData: inputData) + public func makeStarsTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>) -> ViewController { + return StarsTransferScreen(context: context, starsContext: starsContext, invoice: invoice, source: source, inputData: inputData) + } + + public func makeStarsTransactionScreen(context: AccountContext, transaction: StarsContext.State.Transaction) -> ViewController { + return StarsTransactionScreen(context: context, subject: .transaction(transaction), action: {}) } } diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 376b81e679..49c4d92664 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -865,9 +865,10 @@ public final class WebAppController: ViewController, AttachmentContainable { return .single(nil) }) if invoice.currency == "XTR" { + let starsContext = strongSelf.context.engine.payments.peerStarsContext(peerId: strongSelf.context.account.peerId) let starsInputData = combineLatest( inputData.get(), - strongSelf.context.engine.payments.peerStarsState(peerId: strongSelf.context.account.peerId) + starsContext.state ) |> map { data, state -> (StarsContext.State, BotPaymentForm, EnginePeer?)? in if let data, let state { @@ -876,8 +877,10 @@ public final class WebAppController: ViewController, AttachmentContainable { return nil } } - let controller = strongSelf.context.sharedContext.makeStarsTransferScreen(context: strongSelf.context, invoice: invoice, source: .slug(slug), inputData: starsInputData) - navigationController.pushViewController(controller) + let _ = (starsInputData |> filter { $0 != nil } |> take(1) |> deliverOnMainQueue).start(next: { _ in + let controller = strongSelf.context.sharedContext.makeStarsTransferScreen(context: strongSelf.context, starsContext: starsContext, invoice: invoice, source: .slug(slug), inputData: starsInputData) + navigationController.pushViewController(controller) + }) } else { let checkoutController = BotCheckoutController(context: strongSelf.context, invoice: invoice, source: .slug(slug), inputData: inputData, completed: { currencyValue, receiptMessageId in self?.sendInvoiceClosedEvent(slug: slug, result: .paid)