From 6a09d2e4e61bf6b219dc1cc691523a0a5737823a Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 3 Dec 2025 17:59:09 +0400 Subject: [PATCH] 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 {