Merge commit '6db5077642' into experimental-3

This commit is contained in:
Isaac 2025-12-05 14:35:56 +08:00
commit 1969b1b280
46 changed files with 2802 additions and 1203 deletions

View file

@ -15506,3 +15506,120 @@ Error: %8$@";
"Gift.Auction.Extension" = "+ %1$@ for late bids in top %2$@";
"ChatList.Auctions.UpcomingAuction" = "Upcoming Auction";
"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";
"Attachment.SharedAudio" = "SHARED AUDIO";
"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.";
"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.";
"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" = "Gift 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";
"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";

View file

@ -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<ChatLocationContextHolder?>, 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<PresentationData, NoError>)?, 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<Never, TransferStarGiftError>)?) -> 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<PresentationData, NoError>)?, 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<StickerPickerInput>, 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<PresentationData, NoError>)?, peerId: EnginePeer.Id, boosts: Bool, boostStatus: ChannelBoostStatus?) -> ViewController
func makeMessagesStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, messageId: EngineMessage.Id) -> ViewController
func makeStoryStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, 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
@ -1426,37 +1407,28 @@ 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 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<MiniAppListScreenInitialData, NoError>
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<PresentationData, NoError>)?, 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<AffiliateProgramSetupScreenInitialData, NoError>
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<PresentationData, NoError>)?, 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

View file

@ -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

View file

@ -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
}

View file

@ -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

View file

@ -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 {

View file

@ -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)

View file

@ -1075,6 +1075,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):

View file

@ -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)
}

View file

@ -508,6 +508,7 @@ swift_library(
"//submodules/TelegramUI/Components/LegacyChatHeaderPanelComponent",
"//submodules/TelegramUI/Components/Chat/ChatSearchNavigationContentNode",
"//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": [],

View file

@ -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 {

View file

@ -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

View file

@ -2835,16 +2835,15 @@ 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
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: [],

View file

@ -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()

View file

@ -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

View file

@ -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)

View file

@ -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<StarGift.UniqueGift.Attribute.AttributeType>
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<StarGift.UniqueGift.Attribute.AttributeType> = 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
}
@ -718,12 +727,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))
@ -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 {

View file

@ -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",
],
)

View file

