From 4bfaf50de83f5c2f99a2493b96672dca2013cfb3 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 27 Sep 2024 10:48:34 +0800 Subject: [PATCH 1/9] Remove vfsoverlay option (cherry picked from commit 6d131ccd2fa60617c8e7d4e1ce758d3b39922f8e) --- .bazelrc | 1 - 1 file changed, 1 deletion(-) diff --git a/.bazelrc b/.bazelrc index 5ecdbaff0d..43ccf92e1f 100644 --- a/.bazelrc +++ b/.bazelrc @@ -29,7 +29,6 @@ build --features=debug_prefix_map_pwd_is_dot build --features=swift.cacheable_swiftmodules build --features=swift.debug_prefix_map build --features=swift.enable_vfsoverlays -build --features=swift.vfsoverlay build --strategy=Genrule=standalone build --spawn_strategy=standalone From 0a5d8fcb10c57577b10034a42a987dc77e432cae Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 27 Sep 2024 20:26:44 +0800 Subject: [PATCH 2/9] Draw strikethrough and underline manually on iOS 18 (cherry picked from commit d70a0cf0e09db603cf5c5e41582f6a20a3ac4b9d) --- .../Sources/InteractiveTextComponent.swift | 61 ++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift index 2868bce605..4a521443bf 100644 --- a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift +++ b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift @@ -112,13 +112,14 @@ private final class InteractiveTextNodeLine { let isTruncated: Bool let isRTL: Bool var strikethroughs: [InteractiveTextNodeStrikethrough] + var underlines: [InteractiveTextNodeStrikethrough] var spoilers: [InteractiveTextNodeSpoiler] var spoilerWords: [InteractiveTextNodeSpoiler] var embeddedItems: [InteractiveTextNodeEmbeddedItem] var attachments: [InteractiveTextNodeAttachment] let additionalTrailingLine: (CTLine, Double)? - init(line: CTLine, constrainedWidth: CGFloat, frame: CGRect, intrinsicWidth: CGFloat, ascent: CGFloat, descent: CGFloat, range: NSRange?, isTruncated: Bool, isRTL: Bool, strikethroughs: [InteractiveTextNodeStrikethrough], spoilers: [InteractiveTextNodeSpoiler], spoilerWords: [InteractiveTextNodeSpoiler], embeddedItems: [InteractiveTextNodeEmbeddedItem], attachments: [InteractiveTextNodeAttachment], additionalTrailingLine: (CTLine, Double)?) { + init(line: CTLine, constrainedWidth: CGFloat, frame: CGRect, intrinsicWidth: CGFloat, ascent: CGFloat, descent: CGFloat, range: NSRange?, isTruncated: Bool, isRTL: Bool, strikethroughs: [InteractiveTextNodeStrikethrough], underlines: [InteractiveTextNodeStrikethrough], spoilers: [InteractiveTextNodeSpoiler], spoilerWords: [InteractiveTextNodeSpoiler], embeddedItems: [InteractiveTextNodeEmbeddedItem], attachments: [InteractiveTextNodeAttachment], additionalTrailingLine: (CTLine, Double)?) { self.line = line self.constrainedWidth = constrainedWidth self.frame = frame @@ -129,6 +130,7 @@ private final class InteractiveTextNodeLine { self.isTruncated = isTruncated self.isRTL = isRTL self.strikethroughs = strikethroughs + self.underlines = underlines self.spoilers = spoilers self.spoilerWords = spoilerWords self.embeddedItems = embeddedItems @@ -1452,6 +1454,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn isTruncated: false, isRTL: false, strikethroughs: [], + underlines: [], spoilers: [], spoilerWords: [], embeddedItems: [], @@ -1493,6 +1496,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn isTruncated: false, isRTL: isRTL && segment.blockQuote == nil, strikethroughs: [], + underlines: [], spoilers: [], spoilerWords: [], embeddedItems: [], @@ -1551,6 +1555,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn isTruncated: true, isRTL: lastLine.isRTL, strikethroughs: [], + underlines: [], spoilers: [], spoilerWords: [], embeddedItems: [], @@ -1605,6 +1610,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn isTruncated: true, isRTL: lastLine.isRTL, strikethroughs: [], + underlines: [], spoilers: [], spoilerWords: [], embeddedItems: [], @@ -1736,6 +1742,11 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn let upperX = ceil(CTLineGetOffsetForStringIndex(line.line, range.location + range.length, nil)) let x = lowerX < upperX ? lowerX : upperX line.strikethroughs.append(InteractiveTextNodeStrikethrough(range: range, frame: CGRect(x: x, y: 0.0, width: abs(upperX - lowerX), height: line.frame.height))) + } else if let _ = attributes[NSAttributedString.Key.underlineStyle] { + let lowerX = floor(CTLineGetOffsetForStringIndex(line.line, range.location, nil)) + let upperX = ceil(CTLineGetOffsetForStringIndex(line.line, range.location + range.length, nil)) + let x = lowerX < upperX ? lowerX : upperX + line.underlines.append(InteractiveTextNodeStrikethrough(range: range, frame: CGRect(x: x, y: 0.0, width: abs(upperX - lowerX), height: line.frame.height))) } if let embeddedItem = (attributes[NSAttributedString.Key(rawValue: "TelegramEmbeddedItem")] as? AnyHashable ?? attributes[NSAttributedString.Key(rawValue: "Attribute__EmbeddedItem")] as? AnyHashable) { @@ -2090,6 +2101,14 @@ final class TextContentItem { } } +private let drawUnderlinesManually: Bool = { + if #available(iOS 18.0, *) { + return true + } else { + return false + } +}() + final class TextContentItemLayer: SimpleLayer { final class Params { let item: TextContentItem @@ -2322,6 +2341,46 @@ final class TextContentItemLayer: SimpleLayer { } } + if drawUnderlinesManually { + if !line.strikethroughs.isEmpty { + for strikethrough in line.strikethroughs { + guard let lineRange = line.range else { + continue + } + var textColor: UIColor? + params.item.attributedString?.enumerateAttributes(in: NSMakeRange(lineRange.location, lineRange.length), options: []) { attributes, range, _ in + if range == strikethrough.range, let color = attributes[NSAttributedString.Key.foregroundColor] as? UIColor { + textColor = color + } + } + if let textColor = textColor { + context.setFillColor(textColor.cgColor) + } + let frame = strikethrough.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY) + context.fill(CGRect(x: frame.minX, y: frame.midY, width: frame.width, height: 1.0)) + } + } + + if !line.underlines.isEmpty { + for strikethrough in line.underlines { + guard let lineRange = line.range else { + continue + } + var textColor: UIColor? + params.item.attributedString?.enumerateAttributes(in: NSMakeRange(lineRange.location, lineRange.length), options: []) { attributes, range, _ in + if range == strikethrough.range, let color = attributes[NSAttributedString.Key.foregroundColor] as? UIColor { + textColor = color + } + } + if let textColor = textColor { + context.setFillColor(textColor.cgColor) + } + let frame = strikethrough.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY) + context.fill(CGRect(x: frame.minX, y: frame.maxY - 2.0, width: frame.width, height: 1.0)) + } + } + } + if let (additionalTrailingLine, _) = line.additionalTrailingLine { context.textPosition = CGPoint(x: lineFrame.minX + line.intrinsicWidth, y: lineFrame.maxY - line.descent) From 9d7d4bd245d6b86789f2e34cdb946e31a50448b5 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 27 Sep 2024 20:29:28 +0800 Subject: [PATCH 3/9] Re-disable video chats for now --- submodules/TelegramCallsUI/Sources/VoiceChatController.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index db52366c58..3d6d2ebb00 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -7098,7 +7098,10 @@ final class VoiceChatContextReferenceContentSource: ContextReferenceContentSourc } public func shouldUseV2VideoChatImpl(context: AccountContext) -> Bool { - var useV2 = true + var useV2 = false + if let data = context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_enable_videochatui_v2"] { + useV2 = true + } if context.sharedContext.immediateExperimentalUISettings.disableCallV2 { useV2 = false } From 1d1ea447ad3fdbf9483ce337c532cf0793f3ab3a Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 27 Sep 2024 17:03:21 +0400 Subject: [PATCH 4/9] Various fixes --- .../Sources/ChatEntityKeyboardInputNode.swift | 4 ++-- .../Sources/GiftSetupScreen.swift | 3 ++- .../Animations/ChatListNoResults.tgs | Bin 8590 -> 11826 bytes 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index 4bfea4558a..71c184ae51 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -468,7 +468,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { |> distinctUntilChanged } - public init(context: AccountContext, currentInputData: InputData, updatedInputData: Signal, defaultToEmojiTab: Bool, opaqueTopPanelBackground: Bool = false, useOpaqueTheme: Bool = false, interaction: ChatEntityKeyboardInputNode.Interaction?, chatPeerId: PeerId?, stateContext: StateContext?) { + public init(context: AccountContext, currentInputData: InputData, updatedInputData: Signal, defaultToEmojiTab: Bool, opaqueTopPanelBackground: Bool = false, useOpaqueTheme: Bool = false, interaction: ChatEntityKeyboardInputNode.Interaction?, chatPeerId: PeerId?, stateContext: StateContext?, forceHasPremium: Bool = false) { self.context = context self.currentInputData = currentInputData self.defaultToEmojiTab = defaultToEmojiTab @@ -689,7 +689,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } } - if file.isPremiumEmoji && !hasPremium && groupId != AnyHashable("peerSpecific") { + if file.isPremiumEmoji && !hasPremium && groupId != AnyHashable("peerSpecific") && !forceHasPremium { var animateInAsReplacement = false if let currentUndoOverlayController = strongSelf.currentUndoOverlayController { currentUndoOverlayController.dismissWithCommitActionAndReplacementAnimation() diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index a44ba40f60..5f939cebbe 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -914,7 +914,8 @@ final class GiftSetupScreenComponent: Component { useOpaqueTheme: true, interaction: self.inputMediaInteraction, chatPeerId: nil, - stateContext: self.inputMediaNodeStateContext + stateContext: self.inputMediaNodeStateContext, + forceHasPremium: true ) inputMediaNode.clipsToBounds = true diff --git a/submodules/TelegramUI/Resources/Animations/ChatListNoResults.tgs b/submodules/TelegramUI/Resources/Animations/ChatListNoResults.tgs index 62a21279fb766aa9aa25ddc077936f2f51f34031..1cde51426d640fc5ed44f94df27a589eacc73062 100644 GIT binary patch literal 11826 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 literal 8590 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 From a2ff1313ca07c9d3df5693626fc1c2608833d0fe Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 1 Oct 2024 17:32:43 +0400 Subject: [PATCH 5/9] Gift improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 19 +- .../AccountContext/Sources/Premium.swift | 5 + .../Sources/InAppPurchaseManager.swift | 20 +- .../Sources/CreateGiveawayController.swift | 2 +- .../PremiumUI/Sources/PremiumGiftScreen.swift | 2 +- submodules/TelegramApi/Sources/Api0.swift | 6 +- submodules/TelegramApi/Sources/Api12.swift | 20 +- submodules/TelegramApi/Sources/Api14.swift | 40 ++- .../ApiUtils/StoreMessage_Telegram.swift | 2 +- .../ApiUtils/TelegramMediaAction.swift | 28 +- .../Sources/State/Serialization.swift | 2 +- .../SyncCore_TelegramMediaAction.swift | 31 +- .../TelegramEngine/Payments/AppStore.swift | 13 +- .../Payments/BotPaymentForm.swift | 2 +- .../Sources/ServiceMessageStrings.swift | 6 +- .../ChatMessageGiftBubbleContentNode.swift | 131 +++++++- .../Sources/GiftOptionsScreen.swift | 182 ++++------- .../Components/Gifts/GiftSetupScreen/BUILD | 2 + .../Sources/ChatGiftPreviewItem.swift | 29 +- .../Sources/GiftSetupScreen.swift | 295 ++++++++++++++---- .../Sources/GiftViewScreen.swift | 10 +- .../Sources/DynamicIslandBlurNode.swift | 4 +- .../TelegramUI/Sources/ChatController.swift | 4 +- 23 files changed, 584 insertions(+), 271 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 68fc11e9fb..5bf388371a 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12950,7 +12950,7 @@ Sorry for the inconvenience."; "SharedMedia.GiftCount_any" = "%@ gifts"; "Stars.Info.Title" = "What are Stars?"; -"Stars.Info.Description" = "Buy packages of Stars on Telegram that let you do following:"; +"Stars.Info.Description" = "Telegram Stars allow users to:"; "Stars.Info.Gift.Title" = "Send Gifts to Friends"; "Stars.Info.Gift.Text" = "Give your friends gifts that can be kept on their profiles or converted to Stars."; "Stars.Info.Miniapp.Title" = "Use Stars in Miniapps"; @@ -12976,6 +12976,7 @@ Sorry for the inconvenience."; "Gift.View.HiddenName" = "Hidden Name"; "Gift.View.Date" = "Date"; "Gift.View.Availability" = "Availability"; +"Gift.View.Availability.Of" = "%1$@ of %2$@"; "Gift.View.Hide" = "Hide from My Page"; "Gift.View.Display" = "Display on My Page"; "Gift.View.Convert" = "Convert to %@"; @@ -12986,7 +12987,7 @@ Sorry for the inconvenience."; "Gift.Hidden.Title" = "Gift Removed from Profile"; "Gift.Hidden.Text" = "The gift is no longer displayed in [your profile]()."; "Gift.Convert.Title" = "Convert Gift to Stars"; -"Gift.Convert.Text" = "Do you want to convert this gift from **%1$@** to **%2$@**?\n\nThis action cannot be undone."; +"Gift.Convert.Text" = "Do you want to convert this gift from **%1$@** to **%2$@**?\n\nThis will permanently destroy the gift."; "Gift.Convert.Stars_1" = "%@ Star"; "Gift.Convert.Stars_any" = "%@ Stars"; "Gift.Convert.Convert" = "Convert"; @@ -13032,7 +13033,8 @@ Sorry for the inconvenience."; "Gift.Send.Title" = "Send a Gift"; "Gift.Send.Customize.Title" = "CUSTOMIZE YOUR GIFT"; -"Gift.Send.Customize.MessagePlaceholder" = "Enter Message"; +"Gift.Send.Customize.MessagePlaceholder" = "Enter Message (Optional)"; +"Gift.Send.Customize.Info" = "Only %@ will see your message."; "Gift.Send.HideMyName" = "Hide My Name"; "Gift.Send.HideMyName.Info" = "Hide my name and message from visitors to %1$@'s profile. %2$@ will still see your name and message."; "Gift.Send.Send" = "Send a Gift for"; @@ -13051,3 +13053,14 @@ Sorry for the inconvenience."; "Report.Comment.Placeholder.Optional" = "Add Comment (Optional)"; "Report.Comment.Info" = "Please help us by telling what is wrong with the message you have selected."; "Report.Send" = "Send Report"; + +"Notification.PremiumGift.MonthsTitle_1" = "%@ Month Premium"; +"Notification.PremiumGift.MonthsTitle_any" = "%@ Months Premium"; + +"Notification.PremiumGift.YearsTitle_1" = "%@ Year Premium"; +"Notification.PremiumGift.YearsTitle_any" = "%@ Years Premium"; + +"Notification.PremiumGift.SubscriptionDescription" = "Subscription for exclusive Telegram features."; + +"Notification.StarsGift.Stars_1" = "%@ Star"; +"Notification.StarsGift.Stars_any" = "%@ Stars"; diff --git a/submodules/AccountContext/Sources/Premium.swift b/submodules/AccountContext/Sources/Premium.swift index 0edf8c349c..9134f6d26b 100644 --- a/submodules/AccountContext/Sources/Premium.swift +++ b/submodules/AccountContext/Sources/Premium.swift @@ -143,6 +143,7 @@ public struct PremiumConfiguration { showPremiumGiftInTextField: false, giveawayGiftsPurchaseAvailable: false, starsGiftsPurchaseAvailable: false, + starGiftsPurchaseBlocked: true, boostsPerGiftCount: 3, audioTransciptionTrialMaxDuration: 300, audioTransciptionTrialCount: 2, @@ -170,6 +171,7 @@ public struct PremiumConfiguration { public let showPremiumGiftInTextField: Bool public let giveawayGiftsPurchaseAvailable: Bool public let starsGiftsPurchaseAvailable: Bool + public let starGiftsPurchaseBlocked: Bool public let boostsPerGiftCount: Int32 public let audioTransciptionTrialMaxDuration: Int32 public let audioTransciptionTrialCount: Int32 @@ -196,6 +198,7 @@ public struct PremiumConfiguration { showPremiumGiftInTextField: Bool, giveawayGiftsPurchaseAvailable: Bool, starsGiftsPurchaseAvailable: Bool, + starGiftsPurchaseBlocked: Bool, boostsPerGiftCount: Int32, audioTransciptionTrialMaxDuration: Int32, audioTransciptionTrialCount: Int32, @@ -221,6 +224,7 @@ public struct PremiumConfiguration { self.showPremiumGiftInTextField = showPremiumGiftInTextField self.giveawayGiftsPurchaseAvailable = giveawayGiftsPurchaseAvailable self.starsGiftsPurchaseAvailable = starsGiftsPurchaseAvailable + self.starGiftsPurchaseBlocked = starGiftsPurchaseBlocked self.boostsPerGiftCount = boostsPerGiftCount self.audioTransciptionTrialMaxDuration = audioTransciptionTrialMaxDuration self.audioTransciptionTrialCount = audioTransciptionTrialCount @@ -254,6 +258,7 @@ public struct PremiumConfiguration { showPremiumGiftInTextField: data["premium_gift_text_field_icon"] as? Bool ?? defaultValue.showPremiumGiftInTextField, giveawayGiftsPurchaseAvailable: data["giveaway_gifts_purchase_available"] as? Bool ?? defaultValue.giveawayGiftsPurchaseAvailable, starsGiftsPurchaseAvailable: data["stars_gifts_enabled"] as? Bool ?? defaultValue.starsGiftsPurchaseAvailable, + starGiftsPurchaseBlocked: data["stargifts_blocked"] as? Bool ?? defaultValue.starGiftsPurchaseBlocked, boostsPerGiftCount: get(data["boosts_per_sent_gift"]) ?? defaultValue.boostsPerGiftCount, audioTransciptionTrialMaxDuration: get(data["transcribe_audio_trial_duration_max"]) ?? defaultValue.audioTransciptionTrialMaxDuration, audioTransciptionTrialCount: get(data["transcribe_audio_trial_weekly_number"]) ?? defaultValue.audioTransciptionTrialCount, diff --git a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift index b607216341..0c955d674c 100644 --- a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift +++ b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift @@ -623,6 +623,8 @@ private final class PendingInAppPurchaseState: Codable { case untilDate case stars case users + case text + case entities } enum PurposeType: Int32 { @@ -641,7 +643,7 @@ private final class PendingInAppPurchaseState: Codable { case upgrade case restore case gift(peerId: EnginePeer.Id) - case giftCode(peerIds: [EnginePeer.Id], boostPeer: EnginePeer.Id?) + case giftCode(peerIds: [EnginePeer.Id], boostPeer: EnginePeer.Id?, text: String?, entities: [MessageTextEntity]?) case giveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32) case stars(count: Int64) case starsGift(peerId: EnginePeer.Id, count: Int64) @@ -665,7 +667,9 @@ private final class PendingInAppPurchaseState: Codable { case .giftCode: self = .giftCode( peerIds: try container.decode([Int64].self, forKey: .peers).map { EnginePeer.Id($0) }, - boostPeer: try container.decodeIfPresent(Int64.self, forKey: .boostPeer).flatMap({ EnginePeer.Id($0) }) + boostPeer: try container.decodeIfPresent(Int64.self, forKey: .boostPeer).flatMap({ EnginePeer.Id($0) }), + text: try container.decodeIfPresent(String.self, forKey: .text), + entities: try container.decodeIfPresent([MessageTextEntity].self, forKey: .entities) ) case .giveaway: self = .giveaway( @@ -718,10 +722,12 @@ private final class PendingInAppPurchaseState: Codable { case let .gift(peerId): try container.encode(PurposeType.gift.rawValue, forKey: .type) try container.encode(peerId.toInt64(), forKey: .peer) - case let .giftCode(peerIds, boostPeer): + case let .giftCode(peerIds, boostPeer, text, entities): try container.encode(PurposeType.giftCode.rawValue, forKey: .type) try container.encode(peerIds.map { $0.toInt64() }, forKey: .peers) try container.encodeIfPresent(boostPeer?.toInt64(), forKey: .boostPeer) + try container.encodeIfPresent(text, forKey: .text) + try container.encodeIfPresent(entities, forKey: .entities) case let .giveaway(boostPeer, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate): try container.encode(PurposeType.giveaway.rawValue, forKey: .type) try container.encode(boostPeer.toInt64(), forKey: .boostPeer) @@ -764,8 +770,8 @@ private final class PendingInAppPurchaseState: Codable { self = .restore case let .gift(peerId, _, _): self = .gift(peerId: peerId) - case let .giftCode(peerIds, boostPeer, _, _): - self = .giftCode(peerIds: peerIds, boostPeer: boostPeer) + case let .giftCode(peerIds, boostPeer, _, _, text, entities): + self = .giftCode(peerIds: peerIds, boostPeer: boostPeer, text: text, entities: entities) case let .giveaway(boostPeer, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate, _, _): self = .giveaway(boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, showWinners: showWinners, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate) case let .stars(count, _, _): @@ -788,8 +794,8 @@ private final class PendingInAppPurchaseState: Codable { return .restore case let .gift(peerId): return .gift(peerId: peerId, currency: currency, amount: amount) - case let .giftCode(peerIds, boostPeer): - return .giftCode(peerIds: peerIds, boostPeer: boostPeer, currency: currency, amount: amount) + case let .giftCode(peerIds, boostPeer, text, entities): + return .giftCode(peerIds: peerIds, boostPeer: boostPeer, currency: currency, amount: amount, text: text, entities: entities) case let .giveaway(boostPeer, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate): return .giveaway(boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, showWinners: showWinners, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount) case let .stars(count): diff --git a/submodules/PremiumUI/Sources/CreateGiveawayController.swift b/submodules/PremiumUI/Sources/CreateGiveawayController.swift index 3e6c32d5b0..b878cc81f9 100644 --- a/submodules/PremiumUI/Sources/CreateGiveawayController.swift +++ b/submodules/PremiumUI/Sources/CreateGiveawayController.swift @@ -1356,7 +1356,7 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio return } let (currency, amount) = selectedProduct.storeProduct.priceCurrencyAndAmount - purpose = .giftCode(peerIds: state.peers, boostPeer: peerId, currency: currency, amount: amount) + purpose = .giftCode(peerIds: state.peers, boostPeer: peerId, currency: currency, amount: amount, text: nil, entities: nil) quantity = Int32(state.peers.count) storeProduct = selectedProduct.storeProduct case .starsGiveaway: diff --git a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift index bc802fe14c..af760b9b64 100644 --- a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift @@ -910,7 +910,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent { if self.source == .profile || self.source == .attachMenu, let peerId = self.peerIds.first { purpose = .gift(peerId: peerId, currency: currency, amount: amount) } else { - purpose = .giftCode(peerIds: self.peerIds, boostPeer: nil, currency: currency, amount: amount) + purpose = .giftCode(peerIds: self.peerIds, boostPeer: nil, currency: currency, amount: amount, text: nil, entities: nil) quantity = Int32(self.peerIds.count) } diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index aa22fb967b..14becffa38 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -467,7 +467,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[70813275] = { return Api.InputStickeredMedia.parse_inputStickeredMediaDocument($0) } dict[1251549527] = { return Api.InputStickeredMedia.parse_inputStickeredMediaPhoto($0) } dict[1634697192] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentGiftPremium($0) } - dict[-1551868097] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumGiftCode($0) } + dict[-75955309] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumGiftCode($0) } dict[369444042] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumGiveaway($0) } dict[-1502273946] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumSubscription($0) } dict[494149367] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentStarsGift($0) } @@ -551,8 +551,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1230047312] = { return Api.MessageAction.parse_messageActionEmpty($0) } dict[-1834538890] = { return Api.MessageAction.parse_messageActionGameScore($0) } dict[-1730095465] = { return Api.MessageAction.parse_messageActionGeoProximityReached($0) } - dict[1737240073] = { return Api.MessageAction.parse_messageActionGiftCode($0) } - dict[-935499028] = { return Api.MessageAction.parse_messageActionGiftPremium($0) } + dict[1456486804] = { return Api.MessageAction.parse_messageActionGiftCode($0) } + dict[1818391802] = { return Api.MessageAction.parse_messageActionGiftPremium($0) } dict[1171632161] = { return Api.MessageAction.parse_messageActionGiftStars($0) } dict[-1475391004] = { return Api.MessageAction.parse_messageActionGiveawayLaunch($0) } dict[-2015170219] = { return Api.MessageAction.parse_messageActionGiveawayResults($0) } diff --git a/submodules/TelegramApi/Sources/Api12.swift b/submodules/TelegramApi/Sources/Api12.swift index d626af8099..5a3bc02c8b 100644 --- a/submodules/TelegramApi/Sources/Api12.swift +++ b/submodules/TelegramApi/Sources/Api12.swift @@ -711,7 +711,7 @@ public extension Api { public extension Api { indirect enum InputStorePaymentPurpose: TypeConstructorDescription { case inputStorePaymentGiftPremium(userId: Api.InputUser, currency: String, amount: Int64) - case inputStorePaymentPremiumGiftCode(flags: Int32, users: [Api.InputUser], boostPeer: Api.InputPeer?, currency: String, amount: Int64) + case inputStorePaymentPremiumGiftCode(flags: Int32, users: [Api.InputUser], boostPeer: Api.InputPeer?, currency: String, amount: Int64, message: Api.TextWithEntities?) case inputStorePaymentPremiumGiveaway(flags: Int32, boostPeer: Api.InputPeer, additionalPeers: [Api.InputPeer]?, countriesIso2: [String]?, prizeDescription: String?, randomId: Int64, untilDate: Int32, currency: String, amount: Int64) case inputStorePaymentPremiumSubscription(flags: Int32) case inputStorePaymentStarsGift(userId: Api.InputUser, stars: Int64, currency: String, amount: Int64) @@ -728,9 +728,9 @@ public extension Api { serializeString(currency, buffer: buffer, boxed: false) serializeInt64(amount, buffer: buffer, boxed: false) break - case .inputStorePaymentPremiumGiftCode(let flags, let users, let boostPeer, let currency, let amount): + case .inputStorePaymentPremiumGiftCode(let flags, let users, let boostPeer, let currency, let amount, let message): if boxed { - buffer.appendInt32(-1551868097) + buffer.appendInt32(-75955309) } serializeInt32(flags, buffer: buffer, boxed: false) buffer.appendInt32(481674261) @@ -741,6 +741,7 @@ public extension Api { if Int(flags) & Int(1 << 0) != 0 {boostPeer!.serialize(buffer, true)} serializeString(currency, buffer: buffer, boxed: false) serializeInt64(amount, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 1) != 0 {message!.serialize(buffer, true)} break case .inputStorePaymentPremiumGiveaway(let flags, let boostPeer, let additionalPeers, let countriesIso2, let prizeDescription, let randomId, let untilDate, let currency, let amount): if boxed { @@ -818,8 +819,8 @@ public extension Api { switch self { case .inputStorePaymentGiftPremium(let userId, let currency, let amount): return ("inputStorePaymentGiftPremium", [("userId", userId as Any), ("currency", currency as Any), ("amount", amount as Any)]) - case .inputStorePaymentPremiumGiftCode(let flags, let users, let boostPeer, let currency, let amount): - return ("inputStorePaymentPremiumGiftCode", [("flags", flags as Any), ("users", users as Any), ("boostPeer", boostPeer as Any), ("currency", currency as Any), ("amount", amount as Any)]) + case .inputStorePaymentPremiumGiftCode(let flags, let users, let boostPeer, let currency, let amount, let message): + return ("inputStorePaymentPremiumGiftCode", [("flags", flags as Any), ("users", users as Any), ("boostPeer", boostPeer as Any), ("currency", currency as Any), ("amount", amount as Any), ("message", message as Any)]) case .inputStorePaymentPremiumGiveaway(let flags, let boostPeer, let additionalPeers, let countriesIso2, let prizeDescription, let randomId, let untilDate, let currency, let amount): return ("inputStorePaymentPremiumGiveaway", [("flags", flags as Any), ("boostPeer", boostPeer as Any), ("additionalPeers", additionalPeers as Any), ("countriesIso2", countriesIso2 as Any), ("prizeDescription", prizeDescription as Any), ("randomId", randomId as Any), ("untilDate", untilDate as Any), ("currency", currency as Any), ("amount", amount as Any)]) case .inputStorePaymentPremiumSubscription(let flags): @@ -867,13 +868,18 @@ public extension Api { _4 = parseString(reader) var _5: Int64? _5 = reader.readInt64() + var _6: Api.TextWithEntities? + if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { + _6 = Api.parse(reader, signature: signature) as? Api.TextWithEntities + } } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.InputStorePaymentPurpose.inputStorePaymentPremiumGiftCode(flags: _1!, users: _2!, boostPeer: _3, currency: _4!, amount: _5!) + let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.InputStorePaymentPurpose.inputStorePaymentPremiumGiftCode(flags: _1!, users: _2!, boostPeer: _3, currency: _4!, amount: _5!, message: _6) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api14.swift b/submodules/TelegramApi/Sources/Api14.swift index 7f7ead710e..ef5c9801fb 100644 --- a/submodules/TelegramApi/Sources/Api14.swift +++ b/submodules/TelegramApi/Sources/Api14.swift @@ -986,8 +986,8 @@ public extension Api { case messageActionEmpty case messageActionGameScore(gameId: Int64, score: Int32) case messageActionGeoProximityReached(fromId: Api.Peer, toId: Api.Peer, distance: Int32) - case messageActionGiftCode(flags: Int32, boostPeer: Api.Peer?, months: Int32, slug: String, currency: String?, amount: Int64?, cryptoCurrency: String?, cryptoAmount: Int64?) - case messageActionGiftPremium(flags: Int32, currency: String, amount: Int64, months: Int32, cryptoCurrency: String?, cryptoAmount: Int64?) + case messageActionGiftCode(flags: Int32, boostPeer: Api.Peer?, months: Int32, slug: String, currency: String?, amount: Int64?, cryptoCurrency: String?, cryptoAmount: Int64?, message: Api.TextWithEntities?) + case messageActionGiftPremium(flags: Int32, currency: String, amount: Int64, months: Int32, cryptoCurrency: String?, cryptoAmount: Int64?, message: Api.TextWithEntities?) case messageActionGiftStars(flags: Int32, currency: String, amount: Int64, stars: Int64, cryptoCurrency: String?, cryptoAmount: Int64?, transactionId: String?) case messageActionGiveawayLaunch(flags: Int32, stars: Int64?) case messageActionGiveawayResults(flags: Int32, winnersCount: Int32, unclaimedCount: Int32) @@ -1141,9 +1141,9 @@ public extension Api { toId.serialize(buffer, true) serializeInt32(distance, buffer: buffer, boxed: false) break - case .messageActionGiftCode(let flags, let boostPeer, let months, let slug, let currency, let amount, let cryptoCurrency, let cryptoAmount): + case .messageActionGiftCode(let flags, let boostPeer, let months, let slug, let currency, let amount, let cryptoCurrency, let cryptoAmount, let message): if boxed { - buffer.appendInt32(1737240073) + buffer.appendInt32(1456486804) } serializeInt32(flags, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 1) != 0 {boostPeer!.serialize(buffer, true)} @@ -1153,10 +1153,11 @@ public extension Api { if Int(flags) & Int(1 << 2) != 0 {serializeInt64(amount!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 3) != 0 {serializeString(cryptoCurrency!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 3) != 0 {serializeInt64(cryptoAmount!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 4) != 0 {message!.serialize(buffer, true)} break - case .messageActionGiftPremium(let flags, let currency, let amount, let months, let cryptoCurrency, let cryptoAmount): + case .messageActionGiftPremium(let flags, let currency, let amount, let months, let cryptoCurrency, let cryptoAmount, let message): if boxed { - buffer.appendInt32(-935499028) + buffer.appendInt32(1818391802) } serializeInt32(flags, buffer: buffer, boxed: false) serializeString(currency, buffer: buffer, boxed: false) @@ -1164,6 +1165,7 @@ public extension Api { serializeInt32(months, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {serializeString(cryptoCurrency!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 0) != 0 {serializeInt64(cryptoAmount!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {message!.serialize(buffer, true)} break case .messageActionGiftStars(let flags, let currency, let amount, let stars, let cryptoCurrency, let cryptoAmount, let transactionId): if boxed { @@ -1439,10 +1441,10 @@ public extension Api { return ("messageActionGameScore", [("gameId", gameId as Any), ("score", score as Any)]) case .messageActionGeoProximityReached(let fromId, let toId, let distance): return ("messageActionGeoProximityReached", [("fromId", fromId as Any), ("toId", toId as Any), ("distance", distance as Any)]) - case .messageActionGiftCode(let flags, let boostPeer, let months, let slug, let currency, let amount, let cryptoCurrency, let cryptoAmount): - return ("messageActionGiftCode", [("flags", flags as Any), ("boostPeer", boostPeer as Any), ("months", months as Any), ("slug", slug as Any), ("currency", currency as Any), ("amount", amount as Any), ("cryptoCurrency", cryptoCurrency as Any), ("cryptoAmount", cryptoAmount as Any)]) - case .messageActionGiftPremium(let flags, let currency, let amount, let months, let cryptoCurrency, let cryptoAmount): - return ("messageActionGiftPremium", [("flags", flags as Any), ("currency", currency as Any), ("amount", amount as Any), ("months", months as Any), ("cryptoCurrency", cryptoCurrency as Any), ("cryptoAmount", cryptoAmount as Any)]) + case .messageActionGiftCode(let flags, let boostPeer, let months, let slug, let currency, let amount, let cryptoCurrency, let cryptoAmount, let message): + return ("messageActionGiftCode", [("flags", flags as Any), ("boostPeer", boostPeer as Any), ("months", months as Any), ("slug", slug as Any), ("currency", currency as Any), ("amount", amount as Any), ("cryptoCurrency", cryptoCurrency as Any), ("cryptoAmount", cryptoAmount as Any), ("message", message as Any)]) + case .messageActionGiftPremium(let flags, let currency, let amount, let months, let cryptoCurrency, let cryptoAmount, let message): + return ("messageActionGiftPremium", [("flags", flags as Any), ("currency", currency as Any), ("amount", amount as Any), ("months", months as Any), ("cryptoCurrency", cryptoCurrency as Any), ("cryptoAmount", cryptoAmount as Any), ("message", message as Any)]) case .messageActionGiftStars(let flags, let currency, let amount, let stars, let cryptoCurrency, let cryptoAmount, let transactionId): return ("messageActionGiftStars", [("flags", flags as Any), ("currency", currency as Any), ("amount", amount as Any), ("stars", stars as Any), ("cryptoCurrency", cryptoCurrency as Any), ("cryptoAmount", cryptoAmount as Any), ("transactionId", transactionId as Any)]) case .messageActionGiveawayLaunch(let flags, let stars): @@ -1718,6 +1720,10 @@ public extension Api { if Int(_1!) & Int(1 << 3) != 0 {_7 = parseString(reader) } var _8: Int64? if Int(_1!) & Int(1 << 3) != 0 {_8 = reader.readInt64() } + var _9: Api.TextWithEntities? + if Int(_1!) & Int(1 << 4) != 0 {if let signature = reader.readInt32() { + _9 = Api.parse(reader, signature: signature) as? Api.TextWithEntities + } } let _c1 = _1 != nil let _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil let _c3 = _3 != nil @@ -1726,8 +1732,9 @@ public extension Api { let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil let _c7 = (Int(_1!) & Int(1 << 3) == 0) || _7 != nil let _c8 = (Int(_1!) & Int(1 << 3) == 0) || _8 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { - return Api.MessageAction.messageActionGiftCode(flags: _1!, boostPeer: _2, months: _3!, slug: _4!, currency: _5, amount: _6, cryptoCurrency: _7, cryptoAmount: _8) + let _c9 = (Int(_1!) & Int(1 << 4) == 0) || _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return Api.MessageAction.messageActionGiftCode(flags: _1!, boostPeer: _2, months: _3!, slug: _4!, currency: _5, amount: _6, cryptoCurrency: _7, cryptoAmount: _8, message: _9) } else { return nil @@ -1746,14 +1753,19 @@ public extension Api { if Int(_1!) & Int(1 << 0) != 0 {_5 = parseString(reader) } var _6: Int64? if Int(_1!) & Int(1 << 0) != 0 {_6 = reader.readInt64() } + var _7: Api.TextWithEntities? + if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { + _7 = Api.parse(reader, signature: signature) as? Api.TextWithEntities + } } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.MessageAction.messageActionGiftPremium(flags: _1!, currency: _2!, amount: _3!, months: _4!, cryptoCurrency: _5, cryptoAmount: _6) + let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.MessageAction.messageActionGiftPremium(flags: _1!, currency: _2!, amount: _3!, months: _4!, cryptoCurrency: _5, cryptoAmount: _6, message: _7) } else { return nil diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index 302ec5da6b..3fa705177b 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -254,7 +254,7 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] { } case let .messageActionRequestedPeer(_, peers): result.append(contentsOf: peers.map(\.peerId)) - case let .messageActionGiftCode(_, boostPeer, _, _, _, _, _, _): + case let .messageActionGiftCode(_, boostPeer, _, _, _, _, _, _, _): if let boostPeer = boostPeer { result.append(boostPeer.peerId) } diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift index 2557eb591c..de186a7a37 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift @@ -100,8 +100,18 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe return TelegramMediaAction(action: .joinedByRequest) case let .messageActionWebViewDataSentMe(text, _), let .messageActionWebViewDataSent(text): return TelegramMediaAction(action: .webViewData(text)) - case let .messageActionGiftPremium(_, currency, amount, months, cryptoCurrency, cryptoAmount): - return TelegramMediaAction(action: .giftPremium(currency: currency, amount: amount, months: months, cryptoCurrency: cryptoCurrency, cryptoAmount: cryptoAmount)) + case let .messageActionGiftPremium(_, currency, amount, months, cryptoCurrency, cryptoAmount, message): + let text: String? + let entities: [MessageTextEntity]? + switch message { + case let .textWithEntities(textValue, entitiesValue): + text = textValue + entities = messageTextEntitiesFromApiEntities(entitiesValue) + default: + text = nil + entities = nil + } + return TelegramMediaAction(action: .giftPremium(currency: currency, amount: amount, months: months, cryptoCurrency: cryptoCurrency, cryptoAmount: cryptoAmount, text: text, entities: entities)) case let .messageActionGiftStars(_, currency, amount, stars, cryptoCurrency, cryptoAmount, transactionId): return TelegramMediaAction(action: .giftStars(currency: currency, amount: amount, count: stars, cryptoCurrency: cryptoCurrency, cryptoAmount: cryptoAmount, transactionId: transactionId)) case let .messageActionTopicCreate(_, title, iconColor, iconEmojiId): @@ -133,8 +143,18 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe } else { return TelegramMediaAction(action: .setChatWallpaper(wallpaper: TelegramWallpaper(apiWallpaper: wallpaper), forBoth: (flags & (1 << 1)) != 0)) } - case let .messageActionGiftCode(flags, boostPeer, months, slug, currency, amount, cryptoCurrency, cryptoAmount): - return TelegramMediaAction(action: .giftCode(slug: slug, fromGiveaway: (flags & (1 << 0)) != 0, isUnclaimed: (flags & (1 << 2)) != 0, boostPeerId: boostPeer?.peerId, months: months, currency: currency, amount: amount, cryptoCurrency: cryptoCurrency, cryptoAmount: cryptoAmount)) + case let .messageActionGiftCode(flags, boostPeer, months, slug, currency, amount, cryptoCurrency, cryptoAmount, message): + let text: String? + let entities: [MessageTextEntity]? + switch message { + case let .textWithEntities(textValue, entitiesValue): + text = textValue + entities = messageTextEntitiesFromApiEntities(entitiesValue) + default: + text = nil + entities = nil + } + return TelegramMediaAction(action: .giftCode(slug: slug, fromGiveaway: (flags & (1 << 0)) != 0, isUnclaimed: (flags & (1 << 2)) != 0, boostPeerId: boostPeer?.peerId, months: months, currency: currency, amount: amount, cryptoCurrency: cryptoCurrency, cryptoAmount: cryptoAmount, text: text, entities: entities)) case let .messageActionGiveawayLaunch(_, stars): return TelegramMediaAction(action: .giveawayLaunched(stars: stars)) case let .messageActionGiveawayResults(flags, winners, unclaimed): diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index 12c50ddd30..901664c8fd 100644 --- a/submodules/TelegramCore/Sources/State/Serialization.swift +++ b/submodules/TelegramCore/Sources/State/Serialization.swift @@ -210,7 +210,7 @@ public class BoxedMessage: NSObject { public class Serialization: NSObject, MTSerialization { public func currentLayer() -> UInt { - return 189 + return 190 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift index 59239b2aba..8fb86d7327 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift @@ -114,7 +114,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case setChatTheme(emoji: String) case joinedByRequest case webViewData(String) - case giftPremium(currency: String, amount: Int64, months: Int32, cryptoCurrency: String?, cryptoAmount: Int64?) + case giftPremium(currency: String, amount: Int64, months: Int32, cryptoCurrency: String?, cryptoAmount: Int64?, text: String?, entities: [MessageTextEntity]?) case topicCreated(title: String, iconColor: Int32, iconFileId: Int64?) case topicEdited(components: [ForumTopicEditComponent]) case suggestedProfilePhoto(image: TelegramMediaImage?) @@ -122,7 +122,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case requestedPeer(buttonId: Int32, peerIds: [PeerId]) case setChatWallpaper(wallpaper: TelegramWallpaper, forBoth: Bool) case setSameChatWallpaper(wallpaper: TelegramWallpaper) - case giftCode(slug: String, fromGiveaway: Bool, isUnclaimed: Bool, boostPeerId: PeerId?, months: Int32, currency: String?, amount: Int64?, cryptoCurrency: String?, cryptoAmount: Int64?) + case giftCode(slug: String, fromGiveaway: Bool, isUnclaimed: Bool, boostPeerId: PeerId?, months: Int32, currency: String?, amount: Int64?, cryptoCurrency: String?, cryptoAmount: Int64?, text: String?, entities: [MessageTextEntity]?) case giveawayLaunched(stars: Int64?) case joinedChannel case giveawayResults(winners: Int32, unclaimed: Int32, stars: Bool) @@ -200,7 +200,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case 26: self = .webViewData(decoder.decodeStringForKey("t", orElse: "")) case 27: - self = .giftPremium(currency: decoder.decodeStringForKey("currency", orElse: ""), amount: decoder.decodeInt64ForKey("amount", orElse: 0), months: decoder.decodeInt32ForKey("months", orElse: 0), cryptoCurrency: decoder.decodeOptionalStringForKey("cryptoCurrency"), cryptoAmount: decoder.decodeOptionalInt64ForKey("cryptoAmount")) + self = .giftPremium(currency: decoder.decodeStringForKey("currency", orElse: ""), amount: decoder.decodeInt64ForKey("amount", orElse: 0), months: decoder.decodeInt32ForKey("months", orElse: 0), cryptoCurrency: decoder.decodeOptionalStringForKey("cryptoCurrency"), cryptoAmount: decoder.decodeOptionalInt64ForKey("cryptoAmount"), text: decoder.decodeOptionalStringForKey("text"), entities: decoder.decodeOptionalObjectArrayWithDecoderForKey("entities")) case 28: self = .topicCreated(title: decoder.decodeStringForKey("title", orElse: ""), iconColor: decoder.decodeInt32ForKey("iconColor", orElse: 0), iconFileId: decoder.decodeOptionalInt64ForKey("iconFileId")) case 29: @@ -230,7 +230,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case 35: self = .botAppAccessGranted(appName: decoder.decodeOptionalStringForKey("app"), type: decoder.decodeOptionalInt32ForKey("atp").flatMap { BotSendMessageAccessGrantedType(rawValue: $0) }) case 36: - self = .giftCode(slug: decoder.decodeStringForKey("slug", orElse: ""), fromGiveaway: decoder.decodeBoolForKey("give", orElse: false), isUnclaimed: decoder.decodeBoolForKey("unclaimed", orElse: false), boostPeerId: decoder.decodeOptionalInt64ForKey("pi").flatMap { PeerId($0) }, months: decoder.decodeInt32ForKey("months", orElse: 0), currency: decoder.decodeOptionalStringForKey("currency"), amount: decoder.decodeOptionalInt64ForKey("amount"), cryptoCurrency: decoder.decodeOptionalStringForKey("cryptoCurrency"), cryptoAmount: decoder.decodeOptionalInt64ForKey("cryptoAmount")) + self = .giftCode(slug: decoder.decodeStringForKey("slug", orElse: ""), fromGiveaway: decoder.decodeBoolForKey("give", orElse: false), isUnclaimed: decoder.decodeBoolForKey("unclaimed", orElse: false), boostPeerId: decoder.decodeOptionalInt64ForKey("pi").flatMap { PeerId($0) }, months: decoder.decodeInt32ForKey("months", orElse: 0), currency: decoder.decodeOptionalStringForKey("currency"), amount: decoder.decodeOptionalInt64ForKey("amount"), cryptoCurrency: decoder.decodeOptionalStringForKey("cryptoCurrency"), cryptoAmount: decoder.decodeOptionalInt64ForKey("cryptoAmount"), text: decoder.decodeOptionalStringForKey("text"), entities: decoder.decodeOptionalObjectArrayWithDecoderForKey("entities")) case 37: self = .giveawayLaunched(stars: decoder.decodeOptionalInt64ForKey("stars")) case 38: @@ -382,7 +382,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case let .webViewData(text): encoder.encodeInt32(26, forKey: "_rawValue") encoder.encodeString(text, forKey: "t") - case let .giftPremium(currency, amount, months, cryptoCurrency, cryptoAmount): + case let .giftPremium(currency, amount, months, cryptoCurrency, cryptoAmount, text, entities): encoder.encodeInt32(27, forKey: "_rawValue") encoder.encodeString(currency, forKey: "currency") encoder.encodeInt64(amount, forKey: "amount") @@ -390,6 +390,16 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { if let cryptoCurrency = cryptoCurrency, let cryptoAmount = cryptoAmount { encoder.encodeString(cryptoCurrency, forKey: "cryptoCurrency") encoder.encodeInt64(cryptoAmount, forKey: "cryptoAmount") + } else { + encoder.encodeNil(forKey: "cryptoCurrency") + encoder.encodeNil(forKey: "cryptoAmount") + } + if let text, let entities { + encoder.encodeString(text, forKey: "text") + encoder.encodeObjectArray(entities, forKey: "entities") + } else { + encoder.encodeNil(forKey: "text") + encoder.encodeNil(forKey: "entities") } case let .topicCreated(title, iconColor, iconFileId): encoder.encodeInt32(28, forKey: "_rawValue") @@ -433,7 +443,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "atp") } - case let .giftCode(slug, fromGiveaway, unclaimed, boostPeerId, months, currency, amount, cryptoCurrency, cryptoAmount): + case let .giftCode(slug, fromGiveaway, unclaimed, boostPeerId, months, currency, amount, cryptoCurrency, cryptoAmount, text, entities): encoder.encodeInt32(36, forKey: "_rawValue") encoder.encodeString(slug, forKey: "slug") encoder.encodeBool(fromGiveaway, forKey: "give") @@ -464,6 +474,13 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "cryptoAmount") } + if let text, let entities { + encoder.encodeString(text, forKey: "text") + encoder.encodeObjectArray(entities, forKey: "entities") + } else { + encoder.encodeNil(forKey: "text") + encoder.encodeNil(forKey: "entities") + } case let .giveawayLaunched(stars): encoder.encodeInt32(37, forKey: "_rawValue") if let stars = stars { @@ -563,7 +580,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { return peerIds case let .requestedPeer(_, peerIds): return peerIds - case let .giftCode(_, _, _, boostPeerId, _, _, _, _, _): + case let .giftCode(_, _, _, boostPeerId, _, _, _, _, _, _, _): return boostPeerId.flatMap { [$0] } ?? [] case let .paymentRefunded(peerId, _, _, _, _): return [peerId] diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift index 95a4ae2dfc..f866267056 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift @@ -15,7 +15,7 @@ public enum AppStoreTransactionPurpose { case upgrade case restore case gift(peerId: EnginePeer.Id, currency: String, amount: Int64) - case giftCode(peerIds: [EnginePeer.Id], boostPeer: EnginePeer.Id?, currency: String, amount: Int64) + case giftCode(peerIds: [EnginePeer.Id], boostPeer: EnginePeer.Id?, currency: String, amount: Int64, text: String?, entities: [MessageTextEntity]?) case giveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32, currency: String, amount: Int64) case stars(count: Int64, currency: String, amount: Int64) case starsGift(peerId: EnginePeer.Id, count: Int64, currency: String, amount: Int64) @@ -43,7 +43,7 @@ private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTran } return .single(.inputStorePaymentGiftPremium(userId: inputUser, currency: currency, amount: amount)) } - case let .giftCode(peerIds, boostPeerId, currency, amount): + case let .giftCode(peerIds, boostPeerId, currency, amount, text, entities): return account.postbox.transaction { transaction -> Api.InputStorePaymentPurpose in var flags: Int32 = 0 var apiBoostPeer: Api.InputPeer? @@ -59,8 +59,13 @@ private func apiInputStorePaymentPurpose(account: Account, purpose: AppStoreTran apiBoostPeer = apiPeer flags |= (1 << 0) } - - return .inputStorePaymentPremiumGiftCode(flags: flags, users: apiInputUsers, boostPeer: apiBoostPeer, currency: currency, amount: amount) + + var message: Api.TextWithEntities? + if let text { + flags |= (1 << 1) + message = .textWithEntities(text: text, entities: apiEntitiesFromMessageTextEntities(entities ?? [], associatedPeers: SimpleDictionary())) + } + return .inputStorePaymentPremiumGiftCode(flags: flags, users: apiInputUsers, boostPeer: apiBoostPeer, currency: currency, amount: amount, message: message) } case let .giveaway(boostPeerId, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate, currency, amount): return account.postbox.transaction { transaction -> Signal in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift index 85b69769eb..1296173518 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift @@ -293,7 +293,7 @@ func _internal_parseInputInvoice(transaction: Transaction, source: BotPaymentInv } } - let inputPurpose: Api.InputStorePaymentPurpose = .inputStorePaymentPremiumGiftCode(flags: 0, users: inputUsers, boostPeer: nil, currency: currency, amount: amount) + let inputPurpose: Api.InputStorePaymentPurpose = .inputStorePaymentPremiumGiftCode(flags: 0, users: inputUsers, boostPeer: nil, currency: currency, amount: amount, message: nil) var flags: Int32 = 0 if let _ = option.storeProductId { diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 134e73fddc..5863b5a9e2 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -735,7 +735,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, } case let .webViewData(text): attributedString = NSAttributedString(string: strings.Notification_WebAppSentData(text).string, font: titleFont, textColor: primaryTextColor) - case let .giftPremium(currency, amount, _, _, _): + case let .giftPremium(currency, amount, _, _, _, _, _): let price = formatCurrencyAmount(amount, currency: currency) if message.author?.id == accountPeerId { attributedString = addAttributesToStringWithRanges(strings.Notification_PremiumGift_SentYou(price)._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) @@ -952,7 +952,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, let resultTitleString = strings.Notification_ChangedToSameWallpaper(compactAuthorName) attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) } - case let .giftCode(_, _, _, boostPeerId, _, currency, amount, _, _): + case let .giftCode(_, _, _, boostPeerId, _, currency, amount, _, _, _, _): if boostPeerId == nil, let currency, let amount { let price = formatCurrencyAmount(amount, currency: currency) if message.author?.id == accountPeerId { @@ -1058,7 +1058,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, let _ = text let _ = entities - let starsPrice = "\(gift.price) Stars" + let starsPrice = strings.Notification_StarsGift_Stars(Int32(gift.price)) var authorName = compactAuthorName var peerIds: [(Int, EnginePeer.Id?)] = [(0, message.author?.id)] if message.id.peerId.namespace == Namespaces.Peer.CloudUser && message.id.peerId.id._internalGetInt64Value() == 777000 { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index d683820320..eecab9955f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -37,6 +37,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { private var mediaBackgroundContent: WallpaperBubbleBackgroundNode? private let titleNode: TextNode private let subtitleNode: TextNodeWithEntities + private let textClippingNode: ASDisplayNode private var dustNode: InvisibleInkDustNode? private let placeholderNode: StickerShimmerEffectNode private let animationNode: AnimatedStickerNode @@ -49,11 +50,17 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { private let buttonStarsNode: PremiumStarsNode private let buttonTitleNode: TextNode + private var maskView: UIImageView? + private var maskOverlayView: UIView? + private var cachedMaskBackgroundImage: (CGPoint, UIImage, [CGRect])? private var absoluteRect: (CGRect, CGSize)? private var isPlaying: Bool = false + private var isExpanded: Bool = false + private var appliedIsExpanded: Bool = false + private var currentProgressDisposable: Disposable? override public var visibility: ListViewItemNodeVisibility { @@ -105,6 +112,9 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { self.subtitleNode.textNode.isUserInteractionEnabled = false self.subtitleNode.textNode.displaysAsynchronously = false + self.textClippingNode = ASDisplayNode() + self.textClippingNode.clipsToBounds = true + self.buttonNode = HighlightTrackingButtonNode() self.buttonNode.clipsToBounds = true self.buttonNode.cornerRadius = 17.0 @@ -133,7 +143,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { self.addSubnode(self.labelNode) self.addSubnode(self.titleNode) - self.addSubnode(self.subtitleNode.textNode) + self.addSubnode(self.textClippingNode) + self.textClippingNode.addSubnode(self.subtitleNode.textNode) self.addSubnode(self.placeholderNode) self.addSubnode(self.animationNode) @@ -251,9 +262,12 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { let makeSubtitleLayout = TextNodeWithEntities.asyncLayout(self.subtitleNode) let makeButtonTitleLayout = TextNode.asyncLayout(self.buttonTitleNode) let makeRibbonTextLayout = TextNode.asyncLayout(self.ribbonTextNode) - + let makeMeasureTextLayout = TextNode.asyncLayout(nil) + let cachedMaskBackgroundImage = self.cachedMaskBackgroundImage + let currentIsExpanded = self.isExpanded + return { item, layoutConstants, _, _, _, _ in let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center) @@ -279,9 +293,19 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { for media in item.message.media { if let action = media as? TelegramMediaAction { switch action.action { - case let .giftPremium(_, _, monthsValue, _, _): + case let .giftPremium(_, _, monthsValue, _, _, giftText, giftEntities): months = monthsValue - text = item.presentationData.strings.Notification_PremiumGift_Subtitle(item.presentationData.strings.Notification_PremiumGift_Months(months)).string + if months == 12 { + title = item.presentationData.strings.Notification_PremiumGift_YearsTitle(1) + } else { + title = item.presentationData.strings.Notification_PremiumGift_MonthsTitle(months) + } + if let giftText, !giftText.isEmpty { + text = giftText + entities = giftEntities ?? [] + } else { + text = item.presentationData.strings.Notification_PremiumGift_SubscriptionDescription + } case let .giftStars(_, _, count, _, _, _): if count <= 1000 { months = 3 @@ -310,10 +334,20 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } title = item.presentationData.strings.Notification_StarsGiveaway_Title text = item.presentationData.strings.Notification_StarsGiveaway_Subtitle(peerName, item.presentationData.strings.Notification_StarsGiveaway_Subtitle_Stars(Int32(count))).string - case let .giftCode(_, fromGiveaway, unclaimed, channelId, monthsValue, _, _, _, _): + case let .giftCode(_, fromGiveaway, unclaimed, channelId, monthsValue, _, _, _, _, giftText, giftEntities): if channelId == nil { months = monthsValue - text = item.presentationData.strings.Notification_PremiumGift_Subtitle(item.presentationData.strings.Notification_PremiumGift_Months(months)).string + if months == 12 { + title = item.presentationData.strings.Notification_PremiumGift_YearsTitle(1) + } else { + title = item.presentationData.strings.Notification_PremiumGift_MonthsTitle(months) + } + if let giftText, !giftText.isEmpty { + text = giftText + entities = giftEntities ?? [] + } else { + text = item.presentationData.strings.Notification_PremiumGift_SubscriptionDescription + } if item.message.author?.id != item.context.account.peerId { buttonTitle = item.presentationData.strings.Notification_PremiumGift_UseGift } @@ -419,13 +453,24 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { ), textAlignment: .center) } - let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + let textConstrainedSize = CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude) + let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets())) + var canExpand = false + var clippedTextHeight: CGFloat = subtitleLayout.size.height + if subtitleLayout.numberOfLines > 4 { + let (measuredTextLayout, _) = makeMeasureTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 4, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets())) + canExpand = true + if !currentIsExpanded { + clippedTextHeight = measuredTextLayout.size.height + } + } + let (buttonTitleLayout, buttonTitleApply) = makeButtonTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: buttonTitle, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) let (ribbonTextLayout, ribbonTextApply) = makeRibbonTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: ribbonTitle, font: Font.semibold(11.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) - giftSize.height = titleLayout.size.height + textSpacing + subtitleLayout.size.height + 164.0 + giftSize.height = titleLayout.size.height + textSpacing + clippedTextHeight + 164.0 if !buttonTitle.isEmpty { giftSize.height += 48.0 } @@ -472,8 +517,18 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } return (backgroundSize.width, { boundingWidth in - return (backgroundSize, { [weak self] animation, synchronousLoads, _ in + return (backgroundSize, { [weak self] animation, synchronousLoads, info in if let strongSelf = self { + let isFirstTime = strongSelf.item == nil + + var isExpandedUpdated = false + if strongSelf.appliedIsExpanded != currentIsExpanded { + strongSelf.appliedIsExpanded = currentIsExpanded + info?.setInvertOffsetDirection() + isExpandedUpdated = true + } + let _ = isExpandedUpdated + let overlayColor = item.presentationData.theme.theme.overallDarkAppearance ? UIColor(rgb: 0xffffff, alpha: 0.12) : UIColor(rgb: 0x000000, alpha: 0.12) let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - giftSize.width) / 2.0), y: hasServiceMessage ? labelLayout.size.height + 16.0 : 0.0), size: giftSize) @@ -551,8 +606,21 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { let titleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - titleLayout.size.width) / 2.0) , y: mediaBackgroundFrame.minY + 151.0), size: titleLayout.size) strongSelf.titleNode.frame = titleFrame + let clippingTextFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: titleFrame.maxY + textSpacing), size: CGSize(width: boundingWidth, height: clippedTextHeight)) + let subtitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: titleFrame.maxY + textSpacing), size: subtitleLayout.size) - strongSelf.subtitleNode.textNode.frame = subtitleFrame + strongSelf.subtitleNode.textNode.frame = CGRect(origin: .zero, size: subtitleLayout.size) + + if isFirstTime { + strongSelf.textClippingNode.frame = clippingTextFrame + } else { + animation.animator.updateFrame(layer: strongSelf.textClippingNode.layer, frame: clippingTextFrame, completion: nil) + } + if let maskView = strongSelf.maskView, let maskOverlayView = strongSelf.maskOverlayView { + animation.animator.updateFrame(layer: maskView.layer, frame: CGRect(origin: .zero, size: CGSize(width: boundingWidth, height: clippingTextFrame.size.height)), completion: nil) + animation.animator.updateFrame(layer: maskOverlayView.layer, frame: CGRect(origin: .zero, size: CGSize(width: boundingWidth, height: clippingTextFrame.size.height)), completion: nil) + } + if !subtitleLayout.spoilers.isEmpty { let dustColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText @@ -573,11 +641,11 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.dustNode = nil } - let buttonTitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonTitleLayout.size.width) / 2.0), y: subtitleFrame.maxY + 18.0), size: buttonTitleLayout.size) + let buttonTitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonTitleLayout.size.width) / 2.0), y: clippingTextFrame.maxY + 18.0), size: buttonTitleLayout.size) strongSelf.buttonTitleNode.frame = buttonTitleFrame let buttonSize = CGSize(width: buttonTitleLayout.size.width + 38.0, height: 34.0) - strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonSize.width) / 2.0), y: subtitleFrame.maxY + 10.0), size: buttonSize) + strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonSize.width) / 2.0), y: clippingTextFrame.maxY + 10.0), size: buttonSize) strongSelf.buttonStarsNode.frame = CGRect(origin: .zero, size: buttonSize) if ribbonTextLayout.size.width > 0.0 { @@ -666,6 +734,26 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.updateAbsoluteRect(rect, within: size) } + if canExpand { + if strongSelf.maskView?.image == nil { + strongSelf.maskView?.image = generateMaskImage() + } + strongSelf.textClippingNode.view.mask = strongSelf.maskView + +// var expandIconFrame: CGRect = .zero +// if let icon = strongSelf.expandIcon.image { +// expandIconFrame = CGRect(origin: CGPoint(x: boundingWidth - icon.size.width - 19.0, y: backgroundFrame.maxY - icon.size.height - 6.0), size: icon.size) +// if wasHidden || isFirstTime { +// strongSelf.expandIcon.position = expandIconFrame.center +// } else { +// animation.animator.updatePosition(layer: strongSelf.expandIcon.layer, position: expandIconFrame.center, completion: nil) +// } +// strongSelf.expandIcon.bounds = CGRect(origin: .zero, size: expandIconFrame.size) +// } + } else { + strongSelf.textClippingNode.view.mask = nil + } + switch strongSelf.visibility { case .none: strongSelf.subtitleNode.visibilityRect = nil @@ -853,3 +941,22 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } } } + +private func generateMaskImage() -> UIImage? { + return generateImage(CGSize(width: 140, height: 30), rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + + context.setFillColor(UIColor.white.cgColor) + context.fill(CGRect(origin: .zero, size: size)) + + var locations: [CGFloat] = [0.0, 0.5, 1.0] + let colors: [CGColor] = [UIColor.white.cgColor, UIColor.white.withAlphaComponent(0.0).cgColor, UIColor.white.withAlphaComponent(0.0).cgColor] + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + context.setBlendMode(.copy) + context.clip(to: CGRect(origin: CGPoint(x: 10.0, y: 8.0), size: CGSize(width: 130.0, height: 22.0))) + context.drawLinearGradient(gradient, start: CGPoint(x: 10.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions()) + })?.resizableImage(withCapInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 22.0, right: 130.0)) +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift index 4facf746b2..f7c9b5873b 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift @@ -306,7 +306,7 @@ final class GiftOptionsScreenComponent: Component { let giftController = GiftSetupScreen( context: component.context, peerId: component.peerId, - gift: gift, + subject: .starGift(gift), completion: component.completion ) mainController.push(giftController) @@ -359,94 +359,6 @@ final class GiftOptionsScreenComponent: Component { } } - private func buyPremium(_ product: PremiumGiftProduct) { - guard let component = self.component, let inAppPurchaseManager = self.component?.context.inAppPurchaseManager, self.inProgressPremiumGift == nil else { - return - } - - self.inProgressPremiumGift = product.id - self.state?.updated() - - let (currency, amount) = product.storeProduct.priceCurrencyAndAmount - - addAppLogEvent(postbox: component.context.account.postbox, type: "premium_gift.promo_screen_accept") - - let purpose: AppStoreTransactionPurpose = .giftCode(peerIds: [component.peerId], boostPeer: nil, currency: currency, amount: amount) - let quantity: Int32 = 1 - - let completion = component.completion - - let _ = (component.context.engine.payments.canPurchasePremium(purpose: purpose) - |> deliverOnMainQueue).start(next: { [weak self] available in - if let strongSelf = self { - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - if available { - strongSelf.purchaseDisposable.set((inAppPurchaseManager.buyProduct(product.storeProduct, quantity: quantity, purpose: purpose) - |> deliverOnMainQueue).start(next: { [weak self] status in - if let completion { - completion() - } else { - guard let self, case .purchased = status, let controller = self.environment?.controller(), let navigationController = controller.navigationController as? NavigationController else { - return - } - - var controllers = navigationController.viewControllers - controllers = controllers.filter { !($0 is GiftOptionsScreen) && !($0 is PeerInfoScreen) && !($0 is ContactSelectionController) } - var foundController = false - for controller in controllers.reversed() { - if let chatController = controller as? ChatController, case .peer(id: component.peerId) = chatController.chatLocation { - chatController.hintPlayNextOutgoingGift() - foundController = true - break - } - } - if !foundController { - let chatController = component.context.sharedContext.makeChatController(context: component.context, chatLocation: .peer(id: component.peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil) - chatController.hintPlayNextOutgoingGift() - controllers.append(chatController) - } - navigationController.setViewControllers(controllers, animated: true) - } - }, error: { [weak self] error in - guard let self, let controller = self.environment?.controller() else { - return - } - self.inProgressPremiumGift = nil - self.state?.updated(transition: .immediate) - - var errorText: String? - switch error { - case .generic: - errorText = presentationData.strings.Premium_Purchase_ErrorUnknown - case .network: - errorText = presentationData.strings.Premium_Purchase_ErrorNetwork - case .notAllowed: - errorText = presentationData.strings.Premium_Purchase_ErrorNotAllowed - case .cantMakePayments: - errorText = presentationData.strings.Premium_Purchase_ErrorCantMakePayments - case .assignFailed: - errorText = presentationData.strings.Premium_Purchase_ErrorUnknown - case .tryLater: - errorText = presentationData.strings.Premium_Purchase_ErrorUnknown - case .cancelled: - break - } - - if let errorText { - addAppLogEvent(postbox: component.context.account.postbox, type: "premium_gift.promo_screen_fail") - - let alertController = textAlertController(context: component.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) - controller.present(alertController, in: .window(.root)) - } - })) - } else { - self?.inProgressPremiumGift = nil - self?.state?.updated(transition: .immediate) - } - } - }) - } - func update(component: GiftOptionsScreenComponent, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { @@ -707,7 +619,7 @@ final class GiftOptionsScreenComponent: Component { var validIds: [AnyHashable] = [] var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: premiumOptionSize) for product in premiumProducts { - let itemId = AnyHashable(product.storeProduct.id) + let itemId = AnyHashable(product.id) validIds.append(itemId) var itemTransition = transition @@ -756,7 +668,23 @@ final class GiftOptionsScreenComponent: Component { ), effectAlignment: .center, action: { [weak self] in - self?.buyPremium(product) + if let self, let component = self.component { + if let controller = controller() as? GiftOptionsScreen { + let mainController: ViewController + if let parentController = controller.parentController() { + mainController = parentController + } else { + mainController = controller + } + let giftController = GiftSetupScreen( + context: component.context, + peerId: component.peerId, + subject: .premium(product), + completion: component.completion + ) + mainController.push(giftController) + } + } }, animateAlpha: false ) @@ -1019,23 +947,43 @@ final class GiftOptionsScreenComponent: Component { } self.peer = peer - let shortestOptionPrice: (Int64, NSDecimalNumber) - if let product = availableProducts.first(where: { $0.id.hasSuffix(".monthly") }) { - shortestOptionPrice = (Int64(Float(product.priceCurrencyAndAmount.amount)), product.priceValue) - } else { - shortestOptionPrice = (1, NSDecimalNumber(decimal: 1)) - } - - var premiumProducts: [PremiumGiftProduct] = [] - for option in premiumOptions { - if let product = availableProducts.first(where: { $0.id == option.storeProductId }), !product.isSubscription { - let fraction = Float(product.priceCurrencyAndAmount.amount) / Float(option.months) / Float(shortestOptionPrice.0) - let discountValue = Int(round((1.0 - fraction) * 20.0) * 5.0) - premiumProducts.append(PremiumGiftProduct(giftOption: option, storeProduct: product, discount: discountValue > 0 ? discountValue : nil)) + if availableProducts.isEmpty { + var premiumProducts: [PremiumGiftProduct] = [] + for option in premiumOptions { + premiumProducts.append( + PremiumGiftProduct( + giftOption: CachedPremiumGiftOption( + months: option.months, + currency: option.currency, + amount: option.amount, + botUrl: "", + storeProductId: option.storeProductId + ), + storeProduct: nil, + discount: nil + ) + ) } + self.premiumProducts = premiumProducts.sorted(by: { $0.months < $1.months }) + } else { + let shortestOptionPrice: (Int64, NSDecimalNumber) + if let product = availableProducts.first(where: { $0.id.hasSuffix(".monthly") }) { + shortestOptionPrice = (Int64(Float(product.priceCurrencyAndAmount.amount)), product.priceValue) + } else { + shortestOptionPrice = (1, NSDecimalNumber(decimal: 1)) + } + + var premiumProducts: [PremiumGiftProduct] = [] + for option in premiumOptions { + if let product = availableProducts.first(where: { $0.id == option.storeProductId }), !product.isSubscription { + let fraction = Float(product.priceCurrencyAndAmount.amount) / Float(option.months) / Float(shortestOptionPrice.0) + let discountValue = Int(round((1.0 - fraction) * 20.0) * 5.0) + premiumProducts.append(PremiumGiftProduct(giftOption: option, storeProduct: product, discount: discountValue > 0 ? discountValue : nil)) + } + } + self.premiumProducts = premiumProducts.sorted(by: { $0.months < $1.months }) } - self.premiumProducts = premiumProducts.sorted(by: { $0.months < $1.months }) - + self.starGifts = starGifts self.updated() @@ -1105,25 +1053,3 @@ open class GiftOptionsScreen: ViewControllerComponentContainer, GiftOptionsScree super.containerLayoutUpdated(layout, transition: transition) } } - -private struct PremiumGiftProduct: Equatable { - let giftOption: CachedPremiumGiftOption - let storeProduct: InAppPurchaseManager.Product - let discount: Int? - - var id: String { - return self.storeProduct.id - } - - var months: Int32 { - return self.giftOption.months - } - - var price: String { - return self.storeProduct.price - } - - var pricePerMonth: String { - return self.storeProduct.pricePerMonth(Int(self.months)) - } -} diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD index a64e80c41d..a5a8867320 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD @@ -17,6 +17,7 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit", "//submodules/TelegramPresentationData", "//submodules/TelegramUIPreferences", + "//submodules/TelegramStringFormatting", "//submodules/AccountContext", "//submodules/PresentationDataUtils", "//submodules/Markdown", @@ -41,6 +42,7 @@ swift_library( "//submodules/BotPaymentsUI", "//submodules/TelegramUI/Components/EmojiSuggestionsComponent", "//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode", + "//submodules/InAppPurchaseManager", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift index cd02508e46..12a43a1017 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift @@ -14,6 +14,10 @@ import WallpaperBackgroundNode import ListItemComponentAdaptor final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAdaptor.ItemGenerator { + enum Subject: Equatable { + case premium(months: Int32, amount: Int64, currency: String) + case starGift(gift: StarGift) + } let context: AccountContext let theme: PresentationTheme let componentTheme: PresentationTheme @@ -26,7 +30,7 @@ final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAd let nameDisplayOrder: PresentationPersonNameOrder let accountPeer: EnginePeer? - let gift: StarGift + let subject: ChatGiftPreviewItem.Subject let text: String let entities: [MessageTextEntity] @@ -42,7 +46,7 @@ final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAd dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, accountPeer: EnginePeer?, - gift: StarGift, + subject: ChatGiftPreviewItem.Subject, text: String, entities: [MessageTextEntity] ) { @@ -57,7 +61,7 @@ final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAd self.dateTimeFormat = dateTimeFormat self.nameDisplayOrder = nameDisplayOrder self.accountPeer = accountPeer - self.gift = gift + self.subject = subject self.text = text self.entities = entities } @@ -206,9 +210,22 @@ final class ChatGiftPreviewItemNode: ListViewItemNode { peers[authorPeerId] = item.accountPeer?._asPeer() - let media: [Media] = [ - TelegramMediaAction(action: .starGift(gift: item.gift, convertStars: item.gift.convertStars, text: item.text, entities: item.entities, nameHidden: false, savedToProfile: false, converted: false)) - ] + let media: [Media] + switch item.subject { + case let .premium(months, amount, currency): + media = [ + TelegramMediaAction( + action: .giftPremium(currency: currency, amount: amount, months: months, cryptoCurrency: nil, cryptoAmount: nil, text: item.text, entities: item.entities) + ) + ] + case let .starGift(gift): + media = [ + TelegramMediaAction( + action: .starGift(gift: gift, convertStars: gift.convertStars, text: item.text, entities: item.entities, nameHidden: false, savedToProfile: false, converted: false) + ) + ] + } + let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[authorPeerId], text: "", attributes: [], media: media, peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false)) } diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index 5f939cebbe..62e6bc47b6 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -7,6 +7,7 @@ import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences +import TelegramStringFormatting import PresentationDataUtils import AccountContext import ComponentFlow @@ -27,24 +28,25 @@ import EmojiSuggestionsComponent import ChatPresentationInterfaceState import AudioToolbox import TextFormat +import InAppPurchaseManager final class GiftSetupScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let peerId: EnginePeer.Id - let gift: StarGift + let subject: GiftSetupScreen.Subject let completion: (() -> Void)? init( context: AccountContext, peerId: EnginePeer.Id, - gift: StarGift, + subject: GiftSetupScreen.Subject, completion: (() -> Void)? = nil ) { self.context = context self.peerId = peerId - self.gift = gift + self.subject = subject self.completion = completion } @@ -55,7 +57,7 @@ final class GiftSetupScreenComponent: Component { if lhs.peerId != rhs.peerId { return false } - if lhs.gift != rhs.gift { + if lhs.subject != rhs.subject { return false } return true @@ -103,6 +105,7 @@ final class GiftSetupScreenComponent: Component { private var currentEmojiSuggestionView: ComponentHostView? private var hideName = false + private var inProgress = false private var previousHadInputHeight: Bool = false private var previousInputHeight: CGFloat? @@ -189,7 +192,108 @@ final class GiftSetupScreenComponent: Component { } func proceed() { - guard let component = self.component, let starsContext = component.context.starsContext, let starsState = starsContext.currentState else { + guard let component = self.component else { + return + } + switch component.subject { + case .premium: + self.proceedWithPremiumGift() + case .starGift: + self.proceedWithStarGift() + } + } + + func proceedWithPremiumGift() { + guard let component = self.component, case let .premium(product) = component.subject, let storeProduct = product.storeProduct, let inAppPurchaseManager = component.context.inAppPurchaseManager else { + return + } + + self.inProgress = true + self.state?.updated() + + let (currency, amount) = storeProduct.priceCurrencyAndAmount + + addAppLogEvent(postbox: component.context.account.postbox, type: "premium_gift.promo_screen_accept") + + let entities = generateChatInputTextEntities(self.textInputState.text) + let purpose: AppStoreTransactionPurpose = .giftCode(peerIds: [component.peerId], boostPeer: nil, currency: currency, amount: amount, text: self.textInputState.text.string, entities: entities) + let quantity: Int32 = 1 + + let completion = component.completion + + let _ = (component.context.engine.payments.canPurchasePremium(purpose: purpose) + |> deliverOnMainQueue).start(next: { [weak self] available in + guard let self else { + return + } + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + if available { + let _ = (inAppPurchaseManager.buyProduct(storeProduct, quantity: quantity, purpose: purpose) + |> deliverOnMainQueue).start(next: { [weak self] status in + if let completion { + completion() + } else { + guard let self, case .purchased = status, let controller = self.environment?.controller(), let navigationController = controller.navigationController as? NavigationController else { + return + } + + var controllers = navigationController.viewControllers + controllers = controllers.filter { !($0 is GiftSetupScreen) && !($0 is GiftOptionsScreenProtocol) && !($0 is PeerInfoScreen) && !($0 is ContactSelectionController) } + var foundController = false + for controller in controllers.reversed() { + if let chatController = controller as? ChatController, case .peer(id: component.peerId) = chatController.chatLocation { + chatController.hintPlayNextOutgoingGift() + foundController = true + break + } + } + if !foundController { + let chatController = component.context.sharedContext.makeChatController(context: component.context, chatLocation: .peer(id: component.peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil) + chatController.hintPlayNextOutgoingGift() + controllers.append(chatController) + } + navigationController.setViewControllers(controllers, animated: true) + } + }, error: { [weak self] error in + guard let self, let controller = self.environment?.controller() else { + return + } + self.state?.updated(transition: .immediate) + + var errorText: String? + switch error { + case .generic: + errorText = presentationData.strings.Premium_Purchase_ErrorUnknown + case .network: + errorText = presentationData.strings.Premium_Purchase_ErrorNetwork + case .notAllowed: + errorText = presentationData.strings.Premium_Purchase_ErrorNotAllowed + case .cantMakePayments: + errorText = presentationData.strings.Premium_Purchase_ErrorCantMakePayments + case .assignFailed: + errorText = presentationData.strings.Premium_Purchase_ErrorUnknown + case .tryLater: + errorText = presentationData.strings.Premium_Purchase_ErrorUnknown + case .cancelled: + break + } + + if let errorText { + addAppLogEvent(postbox: component.context.account.postbox, type: "premium_gift.promo_screen_fail") + + let alertController = textAlertController(context: component.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) + controller.present(alertController, in: .window(.root)) + } + }) + } else { + self.inProgress = false + self.state?.updated(transition: .immediate) + } + }) + } + + func proceedWithStarGift() { + guard let component = self.component, case let .starGift(starGift) = component.subject, let starsContext = component.context.starsContext, let starsState = starsContext.currentState else { return } @@ -198,7 +302,8 @@ final class GiftSetupScreenComponent: Component { return } let entities = generateChatInputTextEntities(self.textInputState.text) - let source: BotPaymentInvoiceSource = .starGift(hideName: self.hideName, peerId: component.peerId, giftId: component.gift.id, text: self.textInputState.text.string, entities: entities) + let source: BotPaymentInvoiceSource = .starGift(hideName: self.hideName, peerId: component.peerId, giftId: starGift.id, text: self.textInputState.text.string, entities: entities) + let inputData = BotCheckoutController.InputData.fetch(context: component.context, source: source) |> map(Optional.init) |> `catch` { _ -> Signal in @@ -246,7 +351,7 @@ final class GiftSetupScreenComponent: Component { }) } - if starsState.balance < component.gift.price { + if starsState.balance < starGift.price { let _ = (self.optionsPromise.get() |> filter { $0 != nil } |> take(1) @@ -258,7 +363,7 @@ final class GiftSetupScreenComponent: Component { context: component.context, starsContext: starsContext, options: options ?? [], - purpose: .starGift(peerId: component.peerId, requiredStars: component.gift.price), + purpose: .starGift(peerId: component.peerId, requiredStars: starGift.price), completion: { [weak starsContext] stars in starsContext?.add(balance: stars) Queue.mainQueue().after(0.1) { @@ -521,6 +626,23 @@ final class GiftSetupScreenComponent: Component { inputHeight = environment.inputHeight } } + + let peerName = self.peerMap[component.peerId]?.compactDisplayTitle ?? "" + + let introFooter: AnyComponent? + switch component.subject { + case .premium: + introFooter = AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Gift_Send_Customize_Info(peerName).string, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )) + case .starGift: + introFooter = nil + } let introSectionSize = self.introSection.update( transition: transition, @@ -534,7 +656,7 @@ final class GiftSetupScreenComponent: Component { )), maximumNumberOfLines: 0 )), - footer: nil, + footer: introFooter, items: introSectionItems )), environment: {}, @@ -553,6 +675,15 @@ final class GiftSetupScreenComponent: Component { let listItemParams = ListViewItemLayoutParams(width: availableSize.width - sideInset * 2.0, leftInset: 0.0, rightInset: 0.0, availableHeight: 10000.0, isStandalone: true) if let accountPeer = self.peerMap[component.context.account.peerId] { + let subject: ChatGiftPreviewItem.Subject + switch component.subject { + case let .premium(product): + let (currency, amount) = product.storeProduct?.priceCurrencyAndAmount ?? ("USD", 1) + subject = .premium(months: product.months, amount: amount, currency: currency) + case let .starGift(gift): + subject = .starGift(gift: gift) + } + let introContentSize = self.introContent.update( transition: transition, component: AnyComponent( @@ -569,7 +700,7 @@ final class GiftSetupScreenComponent: Component { dateTimeFormat: environment.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, accountPeer: accountPeer, - gift: component.gift, + subject: subject, text: self.textInputState.text.string, entities: generateChatInputTextEntities(self.textInputState.text) ), @@ -589,55 +720,56 @@ final class GiftSetupScreenComponent: Component { } } - let peerName = self.peerMap[component.peerId]?.compactDisplayTitle ?? "" - let hideSectionSize = self.hideSection.update( - transition: transition, - component: AnyComponent(ListSectionComponent( - theme: environment.theme, - header: nil, - footer: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: environment.strings.Gift_Send_HideMyName_Info(peerName, peerName).string, - font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), - textColor: environment.theme.list.freeTextColor + if case .starGift = component.subject { + let hideSectionSize = self.hideSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Gift_Send_HideMyName_Info(peerName, peerName).string, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 )), - maximumNumberOfLines: 0 + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Gift_Send_HideMyName, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.hideName, action: { [weak self] _ in + guard let self else { + return + } + self.hideName = !self.hideName + self.state?.updated(transition: .spring(duration: 0.4)) + })), + action: nil + ))) + ] )), - items: [ - AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( - theme: environment.theme, - title: AnyComponent(VStack([ - AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: environment.strings.Gift_Send_HideMyName, - font: Font.regular(presentationData.listsFontSize.baseDisplaySize), - textColor: environment.theme.list.itemPrimaryTextColor - )), - maximumNumberOfLines: 1 - ))), - ], alignment: .left, spacing: 2.0)), - accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.hideName, action: { [weak self] _ in - guard let self else { - return - } - self.hideName = !self.hideName - self.state?.updated(transition: .spring(duration: 0.4)) - })), - action: nil - ))) - ] - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) - ) - let hideSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: hideSectionSize) - if let hideSectionView = self.hideSection.view { - if hideSectionView.superview == nil { - self.scrollView.addSubview(hideSectionView) + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let hideSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: hideSectionSize) + if let hideSectionView = self.hideSection.view { + if hideSectionView.superview == nil { + self.scrollView.addSubview(hideSectionView) + } + transition.setFrame(view: hideSectionView, frame: hideSectionFrame) } - transition.setFrame(view: hideSectionView, frame: hideSectionFrame) + contentHeight += hideSectionSize.height } - contentHeight += hideSectionSize.height contentHeight += bottomContentInset @@ -647,8 +779,18 @@ final class GiftSetupScreenComponent: Component { if self.starImage == nil || self.starImage?.1 !== environment.theme { self.starImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: environment.theme.list.itemCheckColors.foregroundColor)!, environment.theme) } - let amountString = presentationStringsFormattedNumber(Int32(component.gift.price), presentationData.dateTimeFormat.groupingSeparator) - let buttonAttributedString = NSMutableAttributedString(string: "\(environment.strings.Gift_Send_Send) # \(amountString)", font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) + + let buttonString: String + switch component.subject { + case let .premium(product): + let amountString = product.price + buttonString = "\(environment.strings.Gift_Send_Send) \(amountString)" + case let .starGift(starGift): + let amountString = presentationStringsFormattedNumber(Int32(starGift.price), presentationData.dateTimeFormat.groupingSeparator) + buttonString = "\(environment.strings.Gift_Send_Send) # \(amountString)" + } + + let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) if let range = buttonAttributedString.string.range(of: "#"), let starImage = self.starImage?.0 { buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string)) buttonAttributedString.addAttribute(.foregroundColor, value: environment.theme.list.itemCheckColors.foregroundColor, range: NSRange(range, in: buttonAttributedString.string)) @@ -669,7 +811,7 @@ final class GiftSetupScreenComponent: Component { component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString))) ), isEnabled: true, - displaysProgress: false, + displaysProgress: self.inProgress, action: { [weak self] in self?.proceed() } @@ -1039,12 +1181,17 @@ final class GiftSetupScreenComponent: Component { } public final class GiftSetupScreen: ViewControllerComponentContainer { + public enum Subject: Equatable { + case premium(PremiumGiftProduct) + case starGift(StarGift) + } + private let context: AccountContext public init( context: AccountContext, peerId: EnginePeer.Id, - gift: StarGift, + subject: Subject, completion: (() -> Void)? = nil ) { self.context = context @@ -1052,7 +1199,7 @@ public final class GiftSetupScreen: ViewControllerComponentContainer { super.init(context: context, component: GiftSetupScreenComponent( context: context, peerId: peerId, - gift: gift, + subject: subject, completion: completion ), navigationBarAppearance: .default, theme: .default, updatedPresentationData: nil) @@ -1105,3 +1252,31 @@ private struct GiftConfiguration { } } } + +public struct PremiumGiftProduct: Equatable { + public let giftOption: CachedPremiumGiftOption + public let storeProduct: InAppPurchaseManager.Product? + public let discount: Int? + + public var id: String { + return self.storeProduct?.id ?? (self.giftOption.storeProductId ?? "") + } + + public var months: Int32 { + return self.giftOption.months + } + + public var price: String { + return self.storeProduct?.price ?? formatCurrencyAmount(self.giftOption.amount, currency: self.giftOption.currency) + } + + public var pricePerMonth: String { + return self.storeProduct?.pricePerMonth(Int(self.months)) ?? "" + } + + public init(giftOption: CachedPremiumGiftOption, storeProduct: InAppPurchaseManager.Product?, discount: Int?) { + self.giftOption = giftOption + self.storeProduct = storeProduct + self.discount = discount + } +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index ca18d07fb3..d5466b4a84 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -235,11 +235,13 @@ private final class GiftViewSheetContent: CombinedComponent { descriptionText = modifiedString } - let formattedAmount = presentationStringsFormattedNumber(abs(Int32(stars)), dateTimeFormat.groupingSeparator) + var formattedAmount = presentationStringsFormattedNumber(abs(Int32(stars)), dateTimeFormat.groupingSeparator) + if !incoming && stars > 0 { + formattedAmount = "- \(formattedAmount)" + } let countFont: UIFont = Font.semibold(17.0) let amountText = formattedAmount - let countColor = theme.list.itemDisclosureActions.constructive.fillColor - + let countColor = incoming ? theme.list.itemDisclosureActions.constructive.fillColor : theme.list.itemDestructiveColor let title = title.update( component: MultilineTextComponent( @@ -344,7 +346,7 @@ private final class GiftViewSheetContent: CombinedComponent { id: "availability", title: strings.Gift_View_Availability, component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: "\(remains) of \(limitTotal)", font: tableFont, textColor: tableTextColor))) + MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_View_Availability_Of("\(remains)", "\(limitTotal)").string, font: tableFont, textColor: tableTextColor))) ) )) } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/DynamicIslandBlurNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/DynamicIslandBlurNode.swift index 75dfe458b3..20641f2428 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/DynamicIslandBlurNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/DynamicIslandBlurNode.swift @@ -5,7 +5,7 @@ import TelegramPresentationData import AnimationUI import Display -class DynamicIslandMaskNode: ASDisplayNode { +final class DynamicIslandMaskNode: ASDisplayNode { var animationNode: AnimationNode? var isForum = false { @@ -39,7 +39,7 @@ class DynamicIslandMaskNode: ASDisplayNode { } } -class DynamicIslandBlurNode: ASDisplayNode { +final class DynamicIslandBlurNode: ASDisplayNode { private var effectView: UIVisualEffectView? private let fadeNode = ASDisplayNode() let gradientNode = ASImageNode() diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index b84d45e5ef..afbfdfcda4 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1172,7 +1172,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } strongSelf.push(wallpaperPreviewController) return true - case let .giftPremium(_, _, duration, _, _): + case let .giftPremium(_, _, duration, _, _, _, _): strongSelf.chatDisplayNode.dismissInput() let fromPeerId: PeerId = message.author?.id == strongSelf.context.account.peerId ? strongSelf.context.account.peerId : message.id.peerId let toPeerId: PeerId = message.author?.id == strongSelf.context.account.peerId ? message.id.peerId : strongSelf.context.account.peerId @@ -1187,7 +1187,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let controller = strongSelf.context.sharedContext.makeStarsGiftScreen(context: strongSelf.context, message: EngineMessage(message)) strongSelf.push(controller) return true - case let .giftCode(slug, _, _, _, _, _, _, _, _): + case let .giftCode(slug, _, _, _, _, _, _, _, _, _, _): strongSelf.openResolved(result: .premiumGiftCode(slug: slug), sourceMessageId: message.id, progress: params.progress) return true case .prizeStars: From f08fef3991d9e9e85a40097e40f9ddf1957e1e22 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 2 Oct 2024 17:57:59 +0400 Subject: [PATCH 6/9] Gift improvements --- .../Sources/AccountContext.swift | 2 +- .../Sources/ChatListSearchListPaneNode.swift | 3 +- .../Sources/GiftSetupScreen.swift | 53 +- .../Sources/RemainingCountComponent.swift | 827 ++++++++++++++++++ .../Sources/PeerInfoScreen.swift | 3 +- .../Sources/ApplicationContext.swift | 4 +- .../Chat/ChatControllerOpenWebApp.swift | 8 +- .../TelegramUI/Sources/ChatController.swift | 2 +- .../Sources/SharedAccountContext.swift | 4 +- 9 files changed, 885 insertions(+), 21 deletions(-) create mode 100644 submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/RemainingCountComponent.swift diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 5e32fda584..95b279d4b1 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1041,7 +1041,7 @@ public protocol SharedAccountContext: AnyObject { func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal func makeMiniAppListScreen(context: AccountContext, initialData: MiniAppListScreenInitialData) -> ViewController - func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool) + func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?) func makeDebugSettingsController(context: AccountContext?) -> ViewController? diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 3620d86c21..a522784efb 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -3672,7 +3672,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { url: "", simple: true, source: .generic, - skipTermsOfService: true + skipTermsOfService: true, + payload: nil ) } else { self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams( diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index 62e6bc47b6..538c7ca8eb 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -74,6 +74,7 @@ final class GiftSetupScreenComponent: Component { private let scrollView: ScrollView private let navigationTitle = ComponentView() + private let remainingCount = ComponentView() private let introContent = ComponentView() private let introSection = ComponentView() private let hideSection = ComponentView() @@ -296,11 +297,15 @@ final class GiftSetupScreenComponent: Component { guard let component = self.component, case let .starGift(starGift) = component.subject, let starsContext = component.context.starsContext, let starsState = starsContext.currentState else { return } - + let proceed = { [weak self] in guard let self else { return } + + self.inProgress = true + self.state?.updated() + let entities = generateChatInputTextEntities(self.textInputState.text) let source: BotPaymentInvoiceSource = .starGift(hideName: self.hideName, peerId: component.peerId, giftId: starGift.id, text: self.textInputState.text.string, entities: entities) @@ -558,6 +563,43 @@ final class GiftSetupScreenComponent: Component { contentHeight += environment.navigationHeight contentHeight += 26.0 + if case let .starGift(starGift) = component.subject, let availability = starGift.availability { + //TODO:localize + let remains: Int32 = Int32(CGFloat(availability.remains) * 0.66) + let position = CGFloat(remains) / CGFloat(availability.total) + let remainsString = "\(remains)" //presentationStringsFormattedNumber(remains, environment.dateTimeFormat.groupingSeparator) + let totalString = presentationStringsFormattedNumber(availability.total, environment.dateTimeFormat.groupingSeparator) + let remainingCountSize = self.remainingCount.update( + transition: transition, + component: AnyComponent(RemainingCountComponent( + inactiveColor: environment.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3), + activeColors: [UIColor(rgb: 0x5bc2ff), UIColor(rgb: 0x2d9eff)], + inactiveTitle: "Limited", + inactiveValue: "", + inactiveTitleColor: environment.theme.list.itemSecondaryTextColor, + activeTitle: "", + activeValue: totalString, + activeTitleColor: .white, + badgeText: "\(remainsString)", + badgePosition: position, + badgeGraphPosition: position, + invertProgress: true + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let remainingCountFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight - 36.0), size: remainingCountSize) + if let remainingCountView = self.remainingCount.view { + if remainingCountView.superview == nil { + self.scrollView.addSubview(remainingCountView) + } + transition.setFrame(view: remainingCountView, frame: remainingCountFrame) + } + contentHeight += remainingCountSize.height + contentHeight -= 36.0 + contentHeight += sectionSpacing + } + let giftConfiguration = GiftConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 }) var introSectionItems: [AnyComponentWithIdentity] = [] @@ -648,14 +690,7 @@ final class GiftSetupScreenComponent: Component { transition: transition, component: AnyComponent(ListSectionComponent( theme: environment.theme, - header: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: environment.strings.Gift_Send_Customize_Title, - font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), - textColor: environment.theme.list.freeTextColor - )), - maximumNumberOfLines: 0 - )), + header: nil, footer: introFooter, items: introSectionItems )), diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/RemainingCountComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/RemainingCountComponent.swift new file mode 100644 index 0000000000..267382cf3e --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/RemainingCountComponent.swift @@ -0,0 +1,827 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import TelegramCore +import SwiftSignalKit +import AccountContext +import TelegramPresentationData +import PresentationDataUtils +import ComponentFlow +import MultilineTextComponent +import Markdown +import TextFormat +import RoundedRectWithTailPath + +public class RemainingCountComponent: Component { + private let inactiveColor: UIColor + private let activeColors: [UIColor] + private let inactiveTitle: String + private let inactiveValue: String + private let inactiveTitleColor: UIColor + private let activeTitle: String + private let activeValue: String + private let activeTitleColor: UIColor + private let badgeText: String? + private let badgePosition: CGFloat + private let badgeGraphPosition: CGFloat + private let invertProgress: Bool + + public init( + inactiveColor: UIColor, + activeColors: [UIColor], + inactiveTitle: String, + inactiveValue: String, + inactiveTitleColor: UIColor, + activeTitle: String, + activeValue: String, + activeTitleColor: UIColor, + badgeText: String?, + badgePosition: CGFloat, + badgeGraphPosition: CGFloat, + invertProgress: Bool = false + ) { + self.inactiveColor = inactiveColor + self.activeColors = activeColors + self.inactiveTitle = inactiveTitle + self.inactiveValue = inactiveValue + self.inactiveTitleColor = inactiveTitleColor + self.activeTitle = activeTitle + self.activeValue = activeValue + self.activeTitleColor = activeTitleColor + self.badgeText = badgeText + self.badgePosition = badgePosition + self.badgeGraphPosition = badgeGraphPosition + self.invertProgress = invertProgress + } + + public static func ==(lhs: RemainingCountComponent, rhs: RemainingCountComponent) -> Bool { + if lhs.inactiveColor != rhs.inactiveColor { + return false + } + if lhs.activeColors != rhs.activeColors { + return false + } + if lhs.inactiveTitle != rhs.inactiveTitle { + return false + } + if lhs.inactiveValue != rhs.inactiveValue { + return false + } + if lhs.inactiveTitleColor != rhs.inactiveTitleColor { + return false + } + if lhs.activeTitle != rhs.activeTitle { + return false + } + if lhs.activeValue != rhs.activeValue { + return false + } + if lhs.activeTitleColor != rhs.activeTitleColor { + return false + } + if lhs.badgeText != rhs.badgeText { + return false + } + if lhs.badgePosition != rhs.badgePosition { + return false + } + if lhs.badgeGraphPosition != rhs.badgeGraphPosition { + return false + } + if lhs.invertProgress != rhs.invertProgress { + return false + } + return true + } + + public final class View: UIView { + private var component: RemainingCountComponent? + + private let container: UIView + private let inactiveBackground: SimpleLayer + + private let inactiveTitleLabel = ComponentView() + private let inactiveValueLabel = ComponentView() + + private let innerLeftTitleLabel = ComponentView() + private let innerRightTitleLabel = ComponentView() + + private let activeContainer: UIView + private let activeBackground: SimpleLayer + + private let activeTitleLabel = ComponentView() + private let activeValueLabel = ComponentView() + + private let badgeView: UIView + private let badgeMaskView: UIView + private let badgeShapeLayer = CAShapeLayer() + + private let badgeForeground: SimpleLayer + private let badgeLabel: BadgeLabelView + private let badgeLabelMaskView = UIImageView() + + private var badgeTailPosition: CGFloat = 0.0 + private var badgeShapeArguments: (Double, Double, CGSize, CGFloat, CGFloat)? + + override init(frame: CGRect) { + self.container = UIView() + self.container.clipsToBounds = true + self.container.layer.cornerRadius = 9.0 + + self.inactiveBackground = SimpleLayer() + + self.activeContainer = UIView() + self.activeContainer.clipsToBounds = true + + self.activeBackground = SimpleLayer() + self.activeBackground.anchorPoint = CGPoint() + + self.badgeView = UIView() + self.badgeView.alpha = 0.0 + + self.badgeShapeLayer.fillColor = UIColor.white.cgColor + self.badgeShapeLayer.transform = CATransform3DMakeScale(1.0, -1.0, 1.0) + + self.badgeMaskView = UIView() + self.badgeMaskView.layer.addSublayer(self.badgeShapeLayer) + self.badgeView.mask = self.badgeMaskView + + self.badgeForeground = SimpleLayer() + + self.badgeLabel = BadgeLabelView() + let _ = self.badgeLabel.update(value: "0", transition: .immediate) + self.badgeLabel.mask = self.badgeLabelMaskView + + super.init(frame: frame) + + self.addSubview(self.container) + self.container.layer.addSublayer(self.inactiveBackground) + self.container.addSubview(self.activeContainer) + self.activeContainer.layer.addSublayer(self.activeBackground) + + self.addSubview(self.badgeView) + self.badgeView.layer.addSublayer(self.badgeForeground) + self.badgeView.addSubview(self.badgeLabel) + + self.badgeLabelMaskView.contentMode = .scaleToFill + self.badgeLabelMaskView.image = generateImage(CGSize(width: 2.0, height: 30.0), rotatedContext: { size, context in + let bounds = CGRect(origin: .zero, size: size) + context.clear(bounds) + + let colorsArray: [CGColor] = [ + UIColor(rgb: 0xffffff, alpha: 0.0).cgColor, + UIColor(rgb: 0xffffff).cgColor, + UIColor(rgb: 0xffffff).cgColor, + UIColor(rgb: 0xffffff, alpha: 0.0).cgColor, + ] + var locations: [CGFloat] = [0.0, 0.24, 0.76, 1.0] + let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + }) + + self.isUserInteractionEnabled = false + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.badgeShapeAnimator?.invalidate() + } + + private var didPlayAppearanceAnimation = false + func playAppearanceAnimation(component: RemainingCountComponent, badgeFullSize: CGSize, from: CGFloat? = nil) { + if from == nil { + self.badgeView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.4, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) + } + + let rotationAngle: CGFloat + if badgeFullSize.width > 100.0 { + rotationAngle = 0.2 + } else { + rotationAngle = 0.26 + } + + let to: CGFloat = self.badgeView.center.x + + let positionAnimation = CABasicAnimation(keyPath: "position.x") + positionAnimation.fromValue = NSValue(cgPoint: CGPoint(x: from ?? self.container.frame.width, y: 0.0)) + positionAnimation.toValue = NSValue(cgPoint: CGPoint(x: to, y: 0.0)) + positionAnimation.duration = 0.5 + positionAnimation.fillMode = .forwards + positionAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + self.badgeView.layer.add(positionAnimation, forKey: "appearance1") + + if from != to { + let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation.z") + rotateAnimation.fromValue = 0.0 as NSNumber + rotateAnimation.toValue = rotationAngle as NSNumber + rotateAnimation.duration = 0.15 + rotateAnimation.fillMode = .forwards + rotateAnimation.timingFunction = CAMediaTimingFunction(name: .easeOut) + rotateAnimation.isRemovedOnCompletion = false + self.badgeView.layer.add(rotateAnimation, forKey: "appearance2") + + Queue.mainQueue().after(0.5, { + let bounceAnimation = CABasicAnimation(keyPath: "transform.rotation.z") + bounceAnimation.fromValue = rotationAngle as NSNumber + bounceAnimation.toValue = -0.04 as NSNumber + bounceAnimation.duration = 0.2 + bounceAnimation.fillMode = .forwards + bounceAnimation.timingFunction = CAMediaTimingFunction(name: .easeOut) + bounceAnimation.isRemovedOnCompletion = false + self.badgeView.layer.add(bounceAnimation, forKey: "appearance3") + self.badgeView.layer.removeAnimation(forKey: "appearance2") + + Queue.mainQueue().after(0.2) { + let returnAnimation = CABasicAnimation(keyPath: "transform.rotation.z") + returnAnimation.fromValue = -0.04 as NSNumber + returnAnimation.toValue = 0.0 as NSNumber + returnAnimation.duration = 0.15 + returnAnimation.fillMode = .forwards + returnAnimation.timingFunction = CAMediaTimingFunction(name: .easeIn) + self.badgeView.layer.add(returnAnimation, forKey: "appearance4") + self.badgeView.layer.removeAnimation(forKey: "appearance3") + } + }) + } + + if from == nil { + self.badgeView.alpha = 1.0 + self.badgeView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + } + + if let badgeText = component.badgeText { + let transition: ComponentTransition = .easeInOut(duration: from != nil ? 0.3 : 0.5) + var frameTransition = transition + if from == nil { + frameTransition = frameTransition.withAnimation(.none) + } + let badgeLabelSize = self.badgeLabel.update(value: badgeText, transition: transition) + frameTransition.setFrame(view: self.badgeLabel, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((badgeFullSize.width - badgeLabelSize.width) / 2.0), y: -2.0), size: badgeLabelSize)) + } + } + + var previousAvailableSize: CGSize? + func update(component: RemainingCountComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { + self.component = component + self.inactiveBackground.backgroundColor = component.inactiveColor.cgColor + self.activeBackground.backgroundColor = component.activeColors.last?.cgColor + + let size = CGSize(width: availableSize.width, height: 90.0) + + self.badgeLabel.color = component.activeTitleColor + + let lineHeight: CGFloat = 30.0 + let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - lineHeight), size: CGSize(width: size.width, height: lineHeight)) + self.container.frame = containerFrame + + let activityPosition: CGFloat = floor(containerFrame.width * component.badgeGraphPosition) + let activeWidth: CGFloat = containerFrame.width - activityPosition + + let leftTextColor: UIColor + let rightTextColor: UIColor + if component.invertProgress { + leftTextColor = component.inactiveTitleColor + rightTextColor = component.inactiveTitleColor + } else { + leftTextColor = component.inactiveTitleColor + rightTextColor = component.activeTitleColor + } + + if "".isEmpty { + if component.invertProgress { + let innerLeftTitleSize = self.innerLeftTitleLabel.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain( + NSAttributedString( + string: component.inactiveTitle, + font: Font.semibold(15.0), + textColor: component.activeTitleColor + ) + ) + ) + ), + environment: {}, + containerSize: availableSize + ) + if let view = self.innerLeftTitleLabel.view { + if view.superview == nil { + self.activeContainer.addSubview(view) + } + view.frame = CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((lineHeight - innerLeftTitleSize.height) / 2.0)), size: innerLeftTitleSize) + } + + let innerRightTitleSize = self.innerRightTitleLabel.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain( + NSAttributedString( + string: component.activeValue, + font: Font.semibold(15.0), + textColor: component.activeTitleColor + ) + ) + ) + ), + environment: {}, + containerSize: availableSize + ) + if let view = self.innerRightTitleLabel.view { + if view.superview == nil { + self.activeContainer.addSubview(view) + } + view.frame = CGRect(origin: CGPoint(x: containerFrame.width - 12.0 - innerRightTitleSize.width, y: floorToScreenPixels((lineHeight - innerRightTitleSize.height) / 2.0)), size: innerRightTitleSize) + } + } + + let inactiveTitleSize = self.inactiveTitleLabel.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain( + NSAttributedString( + string: component.inactiveTitle, + font: Font.semibold(15.0), + textColor: leftTextColor + ) + ) + ) + ), + environment: {}, + containerSize: availableSize + ) + if let view = self.inactiveTitleLabel.view { + if view.superview == nil { + self.container.addSubview(view) + } + view.frame = CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((lineHeight - inactiveTitleSize.height) / 2.0)), size: inactiveTitleSize) + } + + let inactiveValueSize = self.inactiveValueLabel.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain( + NSAttributedString( + string: component.inactiveValue, + font: Font.semibold(15.0), + textColor: leftTextColor + ) + ) + ) + ), + environment: {}, + containerSize: availableSize + ) + if let view = self.inactiveValueLabel.view { + if view.superview == nil { + self.container.addSubview(view) + } + view.frame = CGRect(origin: CGPoint(x: activityPosition - 12.0 - inactiveValueSize.width, y: floorToScreenPixels((lineHeight - inactiveValueSize.height) / 2.0)), size: inactiveValueSize) + } + + let activeTitleSize = self.activeTitleLabel.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain( + NSAttributedString( + string: component.activeTitle, + font: Font.semibold(15.0), + textColor: rightTextColor + ) + ) + ) + ), + environment: {}, + containerSize: availableSize + ) + if let view = self.activeTitleLabel.view { + if view.superview == nil { + self.container.addSubview(view) + } + view.frame = CGRect(origin: CGPoint(x: activityPosition + 12.0, y: floorToScreenPixels((lineHeight - activeTitleSize.height) / 2.0)), size: activeTitleSize) + } + + let activeValueSize = self.activeValueLabel.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain( + NSAttributedString( + string: component.activeValue, + font: Font.semibold(15.0), + textColor: rightTextColor + ) + ) + ) + ), + environment: {}, + containerSize: availableSize + ) + if let view = self.activeValueLabel.view { + if view.superview == nil { + self.container.addSubview(view) + + if component.invertProgress { + self.container.bringSubviewToFront(self.activeContainer) + } + } + view.frame = CGRect(origin: CGPoint(x: containerFrame.width - 12.0 - activeValueSize.width, y: floorToScreenPixels((lineHeight - activeValueSize.height) / 2.0)), size: activeValueSize) + } + } + + var progressTransition: ComponentTransition = .immediate + if !transition.animation.isImmediate { + progressTransition = .easeInOut(duration: 0.5) + } + if "".isEmpty { + if component.invertProgress { + progressTransition.setFrame(layer: self.inactiveBackground, frame: CGRect(origin: CGPoint(x: activityPosition, y: 0.0), size: CGSize(width: size.width - activityPosition, height: lineHeight))) + progressTransition.setFrame(view: self.activeContainer, frame: CGRect(origin: .zero, size: CGSize(width: activityPosition, height: lineHeight))) + progressTransition.setBounds(layer: self.activeBackground, bounds: CGRect(origin: .zero, size: CGSize(width: containerFrame.width * 1.35, height: lineHeight))) + } else { + progressTransition.setFrame(layer: self.inactiveBackground, frame: CGRect(origin: .zero, size: CGSize(width: activityPosition, height: lineHeight))) + progressTransition.setFrame(view: self.activeContainer, frame: CGRect(origin: CGPoint(x: activityPosition, y: 0.0), size: CGSize(width: activeWidth, height: lineHeight))) + progressTransition.setFrame(layer: self.activeBackground, frame: CGRect(origin: CGPoint(x: -activityPosition, y: 0.0), size: CGSize(width: containerFrame.width * 1.35, height: lineHeight))) + } + if self.activeBackground.animation(forKey: "movement") == nil { + self.activeBackground.position = CGPoint(x: -self.activeContainer.frame.width * 0.35, y: lineHeight / 2.0) + } + } + + let countWidth: CGFloat + if let badgeText = component.badgeText { + countWidth = CGFloat(badgeText.count) * 10.0 + } else { + countWidth = 51.0 + } + let badgeWidth: CGFloat = countWidth + 20.0 + + let badgeSize = CGSize(width: badgeWidth, height: 30.0) + let badgeFullSize = CGSize(width: badgeWidth, height: 30.0 + 8.0) + let tailSize = CGSize(width: 15.0, height: 6.0) + let tailRadius: CGFloat = 3.0 + self.badgeMaskView.frame = CGRect(origin: .zero, size: badgeFullSize) + self.badgeShapeLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -4.0), size: badgeFullSize) + + self.badgeView.bounds = CGRect(origin: .zero, size: badgeFullSize) + + let currentBadgeX = self.badgeView.center.x + + let badgePosition = component.badgePosition + + if badgePosition > 1.0 - 0.15 { + progressTransition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 1.0, y: 1.0)) + + if progressTransition.animation.isImmediate { + self.badgeShapeLayer.path = generateRoundedRectWithTailPath(rectSize: badgeSize, tailSize: tailSize, tailRadius: tailRadius, tailPosition: 1.0).cgPath + } else { + self.badgeShapeArguments = (CACurrentMediaTime(), 0.5, badgeSize, self.badgeTailPosition, 1.0) + self.animateBadgeTailPositionChange() + } + self.badgeTailPosition = 1.0 + + if let _ = self.badgeView.layer.animation(forKey: "appearance1") { + } else { + self.badgeView.center = CGPoint(x: 3.0 + (size.width - 6.0) * badgePosition + 3.0, y: 56.0) + } + } else if badgePosition < 0.15 { + progressTransition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 0.0, y: 1.0)) + + if progressTransition.animation.isImmediate { + self.badgeShapeLayer.path = generateRoundedRectWithTailPath(rectSize: badgeSize, tailSize: tailSize, tailRadius: tailRadius, tailPosition: 0.0).cgPath + } else { + self.badgeShapeArguments = (CACurrentMediaTime(), 0.5, badgeSize, self.badgeTailPosition, 0.0) + self.animateBadgeTailPositionChange() + } + self.badgeTailPosition = 0.0 + + if let _ = self.badgeView.layer.animation(forKey: "appearance1") { + + } else { + self.badgeView.center = CGPoint(x: (size.width - 6.0) * badgePosition, y: 56.0) + } + } else { + progressTransition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 0.5, y: 1.0)) + + if progressTransition.animation.isImmediate { + self.badgeShapeLayer.path = generateRoundedRectWithTailPath(rectSize: badgeSize, tailSize: tailSize, tailRadius: tailRadius, tailPosition: 0.5).cgPath + } else { + self.badgeShapeArguments = (CACurrentMediaTime(), 0.5, badgeSize, self.badgeTailPosition, 0.5) + self.animateBadgeTailPositionChange() + } + self.badgeTailPosition = 0.5 + + if let _ = self.badgeView.layer.animation(forKey: "appearance1") { + + } else { + self.badgeView.center = CGPoint(x: size.width * badgePosition, y: 56.0) + } + } + self.badgeForeground.bounds = CGRect(origin: CGPoint(), size: CGSize(width: badgeFullSize.width * 3.0, height: badgeFullSize.height)) + if self.badgeForeground.animation(forKey: "movement") == nil { + self.badgeForeground.position = CGPoint(x: badgeSize.width * 3.0 / 2.0 - self.badgeForeground.frame.width * 0.35, y: badgeFullSize.height / 2.0) + } + + self.badgeLabelMaskView.frame = CGRect(x: 0.0, y: 0.0, width: 100.0, height: 36.0) + + if !self.didPlayAppearanceAnimation || !transition.animation.isImmediate { + self.didPlayAppearanceAnimation = true + if transition.animation.isImmediate { + if component.badgePosition < 0.1 { + self.badgeView.alpha = 1.0 + if let badgeText = component.badgeText { + let badgeLabelSize = self.badgeLabel.update(value: badgeText, transition: .immediate) + transition.setFrame(view: self.badgeLabel, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((badgeFullSize.width - badgeLabelSize.width) / 2.0), y: -2.0), size: badgeLabelSize)) + } + } else { + self.playAppearanceAnimation(component: component, badgeFullSize: badgeFullSize) + } + } else { + self.playAppearanceAnimation(component: component, badgeFullSize: badgeFullSize, from: currentBadgeX) + } + } + + if self.previousAvailableSize != availableSize { + self.previousAvailableSize = availableSize + + var locations: [CGFloat] = [] + let delta = 1.0 / CGFloat(component.activeColors.count - 1) + for i in 0 ..< component.activeColors.count { + locations.append(delta * CGFloat(i)) + } + + let gradient = generateGradientImage(size: CGSize(width: 200.0, height: 60.0), colors: component.activeColors, locations: locations, direction: .horizontal) + self.badgeForeground.contentsGravity = .resizeAspectFill + self.badgeForeground.contents = gradient?.cgImage + + self.activeBackground.contentsGravity = .resizeAspectFill + self.activeBackground.contents = gradient?.cgImage + + self.setupGradientAnimations() + } + + return size + } + + private var badgeShapeAnimator: ConstantDisplayLinkAnimator? + private func animateBadgeTailPositionChange() { + if self.badgeShapeAnimator == nil { + self.badgeShapeAnimator = ConstantDisplayLinkAnimator(update: { [weak self] in + self?.animateBadgeTailPositionChange() + }) + self.badgeShapeAnimator?.isPaused = true + } + + if let (startTime, duration, badgeSize, initial, target) = self.badgeShapeArguments { + self.badgeShapeAnimator?.isPaused = false + + let t = CGFloat(max(0.0, min(1.0, (CACurrentMediaTime() - startTime) / duration))) + let value = initial + (target - initial) * t + + let tailSize = CGSize(width: 15.0, height: 6.0) + let tailRadius: CGFloat = 3.0 + self.badgeShapeLayer.path = generateRoundedRectWithTailPath(rectSize: badgeSize, tailSize: tailSize, tailRadius: tailRadius, tailPosition: value).cgPath + + if t >= 1.0 { + self.badgeShapeArguments = nil + self.badgeShapeAnimator?.isPaused = true + self.badgeShapeAnimator?.invalidate() + self.badgeShapeAnimator = nil + } + } else { + self.badgeShapeAnimator?.isPaused = true + self.badgeShapeAnimator?.invalidate() + self.badgeShapeAnimator = nil + } + } + + private func setupGradientAnimations() { + guard let _ = self.component else { + return + } + if let _ = self.badgeForeground.animation(forKey: "movement") { + } else { + CATransaction.begin() + + let badgeOffset = (self.badgeForeground.frame.width - self.badgeView.bounds.width) / 2.0 + let badgePreviousValue = self.badgeForeground.position.x + var badgeNewValue: CGFloat = badgeOffset + if badgeOffset - badgePreviousValue < self.badgeForeground.frame.width * 0.25 { + badgeNewValue -= self.badgeForeground.frame.width * 0.35 + } + self.badgeForeground.position = CGPoint(x: badgeNewValue, y: self.badgeForeground.bounds.size.height / 2.0) + + let lineOffset = 0.0 + let linePreviousValue = self.activeBackground.position.x + var lineNewValue: CGFloat = lineOffset + if linePreviousValue < 0.0 { + lineNewValue = 0.0 + } else { + lineNewValue = -self.activeContainer.bounds.width * 0.35 + } + lineNewValue -= self.activeContainer.frame.minX + self.activeBackground.position = CGPoint(x: lineNewValue, y: 0.0) + + let badgeAnimation = CABasicAnimation(keyPath: "position.x") + badgeAnimation.duration = 4.5 + badgeAnimation.fromValue = badgePreviousValue + badgeAnimation.toValue = badgeNewValue + badgeAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + + CATransaction.setCompletionBlock { [weak self] in + self?.setupGradientAnimations() + } + self.badgeForeground.add(badgeAnimation, forKey: "movement") + + let lineAnimation = CABasicAnimation(keyPath: "position.x") + lineAnimation.duration = 4.5 + lineAnimation.fromValue = linePreviousValue + lineAnimation.toValue = lineNewValue + lineAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + self.activeBackground.add(lineAnimation, forKey: "movement") + + CATransaction.commit() + } + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} + + +private let labelWidth: CGFloat = 10.0 +private let labelHeight: CGFloat = 30.0 +private let labelSize = CGSize(width: labelWidth, height: labelHeight) +private let font = Font.with(size: 15.0, design: .regular, weight: .semibold, traits: []) + +final class BadgeLabelView: UIView { + private class StackView: UIView { + var labels: [UILabel] = [] + + var currentValue: Int32 = 0 + + var color: UIColor = .white { + didSet { + for view in self.labels { + view.textColor = self.color + } + } + } + + init() { + super.init(frame: CGRect(origin: .zero, size: labelSize)) + + var height: CGFloat = -labelHeight + for i in -1 ..< 10 { + let label = UILabel() + if i == -1 { + label.text = "9" + } else { + label.text = "\(i)" + } + label.textColor = self.color + label.font = font + label.textAlignment = .center + label.frame = CGRect(x: 0, y: height, width: labelWidth, height: labelHeight) + self.addSubview(label) + self.labels.append(label) + + height += labelHeight + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(value: Int32, isFirst: Bool, isLast: Bool, transition: ComponentTransition) { + let previousValue = self.currentValue + self.currentValue = value + + self.labels[1].alpha = isFirst && !isLast ? 0.0 : 1.0 + + if previousValue == 9 && value < 9 { + self.bounds = CGRect( + origin: CGPoint( + x: 0.0, + y: -1.0 * labelSize.height + ), + size: labelSize + ) + } + + let bounds = CGRect( + origin: CGPoint( + x: 0.0, + y: CGFloat(value) * labelSize.height + ), + size: labelSize + ) + transition.setBounds(view: self, bounds: bounds) + } + } + + private var itemViews: [Int: StackView] = [:] + private var staticLabel = UILabel() + + init() { + super.init(frame: .zero) + + self.clipsToBounds = true + self.isUserInteractionEnabled = false + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + var color: UIColor = .white { + didSet { + self.staticLabel.textColor = self.color + for (_, view) in self.itemViews { + view.color = self.color + } + } + } + + func update(value: String, transition: ComponentTransition) -> CGSize { + if value.contains(" ") { + for (_, view) in self.itemViews { + view.isHidden = true + } + + if self.staticLabel.superview == nil { + self.staticLabel.textColor = self.color + self.staticLabel.font = font + + self.addSubview(self.staticLabel) + } + + self.staticLabel.text = value + let size = self.staticLabel.sizeThatFits(CGSize(width: 100.0, height: 100.0)) + self.staticLabel.frame = CGRect(origin: .zero, size: CGSize(width: size.width, height: labelHeight)) + + return CGSize(width: ceil(self.staticLabel.bounds.width), height: ceil(self.staticLabel.bounds.height)) + } + + let string = value + let stringArray = Array(string.map { String($0) }.reversed()) + + let totalWidth = CGFloat(stringArray.count) * labelWidth + + var validIds: [Int] = [] + for i in 0 ..< stringArray.count { + validIds.append(i) + + let itemView: StackView + var itemTransition = transition + if let current = self.itemViews[i] { + itemView = current + } else { + itemTransition = transition.withAnimation(.none) + itemView = StackView() + itemView.color = self.color + self.itemViews[i] = itemView + self.addSubview(itemView) + } + + let digit = Int32(stringArray[i]) ?? 0 + itemView.update(value: digit, isFirst: i == stringArray.count - 1, isLast: i == 0, transition: transition) + + itemTransition.setFrame( + view: itemView, + frame: CGRect(x: totalWidth - labelWidth * CGFloat(i + 1), y: 0.0, width: labelWidth, height: labelHeight) + ) + } + + var removeIds: [Int] = [] + for (id, itemView) in self.itemViews { + if !validIds.contains(id) { + removeIds.append(id) + + transition.setAlpha(view: itemView, alpha: 0.0, completion: { _ in + itemView.removeFromSuperview() + }) + } + } + for id in removeIds { + self.itemViews.removeValue(forKey: id) + } + return CGSize(width: totalWidth, height: labelHeight) + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 1caa1447d7..f499db410e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -1382,7 +1382,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese url: "", simple: true, source: .generic, - skipTermsOfService: true + skipTermsOfService: true, + payload: nil ) }) } diff --git a/submodules/TelegramUI/Sources/ApplicationContext.swift b/submodules/TelegramUI/Sources/ApplicationContext.swift index 89264f0f67..00ef0e1e1e 100644 --- a/submodules/TelegramUI/Sources/ApplicationContext.swift +++ b/submodules/TelegramUI/Sources/ApplicationContext.swift @@ -945,8 +945,8 @@ final class AuthorizedApplicationContext { chatLocation = .peer(peer) } - if openAppIfAny, let parentController = self.rootController.viewControllers.last as? ViewController { - self.context.sharedContext.openWebApp(context: self.context, parentController: parentController, updatedPresentationData: nil, peer: peer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: true) + if openAppIfAny, case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp), let parentController = self.rootController.viewControllers.last as? ViewController { + self.context.sharedContext.openWebApp(context: self.context, parentController: parentController, updatedPresentationData: nil, peer: peer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: true, payload: nil) } else { self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: self.rootController, context: self.context, chatLocation: chatLocation, subject: isOutgoingMessage ? messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false) } : nil, activateInput: activateInput ? .text : nil)) } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift index b9a165de76..e2d930eb90 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift @@ -14,7 +14,7 @@ import UndoUI import UrlHandling import TelegramPresentationData -func openWebAppImpl(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool) { +func openWebAppImpl(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?) { let presentationData: PresentationData if let parentController = parentController as? ChatControllerImpl { presentationData = parentController.presentationData @@ -195,7 +195,7 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u } else { source = url.isEmpty ? .generic : .simple } - let params = WebAppParameters(source: source, peerId: peer.id, botId: botId, botName: botName, botVerified: botVerified, url: result.url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: result.flags.contains(.fullSize)) + let params = WebAppParameters(source: source, peerId: peer.id, botId: botId, botName: botName, botVerified: botVerified, url: result.url, queryId: nil, payload: payload, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: result.flags.contains(.fullSize)) let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { [weak parentController] url, concealed, commit in ChatControllerImpl.botOpenUrl(context: context, peerId: peer.id, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, present: { c, a in presentImpl?(c, a) @@ -310,7 +310,7 @@ public extension ChatControllerImpl { } self.chatDisplayNode.dismissInput() - self.context.sharedContext.openWebApp(context: self.context, parentController: self, updatedPresentationData: self.updatedPresentationData, peer: EnginePeer(peer), threadId: self.chatLocation.threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: false) + self.context.sharedContext.openWebApp(context: self.context, parentController: self, updatedPresentationData: self.updatedPresentationData, peer: EnginePeer(peer), threadId: self.chatLocation.threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: false, payload: nil) } static func botRequestSwitchInline(context: AccountContext, controller: ChatControllerImpl?, peerId: EnginePeer.Id, botAddress: String, query: String, chatTypes: [ReplyMarkupButtonRequestPeerType]?, completion: @escaping () -> Void) -> Void { @@ -561,7 +561,7 @@ public extension ChatControllerImpl { } }) } else { - self.context.sharedContext.openWebApp(context: self.context, parentController: self, updatedPresentationData: self.updatedPresentationData, peer: botPeer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: false) + self.context.sharedContext.openWebApp(context: self.context, parentController: self, updatedPresentationData: self.updatedPresentationData, peer: botPeer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: false, payload: payload) } } } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 4805717a80..a4a0dff681 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -9721,7 +9721,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G commit() }) } else { - strongSelf.context.sharedContext.openWebApp(context: strongSelf.context, parentController: strongSelf, updatedPresentationData: strongSelf.updatedPresentationData, peer: peer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: false) + strongSelf.context.sharedContext.openWebApp(context: strongSelf.context, parentController: strongSelf, updatedPresentationData: strongSelf.updatedPresentationData, peer: peer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: false, payload: botAppStart.payload) commit() } } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 60de3e50ed..8213dafa0c 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2827,8 +2827,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { return MiniAppListScreen(context: context, initialData: initialData as! MiniAppListScreen.InitialData) } - public func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool) { - openWebAppImpl(context: context, parentController: parentController, updatedPresentationData: updatedPresentationData, peer: peer, threadId: threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: skipTermsOfService) + public func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?) { + openWebAppImpl(context: context, parentController: parentController, updatedPresentationData: updatedPresentationData, peer: peer, threadId: threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: skipTermsOfService, payload: payload) } } From 62517683ad2607fa46680cf934af6f8595f189de Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 5 Oct 2024 01:05:29 +0400 Subject: [PATCH 7/9] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 4 +++ .../Sources/ChatListSearchListPaneNode.swift | 26 +++++++++---------- .../Sources/Node/ChatListNode.swift | 17 +++++++++--- .../Sources/ContactsPeerItem.swift | 14 +++++++++- .../Sources/TelegramEngine/Peers/Peer.swift | 4 +++ .../TelegramEngine/Peers/SearchPeers.swift | 14 +++++++--- .../Sources/ContentReportScreen.swift | 8 ++++-- .../Sources/GiftSetupScreen.swift | 3 +-- 8 files changed, 66 insertions(+), 24 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 5bf388371a..6d7999efb1 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -13049,6 +13049,7 @@ Sorry for the inconvenience."; "Report.Title.User" = "Report User"; "Report.Title.Group" = "Report Group"; "Report.Title.Channel" = "Report Channel"; +"Report.Title.Bot" = "Report Bot"; "Report.Comment.Placeholder" = "Add Comment"; "Report.Comment.Placeholder.Optional" = "Add Comment (Optional)"; "Report.Comment.Info" = "Please help us by telling what is wrong with the message you have selected."; @@ -13064,3 +13065,6 @@ Sorry for the inconvenience."; "Notification.StarsGift.Stars_1" = "%@ Star"; "Notification.StarsGift.Stars_any" = "%@ Stars"; + +"WebBrowser.AuthChallenge.Title" = "Sign in to %@"; +"WebBrowser.AuthChallenge.Text" = "Your login information will be sent securely."; diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index a522784efb..4f211c7658 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -399,7 +399,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { case topic(EnginePeer, ChatListItemContent.ThreadInfo, Int, PresentationTheme, PresentationStrings, ChatListSearchSectionExpandType) case recentlySearchedPeer(EnginePeer, EnginePeer?, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, PeerStoryStats?, Bool) case localPeer(EnginePeer, EnginePeer?, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType, PeerStoryStats?, Bool) - case globalPeer(FoundPeer, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType, PeerStoryStats?, Bool) + case globalPeer(FoundPeer, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType, PeerStoryStats?, Bool, String?) case message(EngineMessage, EngineRenderedPeer, EnginePeerReadCounters?, EngineMessageHistoryThread.Info?, ChatListPresentationData, Int32, Bool?, Bool, MessageOrderingKey, (id: String, size: Int64, isFirstInList: Bool)?, MessageSection, Bool, PeerStoryStats?, Bool) case addContact(String, PresentationTheme, PresentationStrings) @@ -411,7 +411,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { return .localPeerId(peer.id) case let .localPeer(peer, _, _, _, _, _, _, _, _, _, _): return .localPeerId(peer.id) - case let .globalPeer(peer, _, _, _, _, _, _, _, _, _): + case let .globalPeer(peer, _, _, _, _, _, _, _, _, _, _): return .globalPeerId(peer.peer.id) case let .message(message, _, _, _, _, _, _, _, _, _, section, _, _, _): return .messageId(message.id, section) @@ -440,8 +440,8 @@ public enum ChatListSearchEntry: Comparable, Identifiable { } else { return false } - case let .globalPeer(lhsPeer, lhsUnreadBadge, lhsIndex, lhsTheme, lhsStrings, lhsSortOrder, lhsDisplayOrder, lhsExpandType, lhsStoryStats, lhsRequiresPremiumForMessaging): - if case let .globalPeer(rhsPeer, rhsUnreadBadge, rhsIndex, rhsTheme, rhsStrings, rhsSortOrder, rhsDisplayOrder, rhsExpandType, rhsStoryStats, rhsRequiresPremiumForMessaging) = rhs, lhsPeer == rhsPeer && lhsIndex == rhsIndex && lhsTheme === rhsTheme && lhsStrings === rhsStrings && lhsSortOrder == rhsSortOrder && lhsDisplayOrder == rhsDisplayOrder && lhsUnreadBadge?.0 == rhsUnreadBadge?.0 && lhsUnreadBadge?.1 == rhsUnreadBadge?.1 && lhsExpandType == rhsExpandType && lhsStoryStats == rhsStoryStats && lhsRequiresPremiumForMessaging == rhsRequiresPremiumForMessaging { + case let .globalPeer(lhsPeer, lhsUnreadBadge, lhsIndex, lhsTheme, lhsStrings, lhsSortOrder, lhsDisplayOrder, lhsExpandType, lhsStoryStats, lhsRequiresPremiumForMessaging, lhsQuery): + if case let .globalPeer(rhsPeer, rhsUnreadBadge, rhsIndex, rhsTheme, rhsStrings, rhsSortOrder, rhsDisplayOrder, rhsExpandType, rhsStoryStats, rhsRequiresPremiumForMessaging, rhsQuery) = rhs, lhsPeer == rhsPeer && lhsIndex == rhsIndex && lhsTheme === rhsTheme && lhsStrings === rhsStrings && lhsSortOrder == rhsSortOrder && lhsDisplayOrder == rhsDisplayOrder && lhsUnreadBadge?.0 == rhsUnreadBadge?.0 && lhsUnreadBadge?.1 == rhsUnreadBadge?.1 && lhsExpandType == rhsExpandType && lhsStoryStats == rhsStoryStats && lhsRequiresPremiumForMessaging == rhsRequiresPremiumForMessaging, lhsQuery == rhsQuery { return true } else { return false @@ -543,11 +543,11 @@ public enum ChatListSearchEntry: Comparable, Identifiable { case .globalPeer, .message, .addContact: return true } - case let .globalPeer(_, _, lhsIndex, _, _, _, _, _, _, _): + case let .globalPeer(_, _, lhsIndex, _, _, _, _, _, _, _, _): switch rhs { case .topic, .recentlySearchedPeer, .localPeer: return false - case let .globalPeer(_, _, rhsIndex, _, _, _, _, _, _, _): + case let .globalPeer(_, _, rhsIndex, _, _, _, _, _, _, _, _): return lhsIndex <= rhsIndex case .message, .addContact: return true @@ -798,7 +798,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { openStories(peer.id, sourceNode.avatarNode) } }) - case let .globalPeer(peer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType, storyStats, requiresPremiumForMessaging): + case let .globalPeer(peer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType, storyStats, requiresPremiumForMessaging, query): var enabled = true if filter.contains(.onlyWriteable) { enabled = canSendMessagesToPeer(peer.peer) @@ -822,7 +822,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { var suffixString = "" if let subscribers = peer.subscribers, subscribers != 0 { if peer.peer is TelegramUser { - suffixString = ", \(strings.Conversation_StatusSubscribers(subscribers))" + suffixString = ", \(strings.Conversation_StatusBotSubscribers(subscribers))" } else if let channel = peer.peer as? TelegramChannel, case .broadcast = channel.info { suffixString = ", \(strings.Conversation_StatusSubscribers(subscribers))" } else { @@ -858,7 +858,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { isSavedMessages = true } - return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch(isSavedMessages: isSavedMessages), peer: .peer(peer: EnginePeer(peer.peer), chatPeer: EnginePeer(peer.peer)), status: .addressName(suffixString), badge: badge, requiresPremiumForMessaging: requiresPremiumForMessaging, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in + return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch(isSavedMessages: isSavedMessages), peer: .peer(peer: EnginePeer(peer.peer), chatPeer: EnginePeer(peer.peer)), status: .addressName(suffixString), badge: badge, requiresPremiumForMessaging: requiresPremiumForMessaging, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, searchQuery: query, action: { _ in interaction.peerSelected(EnginePeer(peer.peer), nil, nil, nil) }, disabledAction: { _ in interaction.disabledPeerSelected(EnginePeer(peer.peer), nil, requiresPremiumForMessaging ? .premiumRequired : .generic) @@ -2607,7 +2607,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { if !existingPeerIds.contains(peer.peer.id), filteredPeer(EnginePeer(peer.peer), EnginePeer(accountPeer)) { existingPeerIds.insert(peer.peer.id) - entries.append(.globalPeer(peer, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, globalExpandType, nil, false)) + entries.append(.globalPeer(peer, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, globalExpandType, nil, false, finalQuery)) index += 1 numberOfGlobalPeers += 1 } @@ -2953,7 +2953,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { if case let .user(user) = peer, user.flags.contains(.requirePremium) { requiresPremiumForMessagingPeerIds.append(peer.id) } - case let .globalPeer(foundPeer, _, _, _, _, _, _, _, _, _): + case let .globalPeer(foundPeer, _, _, _, _, _, _, _, _, _, _): storyStatsIds.append(foundPeer.peer.id) if let user = foundPeer.peer as? TelegramUser, user.flags.contains(.requirePremium) { requiresPremiumForMessagingPeerIds.append(foundPeer.peer.id) @@ -2994,8 +2994,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { mappedItems[i] = .recentlySearchedPeer(peer, associatedPeer, unreadBadge, index, theme, strings, sortOrder, displayOrder, stats[peer.id] ?? nil, requiresPremiumForMessaging[peer.id] ?? false) case let .localPeer(peer, associatedPeer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, _, _): mappedItems[i] = .localPeer(peer, associatedPeer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, stats[peer.id] ?? nil, requiresPremiumForMessaging[peer.id] ?? false) - case let .globalPeer(peer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, _, _): - mappedItems[i] = .globalPeer(peer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, stats[peer.peer.id] ?? nil, requiresPremiumForMessaging[peer.peer.id] ?? false) + case let .globalPeer(peer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, _, _, searchQuery): + mappedItems[i] = .globalPeer(peer, unreadBadge, index, theme, strings, sortOrder, displayOrder, expandType, stats[peer.peer.id] ?? nil, requiresPremiumForMessaging[peer.peer.id] ?? false, searchQuery) case let .message(message, peer, combinedPeerReadState, threadInfo, presentationData, totalCount, selected, displayCustomHeader, key, resourceId, section, allPaused, _, _): mappedItems[i] = .message(message, peer, combinedPeerReadState, threadInfo, presentationData, totalCount, selected, displayCustomHeader, key, resourceId, section, allPaused, stats[peer.peerId] ?? nil, requiresPremiumForMessaging[peer.peerId] ?? false) default: diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 9d7d970285..fc2bb3c91e 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -1714,9 +1714,20 @@ public final class ChatListNode: ListView { guard let self else { return } - let controller = self.context.sharedContext.makePremiumGiftController(context: self.context, source: .chatList(birthdays), completion: nil) - controller.navigationPresentation = .modal - self.push?(controller) + if let birthdays, birthdays.count == 1, let peerId = birthdays.keys.first { + let _ = (self.context.engine.payments.premiumGiftCodeOptions(peerId: nil, onlyCached: true) + |> filter { !$0.isEmpty } + |> deliverOnMainQueue).start(next: { giftOptions in + let premiumOptions = giftOptions.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) } + let controller = self.context.sharedContext.makeGiftOptionsController(context: self.context, peerId: peerId, premiumOptions: premiumOptions) + controller.navigationPresentation = .modal + self.push?(controller) + }) + } else { + let controller = self.context.sharedContext.makePremiumGiftController(context: self.context, source: .chatList(birthdays), completion: nil) + controller.navigationPresentation = .modal + self.push?(controller) + } }, openPremiumManagement: { [weak self] in guard let self else { return diff --git a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift index 966d2d5a2e..12346ecdb7 100644 --- a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift +++ b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift @@ -175,6 +175,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader { let options: [ItemListPeerItemRevealOption] let additionalActions: [ContactsPeerItemAction] let actionIcon: ContactsPeerItemActionIcon + let searchQuery: String? let action: ((ContactsPeerItemPeer) -> Void)? let disabledAction: ((ContactsPeerItemPeer) -> Void)? let setPeerIdWithRevealedOptions: ((EnginePeer.Id?, EnginePeer.Id?) -> Void)? @@ -215,6 +216,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader { actionIcon: ContactsPeerItemActionIcon = .none, index: SortIndex?, header: ListViewItemHeader?, + searchQuery: String? = nil, action: ((ContactsPeerItemPeer) -> Void)?, disabledAction: ((ContactsPeerItemPeer) -> Void)? = nil, setPeerIdWithRevealedOptions: ((EnginePeer.Id?, EnginePeer.Id?) -> Void)? = nil, @@ -245,6 +247,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader { self.options = options self.additionalActions = additionalActions self.actionIcon = actionIcon + self.searchQuery = searchQuery self.action = action self.disabledAction = disabledAction self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions @@ -880,7 +883,16 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { statusAttributedString = NSAttributedString(string: string, font: statusFont, textColor: activity ? item.presentationData.theme.list.itemAccentColor : item.presentationData.theme.list.itemSecondaryTextColor) } case let .addressName(suffix): - if let addressName = peer.addressName { + var addressName = peer.addressName + if let currentAddressName = addressName, let searchQuery = item.searchQuery?.lowercased(), !peer.usernames.isEmpty && !currentAddressName.lowercased().contains(searchQuery) { + for username in peer.usernames { + if username.username.lowercased().contains(searchQuery) { + addressName = username.username + break + } + } + } + if let addressName { let addressNameString = NSAttributedString(string: "@" + addressName, font: statusFont, textColor: item.presentationData.theme.list.itemAccentColor) if !suffix.isEmpty { let suffixString = NSAttributedString(string: suffix, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift index 97c3c609a0..5ee8026860 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift @@ -462,6 +462,10 @@ public extension EnginePeer { var addressName: String? { return self._asPeer().addressName } + + var usernames: [TelegramPeerUsername] { + return self._asPeer().usernames + } var indexName: EnginePeer.IndexName { return EnginePeer.IndexName(self._asPeer().indexName) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift index f6099c2e55..aa07479d98 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift @@ -51,7 +51,7 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo } } } - + updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers) var renderedMyPeers: [FoundPeer] = [] @@ -61,7 +61,11 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo if let group = peer as? TelegramGroup, group.migrationReference != nil { continue } - renderedMyPeers.append(FoundPeer(peer: peer, subscribers: subscribers[peerId])) + if let user = peer as? TelegramUser { + renderedMyPeers.append(FoundPeer(peer: peer, subscribers: user.subscriberCount)) + } else { + renderedMyPeers.append(FoundPeer(peer: peer, subscribers: subscribers[peerId])) + } } } @@ -72,7 +76,11 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo if let group = peer as? TelegramGroup, group.migrationReference != nil { continue } - renderedPeers.append(FoundPeer(peer: peer, subscribers: subscribers[peerId])) + if let user = peer as? TelegramUser { + renderedPeers.append(FoundPeer(peer: peer, subscribers: user.subscriberCount)) + } else { + renderedPeers.append(FoundPeer(peer: peer, subscribers: subscribers[peerId])) + } } } diff --git a/submodules/TelegramUI/Components/ContentReportScreen/Sources/ContentReportScreen.swift b/submodules/TelegramUI/Components/ContentReportScreen/Sources/ContentReportScreen.swift index 1d258a7b4f..5585d5c5d7 100644 --- a/submodules/TelegramUI/Components/ContentReportScreen/Sources/ContentReportScreen.swift +++ b/submodules/TelegramUI/Components/ContentReportScreen/Sources/ContentReportScreen.swift @@ -468,8 +468,12 @@ private final class SheetContent: CombinedComponent { switch component.subject { case .peer: if let peer = state.peer { - if case .user = peer { - mainTitle = environment.strings.Report_Title_User + if case let .user(user) = peer { + if let _ = user.botInfo { + mainTitle = environment.strings.Report_Title_Bot + } else { + mainTitle = environment.strings.Report_Title_User + } } else if case let .channel(channel) = peer, case .broadcast = channel.info { mainTitle = environment.strings.Report_Title_Channel } else { diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index 538c7ca8eb..e383df3049 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -564,8 +564,7 @@ final class GiftSetupScreenComponent: Component { contentHeight += 26.0 if case let .starGift(starGift) = component.subject, let availability = starGift.availability { - //TODO:localize - let remains: Int32 = Int32(CGFloat(availability.remains) * 0.66) + let remains: Int32 = availability.remains let position = CGFloat(remains) / CGFloat(availability.total) let remainsString = "\(remains)" //presentationStringsFormattedNumber(remains, environment.dateTimeFormat.groupingSeparator) let totalString = presentationStringsFormattedNumber(availability.total, environment.dateTimeFormat.groupingSeparator) From 1dc39230e0505f07460252e0388faf9378b1bf42 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 5 Oct 2024 18:04:09 +0400 Subject: [PATCH 8/9] Various fixes --- .../GiftViewScreen/Sources/GiftViewScreen.swift | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index d5466b4a84..a97db9618a 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -185,6 +185,7 @@ private final class GiftViewSheetContent: CombinedComponent { var savedToProfile = false var converted = false var giftId: Int64 = 0 + var date: Int32 = 0 if let arguments = component.subject.arguments { animationFile = arguments.gift.file stars = arguments.gift.price @@ -196,6 +197,7 @@ private final class GiftViewSheetContent: CombinedComponent { savedToProfile = arguments.savedToProfile converted = arguments.converted giftId = arguments.gift.id + date = arguments.date } else { animationFile = nil stars = 0 @@ -333,7 +335,7 @@ private final class GiftViewSheetContent: CombinedComponent { id: "date", title: strings.Gift_View_Date, component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: Int32(Date().timeIntervalSince1970), strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) + MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) ) )) @@ -342,11 +344,13 @@ private final class GiftViewSheetContent: CombinedComponent { if let gift = state.starGiftsMap[giftId], let availability = gift.availability { remains = availability.remains } + let remainsString = presentationStringsFormattedNumber(remains, environment.dateTimeFormat.groupingSeparator) + let totalString = presentationStringsFormattedNumber(limitTotal, environment.dateTimeFormat.groupingSeparator) tableItems.append(.init( id: "availability", title: strings.Gift_View_Availability, component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_View_Availability_Of("\(remains)", "\(limitTotal)").string, font: tableFont, textColor: tableTextColor))) + MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_View_Availability_Of("\(remainsString)", "\(totalString)").string, font: tableFont, textColor: tableTextColor))) ) )) } @@ -719,14 +723,14 @@ public class GiftViewScreen: ViewControllerComponentContainer { case message(EngineMessage) case profileGift(EnginePeer.Id, ProfileGiftsContext.State.StarGift) - var arguments: (peerId: EnginePeer.Id, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, incoming: Bool, gift: StarGift, convertStars: Int64, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool)? { + var arguments: (peerId: EnginePeer.Id, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, incoming: Bool, gift: StarGift, date: Int32, convertStars: Int64, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool)? { switch self { case let .message(message): if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted) = action.action { - return (message.id.peerId, message.author?.id, message.author?.compactDisplayTitle, message.id, message.flags.contains(.Incoming), gift, convertStars, text, entities, nameHidden, savedToProfile, converted) + return (message.id.peerId, message.author?.id, message.author?.compactDisplayTitle, message.id, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, converted) } case let .profileGift(peerId, gift): - return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, gift.messageId, false, gift.gift, gift.convertStars ?? 0, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, false) + return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, gift.messageId, false, gift.gift, gift.date, gift.convertStars ?? 0, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, false) } return nil } From f174769d2d43be487564995403994ed2dcf6fe63 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 5 Oct 2024 19:46:03 +0400 Subject: [PATCH 9/9] Various fixes --- .../Telegram-iOS/en.lproj/Localizable.strings | 1 + .../MinimizeKeyboardGestureRecognizer.swift | 20 ----------- .../Sources/GiftSetupScreen.swift | 27 ++++++++++----- .../Sources/GiftViewScreen.swift | 34 ++++++++++++++----- .../Sources/PeerInfoGiftsPaneNode.swift | 7 ++-- versions.json | 2 +- 6 files changed, 50 insertions(+), 41 deletions(-) delete mode 100644 submodules/Display/Source/MinimizeKeyboardGestureRecognizer.swift diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 6d7999efb1..39da024bab 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -13038,6 +13038,7 @@ Sorry for the inconvenience."; "Gift.Send.HideMyName" = "Hide My Name"; "Gift.Send.HideMyName.Info" = "Hide my name and message from visitors to %1$@'s profile. %2$@ will still see your name and message."; "Gift.Send.Send" = "Send a Gift for"; +"Gift.Send.Limited" = "Limited"; "Profile.SendGift" = "Send a Gift"; "Settings.SendGift" = "Send a Gift"; diff --git a/submodules/Display/Source/MinimizeKeyboardGestureRecognizer.swift b/submodules/Display/Source/MinimizeKeyboardGestureRecognizer.swift deleted file mode 100644 index b8c2ed10c5..0000000000 --- a/submodules/Display/Source/MinimizeKeyboardGestureRecognizer.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation -import UIKit - -final class MinimizeKeyboardGestureRecognizer: UISwipeGestureRecognizer, UIGestureRecognizerDelegate { - override init(target: Any?, action: Selector?) { - super.init(target: target, action: action) - - self.cancelsTouchesInView = false - self.delaysTouchesBegan = false - self.delaysTouchesEnded = false - self.delegate = self - - self.direction = [.left, .right] - self.numberOfTouchesRequired = 2 - } - - func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { - return true - } -} diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index e383df3049..713deadceb 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -143,6 +143,8 @@ final class GiftSetupScreenComponent: Component { self.addSubview(self.scrollView) self.scrollView.layer.addSublayer(self.topOverscrollLayer) + + self.disablesInteractiveKeyboardGestureRecognizer = true } required init?(coder: NSCoder) { @@ -352,6 +354,8 @@ final class GiftSetupScreenComponent: Component { } navigationController.setViewControllers(controllers, animated: true) } + + starsContext.load(force: true) }) }) } @@ -459,9 +463,7 @@ final class GiftSetupScreenComponent: Component { return } if let textInputView = self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View { - if self.textInputState.isEditing { - textInputView.insertText(text: text) - } + textInputView.insertText(text: text) } }, backwardsDeleteText: { [weak self] in @@ -510,6 +512,16 @@ final class GiftSetupScreenComponent: Component { } } ) + + if case .starGift = component.subject { + self.optionsDisposable = (component.context.engine.payments.starsTopUpOptions() + |> deliverOnMainQueue).start(next: { [weak self] options in + guard let self else { + return + } + self.options = options + }) + } } let environment = environment[EnvironmentType.self].value @@ -566,14 +578,14 @@ final class GiftSetupScreenComponent: Component { if case let .starGift(starGift) = component.subject, let availability = starGift.availability { let remains: Int32 = availability.remains let position = CGFloat(remains) / CGFloat(availability.total) - let remainsString = "\(remains)" //presentationStringsFormattedNumber(remains, environment.dateTimeFormat.groupingSeparator) + let remainsString = "\(remains)" let totalString = presentationStringsFormattedNumber(availability.total, environment.dateTimeFormat.groupingSeparator) let remainingCountSize = self.remainingCount.update( transition: transition, component: AnyComponent(RemainingCountComponent( inactiveColor: environment.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3), activeColors: [UIColor(rgb: 0x5bc2ff), UIColor(rgb: 0x2d9eff)], - inactiveTitle: "Limited", + inactiveTitle: environment.strings.Gift_Send_Limited, inactiveValue: "", inactiveTitleColor: environment.theme.list.itemSecondaryTextColor, activeTitle: "", @@ -1001,7 +1013,7 @@ final class GiftSetupScreenComponent: Component { } } } - if self.recenterOnTag == nil && self.previousHadInputHeight != (environment.inputHeight > 0.0) { + if self.recenterOnTag == nil && self.previousHadInputHeight != (environment.inputHeight > 0.0), case .keyboard = self.currentInputMode { if self.textInputState.isEditing { self.recenterOnTag = self.textInputTag } @@ -1251,9 +1263,6 @@ public final class GiftSetupScreen: ViewControllerComponentContainer { fatalError("init(coder:) has not been implemented") } - deinit { - } - @objc private func cancelPressed() { self.dismiss() } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index a97db9618a..ff09e4d3a4 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -181,6 +181,7 @@ private final class GiftViewSheetContent: CombinedComponent { let text: String? let entities: [MessageTextEntity]? let limitTotal: Int32? + var outgoing = false var incoming = false var savedToProfile = false var converted = false @@ -193,6 +194,11 @@ private final class GiftViewSheetContent: CombinedComponent { entities = arguments.entities limitTotal = arguments.gift.availability?.total convertStars = arguments.convertStars + if case .message = component.subject { + outgoing = !arguments.incoming + } else { + outgoing = false + } incoming = arguments.incoming || arguments.peerId == component.context.account.peerId savedToProfile = arguments.savedToProfile converted = arguments.converted @@ -238,12 +244,12 @@ private final class GiftViewSheetContent: CombinedComponent { } var formattedAmount = presentationStringsFormattedNumber(abs(Int32(stars)), dateTimeFormat.groupingSeparator) - if !incoming && stars > 0 { + if outgoing { formattedAmount = "- \(formattedAmount)" } let countFont: UIFont = Font.semibold(17.0) let amountText = formattedAmount - let countColor = incoming ? theme.list.itemDisclosureActions.constructive.fillColor : theme.list.itemDestructiveColor + let countColor = outgoing ? theme.list.itemDestructiveColor : theme.list.itemDisclosureActions.constructive.fillColor let title = title.update( component: MultilineTextComponent( @@ -367,7 +373,8 @@ private final class GiftViewSheetContent: CombinedComponent { animationCache: component.context.animationCache, animationRenderer: component.context.animationRenderer, placeholderColor: theme.list.mediaPlaceholderColor, - text: .plain(attributedText) + text: .plain(attributedText), + maximumNumberOfLines: 0 ) ) )) @@ -1071,17 +1078,26 @@ private final class TableComponent: CombinedComponent { } else { insets = UIEdgeInsets(top: 0.0, left: horizontalPadding, bottom: 0.0, right: horizontalPadding) } - let valueChild = valueChildren[item.id].update( - component: item.component, - availableSize: CGSize(width: rightColumnWidth - insets.left - insets.right, height: context.availableSize.height), - transition: context.transition - ) - updatedValueChildren.append((valueChild, insets)) var titleHeight: CGFloat = 0.0 if let titleChild = updatedTitleChildren[i] { titleHeight = titleChild.size.height } + + let availableValueWidth: CGFloat + if titleHeight > 0.0 { + availableValueWidth = rightColumnWidth + } else { + availableValueWidth = context.availableSize.width + } + + let valueChild = valueChildren[item.id].update( + component: item.component, + availableSize: CGSize(width: availableValueWidth - insets.left - insets.right, height: context.availableSize.height), + transition: context.transition + ) + updatedValueChildren.append((valueChild, insets)) + let rowHeight = max(40.0, max(titleHeight, valueChild.size.height) + verticalPadding * 2.0) rowHeights[i] = rowHeight totalHeight += rowHeight diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index dce8f22379..4becea5f6d 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -138,8 +138,10 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr var validIds: [AnyHashable] = [] var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: 60.0), size: starsOptionSize) + + var index: Int32 = 0 for product in starsProducts { - let itemId = AnyHashable(product.date) + let itemId = AnyHashable(index) validIds.append(itemId) var itemTransition = transition @@ -221,6 +223,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr itemFrame.origin.x = sideInset itemFrame.origin.y += starsOptionSize.height + optionSpacing } + index += 1 } var removeIds: [AnyHashable] = [] @@ -243,7 +246,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr self.starsItems.removeValue(forKey: id) } - var contentHeight = ceil(CGFloat(starsProducts.count) / 3.0) * starsOptionSize.height + 60.0 + 16.0 + var contentHeight = ceil(CGFloat(starsProducts.count) / 3.0) * (starsOptionSize.height + optionSpacing) - optionSpacing + 60.0 + 16.0 if self.peerId == self.context.account.peerId { let transition = ComponentTransition.immediate diff --git a/versions.json b/versions.json index da4ab2f6e5..3556a03d85 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "11.2", + "app": "11.2.1", "xcode": "16.0", "bazel": "7.3.1", "macos": "15.0"