From a9511e7c575fef9c3c553f212495f0a2f25a2f71 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Wed, 26 Nov 2025 22:49:31 +0400 Subject: [PATCH 1/9] improvements --- .../Sources/TelegramEngine/Payments/StarGifts.swift | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index 66b92fb194..80af6a0d54 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -978,6 +978,15 @@ public extension StarGift { } } + var title: String { + switch self { + case let .generic(gift): + return gift.title ?? "" + case let .unique(gift): + return gift.title + } + } + var releasedBy: EnginePeer.Id? { switch self { case let .generic(gift): From 3b02aeae9d48a3efb59333e784997e0c1a93d5a7 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 2 Dec 2025 22:15:52 +0800 Subject: [PATCH 2/9] Update localization --- .../Telegram-iOS/en.lproj/Localizable.strings | 28 +++++++++++++++++++ ...tionSequencePhoneEntryControllerNode.swift | 5 ++-- .../PrivacyAndSecurityController.swift | 5 ++-- .../Sources/AttachmentFileController.swift | 3 +- .../Sources/AttachmentFileSearchItem.swift | 3 +- .../Sources/ChatMessageBubbleItemNode.swift | 5 ++-- .../Sources/PasskeysScreen.swift | 6 ++-- .../PasskeysScreenIntroComponent.swift | 28 +++++++++++-------- .../Sources/PasskeysScreenListComponent.swift | 19 ++++++------- 9 files changed, 64 insertions(+), 38 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 90e1d0eb5c..c3bab8c3c1 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -15506,3 +15506,31 @@ Error: %8$@"; "Gift.Auction.Extension" = "+ %1$@ for late bids in top %2$@"; "ChatList.Auctions.UpcomingAuction" = "Upcoming Auction"; + +"Login.PhoneWithPasskeySubtitle" = "Enter your phone number\nor [log in using Passkey >](passkey)"; +"PrivacySettings.Passkey" = "Passkey"; +"Attachment.SharedAudio" = "SHARED AUDIO"; +"Attachment.FilesSearchPlaceholder" = "Search shared audio"; +"Chat.GiftPurchaseOffer.Reject" = "Reject"; +"Chat.GiftPurchaseOffer.Accept" = "Accept"; + +"Passkeys.DeleteAlert.Title" = "Delete Passkey?"; +"Passkeys.DeleteAlert.Text" = "Once deleted, this passkey can't be used to log in.\n\nDon't forget to remove it from your password manager too."; +"Passkeys.DeleteAlert.Action" = "Delete"; +"Passkeys.Into.Title" = "Protect your account"; +"Passkeys.Subtitle" = "Log in safely and keep your account secure."; +"Passkeys.Into.Title0" = "Create a Passkey"; +"Passkeys.Into.Text0" = "Make a passkey to sign in easily and safely."; +"Passkeys.Into.Title1" = "Log in with Face ID"; +"Passkeys.Into.Text1" = "Use Face ID, Touch ID, or your passcode to sign in."; +"Passkeys.Into.Title2" = "Store Passkey Securely"; +"Passkeys.Into.Text2" = "Your passkey is safely kept in your iCloud Keychain."; +"Passkeys.ButtonCreate" = "Create Passkey"; +"Passkeys.ButtonSkip" = "Skip"; +"Passkeys.Title" = "Passkeys"; +"Passkeys.Subtitle" = "Log in safely and keep your account secure."; +"Passkeys.EmptyName" = "Passkey"; +"Passkeys.PasskeyCreatedPattern" = "created %@"; +"Passkeys.PasskeyCreatedAndUsedPattern" = "created %1$@ • used %2$@"; +"Passkeys.AddPasskey" = "Create Passkey"; +"Passkeys.ListFooter" = "Your passkeys are stored securely in your password manager."; diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift index 189f8e7c13..69ca7fc799 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift @@ -576,11 +576,10 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { } func updateDisplayPasskeyLoginOption() { - //TODO:localize if self.account == nil { return } - let attributedText = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("Enter your phone number\nor [log in using Passkey >](passkey)", attributes: MarkdownAttributes( + let attributedText = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(self.strings.Login_PhoneWithPasskeySubtitle, attributes: MarkdownAttributes( body: MarkdownAttributeSet(font: Font.regular(17.0), textColor: self.theme.list.itemPrimaryTextColor), bold: MarkdownAttributeSet(font: Font.semibold(17.0), textColor: self.theme.list.itemPrimaryTextColor), link: MarkdownAttributeSet(font: Font.regular(17.0), textColor: self.theme.list.itemAccentColor), @@ -607,7 +606,7 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { let titleInset: CGFloat = layout.size.width > 320.0 ? 18.0 : 0.0 let additionalBottomInset: CGFloat = layout.size.width > 320.0 ? 80.0 : 10.0 - self.titleNode.attributedText = NSAttributedString(string: self.account == nil ? strings.Login_NewNumber : strings.Login_PhoneTitle, font: Font.bold(28.0), textColor: self.theme.list.itemPrimaryTextColor) + self.titleNode.attributedText = NSAttributedString(string: self.account == nil ? self.strings.Login_NewNumber : self.strings.Login_PhoneTitle, font: Font.bold(28.0), textColor: self.theme.list.itemPrimaryTextColor) self.titleActivateAreaNode.accessibilityLabel = self.titleNode.attributedText?.string ?? "" let inset: CGFloat = 24.0 diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift index 755ecdc75d..09aeb82886 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift @@ -673,12 +673,11 @@ private func privacyAndSecurityControllerEntries( entries.append(.twoStepVerification(presentationData.theme, presentationData.strings.PrivacySettings_TwoStepAuth, twoStepAuthString, twoStepAuthData)) if displayPasskeys { - //TODO:localize var passkeysString = "" if let hasPasskeys = hasPasskeys { - passkeysString = hasPasskeys ? "On" : "Off" + passkeysString = hasPasskeys ? presentationData.strings.PrivacySettings_PasscodeOn : presentationData.strings.PrivacySettings_PasscodeOff } - entries.append(.passkeys(presentationData.theme, "Passkey", passkeysString)) + entries.append(.passkeys(presentationData.theme, presentationData.strings.PrivacySettings_Passkey, passkeysString)) } if let privacySettings = privacySettings { diff --git a/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileController.swift b/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileController.swift index e3af2cbb87..59b5b88acf 100644 --- a/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileController.swift +++ b/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileController.swift @@ -207,8 +207,7 @@ private func attachmentFileControllerEntries(presentationData: PresentationData, case .recent: listTitle = presentationData.strings.Attachment_RecentlySentFiles case .audio: - //TODO:localize - listTitle = "SHARED AUDIO" + listTitle = presentationData.strings.Attachment_SharedAudio } if case .audio = mode { diff --git a/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileSearchItem.swift b/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileSearchItem.swift index ed7b71bd06..d1b6135765 100644 --- a/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileSearchItem.swift +++ b/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileSearchItem.swift @@ -138,7 +138,6 @@ private final class AttachmentFileSearchItemNode: ItemListControllerSearchNode { transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: layout.size.height - navigationBarHeight))) self.containerNode.containerLayoutUpdated(layout.withUpdatedSize(CGSize(width: layout.size.width, height: layout.size.height - navigationBarHeight)), navigationBarHeight: 0.0, transition: transition) - //TODO:localize let searchInputSize = self.searchInput.update( transition: .immediate, component: AnyComponent( @@ -147,7 +146,7 @@ private final class AttachmentFileSearchItemNode: ItemListControllerSearchNode { strings: self.presentationData.strings, metrics: layout.metrics, safeInsets: layout.safeInsets, - placeholder: self.mode == .audio ? "Search shared audio" : self.presentationData.strings.Attachment_FilesSearchPlaceholder, + placeholder: self.mode == .audio ? self.presentationData.strings.Attachment_FilesSearchPlaceholder : self.presentationData.strings.Attachment_FilesSearchPlaceholder, updated: { [weak self] query in guard let self else { return diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index f15cdb59df..dd71ee1b79 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -2844,7 +2844,6 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI icon: .suggestedPostApprove ) ] - //TODO:localize let (minWidth, buttonsLayout) = actionButtonsLayout( item.context, item.presentationData.theme, @@ -2854,8 +2853,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI ReplyMarkupMessageAttribute( rows: [ ReplyMarkupRow(buttons: [ - ReplyMarkupButton(title: "Reject", titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonDecline)), - ReplyMarkupButton(title: "Accept", titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonApprove)) + ReplyMarkupButton(title: item.presentationData.strings.Chat_GiftPurchaseOffer_Reject, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonDecline)), + ReplyMarkupButton(title: item.presentationData.strings.Chat_GiftPurchaseOffer_Accept, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonApprove)) ]) ], flags: [], diff --git a/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreen.swift b/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreen.swift index 8443d2297e..ba88ee22dc 100644 --- a/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreen.swift @@ -180,9 +180,8 @@ final class PasskeysScreenComponent: Component { return } let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) - //TODO:localize - controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: "Delete Passkey?", text: "Once deleted, this passkey can't be used to log in.\n\nDon't forget to remove it from your password manager too.", actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - }), TextAlertAction(type: .destructiveAction, title: "Delete", action: { [weak self] in + controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: environment.strings.Passkeys_DeleteAlert_Title, text: environment.strings.Passkeys_DeleteAlert_Text, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: { + }), TextAlertAction(type: .destructiveAction, title: environment.strings.Passkeys_DeleteAlert_Action, action: { [weak self] in guard let self else { return } @@ -266,6 +265,7 @@ final class PasskeysScreenComponent: Component { component: AnyComponent(PasskeysScreenIntroComponent( context: component.context, theme: environment.theme, + strings: environment.strings, insets: UIEdgeInsets(top: environment.statusBarHeight + environment.navigationHeight, left: 0.0, bottom: environment.safeInsets.bottom, right: 0.0), displaySkip: component.displaySkip, createPasskeyAction: { [weak self] in diff --git a/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreenIntroComponent.swift b/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreenIntroComponent.swift index 49ef832830..5a63cb70e6 100644 --- a/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreenIntroComponent.swift +++ b/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreenIntroComponent.swift @@ -13,6 +13,7 @@ import BundleIconComponent final class PasskeysScreenIntroComponent: Component { let context: AccountContext let theme: PresentationTheme + let strings: PresentationStrings let insets: UIEdgeInsets let displaySkip: Bool let createPasskeyAction: () -> Void @@ -21,6 +22,7 @@ final class PasskeysScreenIntroComponent: Component { init( context: AccountContext, theme: PresentationTheme, + strings: PresentationStrings, insets: UIEdgeInsets, displaySkip: Bool, createPasskeyAction: @escaping () -> Void, @@ -28,6 +30,7 @@ final class PasskeysScreenIntroComponent: Component { ) { self.context = context self.theme = theme + self.strings = strings self.insets = insets self.displaySkip = displaySkip self.createPasskeyAction = createPasskeyAction @@ -41,6 +44,9 @@ final class PasskeysScreenIntroComponent: Component { if lhs.theme !== rhs.theme { return false } + if lhs.strings !== rhs.strings { + return false + } if lhs.insets != rhs.insets { return false } @@ -120,7 +126,6 @@ final class PasskeysScreenIntroComponent: Component { var contentHeight: CGFloat = 0.0 - //TODO:localize let iconSize = self.icon.update( transition: .immediate, component: AnyComponent(LottieComponent( @@ -145,7 +150,7 @@ final class PasskeysScreenIntroComponent: Component { let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: "Protect your account", font: Font.bold(27.0), textColor: component.theme.list.itemPrimaryTextColor)), + text: .plain(NSAttributedString(string: component.strings.Passkeys_Into_Title, font: Font.bold(27.0), textColor: component.theme.list.itemPrimaryTextColor)), horizontalAlignment: .center, maximumNumberOfLines: 0 )), @@ -166,7 +171,7 @@ final class PasskeysScreenIntroComponent: Component { let subtitleSize = self.subtitle.update( transition: .immediate, component: AnyComponent(BalancedTextComponent( - text: .plain(NSAttributedString(string: "Log in safely and keep your account secure.", font: Font.regular(16.0), textColor: component.theme.list.itemPrimaryTextColor)), + text: .plain(NSAttributedString(string: component.strings.Passkeys_Subtitle, font: Font.regular(16.0), textColor: component.theme.list.itemPrimaryTextColor)), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.2 @@ -193,18 +198,18 @@ final class PasskeysScreenIntroComponent: Component { let itemDescs: [ItemDesc] = [ ItemDesc( icon: "Settings/Passkeys/Intro1", - title: "Create a Passkey", - text: "Make a passkey to sign in easily and safely." + title: component.strings.Passkeys_Into_Title0, + text: component.strings.Passkeys_Into_Text0 ), ItemDesc( icon: "Settings/Passkeys/Intro2", - title: "Log in with Face ID", - text: "Use Face ID, Touch ID, or your passcode to sign in." + title: component.strings.Passkeys_Into_Title1, + text: component.strings.Passkeys_Into_Text1 ), ItemDesc( icon: "Settings/Passkeys/Intro3", - title: "Store Passkey Securely", - text: "Your passkey is safely kept in your iCloud Keychain." + title: component.strings.Passkeys_Into_Title2, + text: component.strings.Passkeys_Into_Text2 ) ] for i in 0 ..< itemDescs.count { @@ -290,7 +295,7 @@ final class PasskeysScreenIntroComponent: Component { ), content: AnyComponentWithIdentity( id: AnyHashable(0), - component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "Create Passkey", font: Font.semibold(17.0), textColor: component.theme.list.itemCheckColors.foregroundColor)))) + component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: component.strings.Passkeys_ButtonCreate, font: Font.semibold(17.0), textColor: component.theme.list.itemCheckColors.foregroundColor)))) ), action: { [weak self] in guard let self, let component = self.component else { @@ -312,7 +317,6 @@ final class PasskeysScreenIntroComponent: Component { skipButton = ComponentView() self.skipButton = skipButton } - //TODO:localize skipButtonSize = skipButton.update( transition: transition, component: AnyComponent(ButtonComponent( @@ -325,7 +329,7 @@ final class PasskeysScreenIntroComponent: Component { ), content: AnyComponentWithIdentity( id: AnyHashable(0), - component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "Skip", font: Font.semibold(17.0), textColor: component.theme.list.itemCheckColors.foregroundColor)))) + component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: component.strings.Passkeys_ButtonSkip, font: Font.semibold(17.0), textColor: component.theme.list.itemCheckColors.foregroundColor)))) ), action: { [weak self] in guard let self, let component = self.component else { diff --git a/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreenListComponent.swift b/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreenListComponent.swift index 862e6600ed..86c5dccc36 100644 --- a/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreenListComponent.swift +++ b/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreenListComponent.swift @@ -123,7 +123,6 @@ final class PasskeysScreenListComponent: Component { contentHeight += component.insets.top contentHeight += 8.0 - //TODO:localize let iconSize = self.icon.update( transition: .immediate, component: AnyComponent(LottieComponent( @@ -148,7 +147,7 @@ final class PasskeysScreenListComponent: Component { let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: "Passkeys", font: Font.bold(27.0), textColor: component.theme.list.itemPrimaryTextColor)), + text: .plain(NSAttributedString(string: component.strings.Passkeys_Title, font: Font.bold(27.0), textColor: component.theme.list.itemPrimaryTextColor)), horizontalAlignment: .center, maximumNumberOfLines: 0 )), @@ -169,7 +168,7 @@ final class PasskeysScreenListComponent: Component { let subtitleSize = self.subtitle.update( transition: .immediate, component: AnyComponent(BalancedTextComponent( - text: .plain(NSAttributedString(string: "Log in safely and keep your account secure.", font: Font.regular(16.0), textColor: component.theme.list.itemPrimaryTextColor)), + text: .plain(NSAttributedString(string: component.strings.Passkeys_Subtitle, font: Font.regular(16.0), textColor: component.theme.list.itemPrimaryTextColor)), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.2 @@ -229,11 +228,12 @@ final class PasskeysScreenListComponent: Component { ) } - //TODO:localize - var subtitleString = "created \(dateString)" + let subtitleString: String if let lastUsageDate = passkey.lastUsageDate { let lastUsedDateString = dateFormatter.string(from: Date(timeIntervalSince1970: Double(lastUsageDate))) - subtitleString.append(" • used \(lastUsedDateString)") + subtitleString = component.strings.Passkeys_PasskeyCreatedAndUsedPattern(dateString, lastUsedDateString).string + } else { + subtitleString = component.strings.Passkeys_PasskeyCreatedPattern(dateString).string } listSectionItems.append(AnyComponentWithIdentity(id: passkey.id, component: AnyComponent(ListActionItemComponent( @@ -241,7 +241,7 @@ final class PasskeysScreenListComponent: Component { title: AnyComponent(VStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: passkey.name.isEmpty ? "Passkey" : passkey.name, //TODO:localize + string: passkey.name.isEmpty ? component.strings.Passkeys_EmptyName : passkey.name, font: Font.regular(17.0), textColor: component.theme.list.itemPrimaryTextColor )), @@ -283,7 +283,7 @@ final class PasskeysScreenListComponent: Component { title: AnyComponent(VStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: "Create Passkey", //TODO:localize + string: component.strings.Passkeys_AddPasskey, font: Font.regular(17.0), textColor: component.theme.list.itemAccentColor )), @@ -305,7 +305,6 @@ final class PasskeysScreenListComponent: Component { )))) } - //TODO:localize let listSectionSize = self.listSection.update( transition: transition, component: AnyComponent(ListSectionComponent( @@ -314,7 +313,7 @@ final class PasskeysScreenListComponent: Component { header: nil, footer: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: "Your passkeys are stored securely in your password manager.", + string: component.strings.Passkeys_ListFooter, font: Font.regular(13.0), textColor: component.theme.list.freeTextColor )), From ecb12a784e0da82eeef7d33f47d3e59087daff91 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 2 Dec 2025 19:22:06 +0400 Subject: [PATCH 3/9] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 2 + .../Sources/AccountContext.swift | 3 +- .../Payments/StarGiftsAuctions.swift | 9 + .../Sources/ChatMessageBubbleItemNode.swift | 8 +- .../Sources/GiftCompositionComponent.swift | 4 +- .../Sources/GiftItemComponent.swift | 36 +- .../Sources/GiftOptionsScreen.swift | 34 +- .../Gifts/GiftRemainingCountComponent/BUILD | 29 + .../GiftRemainingCountComponent.swift} | 12 +- .../Components/Gifts/GiftSetupScreen/BUILD | 1 + .../Sources/GiftSetupScreen.swift | 3 +- .../Components/Gifts/GiftViewScreen/BUILD | 1 + .../Sources/GiftAuctionBidScreen.swift | 27 +- .../GiftAuctionUpgradePreviewScreen.swift | 427 ------------ .../Sources/GiftAuctionViewScreen.swift | 397 ++++++++--- .../GiftAuctionWearPreviewScreen.swift | 649 ++++++++++++++++++ .../Sources/GiftUpgradePreviewScreen.swift | 34 +- .../Auction/Upgrade.imageset/Contents.json | 12 + .../Auction/Upgrade.imageset/arrow (7).pdf | Bin 0 -> 4621 bytes .../TelegramUI/Sources/OpenResolvedUrl.swift | 34 +- .../Sources/SharedAccountContext.swift | 6 +- 21 files changed, 1130 insertions(+), 598 deletions(-) create mode 100644 submodules/TelegramUI/Components/Gifts/GiftRemainingCountComponent/BUILD rename submodules/TelegramUI/Components/Gifts/{GiftSetupScreen/Sources/RemainingCountComponent.swift => GiftRemainingCountComponent/Sources/GiftRemainingCountComponent.swift} (97%) delete mode 100644 submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionUpgradePreviewScreen.swift create mode 100644 submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionWearPreviewScreen.swift create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Auction/Upgrade.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Auction/Upgrade.imageset/arrow (7).pdf diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 90e1d0eb5c..7a3ff984b6 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -15506,3 +15506,5 @@ Error: %8$@"; "Gift.Auction.Extension" = "+ %1$@ for late bids in top %2$@"; "ChatList.Auctions.UpcomingAuction" = "Upcoming Auction"; + +"Gift.AuctionBid.UpcomingBid" = "You placed an early bid"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 8e1eea45ff..2ab337b12e 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1426,10 +1426,11 @@ public protocol SharedAccountContext: AnyObject { func makeGiftUpgradePreviewScreen(context: AccountContext, attributes: [StarGift.UniqueGift.Attribute], peerName: String) -> ViewController func makeGiftAuctionInfoScreen(context: AccountContext, auctionContext: GiftAuctionContext, completion: (() -> Void)?) -> ViewController func makeGiftAuctionBidScreen(context: AccountContext, toPeerId: EnginePeer.Id, text: String?, entities: [MessageTextEntity]?, hideName: Bool, auctionContext: GiftAuctionContext, acquiredGifts: Signal<[GiftAuctionAcquiredGift], NoError>?) -> ViewController - func makeGiftAuctionViewScreen(context: AccountContext, auctionContext: GiftAuctionContext, completion: @escaping (Signal<[GiftAuctionAcquiredGift], NoError>) -> Void) -> ViewController + func makeGiftAuctionViewScreen(context: AccountContext, auctionContext: GiftAuctionContext, completion: @escaping (Signal<[GiftAuctionAcquiredGift], NoError>, [StarGift.UniqueGift.Attribute]?) -> Void) -> ViewController func makeGiftAuctionActiveBidsScreen(context: AccountContext) -> ViewController func makeGiftOfferScreen(context: AccountContext, gift: StarGift.UniqueGift, peer: EnginePeer, amount: CurrencyAmount, commit: @escaping () -> Void) -> ViewController func makeGiftUpgradeVariantsPreviewScreen(context: AccountContext, gift: StarGift, attributes: [StarGift.UniqueGift.Attribute]) -> ViewController + func makeGiftAuctionWearPreviewScreen(context: AccountContext, auctionContext: GiftAuctionContext, acquiredGifts: Signal<[GiftAuctionAcquiredGift], NoError>?, attributes: [StarGift.UniqueGift.Attribute], completion: @escaping () -> Void) -> ViewController func makeStorySharingScreen(context: AccountContext, subject: StorySharingSubject, parentController: ViewController) -> ViewController diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGiftsAuctions.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGiftsAuctions.swift index 44a24e5f71..10ed47ea5a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGiftsAuctions.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGiftsAuctions.swift @@ -133,6 +133,15 @@ public final class GiftAuctionContext { } } + public var isUpcoming: Bool { + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + if case let .ongoing(_, startTime, _, _, _, _, _, _, _, _, _, _) = self.auctionState { + return currentTime < startTime + } else { + return false + } + } + public convenience init(account: Account, gift: StarGift) { self.init(account: account, gift: gift, initialAuctionState: nil, initialMyState: nil, initialTimeout: nil) } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index f15cdb59df..92ef45461b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -2835,13 +2835,13 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI let buttonApprove = MemoryBuffer(data: Data(bytes: &buttonApproveValue, count: 1)) let customInfos: [MemoryBuffer: ChatMessageActionButtonsNode.CustomInfo] = [ - buttonDecline: ChatMessageActionButtonsNode.CustomInfo( - isEnabled: true, - icon: .suggestedPostReject - ), buttonApprove: ChatMessageActionButtonsNode.CustomInfo( isEnabled: true, icon: .suggestedPostApprove + ), + buttonDecline: ChatMessageActionButtonsNode.CustomInfo( + isEnabled: true, + icon: .suggestedPostReject ) ] //TODO:localize diff --git a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift index cdf93e25d7..69437f55de 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift @@ -718,12 +718,12 @@ public final class GiftCompositionComponent: Component { animationFile = file component.externalState?.previewModel = self.previewModels[Int(self.previewModelIndex)] } - if case let .pattern(_, file, _) = self.previewPatterns[Int(self.previewPatternIndex)] { + if !self.previewPatterns.isEmpty, case let .pattern(_, file, _) = self.previewPatterns[Int(self.previewPatternIndex)] { patternFile = file files[file.fileId.id] = file component.externalState?.previewSymbol = self.previewPatterns[Int(self.previewPatternIndex)] } - if case let .backdrop(_, _, innerColorValue, outerColorValue, patternColorValue, _, _) = self.previewBackdrops[Int(self.previewBackdropIndex)] { + if !self.previewBackdrops.isEmpty, case let .backdrop(_, _, innerColorValue, outerColorValue, patternColorValue, _, _) = self.previewBackdrops[Int(self.previewBackdropIndex)] { backgroundColor = UIColor(rgb: UInt32(bitPattern: outerColorValue)) secondBackgroundColor = UIColor(rgb: UInt32(bitPattern: innerColorValue)) patternColor = UIColor(rgb: UInt32(bitPattern: patternColorValue)) diff --git a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift index 35c39e8d09..db66ff52cd 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift @@ -167,6 +167,7 @@ public final class GiftItemComponent: Component { let isPinned: Bool let isEditing: Bool let isDateLocked: Bool + let isPlaceholder: Bool let mode: Mode let action: (() -> Void)? let contextAction: ((UIView, ContextGesture) -> Void)? @@ -191,6 +192,7 @@ public final class GiftItemComponent: Component { isPinned: Bool = false, isEditing: Bool = false, isDateLocked: Bool = false, + isPlaceholder: Bool = false, mode: Mode = .generic, action: (() -> Void)? = nil, contextAction: ((UIView, ContextGesture) -> Void)? = nil @@ -214,6 +216,7 @@ public final class GiftItemComponent: Component { self.isPinned = isPinned self.isEditing = isEditing self.isDateLocked = isDateLocked + self.isPlaceholder = isPlaceholder self.mode = mode self.action = action self.contextAction = contextAction @@ -277,6 +280,9 @@ public final class GiftItemComponent: Component { if lhs.isDateLocked != rhs.isDateLocked { return false } + if lhs.isPlaceholder != rhs.isPlaceholder { + return false + } if lhs.mode != rhs.mode { return false } @@ -917,7 +923,7 @@ public final class GiftItemComponent: Component { let buttonSize = self.button.update( transition: transition, component: AnyComponent( - ButtonContentComponent( + StarsButtonContentComponent( context: component.context, text: price, color: buttonColor, @@ -1468,28 +1474,34 @@ public final class GiftItemComponent: Component { } } -private final class ButtonContentComponent: Component { +public final class StarsButtonContentComponent: Component { let context: AccountContext let text: String let color: UIColor let tinted: Bool let starsColor: UIColor? + let font: UIFont + let height: CGFloat public init( context: AccountContext, text: String, color: UIColor, tinted: Bool = false, - starsColor: UIColor? = nil + starsColor: UIColor? = nil, + font: UIFont = Font.semibold(11.0), + height: CGFloat = 30.0 ) { self.context = context self.text = text self.color = color self.tinted = tinted self.starsColor = starsColor + self.font = font + self.height = height } - public static func ==(lhs: ButtonContentComponent, rhs: ButtonContentComponent) -> Bool { + public static func ==(lhs: StarsButtonContentComponent, rhs: StarsButtonContentComponent) -> Bool { if lhs.context !== rhs.context { return false } @@ -1505,11 +1517,17 @@ private final class ButtonContentComponent: Component { if lhs.starsColor != rhs.starsColor { return false } + if lhs.font != rhs.font { + return false + } + if lhs.height != rhs.height { + return false + } return true } public final class View: UIView { - private var component: ButtonContentComponent? + private var component: StarsButtonContentComponent? private weak var componentState: EmptyComponentState? private let backgroundLayer = SimpleLayer() @@ -1528,7 +1546,7 @@ private final class ButtonContentComponent: Component { fatalError("init(coder:) has not been implemented") } - func update(component: ButtonContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + func update(component: StarsButtonContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component self.componentState = state @@ -1537,7 +1555,7 @@ private final class ButtonContentComponent: Component { textColor = .white } - let attributedText = NSMutableAttributedString(string: component.text, font: Font.semibold(11.0), textColor: textColor) + let attributedText = NSMutableAttributedString(string: component.text, font: component.font, textColor: textColor) let range = (attributedText.string as NSString).range(of: "#") if range.location != NSNotFound { attributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: component.tinted)), range: range) @@ -1563,7 +1581,7 @@ private final class ButtonContentComponent: Component { ) let padding: CGFloat = 9.0 - let size = CGSize(width: titleSize.width + padding * 2.0, height: 30.0) + let size = CGSize(width: titleSize.width + padding * 2.0, height: component.height) if let starsColor = component.starsColor { let starsLayer: StarsButtonEffectLayer @@ -1574,8 +1592,10 @@ private final class ButtonContentComponent: Component { self.layer.addSublayer(starsLayer) self.starsLayer = starsLayer } + starsLayer.masksToBounds = true starsLayer.frame = CGRect(origin: .zero, size: size) starsLayer.update(color: starsColor, size: size) + starsLayer.cornerRadius = size.height * 0.5 } else { self.starsLayer?.removeFromSuperlayer() self.starsLayer = nil diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift index 23db34004f..97c8323cc4 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift @@ -416,15 +416,31 @@ final class GiftOptionsScreenComponent: Component { let giftController = component.context.sharedContext.makeGiftAuctionViewScreen( context: component.context, auctionContext: auctionContext, - completion: { [weak mainController] acquiredGifts in - let controller = GiftSetupScreen( - context: context, - peerId: component.peerId, - subject: .starGift(gift, nil), - auctionAcquiredGifts: acquiredGifts, - completion: nil - ) - mainController?.push(controller) + completion: { [weak mainController] acquiredGifts, upgradeAttributes in + if component.peerId == context.account.peerId, let upgradeAttributes, let navigationController = mainController?.navigationController as? NavigationController { + let controller = context.sharedContext.makeGiftAuctionWearPreviewScreen(context: context, auctionContext: auctionContext, acquiredGifts: acquiredGifts, attributes: upgradeAttributes, completion: { + let controller = context.sharedContext.makeGiftAuctionBidScreen( + context: context, + toPeerId: context.account.peerId, + text: "", + entities: [], + hideName: true, + auctionContext: auctionContext, + acquiredGifts: acquiredGifts + ) + navigationController.pushViewController(controller) + }) + mainController?.push(controller) + } else { + let controller = GiftSetupScreen( + context: context, + peerId: component.peerId, + subject: .starGift(gift, nil), + auctionAcquiredGifts: acquiredGifts, + completion: nil + ) + mainController?.push(controller) + } } ) mainController?.push(giftController) diff --git a/submodules/TelegramUI/Components/Gifts/GiftRemainingCountComponent/BUILD b/submodules/TelegramUI/Components/Gifts/GiftRemainingCountComponent/BUILD new file mode 100644 index 0000000000..71148481be --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftRemainingCountComponent/BUILD @@ -0,0 +1,29 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "GiftRemainingCountComponent", + module_name = "GiftRemainingCountComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/ComponentFlow", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/AppBundle", + "//submodules/TelegramUI/Components/GlassBackgroundComponent", + "//submodules/Components/MultilineTextComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/RemainingCountComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftRemainingCountComponent/Sources/GiftRemainingCountComponent.swift similarity index 97% rename from submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/RemainingCountComponent.swift rename to submodules/TelegramUI/Components/Gifts/GiftRemainingCountComponent/Sources/GiftRemainingCountComponent.swift index 37ceb3f1ee..eee3bd66cf 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/RemainingCountComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftRemainingCountComponent/Sources/GiftRemainingCountComponent.swift @@ -6,15 +6,11 @@ import TelegramCore import SwiftSignalKit import AccountContext import TelegramPresentationData -import PresentationDataUtils import ComponentFlow import MultilineTextComponent -import Markdown -import TextFormat -import RoundedRectWithTailPath import GlassBackgroundComponent -public class RemainingCountComponent: Component { +public class GiftRemainingCountComponent: Component { private let inactiveColor: UIColor private let activeColors: [UIColor] private let inactiveTitle: String @@ -62,7 +58,7 @@ public class RemainingCountComponent: Component { self.groupingSeparator = groupingSeparator } - public static func ==(lhs: RemainingCountComponent, rhs: RemainingCountComponent) -> Bool { + public static func ==(lhs: GiftRemainingCountComponent, rhs: GiftRemainingCountComponent) -> Bool { if lhs.inactiveColor != rhs.inactiveColor { return false } @@ -109,7 +105,7 @@ public class RemainingCountComponent: Component { } public final class View: UIView { - private var component: RemainingCountComponent? + private var component: GiftRemainingCountComponent? private let container: UIView private let inactiveBackground: SimpleLayer @@ -162,7 +158,7 @@ public class RemainingCountComponent: Component { } var previousAvailableSize: CGSize? - func update(component: RemainingCountComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { + func update(component: GiftRemainingCountComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { self.component = component self.inactiveBackground.backgroundColor = component.inactiveColor.cgColor self.activeBackground.backgroundColor = component.activeColors.last?.cgColor diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD index 7df5ae4068..7e874b3f9b 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD @@ -51,6 +51,7 @@ swift_library( "//submodules/TelegramUI/Components/AnimatedTextComponent", "//submodules/TelegramUI/Components/GlassBackgroundComponent", "//submodules/TelegramUI/Components/MessageInputPanelComponent", + "//submodules/TelegramUI/Components/Gifts/GiftRemainingCountComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index 080ca37fc3..2a8e16aa7e 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -38,6 +38,7 @@ import EdgeEffect import AnimatedTextComponent import GlassBarButtonComponent import MessageInputPanelComponent +import GiftRemainingCountComponent private final class GiftSetupScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -1719,7 +1720,7 @@ private final class GiftSetupScreenComponent: Component { let sold = total - remains let remainingCountSize = self.remainingCount.update( transition: transition, - component: AnyComponent(RemainingCountComponent( + component: AnyComponent(GiftRemainingCountComponent( inactiveColor: theme.list.itemBlocksBackgroundColor, activeColors: [UIColor(rgb: 0x72d6ff), UIColor(rgb: 0x32a0f9)], inactiveTitle: environment.strings.Gift_Send_Remains(remains), diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD index c4cde11b0e..8944b7ddb9 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD @@ -62,6 +62,7 @@ swift_library( "//submodules/TelegramUI/Components/AnimatedTextComponent", "//submodules/BotPaymentsUI", "//submodules/TelegramUI/Components/SegmentControlComponent", + "//submodules/TelegramUI/Components/Gifts/GiftRemainingCountComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift index 75ff3ecc07..c80ecbb280 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift @@ -612,7 +612,7 @@ private final class PeerComponent: Component { var color = component.theme.list.itemSecondaryTextColor switch component.status { case .winning: - color = component.theme.list.itemDisclosureActions.constructive.fillColor + color = UIColor(rgb: 0x53a939) case .outbid, .returned: color = component.theme.list.itemDestructiveColor default: @@ -2061,7 +2061,7 @@ private final class GiftAuctionBidScreenComponent: Component { if case .finished = auctionState?.auctionState, let controller = self.environment?.controller() { if let navigationController = controller.navigationController as? NavigationController { controller.dismiss() - let auctionController = context.sharedContext.makeGiftAuctionViewScreen(context: context, auctionContext: auctionContext, completion: { _ in }) + let auctionController = context.sharedContext.makeGiftAuctionViewScreen(context: context, auctionContext: auctionContext, completion: { _, _ in }) navigationController.pushViewController(auctionController) } } @@ -2197,8 +2197,12 @@ private final class GiftAuctionBidScreenComponent: Component { var topBidsTitleComponent: AnyComponent? var topBidsComponents: [(EnginePeer.Id, AnyComponent)] = [] + var isUpcoming = false let place: Int32 - if let giftAuctionState = self.giftAuctionState, case let .ongoing(_, _, _, _, bidLevels, topBidders, _, _, _, _, _, lastGiftNumber) = giftAuctionState.auctionState { + if let giftAuctionState = self.giftAuctionState, case let .ongoing(_, startDate, _, _, bidLevels, topBidders, _, _, _, _, _, lastGiftNumber) = giftAuctionState.auctionState { + if currentTime < startDate { + isUpcoming = true + } var myBidAmount = Int64(self.amount.realValue) var myBidDate = currentTime var isBiddingUp = true @@ -2220,6 +2224,9 @@ private final class GiftAuctionBidScreenComponent: Component { if isBiddingUp { bidTitleColor = environment.theme.list.itemSecondaryTextColor bidTitle = environment.strings.Gift_AuctionBid_BidPreview + } else if isUpcoming { + bidTitleColor = environment.theme.list.itemSecondaryTextColor + bidTitle = environment.strings.Gift_AuctionBid_UpcomingBid } else if giftAuctionState.myState.isReturned { bidTitle = environment.strings.Gift_AuctionBid_Outbid bidTitleColor = environment.theme.list.itemDestructiveColor @@ -2688,10 +2695,18 @@ private final class GiftAuctionBidScreenComponent: Component { var initialContentHeight = contentHeight let clippingY: CGFloat + let titleString: String + if isUpcoming { + //TODO:localize + titleString = "Place an Early Bid" + } else { + titleString = environment.strings.Gift_AuctionBid_Title + } + let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: environment.strings.Gift_AuctionBid_Title, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)) + text: .plain(NSAttributedString(string: titleString, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)) )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) @@ -2708,13 +2723,13 @@ private final class GiftAuctionBidScreenComponent: Component { let subtitleSize = self.subtitle.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: subtitleString, font: Font.regular(13.0), textColor: environment.theme.list.itemSecondaryTextColor)) + text: .plain(NSAttributedString(string: isUpcoming ? "" : subtitleString, font: Font.regular(13.0), textColor: environment.theme.list.itemSecondaryTextColor)) )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) ) - let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: 19.0), size: titleSize) + let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: isUpcoming ? 27.0 : 19.0), size: titleSize) if let titleView = self.title.view { if titleView.superview == nil { self.navigationBarContainer.addSubview(titleView) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionUpgradePreviewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionUpgradePreviewScreen.swift deleted file mode 100644 index b963cfc73c..0000000000 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionUpgradePreviewScreen.swift +++ /dev/null @@ -1,427 +0,0 @@ -import Foundation -import UIKit -import Display -import AsyncDisplayKit -import Postbox -import TelegramCore -import SwiftSignalKit -import AccountContext -import TelegramPresentationData -import PresentationDataUtils -import ComponentFlow -import ViewControllerComponent -import SheetComponent -import MultilineTextComponent -import MultilineTextWithEntitiesComponent -import BundleIconComponent -import ButtonComponent -import Markdown -import BalancedTextComponent -import AvatarNode -import TextFormat -import TelegramStringFormatting -import StarsAvatarComponent -import EmojiTextAttachmentView -import EmojiStatusComponent -import UndoUI -import ConfettiEffect -import PlainButtonComponent -import CheckComponent -import TooltipUI -import LottieComponent -import ContextUI -import TelegramNotices -import PremiumLockButtonSubtitleComponent -import StarsBalanceOverlayComponent -import BalanceNeededScreen -import GiftItemComponent -import GiftAnimationComponent -import ChatThemeScreen -import ProfileLevelRatingBarComponent -import AnimatedTextComponent - -private final class GiftAuctionUpgradePreviewSheetContent: CombinedComponent { - typealias EnvironmentType = ViewControllerComponentContainer.Environment - - let context: AccountContext - let gift: StarGift - let animateOut: ActionSlot> - let getController: () -> ViewController? - - init( - context: AccountContext, - gift: StarGift, - animateOut: ActionSlot>, - getController: @escaping () -> ViewController? - ) { - self.context = context - self.gift = gift - self.animateOut = animateOut - self.getController = getController - } - - static func ==(lhs: GiftAuctionUpgradePreviewSheetContent, rhs: GiftAuctionUpgradePreviewSheetContent) -> Bool { - if lhs.context !== rhs.context { - return false - } - if lhs.gift != rhs.gift { - return false - } - return true - } - - final class State: ComponentState { - private let context: AccountContext - - private var peerDisposable: Disposable? - var peerMap: [EnginePeer.Id: EnginePeer] = [:] - - var cachedChevronImage: (UIImage, PresentationTheme)? - var cachedSmallChevronImage: (UIImage, PresentationTheme)? - - var upgradePreviewTimer: SwiftSignalKit.Timer? - - init( - context: AccountContext - ) { - self.context = context - - super.init() - - let peerIds: [EnginePeer.Id] = [context.account.peerId] - - self.peerDisposable = ( - context.engine.data.get(EngineDataMap( - peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.Peer in - return TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) - } - )) - |> deliverOnMainQueue - ).startStrict(next: { [weak self] peers in - if let strongSelf = self { - var peersMap: [EnginePeer.Id: EnginePeer] = [:] - for (peerId, maybePeer) in peers { - if let peer = maybePeer { - peersMap[peerId] = peer - } - } - strongSelf.peerMap = peersMap - strongSelf.updated(transition: .immediate) - } - }) - - self.upgradePreviewTimer = SwiftSignalKit.Timer(timeout: 3.0, repeat: true, completion: { [weak self] in - self?.upgradePreviewTimerTick() - }, queue: Queue.mainQueue()) - self.upgradePreviewTimer?.start() - self.upgradePreviewTimerTick() - } - - deinit { - self.peerDisposable?.dispose() - self.upgradePreviewTimer?.invalidate() - } - - private func upgradePreviewTimerTick() { - - } - } - - - func makeState() -> State { - return State(context: self.context) - } - - static var body: Body { - let animation = Child(GiftCompositionComponent.self) - let avatar = Child(AvatarComponent.self) - let peerName = Child(MultilineTextComponent.self) - //let description = Child(MultilineTextComponent.self) - - //let initialGift = Child(GiftItemComponent.self) - //let upgradedGift = Child(GiftItemComponent.self) - - let button = Child(ButtonComponent.self) - - let giftCompositionExternalState = GiftCompositionComponent.ExternalState() - - return { context in - let environment = context.environment[ViewControllerComponentContainer.Environment.self].value - let component = context.component - let theme = environment.theme - let strings = environment.strings - let nameDisplayOrder = component.context.sharedContext.currentPresentationData.with { $0 }.nameDisplayOrder - let controller = environment.controller - let state = context.state - - let sideInset: CGFloat = 16.0 + environment.safeInsets.left - - var contentHeight: CGFloat = 0.0 - - let headerHeight: CGFloat = 226.0 - let headerSubject: GiftCompositionComponent.Subject = .preview([]) - - var peerNameString = "" - if let peer = state.peerMap[component.context.account.peerId] { - peerNameString = peer.displayTitle(strings: strings, displayOrder: nameDisplayOrder) - } - - let peerName = peerName.update( - component: MultilineTextComponent( - text: .plain(NSAttributedString( - string: peerNameString, - font: Font.bold(20.0), - textColor: .white, - paragraphAlignment: .center - )), - horizontalAlignment: .center, - maximumNumberOfLines: 1 - ), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), - transition: .immediate - ) - - let animationOffset: CGPoint? = CGPoint(x: peerName.size.width / 2.0 + 20.0 - 12.0, y: 79.0) - let animationScale: CGFloat = 0.19 - - let animation = animation.update( - component: GiftCompositionComponent( - context: component.context, - theme: environment.theme, - subject: headerSubject, - animationOffset: animationOffset, - animationScale: animationScale, - displayAnimationStars: true, - externalState: giftCompositionExternalState, - requestUpdate: { [weak state] transition in - state?.updated(transition: transition) - } - ), - availableSize: CGSize(width: context.availableSize.width, height: headerHeight), - transition: context.transition - ) - context.add(animation - .position(CGPoint(x: context.availableSize.width / 2.0, y: headerHeight / 2.0)) - ) - contentHeight += headerHeight - - if let peer = state.peerMap[component.context.account.peerId] { - let avatar = avatar.update( - component: AvatarComponent( - context: component.context, - theme: theme, - peer: peer - ), - environment: {}, - availableSize: CGSize(width: 100.0, height: 100.0), - transition: context.transition - ) - context.add(avatar - .position(CGPoint(x: context.availableSize.width / 2.0, y: 86.0)) - ) - } - context.add(peerName - .position(CGPoint(x: context.availableSize.width / 2.0 - 12.0, y: 167.0)) - ) - -// let vibrantColor: UIColor -// if let previewPatternColor = giftCompositionExternalState.previewPatternColor { -// vibrantColor = previewPatternColor.withMultiplied(hue: 1.0, saturation: 1.02, brightness: 1.25).mixedWith(UIColor.white, alpha: 0.3) -// } else { -// vibrantColor = UIColor.white.withAlphaComponent(0.6) -// } - - - let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) - let buttonSize = CGSize(width: context.availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0) - let buttonBackground = ButtonComponent.Background( - style: .glass, - color: theme.list.itemCheckColors.fillColor, - foreground: theme.list.itemCheckColors.foregroundColor, - pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) - ) - let button = button.update( - component: ButtonComponent( - background: buttonBackground, - content: AnyComponentWithIdentity( - id: AnyHashable("ok"), - component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Common_OK, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)))) - ), - isEnabled: true, - displaysProgress: false, - action: { - if let controller = controller() as? GiftAuctionUpgradePreviewScreen { - controller.dismissAnimated() - } - }), - availableSize: buttonSize, - transition: context.transition - ) - - let buttonFrame = CGRect(origin: CGPoint(x: buttonInsets.left, y: contentHeight), size: button.size) - context.add(button - .position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY)) - ) - contentHeight += button.size.height - contentHeight += 7.0 - - let effectiveBottomInset: CGFloat = environment.metrics.isTablet ? 0.0 : environment.safeInsets.bottom - return CGSize(width: context.availableSize.width, height: contentHeight + 5.0 + effectiveBottomInset) - } - } -} - -final class GiftAuctionUpgradePreviewSheetComponent: CombinedComponent { - typealias EnvironmentType = ViewControllerComponentContainer.Environment - - let context: AccountContext - let gift: StarGift - - init( - context: AccountContext, - gift: StarGift - ) { - self.context = context - self.gift = gift - } - - static func ==(lhs: GiftAuctionUpgradePreviewSheetComponent, rhs: GiftAuctionUpgradePreviewSheetComponent) -> Bool { - if lhs.context !== rhs.context { - return false - } - if lhs.gift != rhs.gift { - return false - } - return true - } - - static var body: Body { - let sheet = Child(SheetComponent.self) - let animateOut = StoredActionSlot(Action.self) - - let sheetExternalState = SheetComponent.ExternalState() - - return { context in - let environment = context.environment[EnvironmentType.self] - let controller = environment.controller - - let sheet = sheet.update( - component: SheetComponent( - content: AnyComponent(GiftAuctionUpgradePreviewSheetContent( - context: context.component.context, - gift: context.component.gift, - animateOut: animateOut, - getController: controller - )), - style: .glass, - backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), - followContentSizeChanges: true, - clipsContent: true, - hasDimView: false, - autoAnimateOut: false, - externalState: sheetExternalState, - animateOut: animateOut, - onPan: { - }, - willDismiss: { - if let controller = controller() as? GiftAuctionUpgradePreviewScreen { - controller.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.3).withUserData(ViewControllerComponentContainer.AnimateOutTransition())) - } - } - ), - environment: { - environment - SheetComponentEnvironment( - isDisplaying: environment.value.isVisible, - isCentered: environment.metrics.widthClass == .regular, - hasInputHeight: !environment.inputHeight.isZero, - regularMetricsSize: CGSize(width: 430.0, height: 900.0), - dismiss: { animated in - if animated { - if let controller = controller() as? GiftAuctionUpgradePreviewScreen { - controller.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.3).withUserData(ViewControllerComponentContainer.AnimateOutTransition())) - animateOut.invoke(Action { _ in - controller.dismiss(completion: nil) - }) - } - } else { - if let controller = controller() as? GiftAuctionUpgradePreviewScreen { - controller.dismiss(completion: nil) - } - } - } - ) - }, - availableSize: context.availableSize, - transition: context.transition - ) - - context.add(sheet - .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) - ) - - if let controller = controller(), !controller.automaticallyControlPresentationContextLayout { - var sideInset: CGFloat = 0.0 - var bottomInset: CGFloat = max(environment.safeInsets.bottom, sheetExternalState.contentHeight) - if case .regular = environment.metrics.widthClass { - sideInset = floor((context.availableSize.width - 430.0) / 2.0) - 12.0 - bottomInset = (context.availableSize.height - sheetExternalState.contentHeight) / 2.0 + sheetExternalState.contentHeight - } - - let layout = ContainerViewLayout( - size: context.availableSize, - metrics: environment.metrics, - deviceMetrics: environment.deviceMetrics, - intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0), - safeInsets: UIEdgeInsets(top: 0.0, left: max(sideInset, environment.safeInsets.left), bottom: 0.0, right: max(sideInset, environment.safeInsets.right)), - additionalInsets: .zero, - statusBarHeight: environment.statusBarHeight, - inputHeight: nil, - inputHeightIsInteractivellyChanging: false, - inVoiceOver: false - ) - controller.presentationContext.containerLayoutUpdated(layout, transition: context.transition.containedViewLayoutTransition) - } - - return context.availableSize - } - } -} - -public class GiftAuctionUpgradePreviewScreen: ViewControllerComponentContainer { - private let context: AccountContext - - public init( - context: AccountContext, - gift: StarGift - ) { - self.context = context - - super.init( - context: context, - component: GiftAuctionUpgradePreviewSheetComponent(context: context, gift: gift), - navigationBarAppearance: .none, - statusBarStyle: .ignore, - theme: .default - ) - - self.navigationPresentation = .flatModal - self.automaticallyControlPresentationContextLayout = false - } - - required public init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - public override func viewDidLoad() { - super.viewDidLoad() - - self.view.disablesInteractiveModalDismiss = true - } - - public func dismissAnimated() { - if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { - view.dismissAnimated() - } - } -} diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift index b36203f498..44cba549ca 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift @@ -72,8 +72,15 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { private var giftAuctionAcquiredGiftsPromise = ValuePromise<[GiftAuctionAcquiredGift]>() private var giftAuctionAcquiredGiftsDisposable = MetaDisposable() - private(set) var giftValueInfo: StarGift.UniqueGift.ValueInfo? - private var giftValueInfoDisposable: Disposable? + private(set) var giftUpgradeAttributes: [StarGift.UniqueGift.Attribute]? + private var previewTimer: SwiftSignalKit.Timer? + private(set) var previewModelIndex: Int = -1 + private(set) var previewBackdropIndex: Int = -1 + private(set) var previewSymbolIndex: Int = -1 + + private(set) var previewModels: [StarGift.UniqueGift.Attribute] = [] + private(set) var previewBackdrops: [StarGift.UniqueGift.Attribute] = [] + private(set) var previewSymbols: [StarGift.UniqueGift.Attribute] = [] var cachedStarImage: (UIImage, PresentationTheme)? @@ -105,30 +112,87 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { if let acquiredCount = auctionState?.myState.acquiredCount, acquiredCount > (previousState?.myState.acquiredCount ?? 0) { self.loadAcquiredGifts() } - - if !"".isEmpty, self.giftValueInfoDisposable == nil, let auctionState, case let .generic(gift) = auctionState.gift, case .finished = auctionState.auctionState, let slug = gift.auctionSlug { - self.giftValueInfoDisposable = (self.context.engine.payments.getUniqueStarGiftValueInfo(slug: "\(slug)-1") - |> deliverOnMainQueue).start(next: { [weak self] valueInfo in - guard let self else { - return - } - self.giftValueInfo = valueInfo - self.updated(transition: .easeInOut(duration: 0.25)) - }) - } }) - + self.giftAuctionTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in self?.updated() }, queue: Queue.mainQueue()) self.giftAuctionTimer?.start() + + if case let .generic(gift) = auctionContext.gift, let upgradeVariantsCount = gift.upgradeVariantsCount, upgradeVariantsCount > 0 { + let _ = (context.engine.payments.getStarGiftUpgradeAttributes(giftId: gift.id) + |> deliverOnMainQueue).start(next: { [weak self] attributes in + guard let self, let attributes else { + return + } + self.giftUpgradeAttributes = attributes + + let randomModels = Array(attributes.filter({ attribute in + if case .model = attribute { + return true + } else { + return false + } + }).shuffled().prefix(5)) + self.previewModels = randomModels + + let randomBackdrops = Array(attributes.filter({ attribute in + if case .backdrop = attribute { + return true + } else { + return false + } + }).shuffled()) + self.previewBackdrops = randomBackdrops + + let randomSymbols = Array(attributes.filter({ attribute in + if case .pattern = attribute { + return true + } else { + return false + } + }).shuffled().prefix(5)) + self.previewSymbols = randomSymbols + + self.updated() + }) + + self.previewTimer = SwiftSignalKit.Timer(timeout: 3.0, repeat: true, completion: { [weak self] in + guard let self else { + return + } + self.previewTimerTick() + }, queue: Queue.mainQueue()) + self.previewTimer?.start() + } } deinit { self.disposable?.dispose() self.giftAuctionAcquiredGiftsDisposable.dispose() self.giftAuctionTimer?.invalidate() - self.giftValueInfoDisposable?.dispose() + self.previewTimer?.invalidate() + } + + private func previewTimerTick() { + guard !self.previewModels.isEmpty else { return } + self.previewModelIndex = (self.previewModelIndex + 1) % self.previewModels.count + + let previousSymbolIndex = self.previewSymbolIndex + var randomSymbolIndex = previousSymbolIndex + while randomSymbolIndex == previousSymbolIndex && !self.previewSymbols.isEmpty { + randomSymbolIndex = Int.random(in: 0 ..< self.previewSymbols.count) + } + if !self.previewSymbols.isEmpty { self.previewSymbolIndex = randomSymbolIndex } + + let previousBackdropIndex = self.previewBackdropIndex + var randomBackdropIndex = previousBackdropIndex + while randomBackdropIndex == previousBackdropIndex && !self.previewBackdrops.isEmpty { + randomBackdropIndex = Int.random(in: 0 ..< self.previewBackdrops.count) + } + if !self.previewBackdrops.isEmpty { self.previewBackdropIndex = randomBackdropIndex } + + self.updated(transition: .easeInOut(duration: 0.25)) } func loadAcquiredGifts() { @@ -186,7 +250,14 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { } self.dismiss(animated: true) - controller.completion(self.giftAuctionAcquiredGiftsPromise.get()) + var compactUpgradeAttributes: [StarGift.UniqueGift.Attribute] = [] + if !self.previewModels.isEmpty { + compactUpgradeAttributes.append(contentsOf: self.previewModels) + compactUpgradeAttributes.append(contentsOf: self.previewBackdrops) + compactUpgradeAttributes.append(contentsOf: self.previewSymbols) + } + + controller.completion(self.giftAuctionAcquiredGiftsPromise.get(), !compactUpgradeAttributes.isEmpty ? compactUpgradeAttributes : nil) } func openPeer(_ peer: EnginePeer, dismiss: Bool = true) { @@ -319,7 +390,7 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { } func openGiftFragmentResale() { - guard let controller = self.getController() as? GiftAuctionViewScreen, let navigationController = controller.navigationController as? NavigationController, let url = self.giftValueInfo?.fragmentListedUrl else { + guard let controller = self.getController() as? GiftAuctionViewScreen, let navigationController = controller.navigationController as? NavigationController, case let .finished(_, _, _, _, _, fragmentListedUrl) = self.giftAuctionState?.auctionState, let url = fragmentListedUrl else { return } let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } @@ -390,23 +461,27 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { } static var body: Body { + let header = Child(GiftCompositionComponent.self) let closeButton = Child(GlassBarButtonComponent.self) let moreButton = Child(GlassBarButtonComponent.self) - let animation = Child(GiftItemComponent.self) let title = Child(MultilineTextComponent.self) - let description = Child(BalancedTextComponent.self) + let description = Child(StarsButtonContentComponent.self) + let learnMore = Child(PlainButtonComponent.self) let table = Child(TableComponent.self) let button = Child(ButtonComponent.self) + let variantsButton = Child(PlainButtonComponent.self) let acquiredButton = Child(PlainButtonComponent.self) let telegramSaleButton = Child(PlainButtonComponent.self) let fragmentSaleButton = Child(PlainButtonComponent.self) let moreButtonPlayOnce = ActionSlot() + let giftCompositionExternalState = GiftCompositionComponent.ExternalState() + return { context in let environment = context.environment[ViewControllerComponentContainer.Environment.self].value @@ -420,49 +495,81 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { let sideInset: CGFloat = 16.0 + environment.safeInsets.left var titleString: String = "" - var giftIconSubject: GiftItemComponent.Subject? var genericGift: StarGift.Gift? switch component.auctionContext.gift { case let .generic(gift): titleString = gift.title ?? "" - giftIconSubject = .starGift(gift: gift, price: "") genericGift = gift default: break } - - let _ = giftIconSubject - let _ = genericGift - + var originY: CGFloat = 0.0 - + + var buttonColor: UIColor = .white.withAlphaComponent(0.1) + var secondaryTextColor: UIColor = .white.withAlphaComponent(0.4) if let genericGift { - let animation = animation.update( - component: GiftItemComponent( + var attributes: [StarGift.UniqueGift.Attribute] = [] + if state.previewModelIndex == -1 { + attributes.append(.model(name: "", file: genericGift.file, rarity: 0)) + if let background = genericGift.background { + attributes.append(.backdrop(name: "", id: 0, innerColor: background.centerColor, outerColor: background.edgeColor, patternColor: 0, textColor: 0, rarity: 0)) + } + } else if !state.previewModels.isEmpty { + attributes.append(state.previewModels[state.previewModelIndex]) + if !state.previewBackdrops.isEmpty { + attributes.append(state.previewBackdrops[state.previewBackdropIndex]) + } + if !state.previewSymbols.isEmpty { + attributes.append(state.previewSymbols[state.previewSymbolIndex]) + } + } + + if let backdropAttribute = attributes.first(where: { attribute in + if case .backdrop = attribute { + return true + } else { + return false + } + }), case let .backdrop(_, _, innerColor, outerColor, _, _, _) = backdropAttribute { + let topColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultiplied(hue: 1.01, saturation: 1.22, brightness: 1.04) + let bottomColor = UIColor(rgb: UInt32(bitPattern: outerColor)).withMultiplied(hue: 0.97, saturation: 1.45, brightness: 0.89) + buttonColor = topColor.mixedWith(bottomColor, alpha: 0.8) + + secondaryTextColor = topColor.withMultiplied(hue: 1.0, saturation: 1.02, brightness: 1.25).mixedWith(UIColor.white, alpha: 0.5) + } + + let header = header.update( + component: GiftCompositionComponent( context: component.context, - theme: environment.theme, - strings: environment.strings, - subject: .starGift(gift: genericGift, price: ""), - ribbon: GiftItemComponent.Ribbon(text: strings.Gift_Auction_Auction, color: .orange), - outline: .orange, - mode: .header + theme: theme, + subject: .preview(attributes), + animationOffset: CGPoint(x: 0.0, y: -4.0), + animationScale: nil, + displayAnimationStars: false, + revealedAttributes: Set(), + externalState: giftCompositionExternalState, + requestUpdate: { [weak state] transition in + state?.updated(transition: transition) + } ), - availableSize: CGSize(width: 120.0, height: 120.0), + availableSize: CGSize(width: context.availableSize.width, height: 264.0), transition: context.transition ) - context.add(animation - .position(CGPoint(x: context.availableSize.width / 2.0, y: 92.0)) + context.add(header + .position(CGPoint(x: context.availableSize.width / 2.0, y: header.size.height * 0.5)) ) + originY += header.size.height + originY += 16.0 } - originY += 177.0 let title = title.update( component: MultilineTextComponent( text: .plain(NSAttributedString( string: titleString, - font: Font.bold(24.0), - textColor: theme.list.itemPrimaryTextColor, + font: Font.bold(20.0), + textColor: .white, paragraphAlignment: .center )), horizontalAlignment: .center, @@ -472,11 +579,10 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { transition: .immediate ) context.add(title - .position(CGPoint(x: context.availableSize.width / 2.0, y: 174.0)) + .position(CGPoint(x: context.availableSize.width / 2.0, y: 209.0)) ) var descriptionText: String = "" - var descriptionColor = theme.list.itemSecondaryTextColor let tableFont = Font.regular(15.0) let tableTextColor = theme.list.itemPrimaryTextColor @@ -502,7 +608,6 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { if isEnded { descriptionText = strings.Gift_Auction_Ended - descriptionColor = theme.list.itemDestructiveColor tableItems.append(.init( id: "firstSale", @@ -572,11 +677,11 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { ) )) } else { - var auctionGiftsPerRound: Int32 = 50 - if let auctionGiftsPerRoundValue = gift.auctionGiftsPerRound { - auctionGiftsPerRound = auctionGiftsPerRoundValue - } - descriptionText = strings.Gift_Auction_Description("\(auctionGiftsPerRound)", gift.title ?? "").string +// var auctionGiftsPerRound: Int32 = 50 +// if let auctionGiftsPerRoundValue = gift.auctionGiftsPerRound { +// auctionGiftsPerRound = auctionGiftsPerRoundValue +// } + descriptionText = isUpcoming ? "Upcoming Auction" : "Gift Auction" //strings.Gift_Auction_Description("\(auctionGiftsPerRound)", gift.title ?? "").string tableItems.append(.init( id: "start", @@ -670,11 +775,28 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { } } } - - let textFont = Font.regular(15.0) - let boldTextFont = Font.semibold(15.0) - let textColor = descriptionColor - let linkColor = theme.list.itemAccentColor + + let description = description.update( + component: StarsButtonContentComponent( + context: component.context, + text: descriptionText, + color: buttonColor, + tinted: true, + starsColor: UIColor(white: 1.0, alpha: 0.5), + font: Font.medium(13.0), + height: 24.0 + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 50.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + context.add(description + .position(CGPoint(x: context.availableSize.width / 2.0, y: 167.0 + description.size.height / 2.0)) + ) + + let textFont = Font.regular(13.0) + let boldTextFont = Font.semibold(13.0) + let textColor = UIColor.white + let linkColor = UIColor.white let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in return (TelegramTextAttributes.URL, contents) }) @@ -685,48 +807,35 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { if state.cachedSmallChevronImage == nil || state.cachedSmallChevronImage?.1 !== environment.theme { state.cachedSmallChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: linkColor)!, theme) } - descriptionText = descriptionText.replacingOccurrences(of: " >]", with: "\u{00A0}>]") - let attributedString = parseMarkdownIntoAttributedString(descriptionText, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString - if let range = attributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 { - attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string)) + //TODO:localize + let learnMoreAttributedString = parseMarkdownIntoAttributedString("Learn more about Telegram Gifts >", attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString + if let range = learnMoreAttributedString.string.range(of: ">"), let chevronImage = state.cachedSmallChevronImage?.0 { + learnMoreAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: learnMoreAttributedString.string)) } - - let description = description.update( - component: BalancedTextComponent( - text: .plain(attributedString), - maximumNumberOfLines: 0, - lineSpacing: 0.2, - highlightColor: linkColor.withAlphaComponent(0.1), - highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), - highlightAction: { attributes in - if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { - return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) - } else { - return nil - } + let learnMore = learnMore.update( + component: PlainButtonComponent( + content: AnyComponent(BalancedTextComponent( + text: .plain(learnMoreAttributedString), + maximumNumberOfLines: 1, + tintColor: secondaryTextColor + )), + action: { + let controller = component.context.sharedContext.makeGiftAuctionInfoScreen( + context: component.context, + auctionContext: component.auctionContext, + completion: nil + ) + environment.controller()?.push(controller) }, - tapAction: { attributes, _ in - if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { - let controller = component.context.sharedContext.makeGiftAuctionInfoScreen( - context: component.context, - auctionContext: component.auctionContext, - completion: nil - ) - environment.controller()?.push(controller) - } - } + animateScale: false ), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 50.0, height: CGFloat.greatestFiniteMagnitude), - transition: .immediate + availableSize: context.availableSize, + transition: context.transition ) - context.add(description - .position(CGPoint(x: context.availableSize.width / 2.0, y: 198.0 + description.size.height / 2.0)) - .appear(.default(alpha: true)) - .disappear(.default(alpha: true)) + context.add(learnMore + .position(CGPoint(x: context.availableSize.width / 2.0, y: 238.0)) ) - originY += description.size.height - originY += 42.0 let table = table.update( component: TableComponent( @@ -738,13 +847,97 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { ) context.add(table .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + table.size.height / 2.0)) - .appear(.default(alpha: true)) - .disappear(.default(alpha: true)) ) originY += table.size.height + 26.0 var hasAdditionalButtons = false + if case let .generic(gift) = component.auctionContext.gift, let upgradeVariantsCount = gift.upgradeVariantsCount { + originY += 5.0 + + + var variant1: GiftItemComponent.Subject = .starGift(gift: gift, price: "") + var variant2: GiftItemComponent.Subject = .starGift(gift: gift, price: "") + var variant3: GiftItemComponent.Subject = .starGift(gift: gift, price: "") + if !state.previewModels.isEmpty { + if state.previewModels.count > 0 { + variant1 = .preview(attributes: [state.previewModels[0]], rarity: 0) + } + if state.previewModels.count > 1 { + variant2 = .preview(attributes: [state.previewModels[1]], rarity: 0) + } + if state.previewModels.count > 2 { + variant3 = .preview(attributes: [state.previewModels[2]], rarity: 0) + } + } + + let variantsButton = variantsButton.update( + component: PlainButtonComponent(content: AnyComponent( + HStack([ + AnyComponentWithIdentity(id: "view", component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Auction_ViewVariants, font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor))) + )), + AnyComponentWithIdentity(id: "spacing", component: AnyComponent( + Rectangle(color: .clear, width: 8.0, height: 1.0) + )), + AnyComponentWithIdentity(id: "icon1", component: AnyComponent( + GiftItemComponent( + context: component.context, + theme: theme, + strings: strings, + peer: nil, + subject: variant1, + isPlaceholder: state.previewModels.isEmpty, + mode: .buttonIcon + ) + )), + AnyComponentWithIdentity(id: "icon2", component: AnyComponent( + GiftItemComponent( + context: component.context, + theme: theme, + strings: strings, + peer: nil, + subject: variant2, + isPlaceholder: state.previewModels.isEmpty, + mode: .buttonIcon + ) + )), + AnyComponentWithIdentity(id: "icon3", component: AnyComponent( + GiftItemComponent( + context: component.context, + theme: theme, + strings: strings, + peer: nil, + subject: variant3, + isPlaceholder: state.previewModels.isEmpty, + mode: .buttonIcon + ) + )), + AnyComponentWithIdentity(id: "text", component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: " \(strings.Gift_Auction_Variants(upgradeVariantsCount))", font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor))) + )), + AnyComponentWithIdentity(id: "arrow", component: AnyComponent( + BundleIconComponent(name: "Chat/Context Menu/Arrow", tintColor: theme.actionSheet.controlAccentColor) + )) + ], spacing: 0.0) + ), action: { [weak state] in + guard let state, let attributes = state.giftUpgradeAttributes else { + return + } + let variantsController = component.context.sharedContext.makeGiftUpgradeVariantsPreviewScreen(context: component.context, gift: .generic(gift), attributes: attributes) + environment.controller()?.push(variantsController) + }, animateScale: false), + availableSize: CGSize(width: context.availableSize.width - 64.0, height: context.availableSize.height), + transition: context.transition + ) + context.add(variantsButton + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + variantsButton.size.height / 2.0))) + originY += variantsButton.size.height + originY += 12.0 + + hasAdditionalButtons = true + } + let acquiredGiftsCount = state.giftAuctionState?.myState.acquiredCount ?? 0 if acquiredGiftsCount > 0, case let .generic(gift) = component.auctionContext.gift { originY += 5.0 @@ -793,8 +986,8 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { hasAdditionalButtons = true } - if let giftValueInfo = state.giftValueInfo, case let .generic(gift) = component.auctionContext.gift { - if let listedCount = giftValueInfo.listedCount, listedCount > 0 { + if case let .finished(_, _, _, listedCount, fragmentListedCount, _) = state.giftAuctionState?.auctionState, case let .generic(gift) = component.auctionContext.gift { + if let listedCount = listedCount, listedCount > 0 { originY += 5.0 let telegramSaleButton = telegramSaleButton.update( @@ -840,7 +1033,7 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { hasAdditionalButtons = true } - if let listedCount = giftValueInfo.fragmentListedCount, listedCount > 0 { + if let listedCount = fragmentListedCount, listedCount > 0 { let fragmentSaleButton = fragmentSaleButton.update( component: PlainButtonComponent(content: AnyComponent( HStack([ @@ -1007,13 +1200,13 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { let closeButton = closeButton.update( component: GlassBarButtonComponent( size: CGSize(width: 40.0, height: 40.0), - backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor, + backgroundColor: buttonColor.withMultipliedBrightnessBy(1.2), isDark: theme.overallDarkAppearance, - state: .generic, + state: .tintedGlass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", - tintColor: theme.rootController.navigationBar.glassBarButtonForegroundColor + tintColor: .white ) )), action: { [weak state] _ in @@ -1033,15 +1226,15 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { let moreButton = moreButton.update( component: GlassBarButtonComponent( size: CGSize(width: 40.0, height: 40.0), - backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor, + backgroundColor: buttonColor.withMultipliedBrightnessBy(1.2), isDark: theme.overallDarkAppearance, - state: .generic, + state: .tintedGlass, component: AnyComponentWithIdentity(id: "more", component: AnyComponent( LottieComponent( content: LottieComponent.AppBundleContent( name: "anim_morewide" ), - color: theme.rootController.navigationBar.glassBarButtonForegroundColor, + color: .white, size: CGSize(width: 34.0, height: 34.0), playOnce: moreButtonPlayOnce ) @@ -1181,12 +1374,12 @@ final class GiftAuctionViewSheetComponent: CombinedComponent { } public final class GiftAuctionViewScreen: ViewControllerComponentContainer { - fileprivate let completion: (Signal<[GiftAuctionAcquiredGift], NoError>) -> Void + fileprivate let completion: (Signal<[GiftAuctionAcquiredGift], NoError>, [StarGift.UniqueGift.Attribute]?) -> Void public init( context: AccountContext, auctionContext: GiftAuctionContext, - completion: @escaping (Signal<[GiftAuctionAcquiredGift], NoError>) -> Void + completion: @escaping (Signal<[GiftAuctionAcquiredGift], NoError>, [StarGift.UniqueGift.Attribute]?) -> Void ) { self.completion = completion diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionWearPreviewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionWearPreviewScreen.swift new file mode 100644 index 0000000000..c4ec1b9b62 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionWearPreviewScreen.swift @@ -0,0 +1,649 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit +import AccountContext +import TelegramPresentationData +import PresentationDataUtils +import ComponentFlow +import ViewControllerComponent +import SheetComponent +import MultilineTextComponent +import MultilineTextWithEntitiesComponent +import BundleIconComponent +import ButtonComponent +import Markdown +import BalancedTextComponent +import AvatarNode +import TextFormat +import TelegramStringFormatting +import PlainButtonComponent +import GiftItemComponent +import GiftAnimationComponent +import GlassBarButtonComponent +import GiftRemainingCountComponent + +private final class GiftAuctionWearPreviewSheetContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let auctionContext: GiftAuctionContext + let attributes: [StarGift.UniqueGift.Attribute] + let animateOut: ActionSlot> + let getController: () -> ViewController? + + init( + context: AccountContext, + auctionContext: GiftAuctionContext, + attributes: [StarGift.UniqueGift.Attribute], + animateOut: ActionSlot>, + getController: @escaping () -> ViewController? + ) { + self.context = context + self.auctionContext = auctionContext + self.attributes = attributes + self.animateOut = animateOut + self.getController = getController + } + + static func ==(lhs: GiftAuctionWearPreviewSheetContent, rhs: GiftAuctionWearPreviewSheetContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + final class State: ComponentState { + private let context: AccountContext + + private var peerDisposable: Disposable? + var peerMap: [EnginePeer.Id: EnginePeer] = [:] + + var cachedSmallChevronImage: (UIImage, PresentationTheme)? + + private var previewTimer: SwiftSignalKit.Timer? + private(set) var previewModelIndex: Int = 0 + private(set) var previewBackdropIndex: Int = 0 + private(set) var previewSymbolIndex: Int = 0 + + private(set) var previewModels: [StarGift.UniqueGift.Attribute] = [] + private(set) var previewBackdrops: [StarGift.UniqueGift.Attribute] = [] + private(set) var previewSymbols: [StarGift.UniqueGift.Attribute] = [] + + init( + context: AccountContext, + attributes: [StarGift.UniqueGift.Attribute] + ) { + self.context = context + + super.init() + + + for attribute in attributes { + switch attribute { + case .model: + self.previewModels.append(attribute) + case .backdrop: + self.previewBackdrops.append(attribute) + case .pattern: + self.previewSymbols.append(attribute) + default: + break + } + } + + let peerIds: [EnginePeer.Id] = [context.account.peerId] + self.peerDisposable = ( + context.engine.data.get(EngineDataMap( + peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.Peer in + return TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) + } + )) + |> deliverOnMainQueue + ).startStrict(next: { [weak self] peers in + if let strongSelf = self { + var peersMap: [EnginePeer.Id: EnginePeer] = [:] + for (peerId, maybePeer) in peers { + if let peer = maybePeer { + peersMap[peerId] = peer + } + } + strongSelf.peerMap = peersMap + strongSelf.updated(transition: .immediate) + } + }) + + self.previewTimer = SwiftSignalKit.Timer(timeout: 3.0, repeat: true, completion: { [weak self] in + guard let self else { + return + } + self.previewTimerTick() + }, queue: Queue.mainQueue()) + self.previewTimer?.start() + } + + deinit { + self.peerDisposable?.dispose() + self.previewTimer?.invalidate() + } + + private func previewTimerTick() { + guard !self.previewModels.isEmpty else { return } + self.previewModelIndex = (self.previewModelIndex + 1) % self.previewModels.count + + let previousSymbolIndex = self.previewSymbolIndex + var randomSymbolIndex = previousSymbolIndex + while randomSymbolIndex == previousSymbolIndex && !self.previewSymbols.isEmpty { + randomSymbolIndex = Int.random(in: 0 ..< self.previewSymbols.count) + } + if !self.previewSymbols.isEmpty { self.previewSymbolIndex = randomSymbolIndex } + + let previousBackdropIndex = self.previewBackdropIndex + var randomBackdropIndex = previousBackdropIndex + while randomBackdropIndex == previousBackdropIndex && !self.previewBackdrops.isEmpty { + randomBackdropIndex = Int.random(in: 0 ..< self.previewBackdrops.count) + } + if !self.previewBackdrops.isEmpty { self.previewBackdropIndex = randomBackdropIndex } + + self.updated(transition: .easeInOut(duration: 0.25)) + } + } + + + func makeState() -> State { + return State(context: self.context, attributes: self.attributes) + } + + static var body: Body { + let closeButton = Child(GlassBarButtonComponent.self) + let animation = Child(GiftCompositionComponent.self) + let avatar = Child(AvatarComponent.self) + let peerName = Child(MultilineTextComponent.self) + let learnMore = Child(PlainButtonComponent.self) + + let initialGift = Child(GiftItemComponent.self) + let upgradedGift = Child(GiftItemComponent.self) + let arrow = Child(BundleIconComponent.self) + let upgradeLabel = Child(MultilineTextComponent.self) + let remainingCount = Child(GiftRemainingCountComponent.self) + //let auctionFooter = Child(MultilineTextComponent.self) + + let button = Child(ButtonComponent.self) + + let giftCompositionExternalState = GiftCompositionComponent.ExternalState() + + return { context in + let environment = context.environment[ViewControllerComponentContainer.Environment.self].value + let component = context.component + let theme = environment.theme + let strings = environment.strings + let nameDisplayOrder = component.context.sharedContext.currentPresentationData.with { $0 }.nameDisplayOrder + let controller = environment.controller + let state = context.state + + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + + var contentHeight: CGFloat = 0.0 + + let headerHeight: CGFloat = 226.0 + + var peerNameString = "" + if let peer = state.peerMap[component.context.account.peerId] { + peerNameString = peer.displayTitle(strings: strings, displayOrder: nameDisplayOrder) + } + + let peerName = peerName.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: peerNameString, + font: Font.bold(20.0), + textColor: .white, + paragraphAlignment: .center + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + + let animationOffset: CGPoint? = CGPoint(x: peerName.size.width / 2.0 + 20.0 - 12.0, y: 79.0) + let animationScale: CGFloat = 0.19 + + var attributes: [StarGift.UniqueGift.Attribute] = [] + if !state.previewModels.isEmpty { + attributes.append(state.previewModels[state.previewModelIndex]) + if !state.previewBackdrops.isEmpty { + attributes.append(state.previewBackdrops[state.previewBackdropIndex]) + } + if !state.previewSymbols.isEmpty { + attributes.append(state.previewSymbols[state.previewSymbolIndex]) + } + } + let animation = animation.update( + component: GiftCompositionComponent( + context: component.context, + theme: environment.theme, + subject: .preview(attributes), + animationOffset: animationOffset, + animationScale: animationScale, + displayAnimationStars: true, + externalState: giftCompositionExternalState, + requestUpdate: { [weak state] transition in + state?.updated(transition: transition) + } + ), + availableSize: CGSize(width: context.availableSize.width, height: headerHeight), + transition: context.transition + ) + context.add(animation + .position(CGPoint(x: context.availableSize.width / 2.0, y: headerHeight / 2.0)) + ) + contentHeight += headerHeight + + if let peer = state.peerMap[component.context.account.peerId] { + let avatar = avatar.update( + component: AvatarComponent( + context: component.context, + theme: theme, + peer: peer + ), + environment: {}, + availableSize: CGSize(width: 100.0, height: 100.0), + transition: context.transition + ) + context.add(avatar + .position(CGPoint(x: context.availableSize.width / 2.0, y: 86.0)) + ) + } + context.add(peerName + .position(CGPoint(x: context.availableSize.width / 2.0 - 12.0, y: 167.0)) + ) + + var buttonColor: UIColor = .white.withAlphaComponent(0.1) + var secondaryTextColor: UIColor = .white.withAlphaComponent(0.4) + if let backdropAttribute = attributes.first(where: { attribute in + if case .backdrop = attribute { + return true + } else { + return false + } + }), case let .backdrop(_, _, innerColor, outerColor, _, _, _) = backdropAttribute { + let topColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultiplied(hue: 1.01, saturation: 1.22, brightness: 1.04) + let bottomColor = UIColor(rgb: UInt32(bitPattern: outerColor)).withMultiplied(hue: 0.97, saturation: 1.45, brightness: 0.89) + buttonColor = topColor.mixedWith(bottomColor, alpha: 0.8) + + secondaryTextColor = topColor.withMultiplied(hue: 1.0, saturation: 1.02, brightness: 1.25).mixedWith(UIColor.white, alpha: 0.5) + } + + let closeButton = closeButton.update( + component: GlassBarButtonComponent( + size: CGSize(width: 40.0, height: 40.0), + backgroundColor: buttonColor.withMultipliedBrightnessBy(1.2), + isDark: theme.overallDarkAppearance, + state: .tintedGlass, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: .white + ) + )), + action: { _ in + (controller() as? GiftAuctionWearPreviewScreen)?.dismissAnimated() + } + ), + availableSize: CGSize(width: 40.0, height: 40.0), + transition: .immediate + ) + context.add(closeButton + .position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0)) + ) + + let textFont = Font.regular(13.0) + let boldTextFont = Font.semibold(13.0) + let textColor = UIColor.white + let linkColor = UIColor.white + let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + }) + if state.cachedSmallChevronImage == nil || state.cachedSmallChevronImage?.1 !== environment.theme { + state.cachedSmallChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: linkColor)!, theme) + } + + //TODO:localize + let learnMoreAttributedString = parseMarkdownIntoAttributedString("Learn more about wearing Telegram Gifts >", attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString + if let range = learnMoreAttributedString.string.range(of: ">"), let chevronImage = state.cachedSmallChevronImage?.0 { + learnMoreAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: learnMoreAttributedString.string)) + } + let learnMore = learnMore.update( + component: PlainButtonComponent( + content: AnyComponent(BalancedTextComponent( + text: .plain(learnMoreAttributedString), + maximumNumberOfLines: 1, + tintColor: secondaryTextColor + )), + action: { + let controller = component.context.sharedContext.makeGiftWearPreviewScreen(context: component.context, gift: component.auctionContext.gift, attributes: component.attributes) + environment.controller()?.push(controller) + }, + animateScale: false + ), + availableSize: context.availableSize, + transition: context.transition + ) + context.add(learnMore + .position(CGPoint(x: context.availableSize.width / 2.0, y: 198.0)) + ) + + if case let .generic(gift) = component.auctionContext.gift { + let initialGift = initialGift.update( + component: GiftItemComponent( + context: component.context, + theme: theme, + strings: strings, + subject: .starGift(gift: gift, price: ""), + ribbon: GiftItemComponent.Ribbon(text: "limited", color: .blue), + mode: .thumbnail + ), + availableSize: CGSize(width: 120.0, height: 120.0), + transition: context.transition + ) + context.add(initialGift + .position(CGPoint(x: sideInset + initialGift.size.width * 0.5, y: contentHeight + 76.0)) + ) + } + + var ribbonColor: GiftItemComponent.Ribbon.Color = .blue + for attribute in attributes { + if case let .backdrop(_, _, innerColor, outerColor, _, _, _) = attribute { + ribbonColor = .custom(outerColor, innerColor) + break + } + } + let upgradedGift = upgradedGift.update( + component: GiftItemComponent( + context: component.context, + theme: theme, + strings: strings, + subject: .preview(attributes: attributes, rarity: 0), + ribbon: GiftItemComponent.Ribbon(text: "upgraded", color: ribbonColor), + mode: .thumbnail + ), + availableSize: CGSize(width: 120.0, height: 120.0), + transition: context.transition + ) + context.add(upgradedGift + .position(CGPoint(x: context.availableSize.width - sideInset - upgradedGift.size.width * 0.5, y: contentHeight + 76.0)) + ) + + + let arrow = arrow.update( + component: BundleIconComponent(name: "Premium/Auction/Upgrade", tintColor: theme.list.itemSecondaryTextColor), + availableSize: context.availableSize, + transition: context.transition + ) + context.add(arrow + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + 56.0)) + ) + + let upgradeLabel = upgradeLabel.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString(string: "Free\nUpgrade", font: Font.medium(13.0), textColor: theme.list.itemSecondaryTextColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 2, + lineSpacing: 0.1 + ), + availableSize: context.availableSize, + transition: context.transition + ) + context.add(upgradeLabel + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + 94.0)) + ) + + contentHeight += 137.0 + + if case let .generic(gift) = component.auctionContext.gift, let availability = gift.availability { + let remains: Int32 = availability.remains +// if let auctionState = component.auctionContext.currentState { +// switch auctionState.auctionState { +// case let .ongoing(_, _, _, _, _, _, _, giftsLeft, _, _, _, _): +// remains = giftsLeft +// case .finished: +// remains = 0 +// } +// } + let total: Int32 = availability.total + let position = CGFloat(remains) / CGFloat(total) + let remainingCount = remainingCount.update( + component: GiftRemainingCountComponent( + inactiveColor: theme.list.itemBlocksBackgroundColor, + activeColors: [UIColor(rgb: 0x72d6ff), UIColor(rgb: 0x32a0f9)], + inactiveTitle: strings.Gift_Send_Remains(remains), + inactiveValue: "", + inactiveTitleColor: theme.list.itemSecondaryTextColor, + activeTitle: "", + activeValue: "", + activeTitleColor: .white, + badgeText: "", + badgePosition: position, + badgeGraphPosition: position, + invertProgress: true, + leftString: "", + groupingSeparator: environment.dateTimeFormat.groupingSeparator + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), + transition: context.transition + ) + context.add(remainingCount + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight)) + ) + } + contentHeight += 110.0 + + var buttonTitle = strings.Gift_Auction_Join + if component.auctionContext.isUpcoming { + buttonTitle = strings.Gift_Auction_EarlyBid + } + + let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) + let buttonSize = CGSize(width: context.availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0) + let buttonBackground = ButtonComponent.Background( + style: .glass, + color: theme.list.itemCheckColors.fillColor, + foreground: theme.list.itemCheckColors.foregroundColor, + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ) + let button = button.update( + component: ButtonComponent( + background: buttonBackground, + content: AnyComponentWithIdentity( + id: AnyHashable("ok"), + component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: buttonTitle, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)))) + ), + isEnabled: true, + displaysProgress: false, + action: { + if let controller = controller() as? GiftAuctionWearPreviewScreen { + controller.completion() + controller.dismissAnimated() + } + }), + availableSize: buttonSize, + transition: context.transition + ) + + let buttonFrame = CGRect(origin: CGPoint(x: buttonInsets.left, y: contentHeight), size: button.size) + context.add(button + .position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY)) + ) + contentHeight += button.size.height + contentHeight += 7.0 + + let effectiveBottomInset: CGFloat = environment.metrics.isTablet ? 0.0 : environment.safeInsets.bottom + return CGSize(width: context.availableSize.width, height: contentHeight + 5.0 + effectiveBottomInset) + } + } +} + +final class GiftAuctionWearPreviewSheetComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let auctionContext: GiftAuctionContext + let attributes: [StarGift.UniqueGift.Attribute] + + init( + context: AccountContext, + auctionContext: GiftAuctionContext, + attributes: [StarGift.UniqueGift.Attribute] + ) { + self.context = context + self.auctionContext = auctionContext + self.attributes = attributes + } + + static func ==(lhs: GiftAuctionWearPreviewSheetComponent, rhs: GiftAuctionWearPreviewSheetComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + static var body: Body { + let sheet = Child(SheetComponent.self) + let animateOut = StoredActionSlot(Action.self) + + let sheetExternalState = SheetComponent.ExternalState() + + return { context in + let environment = context.environment[EnvironmentType.self] + let controller = environment.controller + + let sheet = sheet.update( + component: SheetComponent( + content: AnyComponent(GiftAuctionWearPreviewSheetContent( + context: context.component.context, + auctionContext: context.component.auctionContext, + attributes: context.component.attributes, + animateOut: animateOut, + getController: controller + )), + style: .glass, + backgroundColor: .color(environment.theme.list.blocksBackgroundColor), + followContentSizeChanges: true, + clipsContent: true, + hasDimView: false, + autoAnimateOut: false, + externalState: sheetExternalState, + animateOut: animateOut, + onPan: { + }, + willDismiss: { + if let controller = controller() as? GiftAuctionWearPreviewScreen { + controller.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.3).withUserData(ViewControllerComponentContainer.AnimateOutTransition())) + } + } + ), + environment: { + environment + SheetComponentEnvironment( + isDisplaying: environment.value.isVisible, + isCentered: environment.metrics.widthClass == .regular, + hasInputHeight: !environment.inputHeight.isZero, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { animated in + if animated { + if let controller = controller() as? GiftAuctionWearPreviewScreen { + controller.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.3).withUserData(ViewControllerComponentContainer.AnimateOutTransition())) + animateOut.invoke(Action { _ in + controller.dismiss(completion: nil) + }) + } + } else { + if let controller = controller() as? GiftAuctionWearPreviewScreen { + controller.dismiss(completion: nil) + } + } + } + ) + }, + availableSize: context.availableSize, + transition: context.transition + ) + + context.add(sheet + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + if let controller = controller(), !controller.automaticallyControlPresentationContextLayout { + var sideInset: CGFloat = 0.0 + var bottomInset: CGFloat = max(environment.safeInsets.bottom, sheetExternalState.contentHeight) + if case .regular = environment.metrics.widthClass { + sideInset = floor((context.availableSize.width - 430.0) / 2.0) - 12.0 + bottomInset = (context.availableSize.height - sheetExternalState.contentHeight) / 2.0 + sheetExternalState.contentHeight + } + + let layout = ContainerViewLayout( + size: context.availableSize, + metrics: environment.metrics, + deviceMetrics: environment.deviceMetrics, + intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0), + safeInsets: UIEdgeInsets(top: 0.0, left: max(sideInset, environment.safeInsets.left), bottom: 0.0, right: max(sideInset, environment.safeInsets.right)), + additionalInsets: .zero, + statusBarHeight: environment.statusBarHeight, + inputHeight: nil, + inputHeightIsInteractivellyChanging: false, + inVoiceOver: false + ) + controller.presentationContext.containerLayoutUpdated(layout, transition: context.transition.containedViewLayoutTransition) + } + + return context.availableSize + } + } +} + +public class GiftAuctionWearPreviewScreen: ViewControllerComponentContainer { + private let context: AccountContext + fileprivate let completion: () -> Void + + public init( + context: AccountContext, + auctionContext: GiftAuctionContext, + attributes: [StarGift.UniqueGift.Attribute], + completion: @escaping () -> Void + ) { + self.context = context + self.completion = completion + + super.init( + context: context, + component: GiftAuctionWearPreviewSheetComponent(context: context, auctionContext: auctionContext, attributes: attributes), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: .default + ) + + self.navigationPresentation = .flatModal + self.automaticallyControlPresentationContextLayout = false + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func viewDidLoad() { + super.viewDidLoad() + + self.view.disablesInteractiveModalDismiss = true + } + + public func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { + view.dismissAnimated() + } + } +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradePreviewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradePreviewScreen.swift index 22dfb5ac91..4585ea7dfb 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradePreviewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradePreviewScreen.swift @@ -103,10 +103,9 @@ private final class GiftUpgradePreviewScreenComponent: Component { private let giftCompositionExternalState = GiftCompositionComponent.ExternalState() - private var previewTimer: SwiftSignalKit.Timer? private var isPlaying = true private var showRandomizeTip = false - + private var previewTimer: SwiftSignalKit.Timer? private var previewModelIndex: Int = 0 private var previewBackdropIndex: Int = 0 private var previewSymbolIndex: Int = 0 @@ -272,7 +271,7 @@ private final class GiftUpgradePreviewScreenComponent: Component { self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) } - private func timerTick() { + private func previewTimerTick() { guard !self.previewModels.isEmpty else { return } self.previewModelIndex = (self.previewModelIndex + 1) % self.previewModels.count @@ -299,7 +298,7 @@ private final class GiftUpgradePreviewScreenComponent: Component { guard let self else { return } - self.timerTick() + self.previewTimerTick() }, queue: Queue.mainQueue()) self.previewTimer?.start() } else { @@ -309,15 +308,11 @@ private final class GiftUpgradePreviewScreenComponent: Component { } private var effectiveGifts: [[StarGift.UniqueGift.Attribute]] = [] - private func updateEffectiveGifts() { - guard let component = self.component else { - return - } - + private func updateEffectiveGifts(attributes: [StarGift.UniqueGift.Attribute]) { var effectiveGifts: [[StarGift.UniqueGift.Attribute]] = [] switch self.selectedSection { case .models: - let models = Array(component.attributes.filter({ attribute in + let models = Array(attributes.filter({ attribute in if case .model = attribute { return true } else { @@ -330,7 +325,7 @@ private final class GiftUpgradePreviewScreenComponent: Component { case .backdrops: let selectedModel = self.selectedModel ?? self.previewModels[self.previewModelIndex] let selectedSymbol = self.selectedSymbol ?? self.previewSymbols[self.previewSymbolIndex] - let backdrops = Array(component.attributes.filter({ attribute in + let backdrops = Array(attributes.filter({ attribute in if case .backdrop = attribute { return true } else { @@ -346,7 +341,7 @@ private final class GiftUpgradePreviewScreenComponent: Component { } case .symbols: let selectedBackdrop = self.selectedBackdrop ?? self.previewBackdrops[self.previewBackdropIndex] - let symbols = Array(component.attributes.filter({ attribute in + let symbols = Array(attributes.filter({ attribute in if case .pattern = attribute { return true } else { @@ -568,7 +563,6 @@ private final class GiftUpgradePreviewScreenComponent: Component { let sideInset: CGFloat = rawSideInset + 16.0 if self.component == nil { - var modelCount: Int32 = 0 var backdropCount: Int32 = 0 var symbolCount: Int32 = 0 @@ -615,7 +609,7 @@ private final class GiftUpgradePreviewScreenComponent: Component { }).shuffled().prefix(15)) self.previewSymbols = randomSymbols - self.updateEffectiveGifts() + self.updateEffectiveGifts(attributes: component.attributes) } self.component = component @@ -671,7 +665,7 @@ private final class GiftUpgradePreviewScreenComponent: Component { }), case let .backdrop(_, _, innerColor, outerColor, _, _, _) = backdropAttribute { let topColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultiplied(hue: 1.01, saturation: 1.22, brightness: 1.04) let bottomColor = UIColor(rgb: UInt32(bitPattern: outerColor)).withMultiplied(hue: 0.97, saturation: 1.45, brightness: 0.89) - buttonColor = topColor.mixedWith(bottomColor, alpha: 0.8) + buttonColor = topColor.mixedWith(bottomColor, alpha: 0.8).withMultipliedBrightnessBy(1.2) secondaryTextColor = topColor.withMultiplied(hue: 1.0, saturation: 1.02, brightness: 1.25).mixedWith(UIColor.white, alpha: 0.3) } @@ -803,13 +797,13 @@ private final class GiftUpgradePreviewScreenComponent: Component { ], selectedId: "models", action: { [weak self] id in - guard let self, let id = id.base as? SelectedSection else { + guard let self, let component = self.component, let id = id.base as? SelectedSection else { return } self.selectedSection = id self.isPlaying = false - self.updateEffectiveGifts() + self.updateEffectiveGifts(attributes: component.attributes) self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) //.withUserData(AnimationHint(value: .modeChanged))) })), environment: {}, @@ -953,7 +947,7 @@ private final class GiftUpgradePreviewScreenComponent: Component { transition: .immediate, component: AnyComponent(GlassBarButtonComponent( size: CGSize(width: 40.0, height: 40.0), - backgroundColor: secondaryTextColor, + backgroundColor: buttonColor, isDark: false, state: .tintedGlass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( @@ -1009,7 +1003,7 @@ private final class GiftUpgradePreviewScreenComponent: Component { transition: transition, component: AnyComponent(GlassBarButtonComponent( size: playbackSize, - backgroundColor: secondaryTextColor, + backgroundColor: buttonColor, isDark: false, state: .tintedGlass, component: AnyComponentWithIdentity(id: "content", component: AnyComponent( @@ -1036,7 +1030,7 @@ private final class GiftUpgradePreviewScreenComponent: Component { self.showRandomizeTip = false - self.timerTick() + self.previewTimerTick() } self.state?.updated(transition: .easeInOut(duration: 0.25)) } diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Auction/Upgrade.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Auction/Upgrade.imageset/Contents.json new file mode 100644 index 0000000000..89c9a9e337 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Auction/Upgrade.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "arrow (7).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Auction/Upgrade.imageset/arrow (7).pdf b/submodules/TelegramUI/Images.xcassets/Premium/Auction/Upgrade.imageset/arrow (7).pdf new file mode 100644 index 0000000000000000000000000000000000000000..546ea94f497f958764dfcfd4a35c4bfeee2c723e GIT binary patch literal 4621 zcmai2c|4SD_cvLah_WXP5(+be8K%X)W@$)5#9%OE8O98&ZLKI`) zBYKdKU5xx@w7l>0Jn!f8yZ*T6n(sN+IoEa0eV_Y#Zc(JVrW90K9w-__89*oq0>ar~ zfgsR@3m~wHGXafvMG?>-usYfkV~aM@Ql*F}1FVUm$|%3i6tOzm7H5b4CDB8>IuIN| zGBCLOFT?;OOPMJu0zC+LG|C0&onDcML9!2Xwc@-sqoe=;v!;gI`iV4Ux1}D11UY9P zkE1~yW;_1M@|a=pF%5M(3uOk4lWqi4JGNtnV+{24#ug#Wz;_A?clpkg&^G2GfS7Hb2p8d&P+39DkpRCo#<4p;-|TQ z_;H@gzqO7-fR(1{>Zw2nvV{~>+ayg==NPCn-W`}3AM6Hi+9}n+*o2)$yOaEWV0a_H zkI{wI9y{%DX4{}!gxS!6%+eYJ3T*j+*U?>> zLy+R;LVuC?)*AEH;gk7VXxJuGy%95BMbsoa7@7}NKwD5=dp zitL8vv(~UiDYJK5b&K)P$D>XZALeUHgs^W0(M}@0IZv`aXI#7=LVe~|pw0cs`v1Hu;;}}HeaUEh{ZRXD6P_7g_8y>4!1pC(|=M> zfJ(D^v3UW#0E01n&Hk_Azm0hF)v+o9!Ea_Kbyvhz__qVd9%N>fU~6SJVY=hqVHIJ* zsw`PiZjlhpWT7+1qavi=AZ>)*&z9aBQm)hU)AmynzE9AJe*F0q0CS|^9wC*GAg8*c zZ6cf&XEx(l7FL($mp-P`a5+FN3&||8^g#S>a#@;@R<-&~z3(8cgcq^49V;{Nj;n33 zj*X7`II%XqHs7|AL@P0?Gl3>Q63LGbsk}x8=e{x=dW_6+GeBvh%u03tao~IMnE)+kS_q^aC zCIoY9UP^YTb3)JBv#q%6r0tTUv|I^X&?c5w;GUl5McX&3EjD*FqBSC0&g%MihGA28&&7b@=ABj+FTDFS25(IpeHJF>*1oM0ASTqzSGkWBj%U;?RIiOWjc58Md!O=2 zU-kZs`7E*Oyz1A5w{fu}`efDl)vwn3)F;$k^G@C;`(QQ8rjcug{p%bCAUs7fsCMA_*XyS7?nS9f;z6zAKsr_Re`%Ts z_r1j(t}7xb0@oaiFpl9+uhg8%Zpy~y4G!qe=j{QK8JYOl}dAH+}z+}Tr_1YzGc0cX_=>E-gT?_Xy?>b#_ z!}cAzmtj3&IsjdywD-fgB=1uz{zle4E02d~zRkSl@Vlh<4v`fH(HVMJn2`E#B*{K8 zG3m6v*QJk{ilRWaetTu!A{p}*xfw0|6&uN6Y2J3i`{FioEN3DSejR!}+27;o{D`Gf zX33h_*0j&M&(h8_mUDcSt~|qYrKMQ*atY5XgZB0BtJ7L5w_OpX2~{x60uEt9Kkw@$H9~Y79Q3>KW8+X!NKZ)Dt+A#B zCDVQ6X7ksjs_}Y_O^xs^>n)}&<|X2&Z??wsg%QQTI^aNyIv%JnwD zCqO#@GaOeo5W2&-b&_n6fmB3JnUPEj46BP)_kT7q`vaeW{3Nf`~00r_Hp7-B2f%xz}4Tp5w~;a3h9aK z{>;V|C)%8>Lc`qBNjU{6#NMab%HNAtW@I*T61Qbt|k>N4yHeQSo@)F0^j^Yb4|ZCO}}w?Z=lcV!^!=7>D{HMU^K0N=vjtk77bRN<&~wVkCz+vcU1lgO_`Eng|e;* zN?bVOU%XPabEiFCv!H&R95LAT{)y1#aINYbaA4Q=-kRx>?7J5|vm;##Z(ZJwius*( zZ8ZIE^A$bPkP3HuPDo8ZbBv_cqhO)CWF~#! zH7}lOyBkieFHNy9nobN2(pQFV#RF>MCcamTfP{Q^_d8}4;SAGz)peX{`u^ULn}nRqWn~*k*KoXn@kuTs!nCDSLQ0rEqu{+$M=^rXgJVK_A6L z5&@!J?S3&r%8bf7fii#S=K+2E4a@$4uYY3y4b8j~-KC*4$FO1!imM z%giMjK$1mxc-Dpatz&m-*Ye{eZeC(hPkR=S*A~N}vAfGU97wMBpY7W4-Syh_tjc+6 zIT0|FgB%w!%D(3?IBOsCF_7F4;L_6Gj?1m!&>`-po2_ni27skC+O@*8u_kY@PO(l! z(s{?R0>w4QAx1?@7~zW&J%CN@JYhgdva zx=-(>ix+of?d`ihLSHJecxFl87 z4L=-SD4=>_l{D7_Cgi^Emz^VhY?rWLLRylw=sq`tt2I76st!tJ5VRyYt_+;jm@mW5 znHlp$lHVoIsx7OAVvk7s9%d4KKM`mC0^r-_w2WUZ2BSVIHc#FqDELWFmG9YF@Mm>` zFN}W#z>LFtZAsdc8Ogl1N zZjcm|leZ?yQx03uU;Jjh(I(7#)nl4)J0vBw>yEi)gz@wIV88|OuS@Kk(?L2)&|XC2 zJcCPh*<~k4Mv-UQQYVH`Kp~C~wp8u&k@pPG~sAak2Zn+Vt9MkHIUc2w)Es)2Uc2*TKTz+a6g9id}3cGp@ZLj zCNo;48$ZaDd=3wY=wlmFYGMvq=l1+W7FrY+3-hcHW)JO)H544lBH}XA#=a^iKWD&R z7b>2LskQBX?1+|R9NjO-{48Z4!1g#T)$*EbtVT7V=ye8vzOV1a<_{q!s&BKc<&85~ zA1hx#wu`XKx+L6wZa>-|U|V_9CT}*WU+HfUaBq>SB#=x>5RIB+B7;qpOd$!=O}g-k|Fsh&1G2{AZ#7{|(9h5&pJh zg!aG@@wR9W5Vd4c>j|}^R>Qdx(5@7Z)SC1gM56E%45aW2`mf4Gh5pm2>tgIETLn>1 zKrM;CEfOhfq5s;y2L<^rONhT&GQps|(0C*sZI7n7rf!lN40Vi$n=|UBI?mP@LvTie z2zVlzYU8IP9anqYff^Ezvm@G4z6*$JVjNsh5fu*f2``ETpd8-XxCrTgV1~Y3B`fAgOsSD2qGQH{%*{|B$o3Zwu4 literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index ac1b6fee8b..355eacea91 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -1515,15 +1515,31 @@ func openResolvedUrlImpl( let controller = context.sharedContext.makeGiftAuctionViewScreen( context: context, auctionContext: auctionContext, - completion: { [weak navigationController] acquiredGifts in - let controller = GiftSetupScreen( - context: context, - peerId: context.account.peerId, - subject: .starGift(gift, nil), - auctionAcquiredGifts: acquiredGifts, - completion: nil - ) - navigationController?.pushViewController(controller) + completion: { [weak navigationController] acquiredGifts, upgradeAttributes in + if let upgradeAttributes { + let controller = context.sharedContext.makeGiftAuctionWearPreviewScreen(context: context, auctionContext: auctionContext, acquiredGifts: acquiredGifts, attributes: upgradeAttributes, completion: { + let controller = context.sharedContext.makeGiftAuctionBidScreen( + context: context, + toPeerId: context.account.peerId, + text: "", + entities: [], + hideName: true, + auctionContext: auctionContext, + acquiredGifts: acquiredGifts + ) + navigationController?.pushViewController(controller) + }) + navigationController?.pushViewController(controller) + } else { + let controller = GiftSetupScreen( + context: context, + peerId: context.account.peerId, + subject: .starGift(gift, nil), + auctionAcquiredGifts: acquiredGifts, + completion: nil + ) + navigationController?.pushViewController(controller) + } } ) navigationController?.pushViewController(controller) diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index ccf460e4a0..58e81cfc6b 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -3855,7 +3855,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { return GiftAuctionBidScreen(context: context, toPeerId: toPeerId, text: text, entities: entities, hideName: hideName, auctionContext: auctionContext, acquiredGifts: acquiredGifts) } - public func makeGiftAuctionViewScreen(context: AccountContext, auctionContext: GiftAuctionContext, completion: @escaping (Signal<[GiftAuctionAcquiredGift], NoError>) -> Void) -> ViewController { + public func makeGiftAuctionViewScreen(context: AccountContext, auctionContext: GiftAuctionContext, completion: @escaping (Signal<[GiftAuctionAcquiredGift], NoError>, [StarGift.UniqueGift.Attribute]?) -> Void) -> ViewController { return GiftAuctionViewScreen(context: context, auctionContext: auctionContext, completion: completion) } @@ -3871,6 +3871,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { return GiftUpgradePreviewScreen(context: context, gift: gift, attributes: attributes) } + public func makeGiftAuctionWearPreviewScreen(context: AccountContext, auctionContext: GiftAuctionContext, acquiredGifts: Signal<[GiftAuctionAcquiredGift], NoError>?, attributes: [StarGift.UniqueGift.Attribute], completion: @escaping () -> Void) -> ViewController { + return GiftAuctionWearPreviewScreen(context: context, auctionContext: auctionContext, attributes: attributes, completion: completion) + } + public func makeStorySharingScreen(context: AccountContext, subject: StorySharingSubject, parentController: ViewController) -> ViewController { let editorSubject: Signal switch subject { From 0044bde566534ddeaea822dea6f41b5581c3e0d8 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 2 Dec 2025 23:02:12 +0400 Subject: [PATCH 4/9] Various improvements --- .../Sources/GiftCompositionComponent.swift | 16 +- .../Sources/GiftItemComponent.swift | 30 ++- .../Sources/GiftOptionsScreen.swift | 2 +- .../Sources/GiftAuctionViewScreen.swift | 195 +++++++++--------- .../GiftAuctionWearPreviewScreen.swift | 58 +++++- .../Sources/GiftUpgradePreviewScreen.swift | 5 +- 6 files changed, 199 insertions(+), 107 deletions(-) diff --git a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift index 69437f55de..bad7a1666f 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift @@ -39,6 +39,8 @@ public final class GiftCompositionComponent: Component { let animationOffset: CGPoint? let animationScale: CGFloat? let displayAnimationStars: Bool + let animateScaleOnTransition: Bool + let alwaysAnimateTransition: Bool let revealedAttributes: Set let externalState: ExternalState? let requestUpdate: (ComponentTransition) -> Void @@ -50,6 +52,8 @@ public final class GiftCompositionComponent: Component { animationOffset: CGPoint? = nil, animationScale: CGFloat? = nil, displayAnimationStars: Bool = false, + animateScaleOnTransition: Bool = true, + alwaysAnimateTransition: Bool = false, revealedAttributes: Set = Set(), externalState: ExternalState? = nil, requestUpdate: @escaping (ComponentTransition) -> Void = { _ in } @@ -60,6 +64,8 @@ public final class GiftCompositionComponent: Component { self.animationOffset = animationOffset self.animationScale = animationScale self.displayAnimationStars = displayAnimationStars + self.animateScaleOnTransition = animateScaleOnTransition + self.alwaysAnimateTransition = alwaysAnimateTransition self.revealedAttributes = revealedAttributes self.externalState = externalState self.requestUpdate = requestUpdate @@ -84,6 +90,9 @@ public final class GiftCompositionComponent: Component { if lhs.displayAnimationStars != rhs.displayAnimationStars { return false } + if lhs.animateScaleOnTransition != rhs.animateScaleOnTransition { + return false + } if lhs.revealedAttributes != rhs.revealedAttributes { return false } @@ -767,7 +776,9 @@ public final class GiftCompositionComponent: Component { } var animateTransition = false - if self.animatePreviewTransition { + if component.alwaysAnimateTransition { + animateTransition = true + } else if self.animatePreviewTransition { animateTransition = true self.animatePreviewTransition = false } else if let previousComponent, case .preview = previousComponent.subject, case .unique = component.subject { @@ -788,6 +799,9 @@ public final class GiftCompositionComponent: Component { bounce = self.previewPatternIndex == -1 background = false } + if !component.animateScaleOnTransition { + bounce = false + } backgroundView.animateTransition(background: background, bounce: bounce) } if animateBackdropSwipe { diff --git a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift index db66ff52cd..301648cda5 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift @@ -168,6 +168,7 @@ public final class GiftItemComponent: Component { let isEditing: Bool let isDateLocked: Bool let isPlaceholder: Bool + let animateChanges: Bool let mode: Mode let action: (() -> Void)? let contextAction: ((UIView, ContextGesture) -> Void)? @@ -193,6 +194,7 @@ public final class GiftItemComponent: Component { isEditing: Bool = false, isDateLocked: Bool = false, isPlaceholder: Bool = false, + animateChanges: Bool = false, mode: Mode = .generic, action: (() -> Void)? = nil, contextAction: ((UIView, ContextGesture) -> Void)? = nil @@ -217,6 +219,7 @@ public final class GiftItemComponent: Component { self.isEditing = isEditing self.isDateLocked = isDateLocked self.isPlaceholder = isPlaceholder + self.animateChanges = animateChanges self.mode = mode self.action = action self.contextAction = contextAction @@ -283,6 +286,9 @@ public final class GiftItemComponent: Component { if lhs.isPlaceholder != rhs.isPlaceholder { return false } + if lhs.animateChanges != rhs.animateChanges { + return false + } if lhs.mode != rhs.mode { return false } @@ -642,12 +648,22 @@ public final class GiftItemComponent: Component { } var animationTransition = transition + var animateBackgroundChange = false if self.animationLayer == nil || self.animationFile?.fileId != animationFile?.fileId, let emoji { animationTransition = .immediate self.animationFile = animationFile + var animateAppearance = false if let animationLayer = self.animationLayer { self.animationLayer = nil - animationLayer.removeFromSuperlayer() + if component.animateChanges { + animateAppearance = true + animateBackgroundChange = true + animationLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + animationLayer.removeFromSuperlayer() + }) + } else { + animationLayer.removeFromSuperlayer() + } } let animationLayer = InlineStickerItemLayer( context: .account(component.context), @@ -670,6 +686,9 @@ public final class GiftItemComponent: Component { } else { self.layer.insertSublayer(animationLayer, above: self.backgroundLayer) } + if animateAppearance { + animationLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } } let animationFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - iconSize.width) / 2.0), y: component.mode == .generic ? animationOffset : (floorToScreenPixels((size.height - iconSize.height) / 2.0) + explicitAnimationOffset)), size: iconSize) @@ -678,6 +697,11 @@ public final class GiftItemComponent: Component { } if let backgroundColor { + if let backgroundView = self.patternView.view as? PeerInfoCoverComponent.View { + if animateBackgroundChange { + backgroundView.animateTransition(background: true, bounce: false) + } + } let _ = self.patternView.update( transition: .immediate, component: AnyComponent(PeerInfoCoverComponent( @@ -694,7 +718,7 @@ public final class GiftItemComponent: Component { environment: {}, containerSize: backgroundSize ) - if let backgroundView = self.patternView.view { + if let backgroundView = self.patternView.view as? PeerInfoCoverComponent.View { if backgroundView.superview == nil { backgroundView.layer.cornerRadius = cornerRadius if #available(iOS 13.0, *) { @@ -1620,7 +1644,7 @@ public final class StarsButtonContentComponent: Component { } } - self.backgroundLayer.backgroundColor = backgroundColor.cgColor + transition.setBackgroundColor(layer: self.backgroundLayer, color: backgroundColor) transition.setFrame(layer: self.backgroundLayer, frame: CGRect(origin: .zero, size: size)) self.backgroundLayer.cornerRadius = size.height / 2.0 diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift index 97c8323cc4..7176042275 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift @@ -692,7 +692,7 @@ final class GiftOptionsScreenComponent: Component { action = environment.strings.Gift_Options_Gift_ViewAuction } subject = .starGift(gift: gift, price: action) - } else if let availability = gift.availability, availability.remains == 0, let minResaleStars = availability.minResaleStars { + } else if let availability = gift.availability, availability.remains == 0, availability.resale > 0, let minResaleStars = availability.minResaleStars { let priceString = presentationStringsFormattedNumber(Int32(minResaleStars), environment.dateTimeFormat.groupingSeparator) if let resaleConfiguration = self.resaleConfiguration, minResaleStars == resaleConfiguration.starGiftResaleMaxStarsAmount || availability.resale == 1 { subject = .starGift(gift: gift, price: "# \(priceString)") diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift index 44cba549ca..7322b9b51b 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift @@ -535,7 +535,7 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { }), case let .backdrop(_, _, innerColor, outerColor, _, _, _) = backdropAttribute { let topColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultiplied(hue: 1.01, saturation: 1.22, brightness: 1.04) let bottomColor = UIColor(rgb: UInt32(bitPattern: outerColor)).withMultiplied(hue: 0.97, saturation: 1.45, brightness: 0.89) - buttonColor = topColor.mixedWith(bottomColor, alpha: 0.8) + buttonColor = topColor.mixedWith(bottomColor, alpha: 0.8).withMultipliedBrightnessBy(1.25) secondaryTextColor = topColor.withMultiplied(hue: 1.0, saturation: 1.02, brightness: 1.25).mixedWith(UIColor.white, alpha: 0.5) } @@ -677,11 +677,8 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { ) )) } else { -// var auctionGiftsPerRound: Int32 = 50 -// if let auctionGiftsPerRoundValue = gift.auctionGiftsPerRound { -// auctionGiftsPerRound = auctionGiftsPerRoundValue -// } - descriptionText = isUpcoming ? "Upcoming Auction" : "Gift Auction" //strings.Gift_Auction_Description("\(auctionGiftsPerRound)", gift.title ?? "").string + //TODO:localize + descriptionText = isUpcoming ? "Upcoming Auction" : "Gift Auction" tableItems.append(.init( id: "start", @@ -787,7 +784,7 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { height: 24.0 ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 50.0, height: CGFloat.greatestFiniteMagnitude), - transition: .immediate + transition: context.transition ) context.add(description .position(CGPoint(x: context.availableSize.width / 2.0, y: 167.0 + description.size.height / 2.0)) @@ -852,92 +849,6 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { var hasAdditionalButtons = false - if case let .generic(gift) = component.auctionContext.gift, let upgradeVariantsCount = gift.upgradeVariantsCount { - originY += 5.0 - - - var variant1: GiftItemComponent.Subject = .starGift(gift: gift, price: "") - var variant2: GiftItemComponent.Subject = .starGift(gift: gift, price: "") - var variant3: GiftItemComponent.Subject = .starGift(gift: gift, price: "") - if !state.previewModels.isEmpty { - if state.previewModels.count > 0 { - variant1 = .preview(attributes: [state.previewModels[0]], rarity: 0) - } - if state.previewModels.count > 1 { - variant2 = .preview(attributes: [state.previewModels[1]], rarity: 0) - } - if state.previewModels.count > 2 { - variant3 = .preview(attributes: [state.previewModels[2]], rarity: 0) - } - } - - let variantsButton = variantsButton.update( - component: PlainButtonComponent(content: AnyComponent( - HStack([ - AnyComponentWithIdentity(id: "view", component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Auction_ViewVariants, font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor))) - )), - AnyComponentWithIdentity(id: "spacing", component: AnyComponent( - Rectangle(color: .clear, width: 8.0, height: 1.0) - )), - AnyComponentWithIdentity(id: "icon1", component: AnyComponent( - GiftItemComponent( - context: component.context, - theme: theme, - strings: strings, - peer: nil, - subject: variant1, - isPlaceholder: state.previewModels.isEmpty, - mode: .buttonIcon - ) - )), - AnyComponentWithIdentity(id: "icon2", component: AnyComponent( - GiftItemComponent( - context: component.context, - theme: theme, - strings: strings, - peer: nil, - subject: variant2, - isPlaceholder: state.previewModels.isEmpty, - mode: .buttonIcon - ) - )), - AnyComponentWithIdentity(id: "icon3", component: AnyComponent( - GiftItemComponent( - context: component.context, - theme: theme, - strings: strings, - peer: nil, - subject: variant3, - isPlaceholder: state.previewModels.isEmpty, - mode: .buttonIcon - ) - )), - AnyComponentWithIdentity(id: "text", component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: " \(strings.Gift_Auction_Variants(upgradeVariantsCount))", font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor))) - )), - AnyComponentWithIdentity(id: "arrow", component: AnyComponent( - BundleIconComponent(name: "Chat/Context Menu/Arrow", tintColor: theme.actionSheet.controlAccentColor) - )) - ], spacing: 0.0) - ), action: { [weak state] in - guard let state, let attributes = state.giftUpgradeAttributes else { - return - } - let variantsController = component.context.sharedContext.makeGiftUpgradeVariantsPreviewScreen(context: component.context, gift: .generic(gift), attributes: attributes) - environment.controller()?.push(variantsController) - }, animateScale: false), - availableSize: CGSize(width: context.availableSize.width - 64.0, height: context.availableSize.height), - transition: context.transition - ) - context.add(variantsButton - .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + variantsButton.size.height / 2.0))) - originY += variantsButton.size.height - originY += 12.0 - - hasAdditionalButtons = true - } - let acquiredGiftsCount = state.giftAuctionState?.myState.acquiredCount ?? 0 if acquiredGiftsCount > 0, case let .generic(gift) = component.auctionContext.gift { originY += 5.0 @@ -1078,6 +989,96 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { } } + if case let .generic(gift) = component.auctionContext.gift, let upgradeVariantsCount = gift.upgradeVariantsCount { + originY += 5.0 + + if !hasAdditionalButtons { + originY -= 13.0 + } + + var variant1: GiftItemComponent.Subject = .starGift(gift: gift, price: "") + var variant2: GiftItemComponent.Subject = .starGift(gift: gift, price: "") + var variant3: GiftItemComponent.Subject = .starGift(gift: gift, price: "") + if !state.previewModels.isEmpty { + if state.previewModels.count > 0 { + variant1 = .preview(attributes: [state.previewModels[0]], rarity: 0) + } + if state.previewModels.count > 1 { + variant2 = .preview(attributes: [state.previewModels[1]], rarity: 0) + } + if state.previewModels.count > 2 { + variant3 = .preview(attributes: [state.previewModels[2]], rarity: 0) + } + } + + let variantsButton = variantsButton.update( + component: PlainButtonComponent(content: AnyComponent( + HStack([ + AnyComponentWithIdentity(id: "view", component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Auction_ViewVariants, font: Font.regular(13.0), textColor: theme.actionSheet.controlAccentColor))) + )), + AnyComponentWithIdentity(id: "spacing", component: AnyComponent( + Rectangle(color: .clear, width: 7.0, height: 1.0) + )), + AnyComponentWithIdentity(id: "icon1", component: AnyComponent( + GiftItemComponent( + context: component.context, + theme: theme, + strings: strings, + peer: nil, + subject: variant1, + isPlaceholder: state.previewModels.isEmpty, + mode: .tableIcon + ) + )), + AnyComponentWithIdentity(id: "icon2", component: AnyComponent( + GiftItemComponent( + context: component.context, + theme: theme, + strings: strings, + peer: nil, + subject: variant2, + isPlaceholder: state.previewModels.isEmpty, + mode: .tableIcon + ) + )), + AnyComponentWithIdentity(id: "icon3", component: AnyComponent( + GiftItemComponent( + context: component.context, + theme: theme, + strings: strings, + peer: nil, + subject: variant3, + isPlaceholder: state.previewModels.isEmpty, + mode: .tableIcon + ) + )), + AnyComponentWithIdentity(id: "text", component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: " \(strings.Gift_Auction_Variants(upgradeVariantsCount))", font: Font.regular(13.0), textColor: theme.actionSheet.controlAccentColor))) + )), + AnyComponentWithIdentity(id: "arrow", component: AnyComponent( + BundleIconComponent(name: "Item List/InlineTextRightArrow", tintColor: theme.actionSheet.controlAccentColor) + )) + ], spacing: 0.0) + ), action: { [weak state] in + guard let state, let attributes = state.giftUpgradeAttributes else { + return + } + let variantsController = component.context.sharedContext.makeGiftUpgradeVariantsPreviewScreen(context: component.context, gift: .generic(gift), attributes: attributes) + environment.controller()?.push(variantsController) + }, animateScale: false), + availableSize: CGSize(width: context.availableSize.width - 64.0, height: context.availableSize.height), + transition: context.transition + ) + context.add(variantsButton + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + variantsButton.size.height / 2.0))) + originY += variantsButton.size.height + originY += 12.0 + originY -= 15.0 + + hasAdditionalButtons = true + } + if hasAdditionalButtons { originY += 21.0 } @@ -1200,7 +1201,7 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { let closeButton = closeButton.update( component: GlassBarButtonComponent( size: CGSize(width: 40.0, height: 40.0), - backgroundColor: buttonColor.withMultipliedBrightnessBy(1.2), + backgroundColor: buttonColor, isDark: theme.overallDarkAppearance, state: .tintedGlass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( @@ -1217,7 +1218,7 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { } ), availableSize: CGSize(width: 40.0, height: 40.0), - transition: .immediate + transition: context.transition ) context.add(closeButton .position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0)) @@ -1226,7 +1227,7 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { let moreButton = moreButton.update( component: GlassBarButtonComponent( size: CGSize(width: 40.0, height: 40.0), - backgroundColor: buttonColor.withMultipliedBrightnessBy(1.2), + backgroundColor: buttonColor, isDark: theme.overallDarkAppearance, state: .tintedGlass, component: AnyComponentWithIdentity(id: "more", component: AnyComponent( @@ -1248,7 +1249,7 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { } ), availableSize: CGSize(width: 40.0, height: 40.0), - transition: .immediate + transition: context.transition ) context.add(moreButton .position(CGPoint(x: context.availableSize.width - 16.0 - moreButton.size.width / 2.0, y: 16.0 + moreButton.size.height / 2.0)) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionWearPreviewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionWearPreviewScreen.swift index c4ec1b9b62..ed781e48f4 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionWearPreviewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionWearPreviewScreen.swift @@ -169,7 +169,7 @@ private final class GiftAuctionWearPreviewSheetContent: CombinedComponent { let arrow = Child(BundleIconComponent.self) let upgradeLabel = Child(MultilineTextComponent.self) let remainingCount = Child(GiftRemainingCountComponent.self) - //let auctionFooter = Child(MultilineTextComponent.self) + let auctionFooter = Child(MultilineTextComponent.self) let button = Child(ButtonComponent.self) @@ -231,6 +231,7 @@ private final class GiftAuctionWearPreviewSheetContent: CombinedComponent { animationOffset: animationOffset, animationScale: animationScale, displayAnimationStars: true, + animateScaleOnTransition: false, externalState: giftCompositionExternalState, requestUpdate: { [weak state] transition in state?.updated(transition: transition) @@ -370,6 +371,7 @@ private final class GiftAuctionWearPreviewSheetContent: CombinedComponent { strings: strings, subject: .preview(attributes: attributes, rarity: 0), ribbon: GiftItemComponent.Ribbon(text: "upgraded", color: ribbonColor), + animateChanges: true, mode: .thumbnail ), availableSize: CGSize(width: 120.0, height: 120.0), @@ -440,8 +442,57 @@ private final class GiftAuctionWearPreviewSheetContent: CombinedComponent { context.add(remainingCount .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight)) ) + + if let giftsPerRound = gift.auctionGiftsPerRound { + let footerAttributes = MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.list.freeTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.list.freeTextColor), + link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.list.itemAccentColor), + linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + } + ) + let parsedString = parseMarkdownIntoAttributedString(strings.Gift_Setup_AuctionInfo(environment.strings.Gift_Setup_AuctionInfo_Gifts(giftsPerRound), strings.Gift_Setup_AuctionInfo_Bidders(giftsPerRound)).string, attributes: footerAttributes) + let auctionFooterText = NSMutableAttributedString(attributedString: parsedString) + + if state.cachedSmallChevronImage == nil || state.cachedSmallChevronImage?.1 !== environment.theme { + state.cachedSmallChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: theme.list.itemAccentColor)!, environment.theme) + } + if let range = auctionFooterText.string.range(of: ">"), let chevronImage = state.cachedSmallChevronImage?.0 { + auctionFooterText.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: auctionFooterText.string)) + } + + let auctionFooter = auctionFooter.update( + component: MultilineTextComponent( + text: .plain(auctionFooterText), + maximumNumberOfLines: 0, + highlightColor: theme.list.itemAccentColor.withAlphaComponent(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { _, _ in + guard let controller = controller() else { + return + } + let infoController = component.context.sharedContext.makeGiftAuctionInfoScreen(context: component.context, auctionContext: component.auctionContext, completion: nil) + controller.push(infoController) + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 16.0 * 2.0, height: 10000.0), + transition: context.transition + ) + context.add(auctionFooter + .position(CGPoint(x: sideInset + 16.0 + auctionFooter.size.width * 0.5, y: contentHeight + 52.0 + auctionFooter.size.height * 0.5)) + ) + contentHeight += auctionFooter.size.height + } } - contentHeight += 110.0 + contentHeight += 80.0 var buttonTitle = strings.Gift_Auction_Join if component.auctionContext.isUpcoming { @@ -521,6 +572,7 @@ final class GiftAuctionWearPreviewSheetComponent: CombinedComponent { return { context in let environment = context.environment[EnvironmentType.self] let controller = environment.controller + let theme = environment.theme.withModalBlocksBackground() let sheet = sheet.update( component: SheetComponent( @@ -532,7 +584,7 @@ final class GiftAuctionWearPreviewSheetComponent: CombinedComponent { getController: controller )), style: .glass, - backgroundColor: .color(environment.theme.list.blocksBackgroundColor), + backgroundColor: .color(theme.list.blocksBackgroundColor), followContentSizeChanges: true, clipsContent: true, hasDimView: false, diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradePreviewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradePreviewScreen.swift index 4585ea7dfb..d66281bfea 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradePreviewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradePreviewScreen.swift @@ -665,7 +665,7 @@ private final class GiftUpgradePreviewScreenComponent: Component { }), case let .backdrop(_, _, innerColor, outerColor, _, _, _) = backdropAttribute { let topColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultiplied(hue: 1.01, saturation: 1.22, brightness: 1.04) let bottomColor = UIColor(rgb: UInt32(bitPattern: outerColor)).withMultiplied(hue: 0.97, saturation: 1.45, brightness: 0.89) - buttonColor = topColor.mixedWith(bottomColor, alpha: 0.8).withMultipliedBrightnessBy(1.2) + buttonColor = topColor.mixedWith(bottomColor, alpha: 0.8).withMultipliedBrightnessBy(1.25) secondaryTextColor = topColor.withMultiplied(hue: 1.0, saturation: 1.02, brightness: 1.25).mixedWith(UIColor.white, alpha: 0.3) } @@ -680,6 +680,7 @@ private final class GiftUpgradePreviewScreenComponent: Component { animationOffset: CGPoint(x: 0.0, y: 20.0), animationScale: nil, displayAnimationStars: false, + alwaysAnimateTransition: true, revealedAttributes: Set(), externalState: self.giftCompositionExternalState, requestUpdate: { [weak state] transition in @@ -944,7 +945,7 @@ private final class GiftUpgradePreviewScreenComponent: Component { self.glassContainerView.frame = CGRect(origin: CGPoint(x: rawSideInset, y: 0.0), size: CGSize(width: fillingSize, height: 64.0)) let closeButtonSize = self.closeButton.update( - transition: .immediate, + transition: transition, component: AnyComponent(GlassBarButtonComponent( size: CGSize(width: 40.0, height: 40.0), backgroundColor: buttonColor, From 6a09d2e4e61bf6b219dc1cc691523a0a5737823a Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 3 Dec 2025 17:59:09 +0400 Subject: [PATCH 5/9] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 14 +- .../Sources/AccountContext.swift | 35 +- .../Sources/PhoneDemoComponent.swift | 16 +- .../PremiumUI/Sources/PremiumDemoScreen.swift | 10 +- .../Sources/GiftAuctionAccessoryPanel.swift | 22 +- submodules/TelegramUI/BUILD | 1 + .../Components/Gifts/GiftDemoScreen/BUILD | 42 ++ .../Sources/GiftDemoScreen.swift | 584 ++++++++++++++++++ .../Components/Gifts/GiftViewScreen/BUILD | 1 + .../Sources/GiftAuctionActiveBidsScreen.swift | 6 +- .../Sources/GiftAuctionBidScreen.swift | 2 +- .../Sources/GiftAuctionViewScreen.swift | 8 +- .../GiftAuctionWearPreviewScreen.swift | 128 +++- .../Sources/GiftViewScreen.swift | 191 +----- .../Gifts/InfoParagraphComponent/BUILD | 28 + .../Sources/InfoParagraphComponent.swift | 187 ++++++ .../Sources/SharedAccountContext.swift | 5 + 17 files changed, 1014 insertions(+), 266 deletions(-) create mode 100644 submodules/TelegramUI/Components/Gifts/GiftDemoScreen/BUILD create mode 100644 submodules/TelegramUI/Components/Gifts/GiftDemoScreen/Sources/GiftDemoScreen.swift create mode 100644 submodules/TelegramUI/Components/Gifts/InfoParagraphComponent/BUILD create mode 100644 submodules/TelegramUI/Components/Gifts/InfoParagraphComponent/Sources/InfoParagraphComponent.swift diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 46619ca1d4..1ca35b318e 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -15507,7 +15507,9 @@ Error: %8$@"; "ChatList.Auctions.UpcomingAuction" = "Upcoming Auction"; -"Gift.AuctionBid.UpcomingBid" = "You placed an early bid"; +"Gift.AuctionBid.UpcomingBid" = "You've placed an early bid"; +"ChatList.Auctions.Status.UpcomingBid" = "You've placed an early bid."; +"Gift.ActiveAuctions.UpcomingBid" = "You've placed an early bid"; "Login.PhoneWithPasskeySubtitle" = "Enter your phone number\nor [log in using Passkey >](passkey)"; "PrivacySettings.Passkey" = "Passkey"; @@ -15536,3 +15538,13 @@ Error: %8$@"; "Passkeys.PasskeyCreatedAndUsedPattern" = "created %1$@ • used %2$@"; "Passkeys.AddPasskey" = "Create Passkey"; "Passkeys.ListFooter" = "Your passkeys are stored securely in your password manager."; + +"Gifts.Demo.Title" = "Unique Collectibles"; +"Gifts.Demo.Description" = "Telegram Gifts are collectible items you can trade or showcase on your profile."; +"Gifts.Demo.Unique.Title" = "Unique"; +"Gifts.Demo.Unique.Text" = "Upgrade your gifts to get a unique number, model, backdrop and symbol."; +"Gifts.Demo.Tradable.Title" = "Tradable"; +"Gifts.Demo.Tradable.Text" = "Sell your gift on Telegram or on third-party NFT marketplaces."; +"Gifts.Demo.Wearable.Title" = "Wearable"; +"Gifts.Demo.Wearable.Text" = "Display gifts on your page and set them as profile covers or statuses."; +"Gifts.Demo.Understood" = "Understood"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index e3516ab903..0081e2c6f0 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1273,6 +1273,7 @@ public protocol SharedAccountContext: AnyObject { func navigateToChat(accountId: AccountRecordId, peerId: PeerId, messageId: MessageId?) func openChatMessage(_ params: OpenChatMessageParams) -> Bool func messageFromPreloadedChatHistoryViewForLocation(id: MessageId, location: ChatHistoryLocationInput, context: AccountContext, chatLocation: ChatLocation, subject: ChatControllerSubject?, chatLocationContextHolder: Atomic, tag: HistoryViewInputTag?) -> Signal<(MessageIndex?, Bool), NoError> + func makeOverlayAudioPlayerController(context: AccountContext, chatLocation: ChatLocation, type: MediaManagerPlayerType, initialMessageId: MessageId, initialOrder: MusicPlaybackSettingsOrder, playlistLocation: SharedMediaPlaylistLocation?, parentNavigationController: NavigationController?) -> ViewController & OverlayAudioPlayerController func makePeerInfoController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, fromChat: Bool, requestsContext: PeerInvitationImportersContext?) -> ViewController? func makeChannelAdminController(context: AccountContext, peerId: PeerId, adminId: PeerId, initialParticipant: ChannelParticipant) -> ViewController? @@ -1356,54 +1357,34 @@ public protocol SharedAccountContext: AnyObject { func makeInstantPageController(context: AccountContext, message: Message, sourcePeerType: MediaAutoDownloadPeerType?) -> ViewController? func makeInstantPageController(context: AccountContext, webPage: TelegramMediaWebpage, anchor: String?, sourceLocation: InstantPageSourceLocation) -> ViewController func openChatWallpaper(context: AccountContext, message: Message, present: @escaping (ViewController, Any?) -> Void) - func makeRecentSessionsController(context: AccountContext, activeSessionsContext: ActiveSessionsContext) -> ViewController & RecentSessionsController - func makeChatQrCodeScreen(context: AccountContext, peer: Peer, threadId: Int64?, temporary: Bool) -> ViewController - func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool, dismissed: (() -> Void)?) -> ViewController func makePremiumIntroController(sharedContext: SharedAccountContext, engine: TelegramEngineUnauthorized, inAppPurchaseManager: InAppPurchaseManager, source: PremiumIntroSource, proceed: (() -> Void)?) -> ViewController - func makePremiumDemoController(context: AccountContext, subject: PremiumDemoSubject, forceDark: Bool, action: @escaping () -> Void, dismissed: (() -> Void)?) -> ViewController func makePremiumLimitController(context: AccountContext, subject: PremiumLimitSubject, count: Int32, forceDark: Bool, cancel: @escaping () -> Void, action: @escaping () -> Bool) -> ViewController - func makeStarsGiftController(context: AccountContext, birthdays: [EnginePeer.Id: TelegramBirthday]?, completion: @escaping (([EnginePeer.Id]) -> Void)) -> ViewController func makePremiumGiftController(context: AccountContext, source: PremiumGiftSource, completion: (([EnginePeer.Id]) -> Signal)?) -> ViewController func makeGiftOptionsController(context: AccountContext, peerId: EnginePeer.Id, premiumOptions: [CachedPremiumGiftOption], hasBirthday: Bool, completion: (() -> Void)?) -> ViewController func makeGiftStoreController(context: AccountContext, peerId: EnginePeer.Id, gift: StarGift.Gift) -> ViewController func makePremiumPrivacyControllerController(context: AccountContext, subject: PremiumPrivacySubject, peerId: EnginePeer.Id) -> ViewController func makePremiumBoostLevelsController(context: AccountContext, peerId: EnginePeer.Id, subject: BoostSubject, boostStatus: ChannelBoostStatus, myBoostStatus: MyBoostStatus, forceDark: Bool, openStats: (() -> Void)?) -> ViewController - func makeStickerPackScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], loadedStickerPacks: [LoadedStickerPack], actionTitle: String?, isEditing: Bool, expandIfNeeded: Bool, parentNavigationController: NavigationController?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?, actionPerformed: ((Bool) -> Void)?) -> ViewController - func makeCameraScreen(context: AccountContext, mode: CameraScreenMode, cameraHolder: Any?, transitionIn: CameraScreenTransitionIn?, transitionOut: @escaping (Bool) -> CameraScreenTransitionOut?, completion: @escaping (Any, @escaping () -> Void) -> Void, transitionedOut: (() -> Void)?) -> ViewController - func makeMediaPickerScreen(context: AccountContext, hasSearch: Bool, completion: @escaping (Any) -> Void) -> ViewController - func makeStoryMediaEditorScreen(context: AccountContext, source: Any?, text: String?, link: (url: String, name: String?)?, remainingCount: Int32, completion: @escaping ([MediaEditorScreenResult], MediaEditorTransitionOutExternalState, @escaping (@escaping () -> Void) -> Void) -> Void) -> ViewController - func makeBotPreviewEditorScreen(context: AccountContext, source: Any?, target: Stories.PendingTarget, transitionArguments: (UIView, CGRect, UIImage?)?, transitionOut: @escaping () -> BotPreviewEditorTransitionOut?, externalState: MediaEditorTransitionOutExternalState, completion: @escaping (MediaEditorScreenResult, @escaping (@escaping () -> Void) -> Void) -> Void, cancelled: @escaping () -> Void) -> ViewController - func makeStickerEditorScreen(context: AccountContext, source: Any?, intro: Bool, transitionArguments: (UIView, CGRect, UIImage?)?, completion: @escaping (TelegramMediaFile, [String], @escaping () -> Void) -> Void, cancelled: @escaping () -> Void) -> ViewController - func makeStickerMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect?, completion: @escaping (Any?, UIView?, CGRect, UIImage?, Bool, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController - func makeAvatarMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect?, canDelete: Bool, performDelete: @escaping () -> Void, completion: @escaping (Any?, UIView?, CGRect, UIImage?, Bool, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController - func makeStoryMediaPickerScreen(context: AccountContext, isDark: Bool, forCollage: Bool, selectionLimit: Int?, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, multipleCompletion: @escaping ([Any], Bool) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController - func makeStickerPickerScreen(context: AccountContext, inputData: Promise, completion: @escaping (FileMediaReference) -> Void) -> ViewController - func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController - func makeDataAndStorageController(context: AccountContext, sensitiveContent: Bool) -> ViewController - func makeInstalledStickerPacksController(context: AccountContext, mode: InstalledStickerPacksControllerMode, forceTheme: PresentationTheme?) -> ViewController - func makeChannelStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: EnginePeer.Id, boosts: Bool, boostStatus: ChannelBoostStatus?) -> ViewController func makeMessagesStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, messageId: EngineMessage.Id) -> ViewController func makeStoryStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: EnginePeer.Id, storyId: Int32, storyItem: EngineStoryItem, fromStory: Bool) -> ViewController - func makeStarsTransactionsScreen(context: AccountContext, starsContext: StarsContext) -> ViewController func makeStarsPurchaseScreen(context: AccountContext, starsContext: StarsContext, options: [Any], purpose: StarsPurchasePurpose, targetPeerId: EnginePeer.Id?, customTheme: PresentationTheme?, completion: @escaping (Int64) -> Void) -> ViewController func makeStarsTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, extendedMedia: [TelegramExtendedMedia], inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?, EnginePeer?)?, NoError>, completion: @escaping (Bool) -> Void) -> ViewController @@ -1431,33 +1412,23 @@ public protocol SharedAccountContext: AnyObject { func makeGiftOfferScreen(context: AccountContext, gift: StarGift.UniqueGift, peer: EnginePeer, amount: CurrencyAmount, commit: @escaping () -> Void) -> ViewController func makeGiftUpgradeVariantsPreviewScreen(context: AccountContext, gift: StarGift, attributes: [StarGift.UniqueGift.Attribute]) -> ViewController func makeGiftAuctionWearPreviewScreen(context: AccountContext, auctionContext: GiftAuctionContext, acquiredGifts: Signal<[GiftAuctionAcquiredGift], NoError>?, attributes: [StarGift.UniqueGift.Attribute], completion: @escaping () -> Void) -> ViewController - + func makeGiftDemoScreen(context: AccountContext) -> ViewController func makeStorySharingScreen(context: AccountContext, subject: StorySharingSubject, parentController: ViewController) -> ViewController - func makeContentReportScreen(context: AccountContext, subject: ReportContentSubject, forceDark: Bool, present: @escaping (ViewController) -> Void, completion: @escaping () -> Void, requestSelectMessages: ((String, Data, String?) -> Void)?) - func makeShareController(context: AccountContext, subject: ShareControllerSubject, forceExternal: Bool, shareStory: (() -> Void)?, enqueued: (([PeerId], [Int64]) -> Void)?, actionCompleted: (() -> Void)?) -> ViewController - func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal func makeMiniAppListScreen(context: AccountContext, initialData: MiniAppListScreenInitialData) -> ViewController - func makeIncomingMessagePrivacyScreen(context: AccountContext, value: GlobalPrivacySettings.NonContactChatsPrivacy, exceptions: SelectivePrivacySettings, update: @escaping (GlobalPrivacySettings.NonContactChatsPrivacy) -> Void) -> ViewController - func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal)?, botPeer: EnginePeer, chatPeer: EnginePeer?, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?, verifyAgeCompletion: ((Int) -> Void)?) - func makeAffiliateProgramSetupScreenInitialData(context: AccountContext, peerId: EnginePeer.Id, mode: AffiliateProgramSetupScreenMode) -> Signal func makeAffiliateProgramSetupScreen(context: AccountContext, initialData: AffiliateProgramSetupScreenInitialData) -> ViewController func makeAffiliateProgramJoinScreen(context: AccountContext, sourcePeer: EnginePeer, commissionPermille: Int32, programDuration: Int32?, revenuePerUser: Double, mode: JoinAffiliateProgramScreenMode) -> ViewController - func makeJoinSubjectScreen(context: AccountContext, mode: JoinSubjectScreenMode) -> ViewController - func makeOldChannelsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, intent: OldChannelsControllerIntent, completed: @escaping (Bool) -> Void) -> ViewController - func makeGalleryController(context: AccountContext, source: GalleryControllerItemSource, streamSingleVideo: Bool, isPreview: Bool) -> ViewController - func makeAccountFreezeInfoScreen(context: AccountContext) -> ViewController func makeSendInviteLinkScreen(context: AccountContext, subject: SendInviteLinkScreenSubject, peers: [TelegramForbiddenInvitePeer], theme: PresentationTheme?) -> ViewController - + @available(iOS 13.0, *) func makePostSuggestionsSettingsScreen(context: AccountContext, peerId: EnginePeer.Id) async -> ViewController diff --git a/submodules/PremiumUI/Sources/PhoneDemoComponent.swift b/submodules/PremiumUI/Sources/PhoneDemoComponent.swift index 7ad7acf566..9fa9fc9528 100644 --- a/submodules/PremiumUI/Sources/PhoneDemoComponent.swift +++ b/submodules/PremiumUI/Sources/PhoneDemoComponent.swift @@ -354,15 +354,15 @@ protocol PhoneDemoDecorationView: UIView { func resetAnimation() } -final class PhoneDemoComponent: Component { - typealias EnvironmentType = DemoPageEnvironment +public final class PhoneDemoComponent: Component { + public typealias EnvironmentType = DemoPageEnvironment - enum Position { + public enum Position { case top case bottom } - enum BackgroundDecoration { + public enum BackgroundDecoration { case none case dataRain case swirlStars @@ -375,7 +375,7 @@ final class PhoneDemoComponent: Component { case todo } - enum Model { + public enum Model { case notch case island } @@ -419,11 +419,11 @@ final class PhoneDemoComponent: Component { return true } - final class View: UIView, ComponentTaggedView { - final class Tag { + public final class View: UIView, ComponentTaggedView { + public final class Tag { } - func matches(tag: Any) -> Bool { + public func matches(tag: Any) -> Bool { if let _ = tag as? Tag, self.isCentral { return true } diff --git a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift index 9ff8e1b6b8..e531a8b126 100644 --- a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift @@ -155,7 +155,7 @@ public final class PremiumGradientBackgroundComponent: Component { } } -final class DemoPageEnvironment: Equatable { +public final class DemoPageEnvironment: Equatable { public let isDisplaying: Bool public let isCentral: Bool public let position: CGFloat @@ -1134,13 +1134,13 @@ private final class DemoSheetContent: CombinedComponent { let closeButton = closeButton.update( component: GlassBarButtonComponent( size: CGSize(width: 40.0, height: 40.0), - backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor, - isDark: theme.overallDarkAppearance, - state: .glass, + backgroundColor: UIColor(rgb: 0x7f76f4), + isDark: false, + state: .tintedGlass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", - tintColor: theme.rootController.navigationBar.glassBarButtonForegroundColor + tintColor: .white ) )), action: { _ in diff --git a/submodules/TelegramBaseController/Sources/GiftAuctionAccessoryPanel.swift b/submodules/TelegramBaseController/Sources/GiftAuctionAccessoryPanel.swift index 4be03f3cb7..c030d22514 100644 --- a/submodules/TelegramBaseController/Sources/GiftAuctionAccessoryPanel.swift +++ b/submodules/TelegramBaseController/Sources/GiftAuctionAccessoryPanel.swift @@ -114,8 +114,21 @@ final class GiftAuctionAccessoryPanel: ASDisplayNode { var buttonAnimatedTitleItems: [AnimatedTextComponent.Item] = [] if self.states.count == 1, let auctionState = self.states.first { + var isUpcoming = false + var startTime = currentTime + var endTime = currentTime + if case let .ongoing(_, startDate, _, _, _, _, nextRoundDate, _, _, _, _, _) = auctionState.auctionState { + startTime = startDate + endTime = nextRoundDate + if currentTime < startDate { + isUpcoming = true + } + } + let place = auctionState.place ?? 1 - if case let .generic(gift) = auctionState.gift, let auctionGiftsPerRound = gift.auctionGiftsPerRound, place > auctionGiftsPerRound { + if isUpcoming { + subtitleText = self.strings.ChatList_Auctions_Status_UpcomingBid + } else if case let .generic(gift) = auctionState.gift, let auctionGiftsPerRound = gift.auctionGiftsPerRound, place > auctionGiftsPerRound { subtitleText = self.strings.ChatList_Auctions_Status_Single_Outbid subtitleTextColor = self.theme.list.itemDestructiveColor isOutbid = true @@ -135,13 +148,6 @@ final class GiftAuctionAccessoryPanel: ASDisplayNode { subtitleText = self.strings.ChatList_Auctions_Status_Single_Winning(placeText).string } - var startTime = currentTime - var endTime = currentTime - if case let .ongoing(_, startDate, _, _, _, _, nextRoundDate, _, _, _, _, _) = auctionState.auctionState { - startTime = startDate - endTime = nextRoundDate - } - let endTimeout: Int32 if currentTime < startTime { endTimeout = max(0, startTime - currentTime) diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index f367043c06..8d395730a6 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -499,6 +499,7 @@ swift_library( "//submodules/TelegramUI/Components/Contacts/NewContactScreen", "//submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu", "//submodules/TelegramUI/Components/Settings/PasskeysScreen", + "//submodules/TelegramUI/Components/Gifts/GiftDemoScreen", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "//build-system:ios_sim_arm64": [], diff --git a/submodules/TelegramUI/Components/Gifts/GiftDemoScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftDemoScreen/BUILD new file mode 100644 index 0000000000..152dfb21de --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftDemoScreen/BUILD @@ -0,0 +1,42 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "GiftDemoScreen", + module_name = "GiftDemoScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/ComponentFlow", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BalancedTextComponent", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/AppBundle", + "//submodules/TelegramStringFormatting", + "//submodules/PresentationDataUtils", + "//submodules/Components/SheetComponent", + "//submodules/TextFormat", + "//submodules/TelegramUI/Components/ScrollComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/TelegramUI/Components/GlassBarButtonComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/PremiumUI", + "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/TelegramUI/Components/EdgeEffect", + "//submodules/TelegramUI/Components/Gifts/InfoParagraphComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Gifts/GiftDemoScreen/Sources/GiftDemoScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftDemoScreen/Sources/GiftDemoScreen.swift new file mode 100644 index 0000000000..cbbe32fd2a --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftDemoScreen/Sources/GiftDemoScreen.swift @@ -0,0 +1,584 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit +import AccountContext +import TelegramPresentationData +import PresentationDataUtils +import ComponentFlow +import ViewControllerComponent +import SheetComponent +import MultilineTextComponent +import BalancedTextComponent +import BundleIconComponent +import ButtonComponent +import Markdown +import GlassBarButtonComponent +import PremiumUI +import ScrollComponent +import LottieComponent +import EdgeEffect +import InfoParagraphComponent + +final class PageComponent: CombinedComponent { + typealias EnvironmentType = ChildEnvironment + + private let content: AnyComponent + private let title: String + private let text: String + private let textColor: UIColor + + init( + content: AnyComponent, + title: String, + text: String, + textColor: UIColor + ) { + self.content = content + self.title = title + self.text = text + self.textColor = textColor + } + + static func ==(lhs: PageComponent, rhs: PageComponent) -> Bool { + if lhs.content != rhs.content { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.text != rhs.text { + return false + } + if lhs.textColor != rhs.textColor { + return false + } + return true + } + + static var body: Body { + let children = ChildMap(environment: ChildEnvironment.self, keyedBy: AnyHashable.self) + let title = Child(MultilineTextComponent.self) + let text = Child(MultilineTextComponent.self) + + return { context in + let availableSize = context.availableSize + let component = context.component + + let sideInset: CGFloat = 16.0 + let textSideInset: CGFloat = 24.0 + + let textColor = component.textColor + let textFont = Font.regular(17.0) + let boldTextFont = Font.semibold(17.0) + + let content = children["main"].update( + component: component.content, + environment: { + context.environment[ChildEnvironment.self] + }, + availableSize: CGSize(width: availableSize.width, height: availableSize.width), + transition: context.transition + ) + + let title = title.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: component.title, + font: boldTextFont, + textColor: component.textColor, + paragraphAlignment: .center + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + + let markdownAttributes = MarkdownAttributes( + body: MarkdownAttributeSet(font: textFont, textColor: textColor), + bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), + link: MarkdownAttributeSet(font: textFont, textColor: textColor), + linkAttribute: { _ in + return nil + } + ) + let text = text.update( + component: MultilineTextComponent( + text: .markdown(text: component.text, attributes: markdownAttributes), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.0 + ), + availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: content.size.height + 40.0)) + ) + context.add(text + .position(CGPoint(x: context.availableSize.width / 2.0, y: content.size.height + 60.0 + text.size.height / 2.0)) + ) + context.add(content + .position(CGPoint(x: content.size.width / 2.0, y: content.size.height / 2.0)) + ) + + return availableSize + } + } +} + +private final class DemoSheetContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let action: () -> Void + let dismiss: () -> Void + + init( + context: AccountContext, + action: @escaping () -> Void, + dismiss: @escaping () -> Void + ) { + self.context = context + self.action = action + self.dismiss = dismiss + } + + static func ==(lhs: DemoSheetContent, rhs: DemoSheetContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + final class State: ComponentState { + private let context: AccountContext + + private var disposable: Disposable? + private(set) var promoConfiguration: PremiumPromoConfiguration? + + fileprivate let playButtonAnimation = ActionSlot() + private var didPlayAnimation = false + + init(context: AccountContext) { + self.context = context + + super.init() + + self.disposable = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.PremiumPromo()) + |> deliverOnMainQueue).start(next: { [weak self] promoConfiguration in + guard let self else { + return + } + self.promoConfiguration = promoConfiguration + self.updated(transition: .immediate) + }) + } + + deinit { + self.disposable?.dispose() + } + + func playAnimationIfNeeded() { + if !self.didPlayAnimation { + self.didPlayAnimation = true + self.playButtonAnimation.invoke(Void()) + } + } + } + + func makeState() -> State { + return State(context: self.context) + } + + static var body: Body { + let closeButton = Child(GlassBarButtonComponent.self) + let background = Child(PremiumGradientBackgroundComponent.self) + let demo = Child(PhoneDemoComponent.self) + let scroll = Child(ScrollComponent.self) + let bottomEdgeEffect = Child(EdgeEffectComponent.self) + let button = Child(ButtonComponent.self) + + return { context in + let environment = context.environment[ViewControllerComponentContainer.Environment.self].value + let component = context.component + let theme = environment.theme + let strings = environment.strings + + let state = context.state + + var contentSize = CGSize(width: context.availableSize.width, height: context.availableSize.width) + + let remainingHeight = 365.0 + let scroll = scroll.update( + component: ScrollComponent( + content: AnyComponent( + GiftDemoListComponent( + context: context.component.context, + theme: environment.theme + ) + ), + contentInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 80.0, right: 0.0), + contentOffsetUpdated: { _, _ in }, + contentOffsetWillCommit: { _ in } + ), + availableSize: CGSize(width: context.availableSize.width, height: remainingHeight), + transition: context.transition + ) + context.add(scroll + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + scroll.size.height / 2.0)) + ) + + let background = background.update( + component: PremiumGradientBackgroundComponent(colors: [ + UIColor(rgb: 0x0077ff), + UIColor(rgb: 0x6b93ff), + UIColor(rgb: 0x8878ff), + UIColor(rgb: 0xe46ace) + ]), + availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.width), + transition: .immediate + ) + context.add(background + .position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0)) + ) + + let demo = demo.update( + component: PhoneDemoComponent( + context: component.context, + position: .top, + model: .island, + videoFile: state.promoConfiguration?.videos["gifts"], + decoration: .badgeStars + ), + environment: { DemoPageEnvironment(isDisplaying: true, isCentral: true, position: 0.0) }, + availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.width), + transition: context.transition + ) + context.add(demo + .position(CGPoint(x: context.availableSize.width / 2.0, y: demo.size.height / 2.0)) + ) + + let closeButton = closeButton.update( + component: GlassBarButtonComponent( + size: CGSize(width: 40.0, height: 40.0), + backgroundColor: UIColor(rgb: 0x7f76f4), + isDark: false, + state: .tintedGlass, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: .white + ) + )), + action: { _ in + component.dismiss() + } + ), + availableSize: CGSize(width: 40.0, height: 40.0), + transition: .immediate + ) + context.add(closeButton + .position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0)) + ) + + contentSize.height += remainingHeight + + let bottomEdgeEffectHeight = 108.0 + let bottomEdgeEffect = bottomEdgeEffect.update( + component: EdgeEffectComponent( + color: .clear, + blur: true, + alpha: 1.0, + size: CGSize(width: context.availableSize.width, height: bottomEdgeEffectHeight), + edge: .bottom, + edgeSize: bottomEdgeEffectHeight + ), + availableSize: CGSize(width: context.availableSize.width, height: bottomEdgeEffectHeight), + transition: context.transition + ) + context.add(bottomEdgeEffect + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height - bottomEdgeEffect.size.height / 2.0)) + ) + + var buttonTitle: [AnyComponentWithIdentity] = [] + buttonTitle.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: "anim_ok"), + color: theme.list.itemCheckColors.foregroundColor, + startingPosition: .begin, + size: CGSize(width: 28.0, height: 28.0), + playOnce: state.playButtonAnimation + )))) + buttonTitle.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(ButtonTextContentComponent( + text: strings.Gifts_Demo_Understood, + badge: 0, + textColor: theme.list.itemCheckColors.foregroundColor, + badgeBackground: theme.list.itemCheckColors.foregroundColor, + badgeForeground: theme.list.itemCheckColors.fillColor + )))) + let button = button.update( + component: ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: theme.list.itemCheckColors.fillColor, + foreground: theme.list.itemCheckColors.foregroundColor, + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), + cornerRadius: 10.0, + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(HStack(buttonTitle, spacing: 2.0)) + ), + action: { + component.dismiss() + } + ), + availableSize: CGSize(width: context.availableSize.width - 30.0 * 2.0, height: 52.0), + transition: .immediate + ) + context.add(button + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height - button.size.height / 2.0 - 30.0)) + ) + + state.playAnimationIfNeeded() + + return contentSize + } + } +} + + +private final class DemoSheetComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let action: () -> Void + + init(context: AccountContext, action: @escaping () -> Void) { + self.context = context + self.action = action + } + + static func ==(lhs: DemoSheetComponent, rhs: DemoSheetComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + static var body: Body { + let sheet = Child(SheetComponent<(EnvironmentType)>.self) + let animateOut = StoredActionSlot(Action.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + + let controller = environment.controller + + let sheet = sheet.update( + component: SheetComponent( + content: AnyComponent(DemoSheetContent( + context: context.component.context, + action: context.component.action, + dismiss: { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } + )), + style: .glass, + backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), + followContentSizeChanges: true, + animateOut: animateOut + ), + environment: { + environment + SheetComponentEnvironment( + isDisplaying: environment.value.isVisible, + isCentered: environment.metrics.widthClass == .regular, + hasInputHeight: !environment.inputHeight.isZero, + regularMetricsSize: nil, + dismiss: { animated in + if animated { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } else { + if let controller = controller() { + controller.dismiss(completion: nil) + } + } + } + ) + }, + availableSize: context.availableSize, + transition: context.transition + ) + + context.add(sheet + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + return context.availableSize + } + } +} + +public class GiftDemoScreen: ViewControllerComponentContainer { + public init(context: AccountContext, action: @escaping () -> Void = {}) { + super.init(context: context, component: DemoSheetComponent(context: context, action: action), navigationBarAppearance: .none, theme: .default) + + self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + + self.navigationPresentation = .flatModal + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func viewDidLoad() { + super.viewDidLoad() + + self.view.disablesInteractiveModalDismiss = true + } + + public func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { + view.dismissAnimated() + } + } +} + +private final class GiftDemoListComponent: CombinedComponent { + typealias EnvironmentType = (Empty, ScrollChildEnvironment) + + let context: AccountContext + let theme: PresentationTheme + + init(context: AccountContext, theme: PresentationTheme) { + self.context = context + self.theme = theme + } + + static func ==(lhs: GiftDemoListComponent, rhs: GiftDemoListComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + return true + } + + static var body: Body { + let title = Child(MultilineTextComponent.self) + let description = Child(BalancedTextComponent.self) + let list = Child(List.self) + + return { context in + let theme = context.component.theme + let strings = context.component.context.sharedContext.currentPresentationData.with { $0 }.strings + + let titleColor = theme.list.itemPrimaryTextColor + let textColor = theme.list.itemSecondaryTextColor + let iconColor = theme.list.itemAccentColor + + var contentSize = CGSize(width: context.availableSize.width, height: 32.0) + + let title = title.update( + component: MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gifts_Demo_Title, font: Font.bold(25.0), textColor: titleColor))), + availableSize: context.availableSize, + transition: context.transition + ) + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height * 0.5)) + ) + contentSize.height += title.size.height + contentSize.height += 9.0 + + let description = description.update( + component: BalancedTextComponent( + text: .plain(NSAttributedString(string: strings.Gifts_Demo_Description, font: Font.regular(15.0), textColor: textColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + ), + availableSize: context.availableSize, + transition: context.transition + ) + context.add(description + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + description.size.height * 0.5)) + ) + contentSize.height += description.size.height + contentSize.height += 26.0 + + var items: [AnyComponentWithIdentity] = [] + items.append( + AnyComponentWithIdentity( + id: "unique", + component: AnyComponent(InfoParagraphComponent( + title: strings.Gifts_Demo_Unique_Title, + titleColor: titleColor, + text: strings.Gifts_Demo_Unique_Text, + textColor: textColor, + accentColor: iconColor, + iconName: "Premium/Collectible/Unique", + iconColor: iconColor + )) + ) + ) + items.append( + AnyComponentWithIdentity( + id: "tradable", + component: AnyComponent(InfoParagraphComponent( + title: strings.Gifts_Demo_Tradable_Title, + titleColor: titleColor, + text: strings.Gifts_Demo_Tradable_Text, + textColor: textColor, + accentColor: iconColor, + iconName: "Premium/Collectible/Transferable", + iconColor: iconColor + )) + ) + ) + items.append( + AnyComponentWithIdentity( + id: "wearable", + component: AnyComponent(InfoParagraphComponent( + title: strings.Gifts_Demo_Wearable_Title, + titleColor: titleColor, + text: strings.Gifts_Demo_Wearable_Text, + textColor: textColor, + accentColor: iconColor, + iconName: "Premium/Collectible/Tradable", + iconColor: iconColor + )) + ) + ) + + let list = list.update( + component: List(items), + availableSize: CGSize(width: context.availableSize.width - 32.0 * 2.0, height: 10000.0), + transition: context.transition + ) + context.add(list + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + list.size.height / 2.0)) + ) + contentSize.height += list.size.height + contentSize.height += 88.0 + + return CGSize(width: context.availableSize.width, height: contentSize.height) + } + } +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD index 8944b7ddb9..a5d6d5dcee 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD @@ -63,6 +63,7 @@ swift_library( "//submodules/BotPaymentsUI", "//submodules/TelegramUI/Components/SegmentControlComponent", "//submodules/TelegramUI/Components/Gifts/GiftRemainingCountComponent", + "//submodules/TelegramUI/Components/Gifts/InfoParagraphComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionActiveBidsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionActiveBidsScreen.swift index 0a22e3e2b1..1904b87058 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionActiveBidsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionActiveBidsScreen.swift @@ -663,11 +663,13 @@ private final class ActiveAuctionComponent: Component { var titleText: String = "" var subtitleText: String = "" var subtitleTextColor = component.theme.list.itemPrimaryTextColor - if case let .ongoing(_, _, _, _, _, _, nextRoundDate, _, currentRound, totalRound, _, _) = component.state.auctionState, let myBid = component.state.myState.bidAmount { + if case let .ongoing(_, startDate, _, _, _, _, nextRoundDate, _, currentRound, totalRound, _, _) = component.state.auctionState, let myBid = component.state.myState.bidAmount { titleText = component.strings.Gift_ActiveAuctions_Round("\(currentRound)", "\(totalRound)").string let bidString = "#\(presentationStringsFormattedNumber(Int32(clamping: myBid), component.dateTimeFormat.groupingSeparator))" - if let place = component.state.place, case let .generic(gift) = component.state.gift, let auctionGiftsPerRound = gift.auctionGiftsPerRound, place > auctionGiftsPerRound { + if component.currentTime < startDate { + subtitleText = component.strings.Gift_ActiveAuctions_UpcomingBid + } else if let place = component.state.place, case let .generic(gift) = component.state.gift, let auctionGiftsPerRound = gift.auctionGiftsPerRound, place > auctionGiftsPerRound { subtitleText = component.strings.Gift_ActiveAuctions_Outbid(bidString).string subtitleTextColor = component.theme.list.itemDestructiveColor } else { diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift index c80ecbb280..1098252814 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift @@ -1870,7 +1870,7 @@ private final class GiftAuctionBidScreenComponent: Component { maxBidAmount = max(maxBidAmount, Int64(Double(firstLevel.amount) * 1.5)) } } - var currentValue = max(Int(minBidAmount), 100) + var currentValue = Int(minBidAmount) var minAllowedRealValue: Int64 = minBidAmount if let myBidAmount = state.myState.bidAmount { if let component, let bidPeerId = state.myState.bidPeerId, bidPeerId != component.toPeerId || forceMinimum, let myMinBidAmount = state.myState.minBidAmount { diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift index 7322b9b51b..06009d5e2a 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift @@ -818,10 +818,8 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { tintColor: secondaryTextColor )), action: { - let controller = component.context.sharedContext.makeGiftAuctionInfoScreen( - context: component.context, - auctionContext: component.auctionContext, - completion: nil + let controller = component.context.sharedContext.makeGiftDemoScreen( + context: component.context ) environment.controller()?.push(controller) }, @@ -1157,7 +1155,7 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { component: ButtonComponent( background: buttonBackground, content: AnyComponentWithIdentity( - id: AnyHashable("buy"), + id: AnyHashable("bid"), component: AnyComponent(VStack(items, spacing: 1.0)) ), isEnabled: true, diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionWearPreviewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionWearPreviewScreen.swift index ed781e48f4..c3a54bbdbc 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionWearPreviewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionWearPreviewScreen.swift @@ -25,6 +25,7 @@ import GiftItemComponent import GiftAnimationComponent import GlassBarButtonComponent import GiftRemainingCountComponent +import AnimatedTextComponent private final class GiftAuctionWearPreviewSheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -63,7 +64,11 @@ private final class GiftAuctionWearPreviewSheetContent: CombinedComponent { var peerMap: [EnginePeer.Id: EnginePeer] = [:] var cachedSmallChevronImage: (UIImage, PresentationTheme)? - + + private var disposable: Disposable? + private(set) var giftAuctionState: GiftAuctionContext.State? + private var giftAuctionTimer: SwiftSignalKit.Timer? + private var previewTimer: SwiftSignalKit.Timer? private(set) var previewModelIndex: Int = 0 private(set) var previewBackdropIndex: Int = 0 @@ -75,13 +80,13 @@ private final class GiftAuctionWearPreviewSheetContent: CombinedComponent { init( context: AccountContext, + auctionContext: GiftAuctionContext, attributes: [StarGift.UniqueGift.Attribute] ) { self.context = context super.init() - for attribute in attributes { switch attribute { case .model: @@ -123,9 +128,25 @@ private final class GiftAuctionWearPreviewSheetContent: CombinedComponent { self.previewTimerTick() }, queue: Queue.mainQueue()) self.previewTimer?.start() + + self.disposable = (auctionContext.state + |> deliverOnMainQueue).start(next: { [weak self] auctionState in + guard let self else { + return + } + self.giftAuctionState = auctionState + self.updated() + }) + + self.giftAuctionTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in + self?.updated() + }, queue: Queue.mainQueue()) + self.giftAuctionTimer?.start() } deinit { + self.disposable?.dispose() + self.giftAuctionTimer?.invalidate() self.peerDisposable?.dispose() self.previewTimer?.invalidate() } @@ -154,7 +175,7 @@ private final class GiftAuctionWearPreviewSheetContent: CombinedComponent { func makeState() -> State { - return State(context: self.context, attributes: self.attributes) + return State(context: self.context, auctionContext: self.auctionContext, attributes: self.attributes) } static var body: Body { @@ -408,17 +429,18 @@ private final class GiftAuctionWearPreviewSheetContent: CombinedComponent { contentHeight += 137.0 if case let .generic(gift) = component.auctionContext.gift, let availability = gift.availability { - let remains: Int32 = availability.remains -// if let auctionState = component.auctionContext.currentState { -// switch auctionState.auctionState { -// case let .ongoing(_, _, _, _, _, _, _, giftsLeft, _, _, _, _): -// remains = giftsLeft -// case .finished: -// remains = 0 -// } -// } - let total: Int32 = availability.total + var remains: Int32 = availability.remains + if let auctionState = state.giftAuctionState { + switch auctionState.auctionState { + case let .ongoing(_, _, _, _, _, _, _, giftsLeft, _, _, _, _): + remains = giftsLeft + case .finished: + remains = 0 + } + } + let total = availability.total let position = CGFloat(remains) / CGFloat(total) + let sold = total - remains let remainingCount = remainingCount.update( component: GiftRemainingCountComponent( inactiveColor: theme.list.itemBlocksBackgroundColor, @@ -427,7 +449,7 @@ private final class GiftAuctionWearPreviewSheetContent: CombinedComponent { inactiveValue: "", inactiveTitleColor: theme.list.itemSecondaryTextColor, activeTitle: "", - activeValue: "", + activeValue: sold > 0 ? strings.Gift_Send_Sold(sold) : "", activeTitleColor: .white, badgeText: "", badgePosition: position, @@ -494,11 +516,77 @@ private final class GiftAuctionWearPreviewSheetContent: CombinedComponent { } contentHeight += 80.0 - var buttonTitle = strings.Gift_Auction_Join - if component.auctionContext.isUpcoming { - buttonTitle = strings.Gift_Auction_EarlyBid + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + var startTime = currentTime + var endTime = currentTime + var isUpcoming = false + + if let auctionState = state.giftAuctionState { + startTime = auctionState.startDate + endTime = auctionState.endDate } + var buttonTitle = strings.Gift_Auction_Join + let endTimeout: Int32 + if currentTime < startTime { + isUpcoming = true + endTimeout = max(0, startTime - currentTime) + } else { + endTimeout = max(0, endTime - currentTime) + } + + let hours = Int(endTimeout / 3600) + let minutes = Int((endTimeout % 3600) / 60) + let seconds = Int(endTimeout % 60) + + let rawString: String + if isUpcoming { + buttonTitle = strings.Gift_Auction_EarlyBid + rawString = hours > 0 ? strings.Gift_Auction_StartsInHours : strings.Gift_Auction_StartsInMinutes + } else { + rawString = hours > 0 ? strings.Gift_Auction_TimeLeftHours : strings.Gift_Auction_TimeLeftMinutes + } + + var buttonAnimatedTitleItems: [AnimatedTextComponent.Item] = [] + var startIndex = rawString.startIndex + while true { + if let range = rawString.range(of: "{", range: startIndex ..< rawString.endIndex) { + if range.lowerBound != startIndex { + buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "prefix_\(buttonAnimatedTitleItems.count)", content: .text(String(rawString[startIndex ..< range.lowerBound])))) + } + + startIndex = range.upperBound + if let endRange = rawString.range(of: "}", range: startIndex ..< rawString.endIndex) { + let controlString = rawString[range.upperBound ..< endRange.lowerBound] + if controlString == "h" { + buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "h", content: .number(hours, minDigits: 2))) + } else if controlString == "m" { + buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "m", content: .number(minutes, minDigits: 2))) + } else if controlString == "s" { + buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "s", content: .number(seconds, minDigits: 2))) + } + + startIndex = endRange.upperBound + } + } else { + break + } + } + if startIndex != rawString.endIndex { + buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "suffix_\(buttonAnimatedTitleItems.count)", content: .text(String(rawString[startIndex ..< rawString.endIndex])))) + } + + let buttonAttributedString = NSMutableAttributedString(string: buttonTitle, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) + let items: [AnyComponentWithIdentity] = [ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))), + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(AnimatedTextComponent( + font: Font.with(size: 12.0, weight: .medium, traits: .monospacedNumbers), + color: theme.list.itemCheckColors.foregroundColor.withAlphaComponent(0.7), + items: buttonAnimatedTitleItems, + noDelay: true + ))) + ] + let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) let buttonSize = CGSize(width: context.availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0) let buttonBackground = ButtonComponent.Background( @@ -511,8 +599,8 @@ private final class GiftAuctionWearPreviewSheetContent: CombinedComponent { component: ButtonComponent( background: buttonBackground, content: AnyComponentWithIdentity( - id: AnyHashable("ok"), - component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: buttonTitle, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)))) + id: AnyHashable("bid"), + component: AnyComponent(VStack(items, spacing: 1.0)) ), isEnabled: true, displaysProgress: false, @@ -523,7 +611,7 @@ private final class GiftAuctionWearPreviewSheetContent: CombinedComponent { } }), availableSize: buttonSize, - transition: context.transition + transition: .spring(duration: 0.2) ) let buttonFrame = CGRect(origin: CGPoint(x: buttonInsets.left, y: contentHeight), size: button.size) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 623ca2a7b7..ecf6d15c17 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -39,6 +39,7 @@ import GiftAnimationComponent import ChatThemeScreen import ProfileLevelRatingBarComponent import AnimatedTextComponent +import InfoParagraphComponent private final class GiftViewSheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -2885,7 +2886,7 @@ private final class GiftViewSheetContent: CombinedComponent { items.append( AnyComponentWithIdentity( id: "badge", - component: AnyComponent(ParagraphComponent( + component: AnyComponent(InfoParagraphComponent( title: strings.Gift_Wear_Badge_Title, titleColor: textColor, text: isChannelGift ? strings.Gift_Wear_Badge_ChannelText : strings.Gift_Wear_Badge_Text, @@ -2899,7 +2900,7 @@ private final class GiftViewSheetContent: CombinedComponent { items.append( AnyComponentWithIdentity( id: "design", - component: AnyComponent(ParagraphComponent( + component: AnyComponent(InfoParagraphComponent( title: strings.Gift_Wear_Design_Title, titleColor: textColor, text: isChannelGift ? strings.Gift_Wear_Design_ChannelText : strings.Gift_Wear_Design_Text, @@ -2913,7 +2914,7 @@ private final class GiftViewSheetContent: CombinedComponent { items.append( AnyComponentWithIdentity( id: "proof", - component: AnyComponent(ParagraphComponent( + component: AnyComponent(InfoParagraphComponent( title: strings.Gift_Wear_Proof_Title, titleColor: textColor, text: isChannelGift ? strings.Gift_Wear_Proof_ChannelText : strings.Gift_Wear_Proof_Text, @@ -3031,7 +3032,7 @@ private final class GiftViewSheetContent: CombinedComponent { items.append( AnyComponentWithIdentity( id: "unique", - component: AnyComponent(ParagraphComponent( + component: AnyComponent(InfoParagraphComponent( title: strings.Gift_Upgrade_Unique_Title, titleColor: textColor, text: uniqueText, @@ -3045,7 +3046,7 @@ private final class GiftViewSheetContent: CombinedComponent { items.append( AnyComponentWithIdentity( id: "transferable", - component: AnyComponent(ParagraphComponent( + component: AnyComponent(InfoParagraphComponent( title: strings.Gift_Upgrade_Transferable_Title, titleColor: textColor, text: transferableText, @@ -3059,7 +3060,7 @@ private final class GiftViewSheetContent: CombinedComponent { items.append( AnyComponentWithIdentity( id: "tradable", - component: AnyComponent(ParagraphComponent( + component: AnyComponent(InfoParagraphComponent( title: strings.Gift_Upgrade_Tradable_Title, titleColor: textColor, text: tradableText, @@ -6046,184 +6047,6 @@ private struct GiftConfiguration { } } -private final class ParagraphComponent: CombinedComponent { - let title: String - let titleColor: UIColor - let text: String - let textColor: UIColor - let accentColor: UIColor - let iconName: String - let iconColor: UIColor - let badge: String? - let action: () -> Void - - public init( - title: String, - titleColor: UIColor, - text: String, - textColor: UIColor, - accentColor: UIColor, - iconName: String, - iconColor: UIColor, - badge: String? = nil, - action: @escaping () -> Void = {} - ) { - self.title = title - self.titleColor = titleColor - self.text = text - self.textColor = textColor - self.accentColor = accentColor - self.iconName = iconName - self.iconColor = iconColor - self.badge = badge - self.action = action - } - - static func ==(lhs: ParagraphComponent, rhs: ParagraphComponent) -> Bool { - if lhs.title != rhs.title { - return false - } - if lhs.titleColor != rhs.titleColor { - return false - } - if lhs.text != rhs.text { - return false - } - if lhs.textColor != rhs.textColor { - return false - } - if lhs.accentColor != rhs.accentColor { - return false - } - if lhs.iconName != rhs.iconName { - return false - } - if lhs.iconColor != rhs.iconColor { - return false - } - if lhs.badge != rhs.badge { - return false - } - return true - } - - static var body: Body { - let title = Child(MultilineTextComponent.self) - let text = Child(MultilineTextComponent.self) - let icon = Child(BundleIconComponent.self) - let badgeBackground = Child(RoundedRectangle.self) - let badgeText = Child(MultilineTextComponent.self) - - return { context in - let component = context.component - - let leftInset: CGFloat = 32.0 - let rightInset: CGFloat = 24.0 - let textSideInset: CGFloat = leftInset + 8.0 - let spacing: CGFloat = 5.0 - - let textTopInset: CGFloat = 9.0 - - let title = title.update( - component: MultilineTextComponent( - text: .plain(NSAttributedString( - string: component.title, - font: Font.semibold(15.0), - textColor: component.titleColor, - paragraphAlignment: .natural - )), - horizontalAlignment: .center, - maximumNumberOfLines: 1 - ), - availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), - transition: .immediate - ) - - let textFont = Font.regular(15.0) - let boldTextFont = Font.semibold(15.0) - let textColor = component.textColor - let accentColor = component.accentColor - let markdownAttributes = MarkdownAttributes( - body: MarkdownAttributeSet(font: textFont, textColor: textColor), - bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), - link: MarkdownAttributeSet(font: textFont, textColor: accentColor), - linkAttribute: { contents in - return (TelegramTextAttributes.URL, contents) - } - ) - - let text = text.update( - component: MultilineTextComponent( - text: .markdown(text: component.text, attributes: markdownAttributes), - horizontalAlignment: .natural, - maximumNumberOfLines: 0, - lineSpacing: 0.2, - highlightAction: { attributes in - if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { - return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) - } else { - return nil - } - }, - tapAction: { _, _ in - component.action() - } - ), - availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: context.availableSize.height), - transition: .immediate - ) - - let icon = icon.update( - component: BundleIconComponent( - name: component.iconName, - tintColor: component.iconColor - ), - availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height), - transition: .immediate - ) - - context.add(title - .position(CGPoint(x: textSideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0)) - ) - - if let badge = component.badge { - let badgeText = badgeText.update( - component: MultilineTextComponent(text: .plain(NSAttributedString(string: badge, font: Font.semibold(11.0), textColor: .white))), - availableSize: context.availableSize, - transition: context.transition - ) - - let badgeWidth = badgeText.size.width + 7.0 - let badgeBackground = badgeBackground.update( - component: RoundedRectangle( - color: component.accentColor, - cornerRadius: 5.0), - availableSize: CGSize(width: badgeWidth, height: 16.0), - transition: context.transition - ) - - context.add(badgeBackground - .position(CGPoint(x: textSideInset + title.size.width + badgeWidth / 2.0 + 5.0, y: textTopInset + title.size.height / 2.0)) - ) - - context.add(badgeText - .position(CGPoint(x: textSideInset + title.size.width + badgeWidth / 2.0 + 5.0, y: textTopInset + title.size.height / 2.0)) - ) - } - - context.add(text - .position(CGPoint(x: textSideInset + text.size.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height / 2.0)) - ) - - context.add(icon - .position(CGPoint(x: 15.0, y: textTopInset + 18.0)) - ) - - return CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + text.size.height + 20.0) - } - } -} - private final class GiftViewContextReferenceContentSource: ContextReferenceContentSource { private let controller: ViewController private let sourceNode: ASDisplayNode diff --git a/submodules/TelegramUI/Components/Gifts/InfoParagraphComponent/BUILD b/submodules/TelegramUI/Components/Gifts/InfoParagraphComponent/BUILD new file mode 100644 index 0000000000..0f3e51fdcd --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/InfoParagraphComponent/BUILD @@ -0,0 +1,28 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "InfoParagraphComponent", + module_name = "InfoParagraphComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/AppBundle", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/Markdown", + "//submodules/TextFormat", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Gifts/InfoParagraphComponent/Sources/InfoParagraphComponent.swift b/submodules/TelegramUI/Components/Gifts/InfoParagraphComponent/Sources/InfoParagraphComponent.swift new file mode 100644 index 0000000000..7e3650cec4 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/InfoParagraphComponent/Sources/InfoParagraphComponent.swift @@ -0,0 +1,187 @@ +import Foundation +import UIKit +import Display +import TelegramPresentationData +import ComponentFlow +import MultilineTextComponent +import BundleIconComponent +import Markdown +import TextFormat + +public final class InfoParagraphComponent: CombinedComponent { + let title: String + let titleColor: UIColor + let text: String + let textColor: UIColor + let accentColor: UIColor + let iconName: String + let iconColor: UIColor + let badge: String? + let action: () -> Void + + public init( + title: String, + titleColor: UIColor, + text: String, + textColor: UIColor, + accentColor: UIColor, + iconName: String, + iconColor: UIColor, + badge: String? = nil, + action: @escaping () -> Void = {} + ) { + self.title = title + self.titleColor = titleColor + self.text = text + self.textColor = textColor + self.accentColor = accentColor + self.iconName = iconName + self.iconColor = iconColor + self.badge = badge + self.action = action + } + + public static func ==(lhs: InfoParagraphComponent, rhs: InfoParagraphComponent) -> Bool { + if lhs.title != rhs.title { + return false + } + if lhs.titleColor != rhs.titleColor { + return false + } + if lhs.text != rhs.text { + return false + } + if lhs.textColor != rhs.textColor { + return false + } + if lhs.accentColor != rhs.accentColor { + return false + } + if lhs.iconName != rhs.iconName { + return false + } + if lhs.iconColor != rhs.iconColor { + return false + } + if lhs.badge != rhs.badge { + return false + } + return true + } + + public static var body: Body { + let title = Child(MultilineTextComponent.self) + let text = Child(MultilineTextComponent.self) + let icon = Child(BundleIconComponent.self) + let badgeBackground = Child(RoundedRectangle.self) + let badgeText = Child(MultilineTextComponent.self) + + return { context in + let component = context.component + + let leftInset: CGFloat = 32.0 + let rightInset: CGFloat = 24.0 + let textSideInset: CGFloat = leftInset + 8.0 + let spacing: CGFloat = 5.0 + + let textTopInset: CGFloat = 9.0 + + let title = title.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: component.title, + font: Font.semibold(15.0), + textColor: component.titleColor, + paragraphAlignment: .natural + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + + let textFont = Font.regular(15.0) + let boldTextFont = Font.semibold(15.0) + let textColor = component.textColor + let accentColor = component.accentColor + let markdownAttributes = MarkdownAttributes( + body: MarkdownAttributeSet(font: textFont, textColor: textColor), + bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), + link: MarkdownAttributeSet(font: textFont, textColor: accentColor), + linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + } + ) + + let text = text.update( + component: MultilineTextComponent( + text: .markdown(text: component.text, attributes: markdownAttributes), + horizontalAlignment: .natural, + maximumNumberOfLines: 0, + lineSpacing: 0.2, + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { _, _ in + component.action() + } + ), + availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: context.availableSize.height), + transition: .immediate + ) + + let icon = icon.update( + component: BundleIconComponent( + name: component.iconName, + tintColor: component.iconColor + ), + availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height), + transition: .immediate + ) + + context.add(title + .position(CGPoint(x: textSideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0)) + ) + + if let badge = component.badge { + let badgeText = badgeText.update( + component: MultilineTextComponent(text: .plain(NSAttributedString(string: badge, font: Font.semibold(11.0), textColor: .white))), + availableSize: context.availableSize, + transition: context.transition + ) + + let badgeWidth = badgeText.size.width + 7.0 + let badgeBackground = badgeBackground.update( + component: RoundedRectangle( + color: component.accentColor, + cornerRadius: 5.0), + availableSize: CGSize(width: badgeWidth, height: 16.0), + transition: context.transition + ) + + context.add(badgeBackground + .position(CGPoint(x: textSideInset + title.size.width + badgeWidth / 2.0 + 5.0, y: textTopInset + title.size.height / 2.0)) + ) + + context.add(badgeText + .position(CGPoint(x: textSideInset + title.size.width + badgeWidth / 2.0 + 5.0, y: textTopInset + title.size.height / 2.0)) + ) + } + + context.add(text + .position(CGPoint(x: textSideInset + text.size.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height / 2.0)) + ) + + context.add(icon + .position(CGPoint(x: 15.0, y: textTopInset + 18.0)) + ) + + return CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + text.size.height + 20.0) + } + } +} diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 695f743dac..90cafd6cc0 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -91,6 +91,7 @@ import GlassBackgroundComponent import AttachmentFileController import NewContactScreen import PasskeysScreen +import GiftDemoScreen private final class AccountUserInterfaceInUseContext { let subscribers = Bag<(Bool) -> Void>() @@ -3876,6 +3877,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { return GiftAuctionWearPreviewScreen(context: context, auctionContext: auctionContext, attributes: attributes, completion: completion) } + public func makeGiftDemoScreen(context: AccountContext) -> ViewController { + return GiftDemoScreen(context: context) + } + public func makeStorySharingScreen(context: AccountContext, subject: StorySharingSubject, parentController: ViewController) -> ViewController { let editorSubject: Signal switch subject { From 52e7a175c4bd25e8713975d6b639545d86c77c85 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 3 Dec 2025 18:15:27 +0400 Subject: [PATCH 6/9] Show generic gift title and number if available --- .../Gifts/GiftViewScreen/Sources/GiftViewScreen.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index ecf6d15c17..382272a140 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -2546,7 +2546,7 @@ private final class GiftViewSheetContent: CombinedComponent { titleString = strings.Gift_View_UnavailableTitle } else if let arguments = subject.arguments { if let toPeerId = arguments.auctionToPeerId { - isSelfGift = arguments.messageId?.peerId.isTelegramNotifications == true && toPeerId == component.context.account.peerId + isSelfGift = arguments.messageId?.peerId.isTelegramNotifications == true && toPeerId == component.context.account.peerId } else { isSelfGift = arguments.messageId?.peerId == component.context.account.peerId } @@ -2604,7 +2604,9 @@ private final class GiftViewSheetContent: CombinedComponent { isMyOwnedUniqueGift = true } - if isSelfGift { + if let number = arguments.giftNumber, let title = genericGift?.title { + titleString = "\(title) #\(formatCollectibleNumber(number, dateTimeFormat: environment.dateTimeFormat))" + } else if isSelfGift { titleString = strings.Gift_View_Self_Title } else { titleString = incoming ? strings.Gift_View_ReceivedTitle : strings.Gift_View_Title From 4660f6727129f1fbadcaad951156143f7c7572f7 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 3 Dec 2025 22:25:26 +0400 Subject: [PATCH 7/9] Update localization --- .../Telegram-iOS/en.lproj/Localizable.strings | 62 ++++++++++++--- .../ChatMessageGiftBubbleContentNode.swift | 3 +- .../Sources/GiftDemoScreen.swift | 18 ++--- .../Sources/GiftAuctionBidScreen.swift | 3 +- .../Sources/GiftAuctionViewScreen.swift | 6 +- .../Sources/GiftUpgradePreviewScreen.swift | 75 ------------------- .../Sources/GiftViewScreen.swift | 3 +- .../Sources/StarsWithdrawalScreen.swift | 51 +++++++------ 8 files changed, 92 insertions(+), 129 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 1ca35b318e..11ad1c5527 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -15539,12 +15539,56 @@ Error: %8$@"; "Passkeys.AddPasskey" = "Create Passkey"; "Passkeys.ListFooter" = "Your passkeys are stored securely in your password manager."; -"Gifts.Demo.Title" = "Unique Collectibles"; -"Gifts.Demo.Description" = "Telegram Gifts are collectible items you can trade or showcase on your profile."; -"Gifts.Demo.Unique.Title" = "Unique"; -"Gifts.Demo.Unique.Text" = "Upgrade your gifts to get a unique number, model, backdrop and symbol."; -"Gifts.Demo.Tradable.Title" = "Tradable"; -"Gifts.Demo.Tradable.Text" = "Sell your gift on Telegram or on third-party NFT marketplaces."; -"Gifts.Demo.Wearable.Title" = "Wearable"; -"Gifts.Demo.Wearable.Text" = "Display gifts on your page and set them as profile covers or statuses."; -"Gifts.Demo.Understood" = "Understood"; +"Gift.Demo.Title" = "Unique Collectibles"; +"Gift.Demo.Description" = "Telegram Gifts are collectible items you can trade or showcase on your profile."; +"Gift.Demo.Unique.Title" = "Unique"; +"Gift.Demo.Unique.Text" = "Upgrade your gifts to get a unique number, model, backdrop and symbol."; +"Gift.Demo.Tradable.Title" = "Tradable"; +"Gift.Demo.Tradable.Text" = "Sell your gift on Telegram or on third-party NFT marketplaces."; +"Gift.Demo.Wearable.Title" = "Wearable"; +"Gift.Demo.Wearable.Text" = "Display gifts on your page and set them as profile covers or statuses."; +"Gift.Demo.Understood" = "Understood"; + +"Gift.Variants.RandomTraits" = "Random Traits"; +"Gift.Variants.SelectedTraits" = "Selected Traits"; +"Gift.Variants.Randomize" = "Randomize Traits"; +"Gift.Variants.Model" = "model"; +"Gift.Variants.Backdrop" = "backdrop"; +"Gift.Variants.Symbol" = "symbol"; +"Gift.Variants.Models" = "Models"; +"Gift.Variants.Backdrops" = "Backdrops"; +"Gift.Variants.Symbols" = "Symbols"; + +"Gift.Variants.CollectionInfo" = "This collection features"; +"Gift.Variants.CollectionInfo.Model_1" = "%@ unique model"; +"Gift.Variants.CollectionInfo.Model_any" = "%@ unique models"; +"Gift.Variants.CollectionInfo.Backdrop_1" = "%@ unique backdrop"; +"Gift.Variants.CollectionInfo.Backdrop_any" = "%@ unique backdrops"; +"Gift.Variants.CollectionInfo.Symbol_1" = "%@ unique symbol"; +"Gift.Variants.CollectionInfo.Symbol_any" = "%@ unique symbols"; + +"Gift.Auction.GiftAuction" = "Upcoming Auction"; +"Gift.Auction.UpcomingAuction" = "Upcoming Auction"; +"Gift.Auction.LearnMore" = "Learn more about Telegram Gifts >"; + +"Gift.AuctionBid.UpcomingTitle" = "Place an Early Bid"; + +"Gift.View.Context.BuyOffer" = "Offer to Buy"; + +"Gift.Offer.Title" = "Offer to Buy"; +"Gift.Offer.OfferStars" = "Offer Stars"; +"Gift.Offer.OfferTon" = "Offer TON"; +"Gift.Offer.PriceSectionStars" = "STARS TO OFFER"; +"Gift.Offer.PriceSectionTon" = "TON TO OFFER"; +"Gift.Offer.PricePlaceholder" = "Price"; +"Gift.Offer.PriceDescriptionStars" = "Enter the number of Stars you'd like to offer for **%@**."; +"Gift.Offer.PriceDescriptionTon" = "Enter the number of TON you'd like to offer for **%@**."; +"Gift.Offer.Duration" = "Offer Duration"; +"Gift.Offer.DurationDescriptionStars" = "Choose how long **%@** can accept your offer. When the time expires, your Stars will be automatically refunded."; +"Gift.Offer.DurationDescriptionTon" = "Choose how long **%@** can accept your offer. When the time expires, your TON will be automatically refunded."; +"Gift.Offer.Duration.Hours_1" = "%@ Hour"; +"Gift.Offer.Duration.Hours_any" = "%@ Hours"; +"Gift.Offer.Offer" = "Offer"; + + +"Notification.StarGift.Sold" = "sold"; diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index 0d4deca999..730e74f103 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -717,8 +717,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } text = isStoryEntity ? "**\(item.presentationData.strings.Notification_StarGift_Collectible) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: item.presentationData.dateTimeFormat))**" : "**\(uniqueGift.title) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: item.presentationData.dateTimeFormat))**" if fromOffer { - //TODO:localize - ribbonTitle = incoming ? "" : "sold" + ribbonTitle = incoming ? "" : item.presentationData.strings.Notification_StarGift_Sold customRibbonColors = [UIColor(rgb: 0xd9433a), UIColor(rgb: 0xff645b)] } else { ribbonTitle = isStoryEntity ? "" : item.presentationData.strings.Notification_StarGift_Gift diff --git a/submodules/TelegramUI/Components/Gifts/GiftDemoScreen/Sources/GiftDemoScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftDemoScreen/Sources/GiftDemoScreen.swift index cbbe32fd2a..a026d104eb 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftDemoScreen/Sources/GiftDemoScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftDemoScreen/Sources/GiftDemoScreen.swift @@ -316,7 +316,7 @@ private final class DemoSheetContent: CombinedComponent { playOnce: state.playButtonAnimation )))) buttonTitle.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(ButtonTextContentComponent( - text: strings.Gifts_Demo_Understood, + text: strings.Gift_Demo_Understood, badge: 0, textColor: theme.list.itemCheckColors.foregroundColor, badgeBackground: theme.list.itemCheckColors.foregroundColor, @@ -497,7 +497,7 @@ private final class GiftDemoListComponent: CombinedComponent { var contentSize = CGSize(width: context.availableSize.width, height: 32.0) let title = title.update( - component: MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gifts_Demo_Title, font: Font.bold(25.0), textColor: titleColor))), + component: MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Demo_Title, font: Font.bold(25.0), textColor: titleColor))), availableSize: context.availableSize, transition: context.transition ) @@ -509,7 +509,7 @@ private final class GiftDemoListComponent: CombinedComponent { let description = description.update( component: BalancedTextComponent( - text: .plain(NSAttributedString(string: strings.Gifts_Demo_Description, font: Font.regular(15.0), textColor: textColor)), + text: .plain(NSAttributedString(string: strings.Gift_Demo_Description, font: Font.regular(15.0), textColor: textColor)), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.2 @@ -528,9 +528,9 @@ private final class GiftDemoListComponent: CombinedComponent { AnyComponentWithIdentity( id: "unique", component: AnyComponent(InfoParagraphComponent( - title: strings.Gifts_Demo_Unique_Title, + title: strings.Gift_Demo_Unique_Title, titleColor: titleColor, - text: strings.Gifts_Demo_Unique_Text, + text: strings.Gift_Demo_Unique_Text, textColor: textColor, accentColor: iconColor, iconName: "Premium/Collectible/Unique", @@ -542,9 +542,9 @@ private final class GiftDemoListComponent: CombinedComponent { AnyComponentWithIdentity( id: "tradable", component: AnyComponent(InfoParagraphComponent( - title: strings.Gifts_Demo_Tradable_Title, + title: strings.Gift_Demo_Tradable_Title, titleColor: titleColor, - text: strings.Gifts_Demo_Tradable_Text, + text: strings.Gift_Demo_Tradable_Text, textColor: textColor, accentColor: iconColor, iconName: "Premium/Collectible/Transferable", @@ -556,9 +556,9 @@ private final class GiftDemoListComponent: CombinedComponent { AnyComponentWithIdentity( id: "wearable", component: AnyComponent(InfoParagraphComponent( - title: strings.Gifts_Demo_Wearable_Title, + title: strings.Gift_Demo_Wearable_Title, titleColor: titleColor, - text: strings.Gifts_Demo_Wearable_Text, + text: strings.Gift_Demo_Wearable_Text, textColor: textColor, accentColor: iconColor, iconName: "Premium/Collectible/Tradable", diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift index 1098252814..afd4229888 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift @@ -2697,8 +2697,7 @@ private final class GiftAuctionBidScreenComponent: Component { let titleString: String if isUpcoming { - //TODO:localize - titleString = "Place an Early Bid" + titleString = environment.strings.Gift_AuctionBid_UpcomingTitle } else { titleString = environment.strings.Gift_AuctionBid_Title } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift index 06009d5e2a..1cce9367c2 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift @@ -677,8 +677,7 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { ) )) } else { - //TODO:localize - descriptionText = isUpcoming ? "Upcoming Auction" : "Gift Auction" + descriptionText = isUpcoming ? strings.Gift_Auction_UpcomingAuction : strings.Gift_Auction_GiftAuction tableItems.append(.init( id: "start", @@ -805,8 +804,7 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { state.cachedSmallChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: linkColor)!, theme) } - //TODO:localize - let learnMoreAttributedString = parseMarkdownIntoAttributedString("Learn more about Telegram Gifts >", attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString + let learnMoreAttributedString = parseMarkdownIntoAttributedString(strings.Gift_Auction_LearnMore, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString if let range = learnMoreAttributedString.string.range(of: ">"), let chevronImage = state.cachedSmallChevronImage?.0 { learnMoreAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: learnMoreAttributedString.string)) } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradePreviewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradePreviewScreen.swift index d66281bfea..dc02043e83 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradePreviewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradePreviewScreen.swift @@ -856,81 +856,6 @@ private final class GiftUpgradePreviewScreenComponent: Component { contentHeight += descriptionSize.height contentHeight += 26.0 - - - - //// - //// var validKeys: Set = Set() - //// for auctionState in self.auctionStates { - //// let id = auctionState.gift.giftId - //// validKeys.insert(id) - //// - //// let itemView: ComponentView - //// if let current = self.itemsViews[id] { - //// itemView = current - //// } else { - //// itemView = ComponentView() - //// self.itemsViews[id] = itemView - //// } - //// - //// let itemSize = itemView.update( - //// transition: transition, - //// component: AnyComponent( - //// ActiveAuctionComponent( - //// context: component.context, - //// theme: theme, - //// strings: environment.strings, - //// dateTimeFormat: environment.dateTimeFormat, - //// state: auctionState, - //// currentTime: currentTime, - //// action: { [weak self] in - //// guard let self, let component = self.component else { - //// return - //// } - //// if let giftAuctionsManager = component.context.giftAuctionsManager { - //// let _ = (giftAuctionsManager.auctionContext(for: .giftId(id)) - //// |> deliverOnMainQueue).start(next: { [weak self] auction in - //// guard let self, let component = self.component, let auction, let controller = environment.controller(), let navigationController = controller.navigationController as? NavigationController else { - //// return - //// } - //// let bidController = component.context.sharedContext.makeGiftAuctionBidScreen(context: component.context, toPeerId: auction.currentBidPeerId ?? component.context.account.peerId, text: nil, entities: nil, hideName: false, auctionContext: auction, acquiredGifts: nil) - //// navigationController.pushViewController(bidController) - //// }) - //// } - //// } - //// ) - //// ), - //// environment: {}, - //// containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) - //// ) - //// let itemFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: itemSize) - //// if let view = itemView.view { - //// if view.superview == nil { - //// self.scrollContentView.addSubview(view) - //// } - //// view.frame = itemFrame - //// } - //// contentHeight += itemSize.height - //// contentHeight += 20.0 - //// } - //// contentHeight -= 10.0 - // - // var removeKeys: [Int64] = [] - // for (id, item) in self.itemsViews { - // if !validKeys.contains(id) { - // removeKeys.append(id) - // - // if let itemView = item.view { - // transition.setAlpha(view: itemView, alpha: 0.0, completion: { _ in - // itemView.removeFromSuperview() - // }) - // } - // } - // } - // for id in removeKeys { - // self.itemsViews.removeValue(forKey: id) - // } - if self.backgroundHandleView.image == nil { self.backgroundHandleView.image = generateStretchableFilledCircleImage(diameter: 5.0, color: .white)?.withRenderingMode(.alwaysTemplate) } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 382272a140..514b601653 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -1438,9 +1438,8 @@ private final class GiftViewSheetContent: CombinedComponent { self?.shareGift() }))) - //TODO:localize if case let .unique(uniqueGift) = arguments.gift, case let .peerId(ownerPeerId) = uniqueGift.owner, ownerPeerId != self.context.account.peerId, uniqueGift.minOfferStars != nil { - items.append(.action(ContextMenuActionItem(text: "Offer to Buy", icon: { theme in + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_BuyOffer, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Grid/Paid"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in c?.dismiss(completion: nil) diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift index aaa02178ac..8f22ab47d2 100644 --- a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift @@ -240,18 +240,18 @@ private final class SheetContent: CombinedComponent { amountLabel = "~\(formatTonUsdValue(amount.value, divide: false, rate: usdRate, dateTimeFormat: environment.dateTimeFormat))" } } - case .starGiftOffer: - titleString = "Offer to Buy" + case let .starGiftOffer(_, gift, _): + titleString = environment.strings.Gift_Offer_Title switch state.currency { case .stars: - amountTitle = "STARS TO OFFER" - minAmount = StarsAmount(value: resaleConfiguration.channelMessageSuggestionMinStarsAmount, nanos: 0) + amountTitle = environment.strings.Gift_Offer_PriceSectionStars + minAmount = StarsAmount(value: gift.minOfferStars ?? resaleConfiguration.starGiftResaleMinStarsAmount, nanos: 0) case .ton: - amountTitle = "TON TO OFFER" - minAmount = StarsAmount(value: 0, nanos: 0) + amountTitle = environment.strings.Gift_Offer_PriceSectionTon + minAmount = StarsAmount(value: resaleConfiguration.starGiftResaleMinTonAmount, nanos: 0) } maxAmount = nil - amountPlaceholder = "Price" + amountPlaceholder = environment.strings.Gift_Offer_PricePlaceholder if let usdWithdrawRate = withdrawConfiguration.usdWithdrawRate, let tonUsdRate = withdrawConfiguration.tonUsdRate, let amount = state.amount, amount > StarsAmount.zero { switch state.currency { @@ -358,10 +358,9 @@ private final class SheetContent: CombinedComponent { tonTitle = environment.strings.Chat_PostSuggestion_Suggest_RequestTon } case .starGiftOffer: - //TODO:localize displayCurrencySelector = true - starsTitle = "Offer Stars" - tonTitle = "Offer TON" + starsTitle = environment.strings.Gift_Offer_OfferStars + tonTitle = environment.strings.Gift_Offer_OfferTon default: starsTitle = "" tonTitle = "" @@ -555,9 +554,9 @@ private final class SheetContent: CombinedComponent { let string: String switch state.currency { case .stars: - string = "Enter the number of Stars you'd like to offer for **\(giftTitle)**." + string = environment.strings.Gift_Offer_PriceDescriptionStars(giftTitle).string case .ton: - string = "Enter the number of TON you'd like to offer for **\(giftTitle)**." + string = environment.strings.Gift_Offer_PriceDescriptionTon(giftTitle).string } let amountInfoString = NSAttributedString(attributedString: parseMarkdownIntoAttributedString(string, attributes: amountMarkdownAttributes, textAlignment: .natural)) amountFooter = AnyComponent(MultilineTextComponent( @@ -801,9 +800,9 @@ private final class SheetContent: CombinedComponent { let footerString: String switch state.currency { case .stars: - footerString = "Choose how long **\(peerName)** can accept your offer. When the time expires, your Stars will be automatically refunded." + footerString = environment.strings.Gift_Offer_DurationDescriptionStars(peerName).string case .ton: - footerString = "Choose how long **\(peerName)** can accept your offer. When the time expires, your TON will be automatically refunded." + footerString = environment.strings.Gift_Offer_DurationDescriptionTon(peerName).string } let periodFooterString = NSAttributedString(attributedString: parseMarkdownIntoAttributedString(footerString, attributes: amountMarkdownAttributes, textAlignment: .natural)) @@ -813,12 +812,7 @@ private final class SheetContent: CombinedComponent { )) let hours = state.duration / 3600 - let durationString: String - if hours == 1 { - durationString = "1 Hour" - } else { - durationString = "\(hours) Hours" - } + let durationString = environment.strings.Gift_Offer_Duration_Hours(hours) let periodSection = periodSection.update( component: ListSectionComponent( @@ -834,7 +828,7 @@ private final class SheetContent: CombinedComponent { title: AnyComponent(VStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: "Offer Duration", + string: environment.strings.Gift_Offer_Duration, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor )), @@ -930,9 +924,9 @@ private final class SheetContent: CombinedComponent { currencySymbol = "$" currencyAmount = formatTonAmountText(amount.value, dateTimeFormat: environment.dateTimeFormat, maxDecimalPositions: nil) } - buttonString = "Offer \(currencySymbol) \(currencyAmount)" + buttonString = "\(environment.strings.Gift_Offer_Offer) \(currencySymbol) \(currencyAmount)" } else { - buttonString = "Offer" + buttonString = environment.strings.Gift_Offer_Offer } } else if let amount = state.amount { buttonString = "\(environment.strings.Stars_Withdraw_Withdraw) # \(presentationStringsFormattedNumber(amount, environment.dateTimeFormat.groupingSeparator))" @@ -1085,10 +1079,11 @@ private final class SheetContent: CombinedComponent { if state.isPickingDuration { + let sourceFrame = CGRect(origin: CGPoint(x: durationFrame.maxX - 52.0, y: durationFrame.minY + 120.0), size: CGSize(width: 52.0, height: 52.0)) let durationPicker = durationPicker.update( component: MenuComponent( theme: theme, - sourceFrame: durationFrame.offsetBy(dx: 0.0, dy: 120.0), + sourceFrame: sourceFrame, component: AnyComponent(DurationMenuComponent( theme: theme, strings: environment.strings, @@ -1147,6 +1142,7 @@ private final class SheetContent: CombinedComponent { fileprivate var component: SheetContent + fileprivate var forceUpdateAmount = false fileprivate var amount: StarsAmount? fileprivate var currency: CurrencyAmount.Currency = .stars fileprivate var timestamp: Int32? @@ -1246,7 +1242,7 @@ private final class SheetContent: CombinedComponent { self.currency = .stars } } else { - let _ = (context.engine.payments.cachedStarGifts() + let _ = (self.context.engine.payments.cachedStarGifts() |> filter { $0 != nil } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] gifts in @@ -1264,6 +1260,7 @@ private final class SheetContent: CombinedComponent { } if case let .generic(genericGift) = matchingGift, let minResaleStars = genericGift.availability?.minResaleStars { self.amount = StarsAmount(value: max(minResaleStars, resaleConfiguration.starGiftResaleMinStarsAmount), nanos: 0) + self.forceUpdateAmount = true self.updated() } }) @@ -1417,6 +1414,7 @@ public final class StarsWithdrawScreen: ViewControllerComponentContainer { if let view = self.node.hostView.findTaggedView(tag: amountTag) as? AmountFieldComponent.View { Queue.mainQueue().after(0.01) { + view.resetValue() view.activateInput() view.selectAll() } @@ -1810,6 +1808,7 @@ public final class AmountFieldComponent: Component { return } self.textField.text = "\(value)" + self.placeholderView.view?.isHidden = self.textField.text?.isEmpty ?? false } func update(component: AmountFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { @@ -1829,10 +1828,10 @@ public final class AmountFieldComponent: Component { text = "\(formatTonAmountText(value, dateTimeFormat: PresentationDateTimeFormat(timeFormat: component.dateTimeFormat.timeFormat, dateFormat: component.dateTimeFormat.dateFormat, dateSeparator: "", dateSuffix: "", requiresFullYear: false, decimalSeparator: ".", groupingSeparator: ""), maxDecimalPositions: nil))" } self.textField.text = text - self.didSetValueOnce = true } else { self.textField.text = "" } + self.didSetValueOnce = true } self.textField.font = Font.regular(17.0) From 2cb0c7f147154e6db1b90217d2930b6d84715ff4 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 4 Dec 2025 07:56:44 +0400 Subject: [PATCH 8/9] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 45 +++- ...hatMessageGiftOfferBubbleContentNode.swift | 28 +- .../Sources/GiftItemComponent.swift | 4 +- .../Sources/GiftSetupScreen.swift | 30 ++- .../Sources/GiftAuctionBidScreen.swift | 14 +- .../Sources/GiftAuctionViewScreen.swift | 168 ++++++------ .../GiftAuctionWearPreviewScreen.swift | 9 +- .../Sources/GiftOfferAlertController.swift | 85 +++++-- .../Sources/GiftUpgradePreviewScreen.swift | 240 ++++++++++++++---- .../Sources/GlassBarButtonComponent.swift | 1 + .../Sources/StarsWithdrawalScreen.swift | 16 +- .../TelegramUI/Sources/ChatController.swift | 16 +- 12 files changed, 464 insertions(+), 192 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 11ad1c5527..ab4058955f 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -15517,6 +15517,9 @@ Error: %8$@"; "Attachment.FilesSearchPlaceholder" = "Search shared audio"; "Chat.GiftPurchaseOffer.Reject" = "Reject"; "Chat.GiftPurchaseOffer.Accept" = "Accept"; +"Chat.GiftPurchaseOffer.RejectConfirmation.Title" = "Reject Offer"; +"Chat.GiftPurchaseOffer.RejectConfirmation.Text" = "Are you sure you want to reject the offer from **%@**?"; +"Chat.GiftPurchaseOffer.RejectConfirmation.Reject" = "Reject"; "Passkeys.DeleteAlert.Title" = "Delete Passkey?"; "Passkeys.DeleteAlert.Text" = "Once deleted, this passkey can't be used to log in.\n\nDon't forget to remove it from your password manager too."; @@ -15559,13 +15562,13 @@ Error: %8$@"; "Gift.Variants.Backdrops" = "Backdrops"; "Gift.Variants.Symbols" = "Symbols"; -"Gift.Variants.CollectionInfo" = "This collection features"; -"Gift.Variants.CollectionInfo.Model_1" = "%@ unique model"; -"Gift.Variants.CollectionInfo.Model_any" = "%@ unique models"; -"Gift.Variants.CollectionInfo.Backdrop_1" = "%@ unique backdrop"; -"Gift.Variants.CollectionInfo.Backdrop_any" = "%@ unique backdrops"; -"Gift.Variants.CollectionInfo.Symbol_1" = "%@ unique symbol"; -"Gift.Variants.CollectionInfo.Symbol_any" = "%@ unique symbols"; +"Gift.Variants.CollectionInfo" = "This collection features %@"; +"Gift.Variants.CollectionInfo.Model_1" = "**%@** unique model"; +"Gift.Variants.CollectionInfo.Model_any" = "**%@** unique models"; +"Gift.Variants.CollectionInfo.Backdrop_1" = "**%@** unique backdrop"; +"Gift.Variants.CollectionInfo.Backdrop_any" = "**%@** unique backdrops"; +"Gift.Variants.CollectionInfo.Symbol_1" = "**%@** unique symbol"; +"Gift.Variants.CollectionInfo.Symbol_any" = "**%@** unique symbols"; "Gift.Auction.GiftAuction" = "Upcoming Auction"; "Gift.Auction.UpcomingAuction" = "Upcoming Auction"; @@ -15589,6 +15592,34 @@ Error: %8$@"; "Gift.Offer.Duration.Hours_1" = "%@ Hour"; "Gift.Offer.Duration.Hours_any" = "%@ Hours"; "Gift.Offer.Offer" = "Offer"; +"Gift.Offer.GiftMinAmountToast.Text" = "You cannot offer less than %@ for this gift"; +"Gift.WearPreview.Limited" = "limited"; +"Gift.WearPreview.Upgraded" = "upgraded"; +"Gift.WearPreview.FreeUpgrade" = "Free\nUpgrade"; +"Gift.WearPreview.LearnMore" = "Learn more about wearing Telegram Gifts >"; "Notification.StarGift.Sold" = "sold"; +"Notification.StarGiftOffer.Offer" = "**%1$@** offered you **%2$@** for your gift **%3$@**."; +"Notification.StarGiftOffer.OfferYou" = "You offered **%1$@** **%2$@** for their gift **%3$@**."; +"Notification.StarGiftOffer.Offer.Stars_1" = "%@ Star"; +"Notification.StarGiftOffer.Offer.Stars_any" = "%@ Stars"; + +"Notification.StarGiftOffer.Status.Accepted" = "This offer was accepted."; +"Notification.StarGiftOffer.Status.Expired" = "This offer has expired."; +"Notification.StarGiftOffer.Status.Expires" = "This offer expires in %@"; +"Notification.StarGiftOffer.Status.Rejected" = "This offer was rejected."; + +"Notification.StarGiftOffer.Expiration.Hours_1" = "%@ h"; +"Notification.StarGiftOffer.Expiration.Hours_any" = "%@ h"; +"Notification.StarGiftOffer.Expiration.Minutes_1" = "%@ m"; +"Notification.StarGiftOffer.Expiration.Minutes_any" = "%@ m"; +"Notification.StarGiftOffer.Expiration.Delimiter" = ""; + +"Chat.GiftPurchaseOffer.AcceptConfirmation.Title" = "Confirm Sale"; +"Chat.GiftPurchaseOffer.AcceptConfirmation.Text" = "Do you want to sell **%1$@** to %2$@ for **%3$@**? You'll receive **%4$@** after fees."; +"Chat.GiftPurchaseOffer.AcceptConfirmation.Text.Stars_1" = "%@ Star"; +"Chat.GiftPurchaseOffer.AcceptConfirmation.Text.Stars_any" = "%@ Stars"; +"Chat.GiftPurchaseOffer.AcceptConfirmation.BadValue" = "The value of this gift is **%@** higher than the offer."; +"Chat.GiftPurchaseOffer.AcceptConfirmation.Confirm" = "Confirm Sale"; + diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftOfferBubbleContentNode/Sources/ChatMessageGiftOfferBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftOfferBubbleContentNode/Sources/ChatMessageGiftOfferBubbleContentNode.swift index 2372c3183d..f50e5b49c3 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftOfferBubbleContentNode/Sources/ChatMessageGiftOfferBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftOfferBubbleContentNode/Sources/ChatMessageGiftOfferBubbleContentNode.swift @@ -129,7 +129,7 @@ public class ChatMessageGiftOfferBubbleContentNode: ChatMessageBubbleContentNode let priceString: String switch amount.currency { case .stars: - priceString = "\(amount.amount) Stars" + priceString = item.presentationData.strings.Notification_StarGiftOffer_Offer_Stars(Int32(clamping: amount.amount.value)) case .ton: priceString = "\(amount.amount) TON" } @@ -143,41 +143,35 @@ public class ChatMessageGiftOfferBubbleContentNode: ChatMessageBubbleContentNode giftTitle = "" } - //TODO:localize if incoming { - text = "**\(peerName)** offered you **\(priceString)** for your gift **\(giftTitle)**." + text = item.presentationData.strings.Notification_StarGiftOffer_Offer(peerName, priceString, giftTitle).string } else { - text = "You offered **\(peerName)** **\(priceString)** for their gift **\(giftTitle)**." + text = item.presentationData.strings.Notification_StarGiftOffer_OfferYou(peerName, priceString, giftTitle).string } if isAccepted { - additionalText = "This offer was accepted." + additionalText = item.presentationData.strings.Notification_StarGiftOffer_Status_Accepted } else if isDeclined { - additionalText = "This offer was rejected." + additionalText = item.presentationData.strings.Notification_StarGiftOffer_Status_Rejected } else if expireDate > currentTimestamp { func textForTimeout(_ value: Int32) -> String { if value < 3600 { let minutes = value / 60 - //let seconds = value % 60 - //let secondsPadding = seconds < 10 ? "0" : "" - return "\(minutes)m" //\(secondsPadding)\(seconds)" + return item.presentationData.strings.Notification_StarGiftOffer_Expiration_Minutes(minutes) } else { let hours = value / 3600 let minutes = (value % 3600) / 60 - let minutesPadding = minutes < 10 ? "0" : "" - //let seconds = value % 60 - //let secondsPadding = seconds < 10 ? "0" : "" - return "\(hours)h \(minutesPadding)\(minutes)m" //:\(secondsPadding)\(seconds)" + return item.presentationData.strings.Notification_StarGiftOffer_Expiration_Hours(hours) + item.presentationData.strings.Notification_StarGiftOffer_Expiration_Delimiter + item.presentationData.strings.Notification_StarGiftOffer_Expiration_Minutes(minutes) } } let delta = expireDate - currentTimestamp - additionalText = "This offer expires in \(textForTimeout(delta))." - + additionalText = item.presentationData.strings.Notification_StarGiftOffer_Status_Expires(textForTimeout(delta)).string + if incoming { hasActionButtons = true } } else { - additionalText = "This offer has expired." + additionalText = item.presentationData.strings.Notification_StarGiftOffer_Status_Expired } } else { text = "" @@ -277,8 +271,6 @@ public class ChatMessageGiftOfferBubbleContentNode: ChatMessageBubbleContentNode ) if let giftIconView = strongSelf.giftIcon.view { if giftIconView.superview == nil { - // backgroundView.layer.cornerRadius = 20.0 - //backgroundView.clipsToBounds = true strongSelf.view.addSubview(giftIconView) } giftIconView.frame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - iconSize.width) / 2.0), y: mediaBackgroundFrame.minY + 17.0), size: iconSize) diff --git a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift index 301648cda5..55b04a7ee3 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift @@ -601,7 +601,9 @@ public final class GiftItemComponent: Component { } case let .preview(attributes, _): animationOffset = 16.0 - explicitAnimationOffset = -4.0 + if component.mode != .tableIcon { + explicitAnimationOffset = -4.0 + } for attribute in attributes { switch attribute { case let .model(_, file, _): diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index 2a8e16aa7e..3df1b80480 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -1727,7 +1727,7 @@ private final class GiftSetupScreenComponent: Component { inactiveValue: "", inactiveTitleColor: theme.list.itemSecondaryTextColor, activeTitle: "", - activeValue: environment.strings.Gift_Send_Sold(sold), + activeValue: sold > 0 ? environment.strings.Gift_Send_Sold(sold) : "", activeTitleColor: .white, badgeText: "", badgePosition: position, @@ -1830,22 +1830,30 @@ private final class GiftSetupScreenComponent: Component { var buttonTitleItems: [AnyComponentWithIdentity] = [] if let _ = self.giftAuction { - let buttonAttributedString = NSMutableAttributedString(string: environment.strings.Gift_Setup_PlaceBid, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) - buttonTitleItems.append(AnyComponentWithIdentity(id: "bid", component: AnyComponent( - MultilineTextComponent(text: .plain(buttonAttributedString)) - ))) + var isUpcoming = false if let giftAuctionState = self.giftAuctionState { switch giftAuctionState.auctionState { - case let .ongoing(_, _, endTime, _, _, _, _, _, _, _, _, _): + case let .ongoing(_, startTime, endTime, _, _, _, _, _, _, _, _, _): let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) - - let endTimeout = max(0, endTime - currentTime) + let endTimeout: Int32 + if currentTime < startTime { + endTimeout = max(0, startTime - currentTime) + isUpcoming = true + } else { + endTimeout = max(0, endTime - currentTime) + } let hours = Int(endTimeout / 3600) let minutes = Int((endTimeout % 3600) / 60) let seconds = Int(endTimeout % 60) - let rawString = hours > 0 ? environment.strings.Gift_Auction_TimeLeftHours : environment.strings.Gift_Auction_TimeLeftMinutes + let rawString: String + if isUpcoming { + rawString = hours > 0 ? environment.strings.Gift_Auction_StartsInHours : environment.strings.Gift_Auction_StartsInMinutes + } else { + rawString = hours > 0 ? environment.strings.Gift_Auction_TimeLeftHours : environment.strings.Gift_Auction_TimeLeftMinutes + } + var buttonAnimatedTitleItems: [AnimatedTextComponent.Item] = [] var startIndex = rawString.startIndex while true { @@ -1885,6 +1893,10 @@ private final class GiftSetupScreenComponent: Component { buttonIsEnabled = false } } + let buttonAttributedString = NSMutableAttributedString(string: isUpcoming ? environment.strings.Gift_Auction_EarlyBid : environment.strings.Gift_Setup_PlaceBid, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) + buttonTitleItems.insert(AnyComponentWithIdentity(id: "bid", component: AnyComponent( + MultilineTextComponent(text: .plain(buttonAttributedString)) + )), at: 0) } else { let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) if let range = buttonAttributedString.string.range(of: "#"), let starImage = self.cachedStarImage?.0 { diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift index afd4229888..9863ae0d7e 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift @@ -2493,7 +2493,19 @@ private final class GiftAuctionBidScreenComponent: Component { untilNextRoundAnimatedItems.append(AnimatedTextComponent.Item(id: "s", content: .number(seconds, minDigits: 2))) } - dropsLeftAnimatedItems = [AnimatedTextComponent.Item(id: "drops", content: .number(Int(dropsLeft), minDigits: 1))] + if dropsLeft >= 10000 { + var compactString = compactNumericCountString(Int(dropsLeft), decimalSeparator: ".", showDecimalPart: false) + let suffix = String(compactString.suffix(1)) + compactString.removeLast() + if let value = Int(compactString) { + dropsLeftAnimatedItems = [ + AnimatedTextComponent.Item(id: "drops", content: .number(value, minDigits: 1)), + AnimatedTextComponent.Item(id: "suffix", content: .text(suffix)) + ] + } + } else { + dropsLeftAnimatedItems = [AnimatedTextComponent.Item(id: "drops", content: .number(Int(dropsLeft), minDigits: 1))] + } } auctionStats.append(( diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift index 1cce9367c2..4d11e0eef5 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift @@ -78,6 +78,8 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { private(set) var previewBackdropIndex: Int = -1 private(set) var previewSymbolIndex: Int = -1 + private var disposables = DisposableSet() + private var fetchedFiles = Set() private(set) var previewModels: [StarGift.UniqueGift.Attribute] = [] private(set) var previewBackdrops: [StarGift.UniqueGift.Attribute] = [] private(set) var previewSymbols: [StarGift.UniqueGift.Attribute] = [] @@ -154,6 +156,16 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { }).shuffled().prefix(5)) self.previewSymbols = randomSymbols + for case let .model(_, file, _) in self.previewModels where !self.fetchedFiles.contains(file.fileId.id) { + self.disposables.add(freeMediaFileResourceInteractiveFetched(account: context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start()) + self.fetchedFiles.insert(file.fileId.id) + } + + for case let .pattern(_, file, _) in self.previewSymbols where !self.fetchedFiles.contains(file.fileId.id) { + self.disposables.add(freeMediaFileResourceInteractiveFetched(account: context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start()) + self.fetchedFiles.insert(file.fileId.id) + } + self.updated() }) @@ -169,6 +181,7 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { deinit { self.disposable?.dispose() + self.disposables.dispose() self.giftAuctionAcquiredGiftsDisposable.dispose() self.giftAuctionTimer?.invalidate() self.previewTimer?.invalidate() @@ -509,78 +522,6 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { var buttonColor: UIColor = .white.withAlphaComponent(0.1) var secondaryTextColor: UIColor = .white.withAlphaComponent(0.4) - if let genericGift { - var attributes: [StarGift.UniqueGift.Attribute] = [] - if state.previewModelIndex == -1 { - attributes.append(.model(name: "", file: genericGift.file, rarity: 0)) - if let background = genericGift.background { - attributes.append(.backdrop(name: "", id: 0, innerColor: background.centerColor, outerColor: background.edgeColor, patternColor: 0, textColor: 0, rarity: 0)) - } - } else if !state.previewModels.isEmpty { - attributes.append(state.previewModels[state.previewModelIndex]) - if !state.previewBackdrops.isEmpty { - attributes.append(state.previewBackdrops[state.previewBackdropIndex]) - } - if !state.previewSymbols.isEmpty { - attributes.append(state.previewSymbols[state.previewSymbolIndex]) - } - } - - if let backdropAttribute = attributes.first(where: { attribute in - if case .backdrop = attribute { - return true - } else { - return false - } - }), case let .backdrop(_, _, innerColor, outerColor, _, _, _) = backdropAttribute { - let topColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultiplied(hue: 1.01, saturation: 1.22, brightness: 1.04) - let bottomColor = UIColor(rgb: UInt32(bitPattern: outerColor)).withMultiplied(hue: 0.97, saturation: 1.45, brightness: 0.89) - buttonColor = topColor.mixedWith(bottomColor, alpha: 0.8).withMultipliedBrightnessBy(1.25) - - secondaryTextColor = topColor.withMultiplied(hue: 1.0, saturation: 1.02, brightness: 1.25).mixedWith(UIColor.white, alpha: 0.5) - } - - let header = header.update( - component: GiftCompositionComponent( - context: component.context, - theme: theme, - subject: .preview(attributes), - animationOffset: CGPoint(x: 0.0, y: -4.0), - animationScale: nil, - displayAnimationStars: false, - revealedAttributes: Set(), - externalState: giftCompositionExternalState, - requestUpdate: { [weak state] transition in - state?.updated(transition: transition) - } - ), - availableSize: CGSize(width: context.availableSize.width, height: 264.0), - transition: context.transition - ) - context.add(header - .position(CGPoint(x: context.availableSize.width / 2.0, y: header.size.height * 0.5)) - ) - originY += header.size.height - originY += 16.0 - } - - let title = title.update( - component: MultilineTextComponent( - text: .plain(NSAttributedString( - string: titleString, - font: Font.bold(20.0), - textColor: .white, - paragraphAlignment: .center - )), - horizontalAlignment: .center, - maximumNumberOfLines: 1 - ), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), - transition: .immediate - ) - context.add(title - .position(CGPoint(x: context.availableSize.width / 2.0, y: 209.0)) - ) var descriptionText: String = "" @@ -771,14 +712,89 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { } } } - + + if let genericGift { + var attributes: [StarGift.UniqueGift.Attribute] = [] + if state.previewModelIndex == -1 { + attributes.append(.model(name: "", file: genericGift.file, rarity: 0)) + if let background = genericGift.background { + attributes.append(.backdrop(name: "", id: 0, innerColor: background.centerColor, outerColor: background.edgeColor, patternColor: 0, textColor: 0, rarity: 0)) + } + } else if !state.previewModels.isEmpty { + attributes.append(state.previewModels[state.previewModelIndex]) + if !state.previewBackdrops.isEmpty { + attributes.append(state.previewBackdrops[state.previewBackdropIndex]) + } + if !state.previewSymbols.isEmpty { + attributes.append(state.previewSymbols[state.previewSymbolIndex]) + } + } + + if let backdropAttribute = attributes.first(where: { attribute in + if case .backdrop = attribute { + return true + } else { + return false + } + }), case let .backdrop(_, _, innerColor, outerColor, _, _, _) = backdropAttribute { + let topColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultiplied(hue: 1.01, saturation: 1.22, brightness: 1.04) + let bottomColor = UIColor(rgb: UInt32(bitPattern: outerColor)).withMultiplied(hue: 0.97, saturation: 1.45, brightness: 0.89) + buttonColor = topColor.mixedWith(bottomColor, alpha: 0.8).withMultipliedBrightnessBy(1.25) + + secondaryTextColor = topColor.withMultiplied(hue: 1.0, saturation: 1.02, brightness: 1.25).mixedWith(UIColor.white, alpha: 0.5) + } + + let headerHeight: CGFloat = 264.0 + + let header = header.update( + component: GiftCompositionComponent( + context: component.context, + theme: theme, + subject: .preview(attributes), + animationOffset: CGPoint(x: 0.0, y: -4.0), + animationScale: nil, + displayAnimationStars: false, + revealedAttributes: Set(), + externalState: giftCompositionExternalState, + requestUpdate: { [weak state] transition in + state?.updated(transition: transition) + } + ), + availableSize: CGSize(width: context.availableSize.width, height: headerHeight), + transition: context.transition + ) + context.add(header + .position(CGPoint(x: context.availableSize.width / 2.0, y: header.size.height * 0.5)) + ) + originY += header.size.height + originY += 16.0 + } + + let title = title.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: titleString, + font: Font.bold(20.0), + textColor: .white, + paragraphAlignment: .center + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: 209.0)) + ) + let description = description.update( component: StarsButtonContentComponent( context: component.context, text: descriptionText, color: buttonColor, tinted: true, - starsColor: UIColor(white: 1.0, alpha: 0.5), + starsColor: isEnded ? .clear : UIColor(white: 1.0, alpha: 0.5), font: Font.medium(13.0), height: 24.0 ), @@ -1198,7 +1214,7 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { component: GlassBarButtonComponent( size: CGSize(width: 40.0, height: 40.0), backgroundColor: buttonColor, - isDark: theme.overallDarkAppearance, + isDark: false, state: .tintedGlass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( @@ -1224,7 +1240,7 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { component: GlassBarButtonComponent( size: CGSize(width: 40.0, height: 40.0), backgroundColor: buttonColor, - isDark: theme.overallDarkAppearance, + isDark: false, state: .tintedGlass, component: AnyComponentWithIdentity(id: "more", component: AnyComponent( LottieComponent( diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionWearPreviewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionWearPreviewScreen.swift index c3a54bbdbc..ffb88c3b4b 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionWearPreviewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionWearPreviewScreen.swift @@ -335,8 +335,7 @@ private final class GiftAuctionWearPreviewSheetContent: CombinedComponent { state.cachedSmallChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: linkColor)!, theme) } - //TODO:localize - let learnMoreAttributedString = parseMarkdownIntoAttributedString("Learn more about wearing Telegram Gifts >", attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString + let learnMoreAttributedString = parseMarkdownIntoAttributedString(strings.Gift_WearPreview_LearnMore, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString if let range = learnMoreAttributedString.string.range(of: ">"), let chevronImage = state.cachedSmallChevronImage?.0 { learnMoreAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: learnMoreAttributedString.string)) } @@ -367,7 +366,7 @@ private final class GiftAuctionWearPreviewSheetContent: CombinedComponent { theme: theme, strings: strings, subject: .starGift(gift: gift, price: ""), - ribbon: GiftItemComponent.Ribbon(text: "limited", color: .blue), + ribbon: GiftItemComponent.Ribbon(text: strings.Gift_WearPreview_Limited, color: .blue), mode: .thumbnail ), availableSize: CGSize(width: 120.0, height: 120.0), @@ -391,7 +390,7 @@ private final class GiftAuctionWearPreviewSheetContent: CombinedComponent { theme: theme, strings: strings, subject: .preview(attributes: attributes, rarity: 0), - ribbon: GiftItemComponent.Ribbon(text: "upgraded", color: ribbonColor), + ribbon: GiftItemComponent.Ribbon(text: strings.Gift_WearPreview_Upgraded, color: ribbonColor), animateChanges: true, mode: .thumbnail ), @@ -414,7 +413,7 @@ private final class GiftAuctionWearPreviewSheetContent: CombinedComponent { let upgradeLabel = upgradeLabel.update( component: MultilineTextComponent( - text: .plain(NSAttributedString(string: "Free\nUpgrade", font: Font.medium(13.0), textColor: theme.list.itemSecondaryTextColor)), + text: .plain(NSAttributedString(string: strings.Gift_WearPreview_FreeUpgrade, font: Font.medium(13.0), textColor: theme.list.itemSecondaryTextColor)), horizontalAlignment: .center, maximumNumberOfLines: 2, lineSpacing: 0.1 diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftOfferAlertController.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftOfferAlertController.swift index 98942c36a1..7f9a368d2a 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftOfferAlertController.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftOfferAlertController.swift @@ -16,6 +16,7 @@ import ChatMessagePaymentAlertController import ActivityIndicator import TooltipUI import MultilineTextComponent +import BalancedTextComponent import TelegramStringFormatting private final class GiftOfferAlertContentNode: AlertContentNode { @@ -24,6 +25,7 @@ private final class GiftOfferAlertContentNode: AlertContentNode { private var presentationTheme: PresentationTheme private let title: String private let text: String + private let amount: CurrencyAmount private let gift: StarGift.UniqueGift private let titleNode: ASTextNode @@ -32,6 +34,7 @@ private final class GiftOfferAlertContentNode: AlertContentNode { private let arrowNode: ASImageNode private let avatarNode: AvatarNode private let tableView = ComponentView() + private let valueDelta = ComponentView() private let modelButtonTag = GenericComponentViewTag() private let backdropButtonTag = GenericComponentViewTag() @@ -68,6 +71,7 @@ private final class GiftOfferAlertContentNode: AlertContentNode { peer: EnginePeer, title: String, text: String, + amount: CurrencyAmount, actions: [TextAlertAction] ) { self.context = context @@ -75,6 +79,7 @@ private final class GiftOfferAlertContentNode: AlertContentNode { self.presentationTheme = ptheme self.title = title self.text = text + self.amount = amount self.gift = gift self.titleNode = ASTextNode() @@ -339,17 +344,6 @@ private final class GiftOfferAlertContentNode: AlertContentNode { } } - if let valueAmount = self.gift.valueAmount, let valueCurrency = self.gift.valueCurrency { - tableItems.append(.init( - id: "fiatValue", - title: strings.Gift_Unique_Value, - component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: "~\(formatCurrencyAmount(valueAmount, currency: valueCurrency))", font: tableFont, textColor: tableTextColor))) - ), - insets: UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 12.0) - )) - } - let tableSize = self.tableView.update( transition: .immediate, component: AnyComponent( @@ -370,7 +364,57 @@ private final class GiftOfferAlertContentNode: AlertContentNode { view.frame = tableFrame } - let resultSize = CGSize(width: contentWidth, height: avatarSize.height + titleSize.height + textSize.height + tableSize.height + actionsHeight + 40.0 + insets.top + insets.bottom) + var valueDeltaHeight: CGFloat = 0.0 + if let valueAmount = self.gift.valueUsdAmount { + let resaleConfiguration = StarsSubscriptionConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 }) + + let usdRate: Double + switch self.amount.currency { + case .stars: + usdRate = Double(resaleConfiguration.usdWithdrawRate) / 1000.0 / 100.0 + case .ton: + usdRate = Double(resaleConfiguration.tonUsdRate) / 1000.0 / 1000000.0 + } + let offerUsdValue = Double(self.amount.amount.value) * usdRate + let giftUsdValue = Double(valueAmount) / 100.0 + + let fraction = giftUsdValue / offerUsdValue + let percentage = Int(fraction * 100) - 100 + + if percentage > 20 { + let textColor = self.presentationTheme.list.itemDestructiveColor + let markdownAttributes = MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: textColor), + bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: textColor), + link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: textColor), + linkAttribute: { url in + return ("URL", url) + } + ) + let valueDeltaSize = self.valueDelta.update( + transition: .immediate, + component: AnyComponent( + BalancedTextComponent( + text: .markdown(text: strings.Chat_GiftPurchaseOffer_AcceptConfirmation_BadValue("\(percentage)%").string, attributes: markdownAttributes), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + ) + ), + environment: {}, + containerSize: CGSize(width: contentWidth - 32.0, height: size.height) + ) + let valueDeltaFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - valueDeltaSize.width) / 2.0), y: avatarSize.height + titleSize.height + textSize.height + 73.0 + tableSize.height), size: valueDeltaSize) + if let view = self.valueDelta.view { + if view.superview == nil { + self.view.addSubview(view) + } + view.frame = valueDeltaFrame + } + valueDeltaHeight += valueDeltaSize.height + 10.0 + } + } + + let resultSize = CGSize(width: contentWidth, height: avatarSize.height + titleSize.height + textSize.height + tableSize.height + actionsHeight + valueDeltaHeight + 40.0 + insets.top + insets.bottom) transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) var actionOffset: CGFloat = 0.0 @@ -380,14 +424,14 @@ private final class GiftOfferAlertContentNode: AlertContentNode { if separatorIndex >= 0 { let separatorNode = self.actionVerticalSeparators[separatorIndex] do { - transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) } } separatorIndex += 1 let currentActionWidth: CGFloat do { - currentActionWidth = resultSize.width + currentActionWidth = resultSize.width } let actionNodeFrame: CGRect @@ -433,13 +477,13 @@ public func giftOfferAlertController( let presentationData = context.sharedContext.currentPresentationData.with { $0 } let strings = presentationData.strings - let title = "Confirm Sale" - let buttonText: String = "Confirm Sale" + let title = strings.Chat_GiftPurchaseOffer_AcceptConfirmation_Title + let buttonText: String = strings.Chat_GiftPurchaseOffer_AcceptConfirmation_Confirm let priceString: String switch amount.currency { case .stars: - priceString = "\(amount.amount) Stars" + priceString = strings.Chat_GiftPurchaseOffer_AcceptConfirmation_Text_Stars(Int32(clamping: amount.amount.value)) case .ton: priceString = "\(amount.amount) TON" } @@ -449,15 +493,14 @@ public func giftOfferAlertController( switch amount.currency { case .stars: let starsValue = Int32(floor(Float(amount.amount.value) * Float(resaleConfiguration.starGiftCommissionStarsPermille) / 1000.0)) - finalPriceString = "\(starsValue) Stars" + finalPriceString = strings.Chat_GiftPurchaseOffer_AcceptConfirmation_Text_Stars(starsValue) case .ton: let tonValue = Int64(Float(amount.amount.value) * Float(resaleConfiguration.starGiftCommissionTonPermille) / 1000.0) finalPriceString = formatTonAmountText(tonValue, dateTimeFormat: presentationData.dateTimeFormat, maxDecimalPositions: 3) + " TON" } let giftTitle = "\(gift.title) #\(formatCollectibleNumber(gift.number, dateTimeFormat: presentationData.dateTimeFormat))" - let text = "Do you want to sell **\(giftTitle)** to \(peer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder)) for **\(priceString)**? You'll receive **\(finalPriceString)** after fees." - + let text = strings.Chat_GiftPurchaseOffer_AcceptConfirmation_Text(giftTitle, peer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder), priceString, finalPriceString).string var contentNode: GiftOfferAlertContentNode? var dismissImpl: ((Bool) -> Void)? @@ -468,7 +511,7 @@ public func giftOfferAlertController( dismissImpl?(true) })] - contentNode = GiftOfferAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: strings, gift: gift, peer: peer, title: title, text: text, actions: actions) + contentNode = GiftOfferAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: strings, gift: gift, peer: peer, title: title, text: text, amount: amount, actions: actions) let controller = ChatMessagePaymentAlertController(context: context, presentationData: presentationData, contentNode: contentNode!, navigationController: nil, chatPeerId: context.account.peerId, showBalance: false) contentNode?.getController = { [weak controller] in diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradePreviewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradePreviewScreen.swift index dc02043e83..89a13a0c49 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradePreviewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradePreviewScreen.swift @@ -94,6 +94,8 @@ private final class GiftUpgradePreviewScreenComponent: Component { private var attributeInfos: [ComponentView] = [] + private let topEdgeSolidView = UIView() + private let topEdgeEffectView: EdgeEffectView private let segmentControl = ComponentView() private let descriptionText = ComponentView() @@ -146,6 +148,9 @@ private final class GiftUpgradePreviewScreenComponent: Component { self.navigationBarContainer = SparseContainerView() + self.topEdgeEffectView = EdgeEffectView() + self.topEdgeEffectView.alpha = 0.0 + self.glassContainerView = GlassBackgroundContainerView() self.scrollView = ScrollView() @@ -235,6 +240,8 @@ private final class GiftUpgradePreviewScreenComponent: Component { var topOffsetFraction = self.scrollView.bounds.minY / 100.0 topOffsetFraction = max(0.0, min(1.0, topOffsetFraction)) + self.topEdgeEffectView.alpha = max(0.0, min(1.0, self.scrollView.bounds.minY / 8.0)) + let minScale: CGFloat = (itemLayout.containerSize.width - 6.0 * 2.0) / itemLayout.containerSize.width let minScaledTranslation: CGFloat = (itemLayout.containerSize.height - itemLayout.containerSize.height * minScale) * 0.5 - 6.0 let minScaledCornerRadius: CGFloat = itemLayout.containerCornerRadius @@ -785,6 +792,14 @@ private final class GiftUpgradePreviewScreenComponent: Component { } } + let edgeEffectHeight: CGFloat = 44.0 + let edgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight + 44.0), size: CGSize(width: availableSize.width, height: edgeEffectHeight)) + let edgeSolidFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: availableSize.width, height: 44.0)) + transition.setFrame(view: self.topEdgeSolidView, frame: edgeSolidFrame) + transition.setFrame(view: self.topEdgeEffectView, frame: edgeEffectFrame) + self.topEdgeSolidView.backgroundColor = theme.list.blocksBackgroundColor + self.topEdgeEffectView.update(content: theme.list.blocksBackgroundColor, blur: true, alpha: 1.0, rect: edgeEffectFrame, edge: .top, edgeSize: edgeEffectFrame.height, transition: transition) + contentHeight += 16.0 let segmentedSize = self.segmentControl.update( @@ -792,9 +807,9 @@ private final class GiftUpgradePreviewScreenComponent: Component { component: AnyComponent(SegmentControlComponent( theme: environment.theme, items: [ - SegmentControlComponent.Item(id: AnyHashable(SelectedSection.models), title: "Models"), - SegmentControlComponent.Item(id: AnyHashable(SelectedSection.backdrops), title: "Backgrounds"), - SegmentControlComponent.Item(id: AnyHashable(SelectedSection.symbols), title: "Symbols") + SegmentControlComponent.Item(id: AnyHashable(SelectedSection.models), title: environment.strings.Gift_Variants_Models), + SegmentControlComponent.Item(id: AnyHashable(SelectedSection.backdrops), title: environment.strings.Gift_Variants_Backdrops), + SegmentControlComponent.Item(id: AnyHashable(SelectedSection.symbols), title: environment.strings.Gift_Variants_Symbols) ], selectedId: "models", action: { [weak self] id in @@ -805,7 +820,7 @@ private final class GiftUpgradePreviewScreenComponent: Component { self.isPlaying = false self.updateEffectiveGifts(attributes: component.attributes) - self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) //.withUserData(AnimationHint(value: .modeChanged))) + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) })), environment: {}, containerSize: CGSize(width: fillingSize - 8.0 * 2.0, height: 100.0) @@ -813,22 +828,30 @@ private final class GiftUpgradePreviewScreenComponent: Component { let segmentedControlFrame = CGRect(origin: CGPoint(x: floor((fillingSize - segmentedSize.width) * 0.5), y: contentHeight), size: segmentedSize) if let segmentedControlComponentView = self.segmentControl.view { if segmentedControlComponentView.superview == nil { - self.scrollContentView.addSubview(segmentedControlComponentView) + self.navigationBarContainer.addSubview(self.topEdgeSolidView) + self.navigationBarContainer.addSubview(self.topEdgeEffectView) + self.navigationBarContainer.addSubview(segmentedControlComponentView) } transition.setFrame(view: segmentedControlComponentView, frame: segmentedControlFrame) } contentHeight += segmentedSize.height contentHeight += 18.0 + let itemHeight: CGFloat = 126.0 + let itemSpacing: CGFloat = 10.0 let descriptionText: String + let itemCount: Int32 switch self.selectedSection { case .models: - descriptionText = "This collection features **\(self.modelCount)** unique models." + descriptionText = environment.strings.Gift_Variants_CollectionInfo(environment.strings.Gift_Variants_CollectionInfo_Model(self.modelCount)).string + itemCount = self.modelCount case .backdrops: - descriptionText = "This collection features **\(self.backdropCount)** unique backdrops." + descriptionText = environment.strings.Gift_Variants_CollectionInfo(environment.strings.Gift_Variants_CollectionInfo_Backdrop(self.backdropCount)).string + itemCount = self.backdropCount case .symbols: - descriptionText = "This collection features **\(self.symbolCount)** unique symbols." + descriptionText = environment.strings.Gift_Variants_CollectionInfo(environment.strings.Gift_Variants_CollectionInfo_Symbol(self.symbolCount)).string + itemCount = self.symbolCount } let descriptionFont = Font.regular(13.0) @@ -839,7 +862,7 @@ private final class GiftUpgradePreviewScreenComponent: Component { }) let descriptionSize = self.descriptionText.update( - transition: transition, + transition: .immediate, component: AnyComponent(MultilineTextComponent( text: .markdown(text: descriptionText, attributes: descriptionMarkdownAttributes) )), @@ -851,11 +874,13 @@ private final class GiftUpgradePreviewScreenComponent: Component { if descriptionView.superview == nil { self.scrollContentView.addSubview(descriptionView) } - transition.setFrame(view: descriptionView, frame: descriptionFrame) + descriptionView.frame = descriptionFrame } contentHeight += descriptionSize.height contentHeight += 26.0 + contentHeight += (itemHeight + itemSpacing) * ceil(CGFloat(itemCount) / 3.0) + if self.backgroundHandleView.image == nil { self.backgroundHandleView.image = generateStretchableFilledCircleImage(diameter: 5.0, color: .white)?.withRenderingMode(.alwaysTemplate) } @@ -896,44 +921,20 @@ private final class GiftUpgradePreviewScreenComponent: Component { if let closeButtonView = self.closeButton.view { if closeButtonView.superview == nil { self.navigationBarContainer.addSubview(self.glassContainerView) - self.glassContainerView.contentView.addSubview(closeButtonView) + self.navigationBarContainer.addSubview(closeButtonView) } transition.setFrame(view: closeButtonView, frame: closeButtonFrame) } - - var playbackItems: [AnyComponentWithIdentity] = [] - var playbackSize = CGSize(width: 40.0, height: 40.0) - if self.isPlaying { - playbackItems.append(AnyComponentWithIdentity(id: "pause", component: AnyComponent( - BundleIconComponent( - name: "Media Gallery/PictureInPicturePause", - tintColor: .white - ) - ))) - } else { - if self.showRandomizeTip { - playbackItems.append(AnyComponentWithIdentity(id: "label", component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: "Randomize Traits", font: Font.semibold(17.0), textColor: .white))) - ))) - playbackSize.width = 186.0 - } - playbackItems.append(AnyComponentWithIdentity(id: "play", component: AnyComponent( - BundleIconComponent( - name: "Media Gallery/PlayButton", - tintColor: .white - ) - ))) - } - + let playbackButtonSize = self.playbackButton.update( transition: transition, component: AnyComponent(GlassBarButtonComponent( - size: playbackSize, + size: nil, backgroundColor: buttonColor, isDark: false, state: .tintedGlass, component: AnyComponentWithIdentity(id: "content", component: AnyComponent( - HStack(playbackItems, spacing: 1.0) + PlayButtonComponent(isPlay: !self.isPlaying, title: !self.isPlaying && self.showRandomizeTip ? environment.strings.Gift_Variants_Randomize : nil) )), action: { [weak self] _ in guard let self else { @@ -962,20 +963,16 @@ private final class GiftUpgradePreviewScreenComponent: Component { } )), environment: {}, - containerSize: playbackSize + containerSize: CGSize(width: 160.0, height: 40.0) ) let playbackButtonFrame = CGRect(origin: CGPoint(x: fillingSize - 16.0 - playbackButtonSize.width, y: 16.0), size: playbackButtonSize) if let playbackButtonView = self.playbackButton.view { if playbackButtonView.superview == nil { - playbackButtonView.clipsToBounds = true self.glassContainerView.contentView.addSubview(playbackButtonView) } transition.setFrame(view: playbackButtonView, frame: playbackButtonFrame) } - //TODO:release - contentHeight += 126.0 * 17.0 - let containerInset: CGFloat = environment.statusBarHeight + 10.0 contentHeight += environment.safeInsets.bottom @@ -1181,15 +1178,15 @@ private final class AttributeInfoComponent: Component { switch component.attribute { case let .model(name, _, rarityValue): title = name - subtitle = "model" + subtitle = component.strings.Gift_Variants_Model rarity = rarityValue case let .backdrop(name, _, _, _, _, _, rarityValue): title = name - subtitle = "backdrop" + subtitle = component.strings.Gift_Variants_Backdrop rarity = rarityValue case let .pattern(name, _, rarityValue): title = name - subtitle = "symbol" + subtitle = component.strings.Gift_Variants_Symbol rarity = rarityValue default: title = "" @@ -1284,3 +1281,154 @@ private final class AttributeInfoComponent: Component { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } + + +private final class PlayButtonComponent: Component { + let isPlay: Bool + let title: String? + + public init( + isPlay: Bool, + title: String? + ) { + self.isPlay = isPlay + self.title = title + } + + static func ==(lhs: PlayButtonComponent, rhs: PlayButtonComponent) -> Bool { + if lhs.isPlay != rhs.isPlay { + return false + } + if lhs.title != rhs.title { + return false + } + return true + } + + final class View: UIView { + private var component: PlayButtonComponent? + private weak var componentState: EmptyComponentState? + + private let containerView = UIView() + private let titleContainerView = UIView() + private let title = ComponentView() + private let play = ComponentView() + private let pause = ComponentView() + + override init(frame: CGRect) { + super.init(frame: frame) + + self.containerView.clipsToBounds = true + self.containerView.layer.cornerRadius = 20.0 + self.addSubview(self.containerView) + + self.titleContainerView.clipsToBounds = true + self.containerView.addSubview(self.titleContainerView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: PlayButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.componentState = state + + var contentSize = CGSize(width: 15.0, height: 21.0) + + var titleSize = CGSize() + if let titleString = component.title { + titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: titleString, font: Font.semibold(17.0), textColor: .white)))), + environment: {}, + containerSize: availableSize + ) + let titleFrame = CGRect(origin: CGPoint(x: 9.0, y: 10.0), size: titleSize) + if let titleView = self.title.view { + titleView.alpha = 1.0 + if titleView.superview == nil { + self.titleContainerView.addSubview(titleView) + transition.animateAlpha(view: titleView, from: 0.0, to: 1.0) + } + titleView.frame = titleFrame + } + contentSize.width += titleSize.width + 4.0 + } else if let titleView = self.title.view { + transition.setAlpha(view: titleView, alpha: 0.0, completion: { finished in + if finished { + titleView.removeFromSuperview() + } + }) + } + transition.setFrame(view: self.titleContainerView, frame: CGRect(origin: .zero, size: CGSize(width: titleSize.width + 14.0, height: 40.0))) + + if component.isPlay { + let iconSize = self.play.update( + transition: .immediate, + component: AnyComponent(BundleIconComponent(name: "Media Gallery/PlayButton", tintColor: .white)), + environment: {}, + containerSize: availableSize + ) + let iconFrame = CGRect(origin: CGPoint(x: contentSize.width - iconSize.width + 21.0, y: 5.0), size: iconSize) + if let iconView = self.play.view { + iconView.alpha = 1.0 + if iconView.superview == nil { + self.containerView.addSubview(iconView) + transition.animateAlpha(view: iconView, from: 0.0, to: 1.0) + transition.animateScale(view: iconView, from: 0.01, to: 1.0) + } + transition.setFrame(view: iconView, frame: iconFrame) + } + } else if let iconView = self.play.view { + transition.setAlpha(view: iconView, alpha: 0.0, completion: { finished in + if finished { + iconView.removeFromSuperview() + } + }) + transition.animateScale(view: iconView, from: 1.0, to: 0.01) + } + + if !component.isPlay { + let iconSize = self.pause.update( + transition: .immediate, + component: AnyComponent(BundleIconComponent(name: "Media Gallery/PictureInPicturePause", tintColor: .white)), + environment: {}, + containerSize: availableSize + ) + let iconFrame = CGRect(origin: CGPoint(x: contentSize.width - iconSize.width + 12.0 - UIScreenPixel, y: 13.0 - UIScreenPixel), size: iconSize) + if let iconView = self.pause.view { + iconView.alpha = 1.0 + if iconView.superview == nil { + self.containerView.addSubview(iconView) + transition.animateAlpha(view: iconView, from: 0.0, to: 1.0) + transition.animateScale(view: iconView, from: 0.01, to: 1.0) + } + transition.setFrame(view: iconView, frame: iconFrame) + } + } else if let iconView = self.pause.view { + transition.setAlpha(view: iconView, alpha: 0.0, completion: { finished in + if finished { + iconView.removeFromSuperview() + } + }) + transition.animateScale(view: iconView, from: 1.0, to: 0.01) + } + + let containerWidth: CGFloat = contentSize.width + 26.0 + let containerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((contentSize.width - containerWidth) / 2.0), y: floorToScreenPixels((contentSize.height - 40.0) / 2.0)), size: CGSize(width: containerWidth, height: 40.0)) + transition.setFrame(view: self.containerView, frame: containerFrame) + + return contentSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + diff --git a/submodules/TelegramUI/Components/GlassBarButtonComponent/Sources/GlassBarButtonComponent.swift b/submodules/TelegramUI/Components/GlassBarButtonComponent/Sources/GlassBarButtonComponent.swift index bee29b5e40..6b012d1097 100644 --- a/submodules/TelegramUI/Components/GlassBarButtonComponent/Sources/GlassBarButtonComponent.swift +++ b/submodules/TelegramUI/Components/GlassBarButtonComponent/Sources/GlassBarButtonComponent.swift @@ -79,6 +79,7 @@ public final class GlassBarButtonComponent: Component { super.init(frame: frame) self.containerView.isUserInteractionEnabled = false + self.containerView.layer.rasterizationScale = UIScreenScale self.addSubview(self.containerView) self.containerView.addSubview(self.genericContainerView) diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift index 8f22ab47d2..1ff5d8140d 100644 --- a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift @@ -1449,6 +1449,14 @@ public final class StarsWithdrawScreen: ViewControllerComponentContainer { case .ton: break } + } else if case .starGiftOffer = self.mode { + switch currency { + case .stars: + text = presentationData.strings.Gift_Offer_GiftMinAmountToast_Text("\(presentationData.strings.Stars_Withdraw_Withdraw_ErrorMinimum_Stars(Int32(clamping: minAmount)))").string + case .ton: + let amountString = formatTonAmountText(minAmount, dateTimeFormat: presentationData.dateTimeFormat) + " TON" + text = presentationData.strings.Gift_Offer_GiftMinAmountToast_Text(amountString).string + } } let resultController = UndoOverlayController( @@ -2682,14 +2690,8 @@ private final class DurationMenuComponent: Component { self.itemViews[value] = itemView } - let repeatString: String let hours = value / 3600 - //TODO:localize - if hours == 1 { - repeatString = "1 hour" - } else { - repeatString = "\(hours) hours" - } + let repeatString: String = component.strings.Gift_Offer_Duration_Hours(Int32(hours)) let itemSize = itemView.update( transition: transition, diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index a67ef82cbd..f1c2916729 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -2425,7 +2425,21 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G switch buttonType { case 0: - let _ = strongSelf.context.engine.payments.resolveStarGiftOffer(messageId: message.id, accept: false).startStandalone() + let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: message.id.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + self.present(textAlertController(context: self.context, updatedPresentationData: self.updatedPresentationData, title: self.presentationData.strings.Chat_GiftPurchaseOffer_RejectConfirmation_Title, text: self.presentationData.strings.Chat_GiftPurchaseOffer_RejectConfirmation_Text(peer.compactDisplayTitle).string, actions: [ + TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {}), + TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Chat_GiftPurchaseOffer_RejectConfirmation_Reject, action: { [weak self] in + guard let self else { + return + } + let _ = self.context.engine.payments.resolveStarGiftOffer(messageId: message.id, accept: false).startStandalone() + }) + ], parseMarkdown: true), in: .window(.root)) + }) case 1: let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: message.id.peerId)) |> deliverOnMainQueue).start(next: { [weak self] peer in From 6db50776429d5a678b7dbbc72f090d3d72302272 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 5 Dec 2025 02:26:17 +0400 Subject: [PATCH 9/9] Various fixes --- .../Telegram-iOS/en.lproj/Localizable.strings | 2 +- .../Sources/ChatMessageForwardInfoNode.swift | 6 +- .../Sources/StarsWithdrawalScreen.swift | 117 ++++++------------ 3 files changed, 40 insertions(+), 85 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index ab4058955f..1ffc2483da 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -15570,7 +15570,7 @@ Error: %8$@"; "Gift.Variants.CollectionInfo.Symbol_1" = "**%@** unique symbol"; "Gift.Variants.CollectionInfo.Symbol_any" = "**%@** unique symbols"; -"Gift.Auction.GiftAuction" = "Upcoming Auction"; +"Gift.Auction.GiftAuction" = "Gift Auction"; "Gift.Auction.UpcomingAuction" = "Upcoming Auction"; "Gift.Auction.LearnMore" = "Learn more about Telegram Gifts >"; diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift index e6699164f0..4afdb9e74d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift @@ -514,6 +514,7 @@ public class ChatMessageForwardInfoNode: ASDisplayNode { } titleNode.frame = CGRect(origin: CGPoint(x: leftOffset, y: 0.0), size: titleLayout.size) + var nameFrame = CGRect() if let (nameLayout, nameApply) = nameLayoutAndApply { let nameNode = nameApply() if node.nameNode == nil { @@ -521,7 +522,8 @@ public class ChatMessageForwardInfoNode: ASDisplayNode { node.nameNode = nameNode node.addSubnode(nameNode) } - nameNode.frame = CGRect(origin: CGPoint(x: leftOffset + authorAvatarInset, y: titleLayout.size.height + titleAuthorSpacing), size: nameLayout.size) + nameFrame = CGRect(origin: CGPoint(x: leftOffset + authorAvatarInset, y: titleLayout.size.height + titleAuthorSpacing), size: nameLayout.size) + nameNode.frame = nameFrame if authorAvatarInset != 0.0 { let avatarNode: AvatarNode @@ -599,7 +601,7 @@ public class ChatMessageForwardInfoNode: ASDisplayNode { node.credibilityIconNode = credibilityIconNode node.addSubnode(credibilityIconNode) } - credibilityIconNode.frame = CGRect(origin: CGPoint(x: titleLayout.size.width + 4.0, y: 16.0), size: credibilityIconImage.size) + credibilityIconNode.frame = CGRect(origin: CGPoint(x: nameFrame.maxX + 4.0, y: 17.0), size: credibilityIconImage.size) credibilityIconNode.image = credibilityIconImage } else { node.credibilityIconNode?.removeFromSupernode() diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift index 1ff5d8140d..80cc633365 100644 --- a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift @@ -66,9 +66,6 @@ private final class SheetContent: CombinedComponent { let timestampSection = Child(ListSectionComponent.self) let onlyTonSection = Child(ListSectionComponent.self) let button = Child(ButtonComponent.self) - let balanceTitle = Child(MultilineTextComponent.self) - let balanceValue = Child(MultilineTextComponent.self) - let balanceIcon = Child(BundleIconComponent.self) let durationPicker = Child(MenuComponent.self) let body: (CombinedComponentContext) -> CGSize = { (context: CombinedComponentContext) -> CGSize in @@ -88,37 +85,7 @@ private final class SheetContent: CombinedComponent { var contentSize = CGSize(width: context.availableSize.width, height: 18.0) let constrainedTitleWidth = context.availableSize.width - 16.0 * 2.0 - - if case let .suggestedPost(mode, _, _, _) = component.mode { - var displayBalance = false - switch mode { - case let .sender(_, isFromAdmin): - displayBalance = !isFromAdmin - case .admin: - break - } - - if displayBalance { - let balance = balance.update( - component: BalanceComponent( - context: component.context, - theme: environment.theme, - strings: environment.strings, - currency: state.currency, - balance: state.currency == .stars ? state.starsBalance : state.tonBalance, - alignment: .right - ), - availableSize: CGSize(width: 200.0, height: 200.0), - transition: .immediate - ) - let balanceFrame = CGRect(origin: CGPoint(x: context.availableSize.width - balance.size.width - 15.0, y: floor((56.0 - balance.size.height) * 0.5)), size: balance.size) - context.add(balance - .anchorPoint(CGPoint(x: 1.0, y: 0.0)) - .position(CGPoint(x: balanceFrame.maxX, y: balanceFrame.minY)) - ) - } - } - + let closeButton = closeButton.update( component: GlassBarButtonComponent( size: CGSize(width: 40.0, height: 40.0), @@ -276,60 +243,45 @@ private final class SheetContent: CombinedComponent { contentSize.height += title.size.height contentSize.height += 56.0 - let balance: StarsAmount? - if case .accountWithdraw = component.mode { - balance = state.starsBalance - } else if case .reaction = component.mode { - balance = state.starsBalance - } else if case let .withdraw(starsState, _) = component.mode { - balance = starsState.balances.availableBalance.amount - } else { - balance = nil + var displayBalance = false + var currentBalance = state.currency == .stars ? state.starsBalance : state.tonBalance + switch component.mode { + case .accountWithdraw, .reaction: + displayBalance = true + case let .suggestedPost(mode, _, _, _): + switch mode { + case let .sender(_, isFromAdmin): + displayBalance = !isFromAdmin + case .admin: + break + } + case let .withdraw(starsState, _): + currentBalance = starsState.balances.availableBalance.amount + displayBalance = true + default: + break } - - if let balance { - let balanceTitle = balanceTitle.update( - component: MultilineTextComponent( - text: .plain(NSAttributedString( - string: environment.strings.Stars_Transfer_Balance, - font: Font.regular(14.0), - textColor: theme.list.itemPrimaryTextColor - )), - maximumNumberOfLines: 1 + + if displayBalance { + let balance = balance.update( + component: BalanceComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + currency: state.currency, + balance: currentBalance, + alignment: .right ), - availableSize: context.availableSize, + availableSize: CGSize(width: 200.0, height: 200.0), transition: .immediate ) - let balanceValue = balanceValue.update( - component: MultilineTextComponent( - text: .plain(NSAttributedString( - string: presentationStringsFormattedNumber(balance, environment.dateTimeFormat.groupingSeparator), - font: Font.semibold(16.0), - textColor: theme.list.itemPrimaryTextColor - )), - maximumNumberOfLines: 1 - ), - availableSize: context.availableSize, - transition: .immediate - ) - let balanceIcon = balanceIcon.update( - component: BundleIconComponent(name: "Premium/Stars/StarSmall", tintColor: nil), - availableSize: context.availableSize, - transition: .immediate + let balanceFrame = CGRect(origin: CGPoint(x: context.availableSize.width - balance.size.width - 20.0, y: floor((66.0 - balance.size.height) * 0.5)), size: balance.size) + context.add(balance + .anchorPoint(CGPoint(x: 1.0, y: 0.0)) + .position(CGPoint(x: balanceFrame.maxX, y: balanceFrame.minY)) ) + } - let topBalanceOriginY = 11.0 - context.add(balanceTitle - .position(CGPoint(x: 16.0 + environment.safeInsets.left + balanceTitle.size.width / 2.0, y: topBalanceOriginY + balanceTitle.size.height / 2.0)) - ) - context.add(balanceIcon - .position(CGPoint(x: 16.0 + environment.safeInsets.left + balanceIcon.size.width / 2.0, y: topBalanceOriginY + balanceTitle.size.height + balanceValue.size.height / 2.0 + 1.0 + UIScreenPixel)) - ) - context.add(balanceValue - .position(CGPoint(x: 16.0 + environment.safeInsets.left + balanceIcon.size.width + 3.0 + balanceValue.size.width / 2.0, y: topBalanceOriginY + balanceTitle.size.height + balanceValue.size.height / 2.0 + 2.0 - UIScreenPixel)) - ) - } - var tonBalanceValue: StarsAmount = .zero if let tonBalance = state.tonBalance { tonBalanceValue = tonBalance @@ -1836,6 +1788,7 @@ public final class AmountFieldComponent: Component { text = "\(formatTonAmountText(value, dateTimeFormat: PresentationDateTimeFormat(timeFormat: component.dateTimeFormat.timeFormat, dateFormat: component.dateTimeFormat.dateFormat, dateSeparator: "", dateSuffix: "", requiresFullYear: false, decimalSeparator: ".", groupingSeparator: ""), maxDecimalPositions: nil))" } self.textField.text = text + self.placeholderView.view?.isHidden = text.isEmpty } else { self.textField.text = "" }