@ -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<ChildEnvironment: Equatable>: CombinedComponent {
typealias EnvironmentType = ChildEnvironment
private let content: AnyComponent<ChildEnvironment>
private let title: String
private let text: String
private let textColor: UIColor
init(
content: AnyComponent<ChildEnvironment>,
title: String,
text: String,
textColor: UIColor
) {
self.content = content
self.title = title
self.text = text
self.textColor = textColor
}
static func ==(lhs: PageComponent<ChildEnvironment>, rhs: PageComponent<ChildEnvironment>) -> 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<Void>()
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<Empty>.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<Empty>(
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<Empty>] = []
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.Gift_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<Void>.self)
return { context in
let environment = context.environment[EnvironmentType.self]
let controller = environment.controller
let sheet = sheet.update(
component: SheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(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<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.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<Empty>.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.Gift_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.Gift_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<Empty>] = []
items.append(
AnyComponentWithIdentity(
id: "unique",
component: AnyComponent(InfoParagraphComponent(
title: strings.Gift_Demo_Unique_Title,
titleColor: titleColor,
text: strings.Gift_Demo_Unique_Text,
textColor: textColor,
accentColor: iconColor,
iconName: "Premium/Collectible/Unique",
iconColor: iconColor
))
)
)
items.append(
AnyComponentWithIdentity(
id: "tradable",
component: AnyComponent(InfoParagraphComponent(
title: strings.Gift_Demo_Tradable_Title,
titleColor: titleColor,
text: strings.Gift_Demo_Tradable_Text,
textColor: textColor,
accentColor: iconColor,
iconName: "Premium/Collectible/Transferable",
iconColor: iconColor
))
)
)
items.append(
AnyComponentWithIdentity(
id: "wearable",
component: AnyComponent(InfoParagraphComponent(
title: strings.Gift_Demo_Wearable_Title,
titleColor: titleColor,
text: strings.Gift_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)
}
}
}

View file

@ -167,6 +167,8 @@ public final class GiftItemComponent: Component {
let isPinned: Bool
let isEditing: Bool
let isDateLocked: Bool
let isPlaceholder: Bool
let animateChanges: Bool
let mode: Mode
let action: (() -> Void)?
let contextAction: ((UIView, ContextGesture) -> Void)?
@ -191,6 +193,8 @@ public final class GiftItemComponent: Component {
isPinned: Bool = false,
isEditing: Bool = false,
isDateLocked: Bool = false,
isPlaceholder: Bool = false,
animateChanges: Bool = false,
mode: Mode = .generic,
action: (() -> Void)? = nil,
contextAction: ((UIView, ContextGesture) -> Void)? = nil
@ -214,6 +218,8 @@ public final class GiftItemComponent: Component {
self.isPinned = isPinned
self.isEditing = isEditing
self.isDateLocked = isDateLocked
self.isPlaceholder = isPlaceholder
self.animateChanges = animateChanges
self.mode = mode
self.action = action
self.contextAction = contextAction
@ -277,6 +283,12 @@ public final class GiftItemComponent: Component {
if lhs.isDateLocked != rhs.isDateLocked {
return false
}
if lhs.isPlaceholder != rhs.isPlaceholder {
return false
}
if lhs.animateChanges != rhs.animateChanges {
return false
}
if lhs.mode != rhs.mode {
return false
}
@ -589,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, _):
@ -636,12 +650,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),
@ -664,6 +688,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)
@ -672,6 +699,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(
@ -688,7 +720,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, *) {
@ -917,7 +949,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 +1500,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 +1543,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 +1572,7 @@ private final class ButtonContentComponent: Component {
fatalError("init(coder:) has not been implemented")
}
func update(component: ButtonContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
func update(component: StarsButtonContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.componentState = state
@ -1537,7 +1581,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 +1607,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 +1618,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
@ -1600,7 +1646,7 @@ private final class ButtonContentComponent: 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

View file

@ -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)
@ -676,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)")

View file

@ -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",
],
)

View file

@ -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

View file

@ -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",

View file

@ -38,6 +38,7 @@ import EdgeEffect
import AnimatedTextComponent
import GlassBarButtonComponent
import MessageInputPanelComponent
import GiftRemainingCountComponent
private final class GiftSetupScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -1718,14 +1719,14 @@ 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),
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,
@ -1828,22 +1829,30 @@ private final class GiftSetupScreenComponent: Component {
var buttonTitleItems: [AnyComponentWithIdentity<Empty>] = []
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 {
@ -1883,6 +1892,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 {

View file

@ -62,6 +62,8 @@ swift_library(
"//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/BotPaymentsUI",
"//submodules/TelegramUI/Components/SegmentControlComponent",
"//submodules/TelegramUI/Components/Gifts/GiftRemainingCountComponent",
"//submodules/TelegramUI/Components/Gifts/InfoParagraphComponent",
],
visibility = [
"//visibility:public",

View file

@ -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 {

View file

@ -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:
@ -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 {
@ -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<Empty>?
var topBidsComponents: [(EnginePeer.Id, AnyComponent<Empty>)] = []
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
@ -2486,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((
@ -2688,10 +2707,17 @@ private final class GiftAuctionBidScreenComponent: Component {
var initialContentHeight = contentHeight
let clippingY: CGFloat
let titleString: String
if isUpcoming {
titleString = environment.strings.Gift_AuctionBid_UpcomingTitle
} 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 +2734,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)

View file

@ -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<Action<()>>
let getController: () -> ViewController?
init(
context: AccountContext,
gift: StarGift,
animateOut: ActionSlot<Action<()>>,
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<EnvironmentType>.self)
let animateOut = StoredActionSlot(Action<Void>.self)
let sheetExternalState = SheetComponent<EnvironmentType>.ExternalState()
return { context in
let environment = context.environment[EnvironmentType.self]
let controller = environment.controller
let sheet = sheet.update(
component: SheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(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<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
}
}
}

View file

@ -72,8 +72,17 @@ 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 var disposables = DisposableSet()
private var fetchedFiles = Set<Int64>()
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 +114,98 @@ 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
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()
})
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.disposables.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 +263,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 +403,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 +474,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<Void>()
let giftCompositionExternalState = GiftCompositionComponent.ExternalState()
return { context in
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
@ -420,63 +508,22 @@ 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
if let genericGift {
let animation = animation.update(
component: GiftItemComponent(
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
),
availableSize: CGSize(width: 120.0, height: 120.0),
transition: context.transition
)
context.add(animation
.position(CGPoint(x: context.availableSize.width / 2.0, y: 92.0))
)
}
originY += 177.0
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: titleString,
font: Font.bold(24.0),
textColor: theme.list.itemPrimaryTextColor,
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: 174.0))
)
var buttonColor: UIColor = .white.withAlphaComponent(0.1)
var secondaryTextColor: UIColor = .white.withAlphaComponent(0.4)
var descriptionText: String = ""
var descriptionColor = theme.list.itemSecondaryTextColor
let tableFont = Font.regular(15.0)
let tableTextColor = theme.list.itemPrimaryTextColor
@ -502,7 +549,6 @@ private final class GiftAuctionViewSheetContent: CombinedComponent {
if isEnded {
descriptionText = strings.Gift_Auction_Ended
descriptionColor = theme.list.itemDestructiveColor
tableItems.append(.init(
id: "firstSale",
@ -572,11 +618,7 @@ 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
descriptionText = isUpcoming ? strings.Gift_Auction_UpcomingAuction : strings.Gift_Auction_GiftAuction
tableItems.append(.init(
id: "start",
@ -670,11 +712,103 @@ 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
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: isEnded ? .clear : 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: context.transition
)
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 +819,32 @@ 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))
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))
}
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.makeGiftDemoScreen(
context: component.context
)
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,8 +856,6 @@ 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
@ -793,8 +909,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 +956,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([
@ -885,6 +1001,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
}
@ -963,7 +1169,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,
@ -1007,13 +1213,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,
isDark: theme.overallDarkAppearance,
state: .generic,
backgroundColor: buttonColor,
isDark: false,
state: .tintedGlass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: theme.rootController.navigationBar.glassBarButtonForegroundColor
tintColor: .white
)
)),
action: { [weak state] _ in
@ -1024,7 +1230,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))
@ -1033,15 +1239,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,
isDark: theme.overallDarkAppearance,
state: .generic,
backgroundColor: buttonColor,
isDark: false,
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
)
@ -1055,7 +1261,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))
@ -1181,12 +1387,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

View file

@ -0,0 +1,788 @@
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
import AnimatedTextComponent
private final class GiftAuctionWearPreviewSheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let auctionContext: GiftAuctionContext
let attributes: [StarGift.UniqueGift.Attribute]
let animateOut: ActionSlot<Action<()>>
let getController: () -> ViewController?
init(
context: AccountContext,
auctionContext: GiftAuctionContext,
attributes: [StarGift.UniqueGift.Attribute],
animateOut: ActionSlot<Action<()>>,
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 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
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,
auctionContext: GiftAuctionContext,
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()
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()
}
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, auctionContext: self.auctionContext, 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,
animateScaleOnTransition: false,
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)
}
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))
}
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: strings.Gift_WearPreview_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: strings.Gift_WearPreview_Upgraded, color: ribbonColor),
animateChanges: true,
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: strings.Gift_WearPreview_FreeUpgrade, 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 {
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,
activeColors: [UIColor(rgb: 0x72d6ff), UIColor(rgb: 0x32a0f9)],
inactiveTitle: strings.Gift_Send_Remains(remains),
inactiveValue: "",
inactiveTitleColor: theme.list.itemSecondaryTextColor,
activeTitle: "",
activeValue: sold > 0 ? strings.Gift_Send_Sold(sold) : "",
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))
)
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 += 80.0
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<Empty>] = [
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(
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("bid"),
component: AnyComponent(VStack(items, spacing: 1.0))
),
isEnabled: true,
displaysProgress: false,
action: {
if let controller = controller() as? GiftAuctionWearPreviewScreen {
controller.completion()
controller.dismissAnimated()
}
}),
availableSize: buttonSize,
transition: .spring(duration: 0.2)
)
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<EnvironmentType>.self)
let animateOut = StoredActionSlot(Action<Void>.self)
let sheetExternalState = SheetComponent<EnvironmentType>.ExternalState()
return { context in
let environment = context.environment[EnvironmentType.self]
let controller = environment.controller
let theme = environment.theme.withModalBlocksBackground()
let sheet = sheet.update(
component: SheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(GiftAuctionWearPreviewSheetContent(
context: context.component.context,
auctionContext: context.component.auctionContext,
attributes: context.component.attributes,
animateOut: animateOut,
getController: controller
)),
style: .glass,
backgroundColor: .color(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<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
}
}
}

View file

@ -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<Empty>()
private let valueDelta = ComponentView<Empty>()
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

View file

@ -94,6 +94,8 @@ private final class GiftUpgradePreviewScreenComponent: Component {
private var attributeInfos: [ComponentView<Empty>] = []
private let topEdgeSolidView = UIView()
private let topEdgeEffectView: EdgeEffectView
private let segmentControl = ComponentView<Empty>()
private let descriptionText = ComponentView<Empty>()
@ -103,10 +105,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
@ -147,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()
@ -236,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
@ -272,7 +278,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 +305,7 @@ private final class GiftUpgradePreviewScreenComponent: Component {
guard let self else {
return
}
self.timerTick()
self.previewTimerTick()
}, queue: Queue.mainQueue())
self.previewTimer?.start()
} else {
@ -309,15 +315,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 +332,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 +348,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 +570,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 +616,7 @@ private final class GiftUpgradePreviewScreenComponent: Component {
}).shuffled().prefix(15))
self.previewSymbols = randomSymbols
self.updateEffectiveGifts()
self.updateEffectiveGifts(attributes: component.attributes)
}
self.component = component
@ -671,7 +672,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.25)
secondaryTextColor = topColor.withMultiplied(hue: 1.0, saturation: 1.02, brightness: 1.25).mixedWith(UIColor.white, alpha: 0.3)
}
@ -686,6 +687,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
@ -790,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(
@ -797,20 +807,20 @@ 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
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.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) //.withUserData(AnimationHint(value: .modeChanged)))
self.updateEffectiveGifts(attributes: component.attributes)
self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)))
})),
environment: {},
containerSize: CGSize(width: fillingSize - 8.0 * 2.0, height: 100.0)
@ -818,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)
@ -844,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)
)),
@ -856,85 +874,12 @@ 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
////
//// var validKeys: Set<Int64> = Set()
//// for auctionState in self.auctionStates {
//// let id = auctionState.gift.giftId
//// validKeys.insert(id)
////
//// let itemView: ComponentView<Empty>
//// 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)
// }
contentHeight += (itemHeight + itemSpacing) * ceil(CGFloat(itemCount) / 3.0)
if self.backgroundHandleView.image == nil {
self.backgroundHandleView.image = generateStretchableFilledCircleImage(diameter: 5.0, color: .white)?.withRenderingMode(.alwaysTemplate)
@ -950,10 +895,10 @@ 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: secondaryTextColor,
backgroundColor: buttonColor,
isDark: false,
state: .tintedGlass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
@ -976,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<Empty>] = []
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,
backgroundColor: secondaryTextColor,
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 {
@ -1036,26 +957,22 @@ private final class GiftUpgradePreviewScreenComponent: Component {
self.showRandomizeTip = false
self.timerTick()
self.previewTimerTick()
}
self.state?.updated(transition: .easeInOut(duration: 0.25))
}
)),
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
@ -1261,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 = ""
@ -1364,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<Empty>()
private let play = ComponentView<Empty>()
private let pause = ComponentView<Empty>()
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<Empty>, 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<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View file

@ -39,6 +39,7 @@ import GiftAnimationComponent
import ChatThemeScreen
import ProfileLevelRatingBarComponent
import AnimatedTextComponent
import InfoParagraphComponent
private final class GiftViewSheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -1437,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)
@ -2545,7 +2545,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
}
@ -2603,7 +2603,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
@ -2885,7 +2887,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 +2901,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 +2915,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 +3033,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 +3047,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 +3061,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 +6048,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

View file

@ -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",
],
)

View file

@ -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)
}
}
}

View file

@ -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)

View file

@ -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

View file

@ -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 {

View file

@ -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
)),

View file

@ -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<SheetContent>) -> CGSize = { (context: CombinedComponentContext<SheetContent>) -> 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),
@ -240,18 +207,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 {
@ -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
@ -358,10 +310,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 +506,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 +752,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 +764,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 +780,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 +876,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 +1031,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 +1094,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 +1194,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 +1212,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 +1366,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()
}
@ -1451,6 +1401,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(
@ -1810,6 +1768,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<EnvironmentType>, transition: ComponentTransition) -> CGSize {
@ -1829,10 +1788,11 @@ 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
self.placeholderView.view?.isHidden = text.isEmpty
} else {
self.textField.text = ""
}
self.didSetValueOnce = true
}
self.textField.font = Font.regular(17.0)
@ -2683,14 +2643,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,

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "arrow (7).pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -2401,7 +2401,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

View file

@ -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)

View file

@ -91,6 +91,7 @@ import GlassBackgroundComponent
import AttachmentFileController
import NewContactScreen
import PasskeysScreen
import GiftDemoScreen
private final class AccountUserInterfaceInUseContext {
let subscribers = Bag<(Bool) -> Void>()
@ -3856,7 +3857,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)
}
@ -3872,6 +3873,14 @@ 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 makeGiftDemoScreen(context: AccountContext) -> ViewController {
return GiftDemoScreen(context: context)
}
public func makeStorySharingScreen(context: AccountContext, subject: StorySharingSubject, parentController: ViewController) -> ViewController {
let editorSubject: Signal<MediaEditorScreenImpl.Subject?, NoError>
switch subject {